From 01fa4859e476fc7c190537096924533529910066 Mon Sep 17 00:00:00 2001 From: YuNing Chen Date: Tue, 25 Mar 2025 00:08:51 +0800 Subject: [PATCH 01/15] chore: mv `DistinctSumAccumulator` to common --- .../src/aggregate.rs | 1 + .../src/aggregate/sum_distinct/mod.rs | 22 ++++ .../src/aggregate/sum_distinct/numeric.rs | 123 ++++++++++++++++++ datafusion/functions-aggregate/src/sum.rs | 88 +------------ 4 files changed, 148 insertions(+), 86 deletions(-) create mode 100644 datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs create mode 100644 datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs diff --git a/datafusion/functions-aggregate-common/src/aggregate.rs b/datafusion/functions-aggregate-common/src/aggregate.rs index c9cbaa8396fc5..56dc58570ac79 100644 --- a/datafusion/functions-aggregate-common/src/aggregate.rs +++ b/datafusion/functions-aggregate-common/src/aggregate.rs @@ -17,3 +17,4 @@ pub mod count_distinct; pub mod groups_accumulator; +pub mod sum_distinct; diff --git a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs new file mode 100644 index 0000000000000..3a645b3d6ef48 --- /dev/null +++ b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs @@ -0,0 +1,22 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Sum distinct accumulator implementations + +pub mod numeric; + +pub use numeric::DistinctSumAccumulator; \ No newline at end of file diff --git a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs new file mode 100644 index 0000000000000..2c56e681c6838 --- /dev/null +++ b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs @@ -0,0 +1,123 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Defines the accumulator for `SUM DISTINCT` for primitive numeric types + +use std::collections::HashSet; +use std::fmt::Debug; +use std::mem::{size_of, size_of_val}; + +use ahash::RandomState; +use arrow::array::Array; +use arrow::array::ArrowNativeTypeOp; +use arrow::array::ArrowPrimitiveType; +use arrow::array::ArrayRef; +use arrow::array::AsArray; +use arrow::datatypes::ArrowNativeType; +use arrow::datatypes::DataType; + +use datafusion_common::Result; +use datafusion_common::ScalarValue; +use datafusion_expr_common::accumulator::Accumulator; + +use crate::utils::Hashable; + +/// Accumulator for computing SUM(DISTINCT expr) +pub struct DistinctSumAccumulator { + values: HashSet, RandomState>, + data_type: DataType, +} + +impl Debug for DistinctSumAccumulator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DistinctSumAccumulator({})", self.data_type) + } +} + +impl DistinctSumAccumulator { + pub fn try_new(data_type: &DataType) -> Result { + Ok(Self { + values: HashSet::default(), + data_type: data_type.clone(), + }) + } + + pub fn distinct_count(&self) -> usize { + self.values.len() + } +} + +impl Accumulator for DistinctSumAccumulator { + fn state(&mut self) -> Result> { + // 1. Stores aggregate state in `ScalarValue::List` + // 2. Constructs `ScalarValue::List` state from distinct numeric stored in hash set + let state_out = { + let distinct_values = self + .values + .iter() + .map(|value| { + ScalarValue::new_primitive::(Some(value.0), &self.data_type) + }) + .collect::>>()?; + + vec![ScalarValue::List(ScalarValue::new_list_nullable( + &distinct_values, + &self.data_type, + ))] + }; + Ok(state_out) + } + + fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> { + if values.is_empty() { + return Ok(()); + } + + let array = values[0].as_primitive::(); + match array.nulls().filter(|x| x.null_count() > 0) { + Some(n) => { + for idx in n.valid_indices() { + self.values.insert(Hashable(array.value(idx))); + } + } + None => array.values().iter().for_each(|x| { + self.values.insert(Hashable(*x)); + }), + } + Ok(()) + } + + fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> { + for x in states[0].as_list::().iter().flatten() { + self.update_batch(&[x])? + } + Ok(()) + } + + fn evaluate(&mut self) -> Result { + let mut acc = T::Native::usize_as(0); + for distinct_value in self.values.iter() { + acc = acc.add_wrapping(distinct_value.0) + } + let v = (!self.values.is_empty()).then_some(acc); + ScalarValue::new_primitive::(v, &self.data_type) + } + + fn size(&self) -> usize { + size_of_val(self) + self.values.capacity() * size_of::() + } +} \ No newline at end of file diff --git a/datafusion/functions-aggregate/src/sum.rs b/datafusion/functions-aggregate/src/sum.rs index 76a1315c2d889..6539ca920ebc3 100644 --- a/datafusion/functions-aggregate/src/sum.rs +++ b/datafusion/functions-aggregate/src/sum.rs @@ -17,17 +17,14 @@ //! Defines `SUM` and `SUM DISTINCT` aggregate accumulators -use ahash::RandomState; use datafusion_expr::utils::AggregateOrderSensitivity; use std::any::Any; -use std::collections::HashSet; -use std::mem::{size_of, size_of_val}; +use std::mem::size_of_val; use arrow::array::Array; use arrow::array::ArrowNativeTypeOp; use arrow::array::{ArrowNumericType, AsArray}; use arrow::datatypes::ArrowNativeType; -use arrow::datatypes::ArrowPrimitiveType; use arrow::datatypes::{ DataType, Decimal128Type, Decimal256Type, Float64Type, Int64Type, UInt64Type, DECIMAL128_MAX_PRECISION, DECIMAL256_MAX_PRECISION, @@ -44,7 +41,7 @@ use datafusion_expr::{ SetMonotonicity, Signature, Volatility, }; use datafusion_functions_aggregate_common::aggregate::groups_accumulator::prim_op::PrimitiveGroupsAccumulator; -use datafusion_functions_aggregate_common::utils::Hashable; +use datafusion_functions_aggregate_common::aggregate::sum_distinct::DistinctSumAccumulator; use datafusion_macros::user_doc; make_udaf_expr_and_func!( @@ -388,84 +385,3 @@ impl Accumulator for SlidingSumAccumulator { true } } - -struct DistinctSumAccumulator { - values: HashSet, RandomState>, - data_type: DataType, -} - -impl std::fmt::Debug for DistinctSumAccumulator { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "DistinctSumAccumulator({})", self.data_type) - } -} - -impl DistinctSumAccumulator { - pub fn try_new(data_type: &DataType) -> Result { - Ok(Self { - values: HashSet::default(), - data_type: data_type.clone(), - }) - } -} - -impl Accumulator for DistinctSumAccumulator { - fn state(&mut self) -> Result> { - // 1. Stores aggregate state in `ScalarValue::List` - // 2. Constructs `ScalarValue::List` state from distinct numeric stored in hash set - let state_out = { - let distinct_values = self - .values - .iter() - .map(|value| { - ScalarValue::new_primitive::(Some(value.0), &self.data_type) - }) - .collect::>>()?; - - vec![ScalarValue::List(ScalarValue::new_list_nullable( - &distinct_values, - &self.data_type, - ))] - }; - Ok(state_out) - } - - fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> { - if values.is_empty() { - return Ok(()); - } - - let array = values[0].as_primitive::(); - match array.nulls().filter(|x| x.null_count() > 0) { - Some(n) => { - for idx in n.valid_indices() { - self.values.insert(Hashable(array.value(idx))); - } - } - None => array.values().iter().for_each(|x| { - self.values.insert(Hashable(*x)); - }), - } - Ok(()) - } - - fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> { - for x in states[0].as_list::().iter().flatten() { - self.update_batch(&[x])? - } - Ok(()) - } - - fn evaluate(&mut self) -> Result { - let mut acc = T::Native::usize_as(0); - for distinct_value in self.values.iter() { - acc = acc.add_wrapping(distinct_value.0) - } - let v = (!self.values.is_empty()).then_some(acc); - ScalarValue::new_primitive::(v, &self.data_type) - } - - fn size(&self) -> usize { - size_of_val(self) + self.values.capacity() * size_of::() - } -} From 3eb166b198b7c4c8fa9ff964fc839a2fcfaa6f76 Mon Sep 17 00:00:00 2001 From: YuNing Chen Date: Tue, 25 Mar 2025 17:04:05 +0800 Subject: [PATCH 02/15] feat: add avg distinct support for float64 type --- .../src/aggregate.rs | 1 + .../src/aggregate/avg_distinct.rs | 20 +++++ .../src/aggregate/avg_distinct/numeric.rs | 78 +++++++++++++++++++ datafusion/functions-aggregate/src/average.rs | 77 +++++++++--------- .../sqllogictest/test_files/aggregate.slt | 36 ++++++++- 5 files changed, 176 insertions(+), 36 deletions(-) create mode 100644 datafusion/functions-aggregate-common/src/aggregate/avg_distinct.rs create mode 100644 datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs diff --git a/datafusion/functions-aggregate-common/src/aggregate.rs b/datafusion/functions-aggregate-common/src/aggregate.rs index 56dc58570ac79..8072900a12bce 100644 --- a/datafusion/functions-aggregate-common/src/aggregate.rs +++ b/datafusion/functions-aggregate-common/src/aggregate.rs @@ -18,3 +18,4 @@ pub mod count_distinct; pub mod groups_accumulator; pub mod sum_distinct; +pub mod avg_distinct; diff --git a/datafusion/functions-aggregate-common/src/aggregate/avg_distinct.rs b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct.rs new file mode 100644 index 0000000000000..3d6889431d613 --- /dev/null +++ b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct.rs @@ -0,0 +1,20 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +mod numeric; + +pub use numeric::Float64DistinctAvgAccumulator; diff --git a/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs new file mode 100644 index 0000000000000..e4ff58a072847 --- /dev/null +++ b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs @@ -0,0 +1,78 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::fmt::Debug; + +use arrow::array::ArrayRef; +use arrow::datatypes::Float64Type; +use datafusion_common::ScalarValue; +use datafusion_expr_common::accumulator::Accumulator; + +use crate::aggregate::sum_distinct::DistinctSumAccumulator; + +/// Specialized implementation of `AVG DISTINCT` for Float64 values, leveraging +/// the existing DistinctSumAccumulator implementation. +#[derive(Debug)] +pub struct Float64DistinctAvgAccumulator { + // We use the DistinctSumAccumulator to handle the set of distinct values + sum_accumulator: DistinctSumAccumulator, +} + +impl Float64DistinctAvgAccumulator { + pub fn new() -> datafusion_common::Result { + Ok(Self { + sum_accumulator: DistinctSumAccumulator::::try_new( + &arrow::datatypes::DataType::Float64, + )?, + }) + } +} + +impl Accumulator for Float64DistinctAvgAccumulator { + fn state(&mut self) -> datafusion_common::Result> { + self.sum_accumulator.state() + } + + fn update_batch(&mut self, values: &[ArrayRef]) -> datafusion_common::Result<()> { + self.sum_accumulator.update_batch(values) + } + + fn merge_batch(&mut self, states: &[ArrayRef]) -> datafusion_common::Result<()> { + self.sum_accumulator.merge_batch(states) + } + + fn evaluate(&mut self) -> datafusion_common::Result { + // Get the sum from the DistinctSumAccumulator + let sum_result = self.sum_accumulator.evaluate()?; + + // Extract the sum value + if let ScalarValue::Float64(Some(sum)) = sum_result { + // Get the count of distinct values + let count = self.sum_accumulator.distinct_count() as f64; + // Calculate average + let avg = sum / count; + Ok(ScalarValue::Float64(Some(avg))) + } else { + // If sum is None, return None (null) + Ok(ScalarValue::Float64(None)) + } + } + + fn size(&self) -> usize { + self.sum_accumulator.size() + } +} diff --git a/datafusion/functions-aggregate/src/average.rs b/datafusion/functions-aggregate/src/average.rs index 141771b0412f2..758a775e06d7c 100644 --- a/datafusion/functions-aggregate/src/average.rs +++ b/datafusion/functions-aggregate/src/average.rs @@ -39,6 +39,7 @@ use datafusion_expr::{ ReversedUDAF, Signature, }; +use datafusion_functions_aggregate_common::aggregate::avg_distinct::Float64DistinctAvgAccumulator; use datafusion_functions_aggregate_common::aggregate::groups_accumulator::accumulate::NullState; use datafusion_functions_aggregate_common::aggregate::groups_accumulator::nulls::{ filtered_null_mask, set_nulls, @@ -113,43 +114,49 @@ impl AggregateUDFImpl for Avg { } fn accumulator(&self, acc_args: AccumulatorArgs) -> Result> { - if acc_args.is_distinct { - return exec_err!("avg(DISTINCT) aggregations are not available"); - } - use DataType::*; - let data_type = acc_args.exprs[0].data_type(acc_args.schema)?; - // instantiate specialized accumulator based for the type - match (&data_type, acc_args.return_type) { - (Float64, Float64) => Ok(Box::::default()), - ( - Decimal128(sum_precision, sum_scale), - Decimal128(target_precision, target_scale), - ) => Ok(Box::new(DecimalAvgAccumulator:: { - sum: None, - count: 0, - sum_scale: *sum_scale, - sum_precision: *sum_precision, - target_precision: *target_precision, - target_scale: *target_scale, - })), + use DataType::*; - ( - Decimal256(sum_precision, sum_scale), - Decimal256(target_precision, target_scale), - ) => Ok(Box::new(DecimalAvgAccumulator:: { - sum: None, - count: 0, - sum_scale: *sum_scale, - sum_precision: *sum_precision, - target_precision: *target_precision, - target_scale: *target_scale, - })), - _ => exec_err!( - "AvgAccumulator for ({} --> {})", - &data_type, - acc_args.return_type - ), + if acc_args.is_distinct { + // instantiate specialized accumulator based for the type + match &data_type { + // Numeric types are converted to Float64 via `coerce_avg_type` during logical plan creation + Float64 => Ok(Box::new(Float64DistinctAvgAccumulator::new()?)), + _ => exec_err!("AVG(DISTINCT) for {} not supported", data_type), + } + } else { + // instantiate specialized accumulator based for the type + match (&data_type, acc_args.return_type) { + (Float64, Float64) => Ok(Box::::default()), + ( + Decimal128(sum_precision, sum_scale), + Decimal128(target_precision, target_scale), + ) => Ok(Box::new(DecimalAvgAccumulator:: { + sum: None, + count: 0, + sum_scale: *sum_scale, + sum_precision: *sum_precision, + target_precision: *target_precision, + target_scale: *target_scale, + })), + + ( + Decimal256(sum_precision, sum_scale), + Decimal256(target_precision, target_scale), + ) => Ok(Box::new(DecimalAvgAccumulator:: { + sum: None, + count: 0, + sum_scale: *sum_scale, + sum_precision: *sum_precision, + target_precision: *target_precision, + target_scale: *target_scale, + })), + _ => exec_err!( + "AvgAccumulator for ({} --> {})", + &data_type, + acc_args.return_type + ), + } } } diff --git a/datafusion/sqllogictest/test_files/aggregate.slt b/datafusion/sqllogictest/test_files/aggregate.slt index 9d8620b100f38..93a532548626c 100644 --- a/datafusion/sqllogictest/test_files/aggregate.slt +++ b/datafusion/sqllogictest/test_files/aggregate.slt @@ -4962,8 +4962,10 @@ select avg(distinct x_dict) from value_dict; ---- 3 -query error +query RR select avg(x_dict), avg(distinct x_dict) from value_dict; +---- +2.625 3 query I select min(x_dict) from value_dict; @@ -6686,3 +6688,35 @@ SELECT a, median(b), arrow_typeof(median(b)) FROM group_median_all_nulls GROUP B ---- group0 NULL Int32 group1 NULL Int32 + +statement ok +create table t_decimal (c decimal(10, 4)) as values (100.00), (125.00), (175.00), (200.00), (200.00), (300.00), (null), (null); + +# Test avg_distinct for Decimal128 +query RT +select avg(distinct c), arrow_typeof(avg(distinct c)) from t_decimal; +---- +180 Decimal128(14, 8) + +statement ok +drop table t_decimal; + +# Test avg_distinct for Decimal256 +statement ok +create table t_decimal256 (c decimal(50, 2)) as values + (100.00), + (125.00), + (175.00), + (200.00), + (200.00), + (300.00), + (null), + (null); + +query RT +select avg(distinct c), arrow_typeof(avg(distinct c)) from t_decimal256; +---- +180 Decimal256(54, 6) + +statement ok +drop table t_decimal256; From 6ae50ab2e86e0c5b6ce064e6bd39ba9bd9552399 Mon Sep 17 00:00:00 2001 From: YuNing Chen Date: Tue, 25 Mar 2025 17:29:37 +0800 Subject: [PATCH 03/15] chore: fmt --- datafusion/functions-aggregate-common/src/aggregate.rs | 2 +- .../src/aggregate/sum_distinct/mod.rs | 2 +- .../src/aggregate/sum_distinct/numeric.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datafusion/functions-aggregate-common/src/aggregate.rs b/datafusion/functions-aggregate-common/src/aggregate.rs index 8072900a12bce..aadce907e7cc3 100644 --- a/datafusion/functions-aggregate-common/src/aggregate.rs +++ b/datafusion/functions-aggregate-common/src/aggregate.rs @@ -15,7 +15,7 @@ // specific language governing permissions and limitations // under the License. +pub mod avg_distinct; pub mod count_distinct; pub mod groups_accumulator; pub mod sum_distinct; -pub mod avg_distinct; diff --git a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs index 3a645b3d6ef48..932bfba0bf0dc 100644 --- a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs +++ b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs @@ -19,4 +19,4 @@ pub mod numeric; -pub use numeric::DistinctSumAccumulator; \ No newline at end of file +pub use numeric::DistinctSumAccumulator; diff --git a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs index 2c56e681c6838..859c82d95660b 100644 --- a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs +++ b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs @@ -23,9 +23,9 @@ use std::mem::{size_of, size_of_val}; use ahash::RandomState; use arrow::array::Array; +use arrow::array::ArrayRef; use arrow::array::ArrowNativeTypeOp; use arrow::array::ArrowPrimitiveType; -use arrow::array::ArrayRef; use arrow::array::AsArray; use arrow::datatypes::ArrowNativeType; use arrow::datatypes::DataType; @@ -120,4 +120,4 @@ impl Accumulator for DistinctSumAccumulator { fn size(&self) -> usize { size_of_val(self) + self.values.capacity() * size_of::() } -} \ No newline at end of file +} From 4a8868dae394288c589b12e5785ec27b368caaac Mon Sep 17 00:00:00 2001 From: YuNing Chen Date: Tue, 25 Mar 2025 21:52:52 +0800 Subject: [PATCH 04/15] refactor: update import for DataType in Float64DistinctAvgAccumulator and remove unused sum_distinct module --- .../src/aggregate/avg_distinct/numeric.rs | 4 ++-- .../src/aggregate/{sum_distinct/mod.rs => sum_distinct.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename datafusion/functions-aggregate-common/src/aggregate/{sum_distinct/mod.rs => sum_distinct.rs} (100%) diff --git a/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs index e4ff58a072847..c9fb14fb10691 100644 --- a/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs +++ b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs @@ -18,7 +18,7 @@ use std::fmt::Debug; use arrow::array::ArrayRef; -use arrow::datatypes::Float64Type; +use arrow::datatypes::{DataType, Float64Type}; use datafusion_common::ScalarValue; use datafusion_expr_common::accumulator::Accumulator; @@ -36,7 +36,7 @@ impl Float64DistinctAvgAccumulator { pub fn new() -> datafusion_common::Result { Ok(Self { sum_accumulator: DistinctSumAccumulator::::try_new( - &arrow::datatypes::DataType::Float64, + &DataType::Float64, )?, }) } diff --git a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct.rs similarity index 100% rename from datafusion/functions-aggregate-common/src/aggregate/sum_distinct/mod.rs rename to datafusion/functions-aggregate-common/src/aggregate/sum_distinct.rs From 1fa2db8a8051796f68f5dc747565b93e4d671007 Mon Sep 17 00:00:00 2001 From: YuNing Chen Date: Thu, 24 Apr 2025 21:40:30 +0800 Subject: [PATCH 05/15] update --- .../actions/setup-rust-runtime/action.yaml | 12 +- .github/workflows/audit.yml | 6 +- .github/workflows/extended.yml | 10 +- .github/workflows/pr_comment_commands.yml | 10 +- .github/workflows/rust.yml | 33 +- Cargo.lock | 360 +- Cargo.toml | 97 +- benchmarks/README.md | 25 +- benchmarks/bench.sh | 7 + benchmarks/queries/clickbench/README.md | 33 +- benchmarks/queries/clickbench/extended.sql | 3 +- benchmarks/queries/clickbench/queries.sql | 22 +- benchmarks/src/sort_tpch.rs | 34 +- benchmarks/src/tpch/convert.rs | 22 +- benchmarks/src/tpch/run.rs | 31 +- datafusion-cli/Cargo.toml | 2 +- datafusion-cli/src/main.rs | 82 +- datafusion-cli/tests/cli_integration.rs | 25 + ...es@explain_plan_environment_overrides.snap | 44 + .../cli_quick_test@can_see_indent_format.snap | 27 + .../cli_quick_test@default_explain_plan.snap | 31 + datafusion-examples/Cargo.toml | 1 + .../examples/advanced_parquet_index.rs | 9 +- datafusion-examples/examples/parquet_index.rs | 2 +- datafusion-examples/examples/sql_dialect.rs | 6 +- datafusion-testing | 2 +- datafusion/catalog/src/lib.rs | 2 +- datafusion/catalog/src/memory/mod.rs | 6 + datafusion/catalog/src/memory/table.rs | 296 ++ datafusion/common-runtime/src/common.rs | 86 +- datafusion/common/Cargo.toml | 4 +- datafusion/common/src/config.rs | 72 +- datafusion/common/src/dfschema.rs | 18 +- .../common/src/file_options/parquet_writer.rs | 3 + .../common/src/functional_dependencies.rs | 10 +- datafusion/common/src/scalar/mod.rs | 87 +- datafusion/common/src/stats.rs | 356 +- datafusion/common/src/utils/memory.rs | 2 +- datafusion/core/Cargo.toml | 4 +- .../core/benches/aggregate_query_sql.rs | 23 +- datafusion/core/benches/csv_load.rs | 11 +- datafusion/core/benches/data_utils/mod.rs | 60 +- datafusion/core/benches/dataframe.rs | 8 +- datafusion/core/benches/distinct_query_sql.rs | 68 +- datafusion/core/benches/filter_query_sql.rs | 9 +- datafusion/core/benches/math_query_sql.rs | 14 +- datafusion/core/benches/physical_plan.rs | 10 +- .../core/benches/sort_limit_query_sql.rs | 15 +- datafusion/core/benches/sql_planner.rs | 52 +- datafusion/core/benches/struct_query_sql.rs | 9 +- datafusion/core/benches/topk_aggregate.rs | 111 +- datafusion/core/benches/window_query_sql.rs | 16 +- .../core/src/bin/print_runtime_config_docs.rs | 23 + datafusion/core/src/dataframe/mod.rs | 79 + .../core/src/datasource/file_format/arrow.rs | 5 +- .../core/src/datasource/file_format/avro.rs | 11 +- .../core/src/datasource/file_format/csv.rs | 21 +- .../core/src/datasource/file_format/json.rs | 2 +- .../core/src/datasource/file_format/mod.rs | 17 +- .../src/datasource/file_format/parquet.rs | 23 +- .../core/src/datasource/listing/table.rs | 141 +- .../datasource/{memory.rs => memory_test.rs} | 371 +- datafusion/core/src/datasource/mod.rs | 7 +- .../datasource/physical_plan/arrow_file.rs | 21 +- .../core/src/datasource/physical_plan/csv.rs | 2 +- .../core/src/datasource/physical_plan/json.rs | 2 +- .../src/datasource/physical_plan/parquet.rs | 375 +- datafusion/core/src/datasource/statistics.rs | 219 - datafusion/core/src/execution/context/mod.rs | 73 +- .../core/src/execution/session_state.rs | 20 +- datafusion/core/src/lib.rs | 23 +- datafusion/core/src/physical_planner.rs | 87 +- datafusion/core/src/test/object_store.rs | 4 +- datafusion/core/src/test_util/parquet.rs | 2 +- datafusion/core/tests/core_integration.rs | 3 + .../tests/dataframe/dataframe_functions.rs | 87 +- datafusion/core/tests/dataframe/mod.rs | 92 + .../core/tests/execution/logical_plan.rs | 44 +- .../core/tests/expr_api/simplification.rs | 4 +- .../core/tests/fuzz_cases/aggregate_fuzz.rs | 256 +- .../aggregation_fuzzer/context_generator.rs | 2 +- .../aggregation_fuzzer/data_generator.rs | 590 +-- .../fuzz_cases/aggregation_fuzzer/fuzzer.rs | 10 +- .../fuzz_cases/aggregation_fuzzer/mod.rs | 3 +- datafusion/core/tests/fuzz_cases/mod.rs | 4 + .../fuzz_cases/record_batch_generator.rs | 644 +++ .../core/tests/fuzz_cases/sort_query_fuzz.rs | 625 +++ .../memory_limit_validation/utils.rs | 12 +- datafusion/core/tests/memory_limit/mod.rs | 133 +- .../core/tests/parquet/custom_reader.rs | 9 +- datafusion/core/tests/parquet/mod.rs | 12 +- datafusion/core/tests/parquet/page_pruning.rs | 2 +- .../enforce_distribution.rs | 45 + .../physical_optimizer/enforce_sorting.rs | 37 +- .../core/tests/physical_optimizer/mod.rs | 1 + .../physical_optimizer/push_down_filter.rs | 542 ++ .../replace_with_order_preserving_variants.rs | 57 +- datafusion/core/tests/sql/mod.rs | 1 + datafusion/core/tests/sql/path_partition.rs | 18 +- datafusion/core/tests/sql/runtime_config.rs | 166 + datafusion/core/tests/sql/sql_api.rs | 17 + .../core/tests/tracing/asserting_tracer.rs | 142 + datafusion/core/tests/tracing/mod.rs | 108 + .../tests/tracing/traceable_object_store.rs | 125 + .../user_defined_window_functions.rs | 2 +- datafusion/datasource-csv/src/source.rs | 1 + datafusion/datasource-json/src/file_format.rs | 1 + datafusion/datasource-json/src/source.rs | 1 + .../datasource-parquet/src/file_format.rs | 90 +- datafusion/datasource-parquet/src/opener.rs | 231 +- .../datasource-parquet/src/page_filter.rs | 22 +- datafusion/datasource-parquet/src/reader.rs | 27 +- .../datasource-parquet/src/row_filter.rs | 4 +- .../src/row_group_filter.rs | 7 +- datafusion/datasource-parquet/src/source.rs | 81 +- datafusion/datasource/Cargo.toml | 7 +- .../benches/split_groups_by_statistics.rs | 108 + datafusion/datasource/src/file.rs | 22 +- datafusion/datasource/src/file_groups.rs | 23 +- datafusion/datasource/src/file_scan_config.rs | 395 +- datafusion/datasource/src/file_sink_config.rs | 1 + datafusion/datasource/src/file_stream.rs | 2 +- datafusion/datasource/src/memory.rs | 92 +- datafusion/datasource/src/mod.rs | 152 +- datafusion/datasource/src/schema_adapter.rs | 2 +- datafusion/datasource/src/source.rs | 79 +- datafusion/datasource/src/statistics.rs | 214 + datafusion/datasource/src/url.rs | 6 +- datafusion/datasource/src/write/demux.rs | 10 +- datafusion/execution/Cargo.toml | 2 +- datafusion/execution/src/config.rs | 8 +- datafusion/execution/src/disk_manager.rs | 102 +- datafusion/execution/src/memory_pool/mod.rs | 87 +- datafusion/execution/src/memory_pool/pool.rs | 172 +- datafusion/execution/src/runtime_env.rs | 54 +- .../expr-common/src/interval_arithmetic.rs | 2 +- datafusion/expr-common/src/signature.rs | 12 +- .../src/type_coercion/aggregates.rs | 3 + .../expr-common/src/type_coercion/binary.rs | 69 + datafusion/expr/src/logical_plan/builder.rs | 121 +- .../expr/src/logical_plan/invariants.rs | 6 +- datafusion/expr/src/logical_plan/plan.rs | 416 ++ .../expr/src/type_coercion/functions.rs | 8 +- datafusion/expr/src/udaf.rs | 38 +- datafusion/ffi/Cargo.toml | 1 + datafusion/ffi/src/lib.rs | 1 + datafusion/ffi/src/table_provider.rs | 55 +- datafusion/ffi/src/tests/mod.rs | 11 +- datafusion/ffi/src/tests/udf_udaf_udwf.rs | 21 +- datafusion/ffi/src/tests/utils.rs | 87 + datafusion/ffi/src/{udf.rs => udf/mod.rs} | 47 +- datafusion/ffi/src/udf/return_info.rs | 53 + datafusion/ffi/src/udf/return_type_args.rs | 142 + datafusion/ffi/src/udtf.rs | 321 ++ datafusion/ffi/tests/ffi_integration.rs | 116 +- datafusion/ffi/tests/ffi_udf.rs | 104 + datafusion/ffi/tests/ffi_udtf.rs | 64 + .../functions-aggregate/benches/array_agg.rs | 28 +- .../functions-aggregate/src/approx_median.rs | 2 +- .../src/approx_percentile_cont.rs | 54 +- .../src/approx_percentile_cont_with_weight.rs | 22 +- .../functions-aggregate/src/array_agg.rs | 4 +- datafusion/functions-aggregate/src/average.rs | 114 +- .../functions-aggregate/src/first_last.rs | 253 +- .../functions-aggregate/src/string_agg.rs | 397 +- datafusion/functions-nested/src/array_has.rs | 4 +- datafusion/functions-nested/src/flatten.rs | 153 +- datafusion/functions-nested/src/sort.rs | 20 +- .../functions-table/src/generate_series.rs | 9 +- .../functions-window-common/src/expr.rs | 4 +- .../functions-window-common/src/field.rs | 4 +- .../functions-window-common/src/partition.rs | 8 +- datafusion/functions-window/src/cume_dist.rs | 21 +- datafusion/functions-window/src/macros.rs | 24 +- datafusion/functions-window/src/nth_value.rs | 43 +- datafusion/functions-window/src/rank.rs | 6 +- datafusion/functions/Cargo.toml | 2 +- datafusion/functions/benches/chr.rs | 10 +- datafusion/functions/benches/regx.rs | 8 +- .../functions/src/datetime/to_timestamp.rs | 31 +- datafusion/optimizer/Cargo.toml | 6 + .../benches/projection_unnecessary.rs | 79 + datafusion/optimizer/src/decorrelate.rs | 5 +- .../optimizer/src/optimize_projections/mod.rs | 35 +- datafusion/optimizer/src/optimizer.rs | 7 +- .../optimizer/src/scalar_subquery_to_join.rs | 216 +- .../simplify_expressions/expr_simplifier.rs | 251 +- .../simplify_expressions/simplify_exprs.rs | 9 +- .../src/simplify_expressions/unwrap_cast.rs | 29 + datafusion/optimizer/src/utils.rs | 72 +- .../optimizer/tests/optimizer_integration.rs | 380 +- .../physical-expr-common/src/physical_expr.rs | 77 + datafusion/physical-expr/Cargo.toml | 5 + datafusion/physical-expr/benches/binary_op.rs | 312 ++ datafusion/physical-expr/src/aggregate.rs | 88 + .../src/equivalence/projection.rs | 4 +- .../src/equivalence/properties/mod.rs | 61 +- .../physical-expr/src/expressions/binary.rs | 506 +- .../src/expressions/dynamic_filters.rs | 474 ++ .../physical-expr/src/expressions/mod.rs | 1 + datafusion/physical-expr/src/lib.rs | 2 +- datafusion/physical-expr/src/planner.rs | 2 +- datafusion/physical-expr/src/utils/mod.rs | 25 + .../src/aggregate_statistics.rs | 1 + .../src/enforce_distribution.rs | 11 +- .../src/enforce_sorting/mod.rs | 6 +- .../replace_with_order_preserving_variants.rs | 17 +- datafusion/physical-optimizer/src/lib.rs | 1 + .../physical-optimizer/src/limit_pushdown.rs | 11 +- .../physical-optimizer/src/optimizer.rs | 5 + datafusion/physical-optimizer/src/pruning.rs | 14 +- .../src/push_down_filter.rs | 535 ++ datafusion/physical-plan/Cargo.toml | 5 + datafusion/physical-plan/benches/spill_io.rs | 123 + .../group_values/multi_group_by/primitive.rs | 2 +- .../src/aggregates/group_values/row.rs | 1 + .../src/aggregates/order/full.rs | 6 +- .../src/aggregates/order/partial.rs | 2 +- .../physical-plan/src/aggregates/row_hash.rs | 14 +- datafusion/physical-plan/src/coalesce/mod.rs | 12 +- .../physical-plan/src/coalesce_batches.rs | 15 + datafusion/physical-plan/src/display.rs | 2 +- .../physical-plan/src/execution_plan.rs | 42 +- datafusion/physical-plan/src/filter.rs | 56 +- .../physical-plan/src/filter_pushdown.rs | 95 + .../physical-plan/src/joins/cross_join.rs | 33 +- .../physical-plan/src/joins/hash_join.rs | 56 +- datafusion/physical-plan/src/joins/mod.rs | 6 + .../src/joins/nested_loop_join.rs | 32 +- .../src/joins/sort_merge_join.rs | 74 +- .../physical-plan/src/joins/test_utils.rs | 8 +- datafusion/physical-plan/src/joins/utils.rs | 23 +- datafusion/physical-plan/src/lib.rs | 2 + datafusion/physical-plan/src/projection.rs | 7 +- .../physical-plan/src/repartition/mod.rs | 26 +- datafusion/physical-plan/src/sorts/cursor.rs | 2 +- datafusion/physical-plan/src/sorts/merge.rs | 16 +- datafusion/physical-plan/src/sorts/sort.rs | 407 +- .../src/spill/in_progress_spill_file.rs | 12 +- datafusion/physical-plan/src/spill/mod.rs | 195 +- .../physical-plan/src/spill/spill_manager.rs | 26 +- datafusion/physical-plan/src/topk/mod.rs | 325 +- .../proto/datafusion_common.proto | 4 + datafusion/proto-common/src/common.rs | 1 + datafusion/proto-common/src/from_proto/mod.rs | 3 + .../proto-common/src/generated/pbjson.rs | 22 + .../proto-common/src/generated/prost.rs | 7 + datafusion/proto-common/src/to_proto/mod.rs | 1 + datafusion/proto/Cargo.toml | 1 - datafusion/proto/proto/datafusion.proto | 4 +- .../src/generated/datafusion_proto_common.rs | 7 + datafusion/proto/src/generated/prost.rs | 4 +- .../proto/src/logical_plan/file_formats.rs | 6 + datafusion/proto/src/logical_plan/mod.rs | 81 +- .../proto/src/physical_plan/from_proto.rs | 16 +- datafusion/proto/src/physical_plan/mod.rs | 4191 ++++++++------- .../proto/src/physical_plan/to_proto.rs | 12 +- .../tests/cases/roundtrip_logical_plan.rs | 39 +- .../tests/cases/roundtrip_physical_plan.rs | 46 +- datafusion/proto/tests/cases/serialize.rs | 2 +- datafusion/sql/Cargo.toml | 1 + datafusion/sql/src/expr/function.rs | 73 +- datafusion/sql/src/expr/value.rs | 2 +- datafusion/sql/src/parser.rs | 183 +- datafusion/sql/src/planner.rs | 2 +- datafusion/sql/src/select.rs | 106 +- datafusion/sql/src/statement.rs | 12 +- datafusion/sql/src/unparser/ast.rs | 15 + datafusion/sql/src/unparser/dialect.rs | 7 +- datafusion/sql/src/unparser/expr.rs | 126 +- datafusion/sql/src/unparser/mod.rs | 4 +- datafusion/sql/src/unparser/plan.rs | 68 +- datafusion/sql/src/unparser/utils.rs | 71 +- datafusion/sql/tests/cases/diagnostic.rs | 115 +- datafusion/sql/tests/cases/plan_to_sql.rs | 1672 +++--- datafusion/sql/tests/sql_integration.rs | 4609 +++++++++++------ datafusion/sqllogictest/Cargo.toml | 4 +- datafusion/sqllogictest/bin/sqllogictests.rs | 7 +- .../sqllogictest/test_files/aggregate.slt | 269 +- datafusion/sqllogictest/test_files/array.slt | 71 +- datafusion/sqllogictest/test_files/binary.slt | 30 +- .../sqllogictest/test_files/clickbench.slt | 28 +- datafusion/sqllogictest/test_files/copy.slt | 2 +- .../test_files/create_external_table.slt | 2 +- datafusion/sqllogictest/test_files/cte.slt | 2 +- datafusion/sqllogictest/test_files/dates.slt | 2 +- .../sqllogictest/test_files/dictionary.slt | 7 + .../sqllogictest/test_files/explain.slt | 3 + .../sqllogictest/test_files/explain_tree.slt | 154 +- .../test_files/expr/date_part.slt | 6 +- .../sqllogictest/test_files/functions.slt | 4 +- .../sqllogictest/test_files/group_by.slt | 27 +- .../test_files/information_schema.slt | 46 +- datafusion/sqllogictest/test_files/joins.slt | 48 + .../sqllogictest/test_files/parquet.slt | 18 + .../test_files/parquet_sorted_statistics.slt | 17 +- datafusion/sqllogictest/test_files/regexp.slt | 898 ---- .../sqllogictest/test_files/regexp/README.md | 59 + .../test_files/regexp/init_data.slt.part | 31 + .../test_files/regexp/regexp_count.slt | 344 ++ .../test_files/regexp/regexp_like.slt | 280 + .../test_files/regexp/regexp_match.slt | 201 + .../test_files/regexp/regexp_replace.slt | 129 + .../sqllogictest/test_files/simplify_expr.slt | 42 + .../sqllogictest/test_files/subquery.slt | 44 +- .../sqllogictest/test_files/timestamps.slt | 42 +- datafusion/sqllogictest/test_files/topk.slt | 162 + datafusion/sqllogictest/test_files/window.slt | 49 +- .../substrait/src/logical_plan/consumer.rs | 3 +- .../substrait/src/physical_plan/producer.rs | 2 +- .../tests/cases/consumer_integration.rs | 27 + .../tests/cases/roundtrip_logical_plan.rs | 375 +- .../testdata/test_plans/multiple_joins.json | 536 ++ datafusion/wasmtest/README.md | 2 - .../datafusion-wasm-app/package-lock.json | 13 +- datafusion/wasmtest/src/lib.rs | 28 +- datafusion/wasmtest/webdriver.json | 15 + dev/changelog/47.0.0.md | 506 ++ dev/update_runtime_config_docs.sh | 76 + docs/source/index.rst | 1 + docs/source/library-user-guide/profiling.md | 43 +- .../library-user-guide/samply_profiler.png | Bin 0 -> 605887 bytes docs/source/library-user-guide/upgrading.md | 120 +- docs/source/user-guide/cli/datasources.md | 37 +- docs/source/user-guide/cli/usage.md | 3 + .../user-guide/concepts-readings-events.md | 6 + docs/source/user-guide/configs.md | 3 +- docs/source/user-guide/introduction.md | 2 + docs/source/user-guide/runtime_configs.md | 40 + .../user-guide/sql/aggregate_functions.md | 50 +- docs/source/user-guide/sql/data_types.md | 28 +- docs/source/user-guide/sql/ddl.md | 2 +- docs/source/user-guide/sql/dml.md | 2 +- docs/source/user-guide/sql/explain.md | 70 +- docs/source/user-guide/sql/format_options.md | 180 + docs/source/user-guide/sql/index.rst | 2 +- .../source/user-guide/sql/window_functions.md | 59 +- docs/source/user-guide/sql/write_options.md | 127 - parquet-testing | 2 +- rust-toolchain.toml | 2 +- test-utils/src/lib.rs | 5 +- 341 files changed, 25056 insertions(+), 9255 deletions(-) create mode 100644 datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap create mode 100644 datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap create mode 100644 datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap create mode 100644 datafusion/catalog/src/memory/table.rs create mode 100644 datafusion/core/src/bin/print_runtime_config_docs.rs rename datafusion/core/src/datasource/{memory.rs => memory_test.rs} (58%) delete mode 100644 datafusion/core/src/datasource/statistics.rs create mode 100644 datafusion/core/tests/fuzz_cases/record_batch_generator.rs create mode 100644 datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs create mode 100644 datafusion/core/tests/physical_optimizer/push_down_filter.rs create mode 100644 datafusion/core/tests/sql/runtime_config.rs create mode 100644 datafusion/core/tests/tracing/asserting_tracer.rs create mode 100644 datafusion/core/tests/tracing/mod.rs create mode 100644 datafusion/core/tests/tracing/traceable_object_store.rs create mode 100644 datafusion/datasource/benches/split_groups_by_statistics.rs create mode 100644 datafusion/ffi/src/tests/utils.rs rename datafusion/ffi/src/{udf.rs => udf/mod.rs} (87%) create mode 100644 datafusion/ffi/src/udf/return_info.rs create mode 100644 datafusion/ffi/src/udf/return_type_args.rs create mode 100644 datafusion/ffi/src/udtf.rs create mode 100644 datafusion/ffi/tests/ffi_udf.rs create mode 100644 datafusion/ffi/tests/ffi_udtf.rs create mode 100644 datafusion/optimizer/benches/projection_unnecessary.rs create mode 100644 datafusion/physical-expr/benches/binary_op.rs create mode 100644 datafusion/physical-expr/src/expressions/dynamic_filters.rs create mode 100644 datafusion/physical-optimizer/src/push_down_filter.rs create mode 100644 datafusion/physical-plan/benches/spill_io.rs create mode 100644 datafusion/physical-plan/src/filter_pushdown.rs delete mode 100644 datafusion/sqllogictest/test_files/regexp.slt create mode 100644 datafusion/sqllogictest/test_files/regexp/README.md create mode 100644 datafusion/sqllogictest/test_files/regexp/init_data.slt.part create mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_count.slt create mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_like.slt create mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_match.slt create mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_replace.slt create mode 100644 datafusion/substrait/tests/testdata/test_plans/multiple_joins.json create mode 100644 datafusion/wasmtest/webdriver.json create mode 100644 dev/changelog/47.0.0.md create mode 100755 dev/update_runtime_config_docs.sh create mode 100644 docs/source/library-user-guide/samply_profiler.png create mode 100644 docs/source/user-guide/runtime_configs.md create mode 100644 docs/source/user-guide/sql/format_options.md delete mode 100644 docs/source/user-guide/sql/write_options.md diff --git a/.github/actions/setup-rust-runtime/action.yaml b/.github/actions/setup-rust-runtime/action.yaml index cd18be9890315..b6fb2c898bf2f 100644 --- a/.github/actions/setup-rust-runtime/action.yaml +++ b/.github/actions/setup-rust-runtime/action.yaml @@ -20,8 +20,10 @@ description: 'Setup Rust Runtime Environment' runs: using: "composite" steps: - - name: Run sccache-cache - uses: mozilla-actions/sccache-action@v0.0.4 + # https://github.com/apache/datafusion/issues/15535 + # disabled because neither version nor git hash works with apache github policy + #- name: Run sccache-cache + # uses: mozilla-actions/sccache-action@65101d47ea8028ed0c98a1cdea8dd9182e9b5133 # v0.0.8 - name: Configure runtime env shell: bash # do not produce debug symbols to keep memory usage down @@ -30,9 +32,11 @@ runs: # # Set debuginfo=line-tables-only as debuginfo=0 causes immensely slow build # See for more details: https://github.com/rust-lang/rust/issues/119560 + # + # readd the following to the run below once sccache-cache is re-enabled + # echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + # echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV run: | - echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV - echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV echo "RUST_BACKTRACE=1" >> $GITHUB_ENV echo "RUSTFLAGS=-C debuginfo=line-tables-only -C incremental=false" >> $GITHUB_ENV diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 0d65b1aa809ff..491fa27c2a56a 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -26,6 +26,8 @@ on: paths: - "**/Cargo.toml" - "**/Cargo.lock" + branches: + - main pull_request: paths: @@ -40,4 +42,6 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit - name: Run audit check - run: cargo audit + # Ignored until https://github.com/apache/datafusion/issues/15571 + # ignored py03 warning until arrow 55 upgrade + run: cargo audit --ignore RUSTSEC-2024-0370 --ignore RUSTSEC-2025-0020 diff --git a/.github/workflows/extended.yml b/.github/workflows/extended.yml index a5d68ff079b56..d80fdb75d932d 100644 --- a/.github/workflows/extended.yml +++ b/.github/workflows/extended.yml @@ -47,7 +47,7 @@ on: permissions: contents: read checks: write - + jobs: # Check crate compiles and base cargo check passes @@ -58,6 +58,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Install Rust @@ -81,6 +82,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Free Disk Space (Ubuntu) @@ -114,6 +116,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Setup Rust toolchain @@ -134,6 +137,7 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Setup Rust toolchain @@ -161,14 +165,14 @@ jobs: echo "workflow_status=completed" >> $GITHUB_OUTPUT echo "conclusion=success" >> $GITHUB_OUTPUT fi - + - name: Update check run uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const workflowRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - + await github.rest.checks.update({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/pr_comment_commands.yml b/.github/workflows/pr_comment_commands.yml index a20a5b15965dd..6aa6caaf34d02 100644 --- a/.github/workflows/pr_comment_commands.yml +++ b/.github/workflows/pr_comment_commands.yml @@ -44,12 +44,12 @@ jobs: repo: context.repo.repo, pull_number: context.payload.issue.number }); - + // Extract the branch name const branchName = pullRequest.head.ref; const headSha = pullRequest.head.sha; const workflowRunsUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions?query=workflow%3A%22Datafusion+extended+tests%22+branch%3A${branchName}`; - + // Create a check run that links to the Actions tab so the run will be visible in GitHub UI const check = await github.rest.checks.create({ owner: context.repo.owner, @@ -69,7 +69,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'extended.yml', - ref: branchName, + ref: 'main', inputs: { pr_number: context.payload.issue.number.toString(), check_run_id: check.data.id.toString(), @@ -77,7 +77,7 @@ jobs: } }); - - name: Add reaction to comment + - name: Add reaction to comment uses: actions/github-script@v7 with: script: | @@ -86,4 +86,4 @@ jobs: repo: context.repo.repo, comment_id: context.payload.comment.id, content: 'rocket' - }); \ No newline at end of file + }); diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1e6cd97acea33..f3b7e19a4970b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -384,25 +384,25 @@ jobs: run: ci/scripts/rust_docs.sh linux-wasm-pack: - name: build with wasm-pack - runs-on: ubuntu-latest - container: - image: amd64/rust + name: build and run with wasm-pack + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - - name: Setup Rust toolchain - uses: ./.github/actions/setup-builder - with: - rust-version: stable + - name: Setup for wasm32 + run: | + rustup target add wasm32-unknown-unknown - name: Install dependencies run: | - apt-get update -qq - apt-get install -y -qq clang - - name: Install wasm-pack - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh - - name: Build with wasm-pack + sudo apt-get update -qq + sudo apt-get install -y -qq clang + - name: Setup wasm-pack + run: | + cargo install wasm-pack + - name: Run tests with headless mode working-directory: ./datafusion/wasmtest - run: wasm-pack build --dev + run: | + wasm-pack test --headless --firefox + wasm-pack test --headless --chrome --chromedriver $CHROMEWEBDRIVER/chromedriver # verify that the benchmark queries return the correct results verify-benchmark-results: @@ -693,6 +693,11 @@ jobs: # If you encounter an error, run './dev/update_function_docs.sh' and commit ./dev/update_function_docs.sh git diff --exit-code + - name: Check if runtime_configs.md has been modified + run: | + # If you encounter an error, run './dev/update_runtime_config_docs.sh' and commit + ./dev/update_runtime_config_docs.sh + git diff --exit-code # Verify MSRV for the crates which are directly used by other projects: # - datafusion diff --git a/Cargo.lock b/Cargo.lock index 8aba95bdcca4a..299ea0dc4c6fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,9 +246,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc208515aa0151028e464cc94a692156e945ce5126abd3537bb7fd6ba2143ed1" +checksum = "3095aaf545942ff5abd46654534f15b03a90fba78299d661e045e5d587222f0d" dependencies = [ "arrow-arith", "arrow-array", @@ -265,14 +265,14 @@ dependencies = [ "arrow-string", "half", "pyo3", - "rand 0.8.5", + "rand 0.9.0", ] [[package]] name = "arrow-arith" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e07e726e2b3f7816a85c6a45b6ec118eeeabf0b2a8c208122ad949437181f49a" +checksum = "00752064ff47cee746e816ddb8450520c3a52cbad1e256f6fa861a35f86c45e7" dependencies = [ "arrow-array", "arrow-buffer", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2262eba4f16c78496adfd559a29fe4b24df6088efc9985a873d58e92be022d5" +checksum = "cebfe926794fbc1f49ddd0cdaf898956ca9f6e79541efce62dabccfd81380472" dependencies = [ "ahash 0.8.11", "arrow-buffer", @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e899dade2c3b7f5642eb8366cfd898958bcca099cde6dfea543c7e8d3ad88d4" +checksum = "0303c7ec4cf1a2c60310fc4d6bbc3350cd051a17bf9e9c0a8e47b4db79277824" dependencies = [ "bytes", "half", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4103d88c5b441525ed4ac23153be7458494c2b0c9a11115848fdb9b81f6f886a" +checksum = "335f769c5a218ea823d3760a743feba1ef7857cba114c01399a891c2fff34285" dependencies = [ "arrow-array", "arrow-buffer", @@ -333,9 +333,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d3cb0914486a3cae19a5cad2598e44e225d53157926d0ada03c20521191a65" +checksum = "510db7dfbb4d5761826516cc611d97b3a68835d0ece95b034a052601109c0b1b" dependencies = [ "arrow-array", "arrow-cast", @@ -349,9 +349,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a329fb064477c9ec5f0870d2f5130966f91055c7c5bce2b3a084f116bc28c3b" +checksum = "e8affacf3351a24039ea24adab06f316ded523b6f8c3dbe28fbac5f18743451b" dependencies = [ "arrow-buffer", "arrow-schema", @@ -361,9 +361,9 @@ dependencies = [ [[package]] name = "arrow-flight" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7408f2bf3b978eddda272c7699f439760ebc4ac70feca25fefa82c5b8ce808d" +checksum = "e2e0fad280f41a918d53ba48288a246ff04202d463b3b380fbc0edecdcb52cfd" dependencies = [ "arrow-arith", "arrow-array", @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddecdeab02491b1ce88885986e25002a3da34dd349f682c7cfe67bab7cc17b86" +checksum = "69880a9e6934d9cba2b8630dd08a3463a91db8693b16b499d54026b6137af284" dependencies = [ "arrow-array", "arrow-buffer", @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b9340013413eb84868682ace00a1098c81a5ebc96d279f7ebf9a4cac3c0fd" +checksum = "d8dafd17a05449e31e0114d740530e0ada7379d7cb9c338fd65b09a8130960b0" dependencies = [ "arrow-array", "arrow-buffer", @@ -413,18 +413,20 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.8.0", + "indexmap 2.9.0", "lexical-core", + "memchr", "num", "serde", "serde_json", + "simdutf8", ] [[package]] name = "arrow-ord" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f841bfcc1997ef6ac48ee0305c4dfceb1f7c786fe31e67c1186edf775e1f1160" +checksum = "895644523af4e17502d42c3cb6b27cb820f0cb77954c22d75c23a85247c849e1" dependencies = [ "arrow-array", "arrow-buffer", @@ -435,9 +437,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eeb55b0a0a83851aa01f2ca5ee5648f607e8506ba6802577afdda9d75cdedcd" +checksum = "9be8a2a4e5e7d9c822b2b8095ecd77010576d824f654d347817640acfc97d229" dependencies = [ "arrow-array", "arrow-buffer", @@ -448,9 +450,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85934a9d0261e0fa5d4e2a5295107d743b543a6e0484a835d4b8db2da15306f9" +checksum = "7450c76ab7c5a6805be3440dc2e2096010da58f7cab301fdc996a4ee3ee74e49" dependencies = [ "bitflags 2.8.0", "serde", @@ -458,9 +460,9 @@ dependencies = [ [[package]] name = "arrow-select" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e2932aece2d0c869dd2125feb9bd1709ef5c445daa3838ac4112dcfa0fda52c" +checksum = "aa5f5a93c75f46ef48e4001535e7b6c922eeb0aa20b73cf58d09e13d057490d8" dependencies = [ "ahash 0.8.11", "arrow-array", @@ -472,9 +474,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "912e38bd6a7a7714c1d9b61df80315685553b7455e8a6045c27531d8ecd5b458" +checksum = "6e7005d858d84b56428ba2a98a107fe88c0132c61793cf6b8232a1f9bfc0452b" dependencies = [ "arrow-array", "arrow-buffer", @@ -1047,9 +1049,9 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" +checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" dependencies = [ "autocfg", "libm", @@ -1117,9 +1119,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17679a8d69b6d7fd9cd9801a536cec9fa5e5970b69f9d4747f70b39b031f5e7" +checksum = "389a099b34312839e16420d499a9cad9650541715937ffbdd40d36f49e77eeb3" dependencies = [ "arrayref", "arrayvec", @@ -1361,9 +1363,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1371,7 +1373,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1446,9 +1448,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.34" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" +checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" dependencies = [ "clap_builder", "clap_derive", @@ -1456,9 +1458,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.34" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" +checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" dependencies = [ "anstream", "anstyle", @@ -1640,7 +1642,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.34", + "clap 4.5.36", "criterion-plot", "futures", "is-terminal", @@ -1671,9 +1673,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -1807,7 +1809,7 @@ dependencies = [ [[package]] name = "datafusion" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "arrow-ipc", @@ -1877,7 +1879,7 @@ dependencies = [ [[package]] name = "datafusion-benchmarks" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "datafusion", @@ -1901,7 +1903,7 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", @@ -1925,7 +1927,7 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", @@ -1947,14 +1949,14 @@ dependencies = [ [[package]] name = "datafusion-cli" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "assert_cmd", "async-trait", "aws-config", "aws-credential-types", - "clap 4.5.34", + "clap 4.5.36", "ctor", "datafusion", "dirs", @@ -1976,7 +1978,7 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "46.0.1" +version = "47.0.0" dependencies = [ "ahash 0.8.11", "apache-avro", @@ -1986,7 +1988,7 @@ dependencies = [ "chrono", "half", "hashbrown 0.14.5", - "indexmap 2.8.0", + "indexmap 2.9.0", "insta", "libc", "log", @@ -2003,7 +2005,7 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "46.0.1" +version = "47.0.0" dependencies = [ "futures", "log", @@ -2012,7 +2014,7 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-compression", @@ -2020,6 +2022,7 @@ dependencies = [ "bytes", "bzip2 0.5.2", "chrono", + "criterion", "datafusion-common", "datafusion-common-runtime", "datafusion-execution", @@ -2046,7 +2049,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-avro" -version = "46.0.1" +version = "47.0.0" dependencies = [ "apache-avro", "arrow", @@ -2071,7 +2074,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", @@ -2094,7 +2097,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", @@ -2117,7 +2120,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-parquet" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", @@ -2147,11 +2150,11 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "46.0.1" +version = "47.0.0" [[package]] name = "datafusion-examples" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "arrow-flight", @@ -2160,6 +2163,7 @@ dependencies = [ "bytes", "dashmap", "datafusion", + "datafusion-ffi", "datafusion-proto", "env_logger", "futures", @@ -2180,7 +2184,7 @@ dependencies = [ [[package]] name = "datafusion-execution" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "chrono", @@ -2198,7 +2202,7 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "chrono", @@ -2210,7 +2214,7 @@ dependencies = [ "datafusion-functions-window-common", "datafusion-physical-expr-common", "env_logger", - "indexmap 2.8.0", + "indexmap 2.9.0", "paste", "recursive", "serde_json", @@ -2219,21 +2223,22 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "datafusion-common", - "indexmap 2.8.0", + "indexmap 2.9.0", "itertools 0.14.0", "paste", ] [[package]] name = "datafusion-ffi" -version = "46.0.1" +version = "47.0.0" dependencies = [ "abi_stable", "arrow", + "arrow-schema", "async-ffi", "async-trait", "datafusion", @@ -2248,7 +2253,7 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "arrow-buffer", @@ -2277,7 +2282,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "46.0.1" +version = "47.0.0" dependencies = [ "ahash 0.8.11", "arrow", @@ -2298,7 +2303,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" -version = "46.0.1" +version = "47.0.0" dependencies = [ "ahash 0.8.11", "arrow", @@ -2311,7 +2316,7 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "arrow-ord", @@ -2332,7 +2337,7 @@ dependencies = [ [[package]] name = "datafusion-functions-table" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", @@ -2346,7 +2351,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "datafusion-common", @@ -2362,7 +2367,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "46.0.1" +version = "47.0.0" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -2370,7 +2375,7 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "46.0.1" +version = "47.0.0" dependencies = [ "datafusion-expr", "quote", @@ -2379,11 +2384,12 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", "chrono", + "criterion", "ctor", "datafusion-common", "datafusion-expr", @@ -2393,7 +2399,8 @@ dependencies = [ "datafusion-physical-expr", "datafusion-sql", "env_logger", - "indexmap 2.8.0", + "indexmap 2.9.0", + "insta", "itertools 0.14.0", "log", "recursive", @@ -2403,7 +2410,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "46.0.1" +version = "47.0.0" dependencies = [ "ahash 0.8.11", "arrow", @@ -2416,7 +2423,8 @@ dependencies = [ "datafusion-physical-expr-common", "half", "hashbrown 0.14.5", - "indexmap 2.8.0", + "indexmap 2.9.0", + "insta", "itertools 0.14.0", "log", "paste", @@ -2427,7 +2435,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "46.0.1" +version = "47.0.0" dependencies = [ "ahash 0.8.11", "arrow", @@ -2439,7 +2447,7 @@ dependencies = [ [[package]] name = "datafusion-physical-optimizer" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "datafusion-common", @@ -2458,7 +2466,7 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "46.0.1" +version = "47.0.0" dependencies = [ "ahash 0.8.11", "arrow", @@ -2479,7 +2487,7 @@ dependencies = [ "futures", "half", "hashbrown 0.14.5", - "indexmap 2.8.0", + "indexmap 2.9.0", "insta", "itertools 0.14.0", "log", @@ -2488,12 +2496,13 @@ dependencies = [ "rand 0.8.5", "rstest", "rstest_reuse", + "tempfile", "tokio", ] [[package]] name = "datafusion-proto" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "chrono", @@ -2516,7 +2525,7 @@ dependencies = [ [[package]] name = "datafusion-proto-common" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "datafusion-common", @@ -2529,7 +2538,7 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", @@ -2551,7 +2560,7 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "bigdecimal", @@ -2563,7 +2572,8 @@ dependencies = [ "datafusion-functions-nested", "datafusion-functions-window", "env_logger", - "indexmap 2.8.0", + "indexmap 2.9.0", + "insta", "log", "paste", "recursive", @@ -2574,14 +2584,14 @@ dependencies = [ [[package]] name = "datafusion-sqllogictest" -version = "46.0.1" +version = "47.0.0" dependencies = [ "arrow", "async-trait", "bigdecimal", "bytes", "chrono", - "clap 4.5.34", + "clap 4.5.36", "datafusion", "env_logger", "futures", @@ -2605,7 +2615,7 @@ dependencies = [ [[package]] name = "datafusion-substrait" -version = "46.0.1" +version = "47.0.0" dependencies = [ "async-recursion", "async-trait", @@ -2625,7 +2635,7 @@ dependencies = [ [[package]] name = "datafusion-wasmtest" -version = "46.0.1" +version = "47.0.0" dependencies = [ "chrono", "console_error_panic_hook", @@ -2795,9 +2805,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.7" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -2918,21 +2928,22 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flatbuffers" -version = "24.12.23" +version = "25.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" +checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.8.0", "rustc_version", ] [[package]] name = "flate2" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -3179,7 +3190,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.2.0", - "indexmap 2.8.0", + "indexmap 2.9.0", "slab", "tokio", "tokio-util", @@ -3188,9 +3199,9 @@ dependencies = [ [[package]] name = "half" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" dependencies = [ "cfg-if", "crunchy", @@ -3628,9 +3639,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3867,9 +3878,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libflate" @@ -3923,9 +3934,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmimalloc-sys" -version = "0.1.40" +version = "0.1.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502" +checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" dependencies = [ "cc", "libc", @@ -3950,10 +3961,19 @@ checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" dependencies = [ "anstream", "anstyle", - "clap 4.5.34", + "clap 4.5.36", "escape8259", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -4000,7 +4020,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" dependencies = [ - "twox-hash", + "twox-hash 1.6.3", ] [[package]] @@ -4047,9 +4067,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.44" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1" +checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" dependencies = [ "libmimalloc-sys", ] @@ -4078,9 +4098,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.4" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -4245,6 +4265,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" +[[package]] +name = "objc2-core-foundation" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +dependencies = [ + "bitflags 2.8.0", +] + [[package]] name = "object" version = "0.36.7" @@ -4256,18 +4285,21 @@ dependencies = [ [[package]] name = "object_store" -version = "0.11.2" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cfccb68961a56facde1163f9319e0d15743352344e7808a11795fb99698dcaf" +checksum = "e9ce831b09395f933addbc56d894d889e4b226eba304d4e7adbab591e26daf1e" dependencies = [ "async-trait", "base64 0.22.1", "bytes", "chrono", + "form_urlencoded", "futures", + "http 1.2.0", + "http-body-util", "humantime", "hyper", - "itertools 0.13.0", + "itertools 0.14.0", "md-5", "parking_lot", "percent-encoding", @@ -4278,7 +4310,8 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "snafu", + "serde_urlencoded", + "thiserror 2.0.12", "tokio", "tracing", "url", @@ -4361,9 +4394,9 @@ dependencies = [ [[package]] name = "parquet" -version = "54.2.1" +version = "55.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88838dca3b84d41444a0341b19f347e8098a3898b0f21536654b8b799e11abd" +checksum = "cd31a8290ac5b19f09ad77ee7a1e6a541f1be7674ad410547d5f1eef6eef4a9c" dependencies = [ "ahash 0.8.11", "arrow-array", @@ -4391,9 +4424,8 @@ dependencies = [ "snap", "thrift", "tokio", - "twox-hash", + "twox-hash 2.1.0", "zstd", - "zstd-sys", ] [[package]] @@ -4486,7 +4518,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.8.0", + "indexmap 2.9.0", ] [[package]] @@ -4840,9 +4872,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.23.5" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" +checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" dependencies = [ "cfg-if", "indoc", @@ -4858,9 +4890,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.23.5" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" +checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" dependencies = [ "once_cell", "target-lexicon", @@ -4868,9 +4900,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.23.5" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" +checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" dependencies = [ "libc", "pyo3-build-config", @@ -4878,9 +4910,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.23.5" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" +checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -4890,9 +4922,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.23.5" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" +checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5689,7 +5721,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.8.0", + "indexmap 2.9.0", "serde", "serde_derive", "serde_json", @@ -5715,7 +5747,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "itoa", "ryu", "serde", @@ -5790,27 +5822,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" -[[package]] -name = "snafu" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" -dependencies = [ - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.100", -] - [[package]] name = "snap" version = "1.1.1" @@ -5847,9 +5858,9 @@ dependencies = [ [[package]] name = "sqllogictest" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b2f0b80fc250ed3fdd82fc88c0ada5ad62ee1ed5314ac5474acfa52082f518" +checksum = "ee6199c1e008acc669b1e5873c138bf3ad4f8709ccd5c5d88913e664ae4f75de" dependencies = [ "async-trait", "educe", @@ -6108,15 +6119,14 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.34.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "rayon", + "objc2-core-foundation", "windows", ] @@ -6128,9 +6138,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" @@ -6461,7 +6471,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.8.0", + "indexmap 2.9.0", "toml_datetime", "winnow", ] @@ -6631,6 +6641,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "twox-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" + [[package]] name = "typed-arena" version = "2.0.2" @@ -7123,6 +7139,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-registry" version = "0.2.0" @@ -7489,6 +7511,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "zlib-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index b6164f89d31e8..5a735666f8e7e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ repository = "https://github.com/apache/datafusion" # Define Minimum Supported Rust Version (MSRV) rust-version = "1.82.0" # Define DataFusion version -version = "46.0.1" +version = "47.0.0" [workspace.dependencies] # We turn off default-features for some dependencies here so the workspaces which inherit them can @@ -87,69 +87,69 @@ ahash = { version = "0.8", default-features = false, features = [ "runtime-rng", ] } apache-avro = { version = "0.17", default-features = false } -arrow = { version = "54.2.1", features = [ +arrow = { version = "55.0.0", features = [ "prettyprint", "chrono-tz", ] } -arrow-buffer = { version = "54.1.0", default-features = false } -arrow-flight = { version = "54.2.1", features = [ +arrow-buffer = { version = "55.0.0", default-features = false } +arrow-flight = { version = "55.0.0", features = [ "flight-sql-experimental", ] } -arrow-ipc = { version = "54.2.0", default-features = false, features = [ +arrow-ipc = { version = "55.0.0", default-features = false, features = [ "lz4", ] } -arrow-ord = { version = "54.1.0", default-features = false } -arrow-schema = { version = "54.1.0", default-features = false } +arrow-ord = { version = "55.0.0", default-features = false } +arrow-schema = { version = "55.0.0", default-features = false } async-trait = "0.1.88" -bigdecimal = "0.4.7" +bigdecimal = "0.4.8" bytes = "1.10" chrono = { version = "0.4.38", default-features = false } criterion = "0.5.1" ctor = "0.2.9" dashmap = "6.0.1" -datafusion = { path = "datafusion/core", version = "46.0.1", default-features = false } -datafusion-catalog = { path = "datafusion/catalog", version = "46.0.1" } -datafusion-catalog-listing = { path = "datafusion/catalog-listing", version = "46.0.1" } -datafusion-common = { path = "datafusion/common", version = "46.0.1", default-features = false } -datafusion-common-runtime = { path = "datafusion/common-runtime", version = "46.0.1" } -datafusion-datasource = { path = "datafusion/datasource", version = "46.0.1", default-features = false } -datafusion-datasource-avro = { path = "datafusion/datasource-avro", version = "46.0.1", default-features = false } -datafusion-datasource-csv = { path = "datafusion/datasource-csv", version = "46.0.1", default-features = false } -datafusion-datasource-json = { path = "datafusion/datasource-json", version = "46.0.1", default-features = false } -datafusion-datasource-parquet = { path = "datafusion/datasource-parquet", version = "46.0.1", default-features = false } -datafusion-doc = { path = "datafusion/doc", version = "46.0.1" } -datafusion-execution = { path = "datafusion/execution", version = "46.0.1" } -datafusion-expr = { path = "datafusion/expr", version = "46.0.1" } -datafusion-expr-common = { path = "datafusion/expr-common", version = "46.0.1" } -datafusion-ffi = { path = "datafusion/ffi", version = "46.0.1" } -datafusion-functions = { path = "datafusion/functions", version = "46.0.1" } -datafusion-functions-aggregate = { path = "datafusion/functions-aggregate", version = "46.0.1" } -datafusion-functions-aggregate-common = { path = "datafusion/functions-aggregate-common", version = "46.0.1" } -datafusion-functions-nested = { path = "datafusion/functions-nested", version = "46.0.1" } -datafusion-functions-table = { path = "datafusion/functions-table", version = "46.0.1" } -datafusion-functions-window = { path = "datafusion/functions-window", version = "46.0.1" } -datafusion-functions-window-common = { path = "datafusion/functions-window-common", version = "46.0.1" } -datafusion-macros = { path = "datafusion/macros", version = "46.0.1" } -datafusion-optimizer = { path = "datafusion/optimizer", version = "46.0.1", default-features = false } -datafusion-physical-expr = { path = "datafusion/physical-expr", version = "46.0.1", default-features = false } -datafusion-physical-expr-common = { path = "datafusion/physical-expr-common", version = "46.0.1", default-features = false } -datafusion-physical-optimizer = { path = "datafusion/physical-optimizer", version = "46.0.1" } -datafusion-physical-plan = { path = "datafusion/physical-plan", version = "46.0.1" } -datafusion-proto = { path = "datafusion/proto", version = "46.0.1" } -datafusion-proto-common = { path = "datafusion/proto-common", version = "46.0.1" } -datafusion-session = { path = "datafusion/session", version = "46.0.1" } -datafusion-sql = { path = "datafusion/sql", version = "46.0.1" } +datafusion = { path = "datafusion/core", version = "47.0.0", default-features = false } +datafusion-catalog = { path = "datafusion/catalog", version = "47.0.0" } +datafusion-catalog-listing = { path = "datafusion/catalog-listing", version = "47.0.0" } +datafusion-common = { path = "datafusion/common", version = "47.0.0", default-features = false } +datafusion-common-runtime = { path = "datafusion/common-runtime", version = "47.0.0" } +datafusion-datasource = { path = "datafusion/datasource", version = "47.0.0", default-features = false } +datafusion-datasource-avro = { path = "datafusion/datasource-avro", version = "47.0.0", default-features = false } +datafusion-datasource-csv = { path = "datafusion/datasource-csv", version = "47.0.0", default-features = false } +datafusion-datasource-json = { path = "datafusion/datasource-json", version = "47.0.0", default-features = false } +datafusion-datasource-parquet = { path = "datafusion/datasource-parquet", version = "47.0.0", default-features = false } +datafusion-doc = { path = "datafusion/doc", version = "47.0.0" } +datafusion-execution = { path = "datafusion/execution", version = "47.0.0" } +datafusion-expr = { path = "datafusion/expr", version = "47.0.0" } +datafusion-expr-common = { path = "datafusion/expr-common", version = "47.0.0" } +datafusion-ffi = { path = "datafusion/ffi", version = "47.0.0" } +datafusion-functions = { path = "datafusion/functions", version = "47.0.0" } +datafusion-functions-aggregate = { path = "datafusion/functions-aggregate", version = "47.0.0" } +datafusion-functions-aggregate-common = { path = "datafusion/functions-aggregate-common", version = "47.0.0" } +datafusion-functions-nested = { path = "datafusion/functions-nested", version = "47.0.0" } +datafusion-functions-table = { path = "datafusion/functions-table", version = "47.0.0" } +datafusion-functions-window = { path = "datafusion/functions-window", version = "47.0.0" } +datafusion-functions-window-common = { path = "datafusion/functions-window-common", version = "47.0.0" } +datafusion-macros = { path = "datafusion/macros", version = "47.0.0" } +datafusion-optimizer = { path = "datafusion/optimizer", version = "47.0.0", default-features = false } +datafusion-physical-expr = { path = "datafusion/physical-expr", version = "47.0.0", default-features = false } +datafusion-physical-expr-common = { path = "datafusion/physical-expr-common", version = "47.0.0", default-features = false } +datafusion-physical-optimizer = { path = "datafusion/physical-optimizer", version = "47.0.0" } +datafusion-physical-plan = { path = "datafusion/physical-plan", version = "47.0.0" } +datafusion-proto = { path = "datafusion/proto", version = "47.0.0" } +datafusion-proto-common = { path = "datafusion/proto-common", version = "47.0.0" } +datafusion-session = { path = "datafusion/session", version = "47.0.0" } +datafusion-sql = { path = "datafusion/sql", version = "47.0.0" } doc-comment = "0.3" env_logger = "0.11" futures = "0.3" -half = { version = "2.5.0", default-features = false } +half = { version = "2.6.0", default-features = false } hashbrown = { version = "0.14.5", features = ["raw"] } -indexmap = "2.8.0" +indexmap = "2.9.0" itertools = "0.14" log = "^0.4" -object_store = { version = "0.11.0", default-features = false } +object_store = { version = "0.12.0", default-features = false } parking_lot = "0.12" -parquet = { version = "54.2.1", default-features = false, features = [ +parquet = { version = "55.0.0", default-features = false, features = [ "arrow", "async", "object_store", @@ -191,13 +191,20 @@ strip = false # Retain debug info for flamegraphs inherits = "dev" incremental = false -# ci turns off debug info, etc for dependencies to allow for smaller binaries making caching more effective +# ci turns off debug info, etc. for dependencies to allow for smaller binaries making caching more effective [profile.ci.package."*"] debug = false debug-assertions = false strip = "debuginfo" incremental = false +# release inherited profile keeping debug information and symbols +# for mem/cpu profiling +[profile.profiling] +inherits = "release" +debug = true +strip = false + [workspace.lints.clippy] # Detects large stack-allocated futures that may cause stack overflow crashes (see threshold in clippy.toml) large_futures = "warn" diff --git a/benchmarks/README.md b/benchmarks/README.md index 8acaa298bd3ad..86b2e1b3b958f 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -200,6 +200,16 @@ cargo run --release --bin tpch -- convert --input ./data --output /mnt/tpch-parq Or if you want to verify and run all the queries in the benchmark, you can just run `cargo test`. +#### Sorted Conversion + +The TPCH tables generated by the dbgen utility are sorted by their first column (their primary key for most tables, the `l_orderkey` column for the `lineitem` table.) + +To preserve this sorted order information during conversion (useful for benchmarking execution on pre-sorted data) include the `--sort` flag: + +```bash +cargo run --release --bin tpch -- convert --input ./data --output /mnt/tpch-sorted-parquet --format parquet --sort +``` + ### Comparing results between runs Any `dfbench` execution with `-o ` argument will produce a @@ -445,20 +455,29 @@ Test performance of end-to-end sort SQL queries. (While the `Sort` benchmark foc Sort integration benchmark runs whole table sort queries on TPCH `lineitem` table, with different characteristics. For example, different number of sort keys, different sort key cardinality, different number of payload columns, etc. +If the TPCH tables have been converted as sorted on their first column (see [Sorted Conversion](#sorted-conversion)), you can use the `--sorted` flag to indicate that the input data is pre-sorted, allowing DataFusion to leverage that order during query execution. + +Additionally, an optional `--limit` flag is available for the sort benchmark. When specified, this flag appends a `LIMIT n` clause to the SQL query, effectively converting the query into a TopK query. Combining the `--sorted` and `--limit` options enables benchmarking of TopK queries on pre-sorted inputs. + See [`sort_tpch.rs`](src/sort_tpch.rs) for more details. ### Sort TPCH Benchmark Example Runs 1. Run all queries with default setting: ```bash - cargo run --release --bin dfbench -- sort-tpch -p '....../datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' + cargo run --release --bin dfbench -- sort-tpch -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' ``` 2. Run a specific query: ```bash - cargo run --release --bin dfbench -- sort-tpch -p '....../datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' --query 2 + cargo run --release --bin dfbench -- sort-tpch -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' --query 2 ``` -3. Run all queries with `bench.sh` script: +3. Run all queries as TopK queries on presorted data: +```bash + cargo run --release --bin dfbench -- sort-tpch --sorted --limit 10 -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' +``` + +4. Run all queries with `bench.sh` script: ```bash ./bench.sh run sort_tpch ``` diff --git a/benchmarks/bench.sh b/benchmarks/bench.sh index 5be825eb0dafd..5d3ad3446ddb9 100755 --- a/benchmarks/bench.sh +++ b/benchmarks/bench.sh @@ -412,7 +412,10 @@ run_tpch() { echo "Running tpch benchmark..." # Optional query filter to run specific query QUERY=$([ -n "$ARG3" ] && echo "--query $ARG3" || echo "") + # debug the target command + set -x $CARGO_COMMAND --bin tpch -- benchmark datafusion --iterations 5 --path "${TPCH_DIR}" --prefer_hash_join "${PREFER_HASH_JOIN}" --format parquet -o "${RESULTS_FILE}" $QUERY + set +x } # Runs the tpch in memory @@ -427,9 +430,13 @@ run_tpch_mem() { RESULTS_FILE="${RESULTS_DIR}/tpch_mem_sf${SCALE_FACTOR}.json" echo "RESULTS_FILE: ${RESULTS_FILE}" echo "Running tpch_mem benchmark..." + # Optional query filter to run specific query QUERY=$([ -n "$ARG3" ] && echo "--query $ARG3" || echo "") + # debug the target command + set -x # -m means in memory $CARGO_COMMAND --bin tpch -- benchmark datafusion --iterations 5 --path "${TPCH_DIR}" --prefer_hash_join "${PREFER_HASH_JOIN}" -m --format parquet -o "${RESULTS_FILE}" $QUERY + set +x } # Runs the cancellation benchmark diff --git a/benchmarks/queries/clickbench/README.md b/benchmarks/queries/clickbench/README.md index 6797797409c1a..fdb7d1676be0f 100644 --- a/benchmarks/queries/clickbench/README.md +++ b/benchmarks/queries/clickbench/README.md @@ -93,12 +93,14 @@ LIMIT 10; Results look like +``` +-------------+---------------------+---+------+------+------+ | ClientIP | WatchID | c | tmin | tmed | tmax | +-------------+---------------------+---+------+------+------+ | 1611957945 | 6655575552203051303 | 2 | 0 | 0 | 0 | | -1402644643 | 8566928176839891583 | 2 | 0 | 0 | 0 | +-------------+---------------------+---+------+------+------+ +``` ### Q5: Response start time distribution analysis (p95) @@ -120,13 +122,42 @@ LIMIT 10; ``` Results look like - +``` +-------------+---------------------+---+------+------+------+ | ClientIP | WatchID | c | tmin | tp95 | tmax | +-------------+---------------------+---+------+------+------+ | 1611957945 | 6655575552203051303 | 2 | 0 | 0 | 0 | | -1402644643 | 8566928176839891583 | 2 | 0 | 0 | 0 | +-------------+---------------------+---+------+------+------+ +``` + +### Q6: How many social shares meet complex multi-stage filtering criteria? +**Question**: What is the count of sharing actions from iPhone mobile users on specific social networks, within common timezones, participating in seasonal campaigns, with high screen resolutions and closely matched UTM parameters? +**Important Query Properties**: Simple filter with high-selectivity, Costly string matching, A large number of filters with high overhead are positioned relatively later in the process + +```sql +SELECT COUNT(*) AS ShareCount +FROM hits +WHERE + -- Stage 1: High-selectivity filters (fast) + "IsMobile" = 1 -- Filter mobile users + AND "MobilePhoneModel" LIKE 'iPhone%' -- Match iPhone models + AND "SocialAction" = 'share' -- Identify social sharing actions + + -- Stage 2: Moderate filters (cheap) + AND "SocialSourceNetworkID" IN (5, 12) -- Filter specific social networks + AND "ClientTimeZone" BETWEEN -5 AND 5 -- Restrict to common timezones + + -- Stage 3: Heavy computations (expensive) + AND regexp_match("Referer", '\/campaign\/(spring|summer)_promo') IS NOT NULL -- Find campaign-specific referrers + AND CASE + WHEN split_part(split_part("URL", 'resolution=', 2), '&', 1) ~ '^\d+$' + THEN split_part(split_part("URL", 'resolution=', 2), '&', 1)::INT + ELSE 0 + END > 1920 -- Extract and validate resolution parameter + AND levenshtein(CAST("UTMSource" AS STRING), CAST("UTMCampaign" AS STRING)) < 3 -- Verify UTM parameter similarity +``` +Result is empty,Since it has already been filtered by `"SocialAction" = 'share'`. ## Data Notes diff --git a/benchmarks/queries/clickbench/extended.sql b/benchmarks/queries/clickbench/extended.sql index fbabaf2a70218..e967583fd6442 100644 --- a/benchmarks/queries/clickbench/extended.sql +++ b/benchmarks/queries/clickbench/extended.sql @@ -3,4 +3,5 @@ SELECT COUNT(DISTINCT "HitColor"), COUNT(DISTINCT "BrowserCountry"), COUNT(DISTI SELECT "BrowserCountry", COUNT(DISTINCT "SocialNetwork"), COUNT(DISTINCT "HitColor"), COUNT(DISTINCT "BrowserLanguage"), COUNT(DISTINCT "SocialAction") FROM hits GROUP BY 1 ORDER BY 2 DESC LIMIT 10; SELECT "SocialSourceNetworkID", "RegionID", COUNT(*), AVG("Age"), AVG("ParamPrice"), STDDEV("ParamPrice") as s, VAR("ParamPrice") FROM hits GROUP BY "SocialSourceNetworkID", "RegionID" HAVING s IS NOT NULL ORDER BY s DESC LIMIT 10; SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, MEDIAN("ResponseStartTiming") tmed, MAX("ResponseStartTiming") tmax FROM hits WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tmed DESC LIMIT 10; -SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, APPROX_PERCENTILE_CONT("ResponseStartTiming", 0.95) tp95, MAX("ResponseStartTiming") tmax FROM 'hits' WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tp95 DESC LIMIT 10; \ No newline at end of file +SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, APPROX_PERCENTILE_CONT("ResponseStartTiming", 0.95) tp95, MAX("ResponseStartTiming") tmax FROM 'hits' WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tp95 DESC LIMIT 10; +SELECT COUNT(*) AS ShareCount FROM hits WHERE "IsMobile" = 1 AND "MobilePhoneModel" LIKE 'iPhone%' AND "SocialAction" = 'share' AND "SocialSourceNetworkID" IN (5, 12) AND "ClientTimeZone" BETWEEN -5 AND 5 AND regexp_match("Referer", '\/campaign\/(spring|summer)_promo') IS NOT NULL AND CASE WHEN split_part(split_part("URL", 'resolution=', 2), '&', 1) ~ '^\d+$' THEN split_part(split_part("URL", 'resolution=', 2), '&', 1)::INT ELSE 0 END > 1920 AND levenshtein(CAST("UTMSource" AS STRING), CAST("UTMCampaign" AS STRING)) < 3; diff --git a/benchmarks/queries/clickbench/queries.sql b/benchmarks/queries/clickbench/queries.sql index 52e72e02e1e0d..9a183cd6e259c 100644 --- a/benchmarks/queries/clickbench/queries.sql +++ b/benchmarks/queries/clickbench/queries.sql @@ -4,7 +4,7 @@ SELECT SUM("AdvEngineID"), COUNT(*), AVG("ResolutionWidth") FROM hits; SELECT AVG("UserID") FROM hits; SELECT COUNT(DISTINCT "UserID") FROM hits; SELECT COUNT(DISTINCT "SearchPhrase") FROM hits; -SELECT MIN("EventDate"::INT::DATE), MAX("EventDate"::INT::DATE) FROM hits; +SELECT MIN("EventDate"), MAX("EventDate") FROM hits; SELECT "AdvEngineID", COUNT(*) FROM hits WHERE "AdvEngineID" <> 0 GROUP BY "AdvEngineID" ORDER BY COUNT(*) DESC; SELECT "RegionID", COUNT(DISTINCT "UserID") AS u FROM hits GROUP BY "RegionID" ORDER BY u DESC LIMIT 10; SELECT "RegionID", SUM("AdvEngineID"), COUNT(*) AS c, AVG("ResolutionWidth"), COUNT(DISTINCT "UserID") FROM hits GROUP BY "RegionID" ORDER BY c DESC LIMIT 10; @@ -21,10 +21,10 @@ SELECT "UserID" FROM hits WHERE "UserID" = 435090932899640449; SELECT COUNT(*) FROM hits WHERE "URL" LIKE '%google%'; SELECT "SearchPhrase", MIN("URL"), COUNT(*) AS c FROM hits WHERE "URL" LIKE '%google%' AND "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; SELECT "SearchPhrase", MIN("URL"), MIN("Title"), COUNT(*) AS c, COUNT(DISTINCT "UserID") FROM hits WHERE "Title" LIKE '%Google%' AND "URL" NOT LIKE '%.google.%' AND "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; -SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; +SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY "EventTime" LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime" LIMIT 10; SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10; -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime"), "SearchPhrase" LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime", "SearchPhrase" LIMIT 10; SELECT "CounterID", AVG(length("URL")) AS l, COUNT(*) AS c FROM hits WHERE "URL" <> '' GROUP BY "CounterID" HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; SELECT REGEXP_REPLACE("Referer", '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(length("Referer")) AS l, COUNT(*) AS c, MIN("Referer") FROM hits WHERE "Referer" <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; SELECT SUM("ResolutionWidth"), SUM("ResolutionWidth" + 1), SUM("ResolutionWidth" + 2), SUM("ResolutionWidth" + 3), SUM("ResolutionWidth" + 4), SUM("ResolutionWidth" + 5), SUM("ResolutionWidth" + 6), SUM("ResolutionWidth" + 7), SUM("ResolutionWidth" + 8), SUM("ResolutionWidth" + 9), SUM("ResolutionWidth" + 10), SUM("ResolutionWidth" + 11), SUM("ResolutionWidth" + 12), SUM("ResolutionWidth" + 13), SUM("ResolutionWidth" + 14), SUM("ResolutionWidth" + 15), SUM("ResolutionWidth" + 16), SUM("ResolutionWidth" + 17), SUM("ResolutionWidth" + 18), SUM("ResolutionWidth" + 19), SUM("ResolutionWidth" + 20), SUM("ResolutionWidth" + 21), SUM("ResolutionWidth" + 22), SUM("ResolutionWidth" + 23), SUM("ResolutionWidth" + 24), SUM("ResolutionWidth" + 25), SUM("ResolutionWidth" + 26), SUM("ResolutionWidth" + 27), SUM("ResolutionWidth" + 28), SUM("ResolutionWidth" + 29), SUM("ResolutionWidth" + 30), SUM("ResolutionWidth" + 31), SUM("ResolutionWidth" + 32), SUM("ResolutionWidth" + 33), SUM("ResolutionWidth" + 34), SUM("ResolutionWidth" + 35), SUM("ResolutionWidth" + 36), SUM("ResolutionWidth" + 37), SUM("ResolutionWidth" + 38), SUM("ResolutionWidth" + 39), SUM("ResolutionWidth" + 40), SUM("ResolutionWidth" + 41), SUM("ResolutionWidth" + 42), SUM("ResolutionWidth" + 43), SUM("ResolutionWidth" + 44), SUM("ResolutionWidth" + 45), SUM("ResolutionWidth" + 46), SUM("ResolutionWidth" + 47), SUM("ResolutionWidth" + 48), SUM("ResolutionWidth" + 49), SUM("ResolutionWidth" + 50), SUM("ResolutionWidth" + 51), SUM("ResolutionWidth" + 52), SUM("ResolutionWidth" + 53), SUM("ResolutionWidth" + 54), SUM("ResolutionWidth" + 55), SUM("ResolutionWidth" + 56), SUM("ResolutionWidth" + 57), SUM("ResolutionWidth" + 58), SUM("ResolutionWidth" + 59), SUM("ResolutionWidth" + 60), SUM("ResolutionWidth" + 61), SUM("ResolutionWidth" + 62), SUM("ResolutionWidth" + 63), SUM("ResolutionWidth" + 64), SUM("ResolutionWidth" + 65), SUM("ResolutionWidth" + 66), SUM("ResolutionWidth" + 67), SUM("ResolutionWidth" + 68), SUM("ResolutionWidth" + 69), SUM("ResolutionWidth" + 70), SUM("ResolutionWidth" + 71), SUM("ResolutionWidth" + 72), SUM("ResolutionWidth" + 73), SUM("ResolutionWidth" + 74), SUM("ResolutionWidth" + 75), SUM("ResolutionWidth" + 76), SUM("ResolutionWidth" + 77), SUM("ResolutionWidth" + 78), SUM("ResolutionWidth" + 79), SUM("ResolutionWidth" + 80), SUM("ResolutionWidth" + 81), SUM("ResolutionWidth" + 82), SUM("ResolutionWidth" + 83), SUM("ResolutionWidth" + 84), SUM("ResolutionWidth" + 85), SUM("ResolutionWidth" + 86), SUM("ResolutionWidth" + 87), SUM("ResolutionWidth" + 88), SUM("ResolutionWidth" + 89) FROM hits; @@ -34,10 +34,10 @@ SELECT "WatchID", "ClientIP", COUNT(*) AS c, SUM("IsRefresh"), AVG("ResolutionWi SELECT "URL", COUNT(*) AS c FROM hits GROUP BY "URL" ORDER BY c DESC LIMIT 10; SELECT 1, "URL", COUNT(*) AS c FROM hits GROUP BY 1, "URL" ORDER BY c DESC LIMIT 10; SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10; -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; -SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; -SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; -SELECT "URLHash", "EventDate"::INT::DATE, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate"::INT::DATE ORDER BY PageViews DESC LIMIT 10 OFFSET 100; -SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; -SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-14' AND "EventDate"::INT::DATE <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; +SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "URLHash", "EventDate", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate" ORDER BY PageViews DESC LIMIT 10 OFFSET 100; +SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; +SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-14' AND "EventDate" <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; diff --git a/benchmarks/src/sort_tpch.rs b/benchmarks/src/sort_tpch.rs index 956bb92b6c78d..176234eca541c 100644 --- a/benchmarks/src/sort_tpch.rs +++ b/benchmarks/src/sort_tpch.rs @@ -63,6 +63,15 @@ pub struct RunOpt { /// Load the data into a MemTable before executing the query #[structopt(short = "m", long = "mem-table")] mem_table: bool, + + /// Mark the first column of each table as sorted in ascending order. + /// The tables should have been created with the `--sort` option for this to have any effect. + #[structopt(short = "t", long = "sorted")] + sorted: bool, + + /// Append a `LIMIT n` clause to the query + #[structopt(short = "l", long = "limit")] + limit: Option, } struct QueryResult { @@ -163,7 +172,7 @@ impl RunOpt { r#" SELECT l_shipmode, l_comment, l_partkey FROM lineitem - ORDER BY l_shipmode; + ORDER BY l_shipmode "#, ]; @@ -212,9 +221,14 @@ impl RunOpt { let start = Instant::now(); let query_idx = query_id - 1; // 1-indexed -> 0-indexed - let sql = Self::SORT_QUERIES[query_idx]; + let base_sql = Self::SORT_QUERIES[query_idx].to_string(); + let sql = if let Some(limit) = self.limit { + format!("{base_sql} LIMIT {limit}") + } else { + base_sql + }; - let row_count = self.execute_query(&ctx, sql).await?; + let row_count = self.execute_query(&ctx, sql.as_str()).await?; let elapsed = start.elapsed(); //.as_secs_f64() * 1000.0; let ms = elapsed.as_secs_f64() * 1000.0; @@ -315,8 +329,18 @@ impl RunOpt { .with_collect_stat(state.config().collect_statistics()); let table_path = ListingTableUrl::parse(path)?; - let config = ListingTableConfig::new(table_path).with_listing_options(options); - let config = config.infer_schema(&state).await?; + let schema = options.infer_schema(&state, &table_path).await?; + let options = if self.sorted { + let key_column_name = schema.fields()[0].name(); + options + .with_file_sort_order(vec![vec![col(key_column_name).sort(true, false)]]) + } else { + options + }; + + let config = ListingTableConfig::new(table_path) + .with_listing_options(options) + .with_schema(schema); Ok(Arc::new(ListingTable::try_new(config)?)) } diff --git a/benchmarks/src/tpch/convert.rs b/benchmarks/src/tpch/convert.rs index 7f391d930045a..5219e09cd3052 100644 --- a/benchmarks/src/tpch/convert.rs +++ b/benchmarks/src/tpch/convert.rs @@ -22,15 +22,14 @@ use std::path::{Path, PathBuf}; use datafusion::common::not_impl_err; +use super::get_tbl_tpch_table_schema; +use super::TPCH_TABLES; use datafusion::error::Result; use datafusion::prelude::*; use parquet::basic::Compression; use parquet::file::properties::WriterProperties; use structopt::StructOpt; -use super::get_tbl_tpch_table_schema; -use super::TPCH_TABLES; - /// Convert tpch .slt files to .parquet or .csv files #[derive(Debug, StructOpt)] pub struct ConvertOpt { @@ -57,6 +56,10 @@ pub struct ConvertOpt { /// Batch size when reading CSV or Parquet files #[structopt(short = "s", long = "batch-size", default_value = "8192")] batch_size: usize, + + /// Sort each table by its first column in ascending order. + #[structopt(short = "t", long = "sort")] + sort: bool, } impl ConvertOpt { @@ -70,6 +73,7 @@ impl ConvertOpt { for table in TPCH_TABLES { let start = Instant::now(); let schema = get_tbl_tpch_table_schema(table); + let key_column_name = schema.fields()[0].name(); let input_path = format!("{input_path}/{table}.tbl"); let options = CsvReadOptions::new() @@ -77,6 +81,13 @@ impl ConvertOpt { .has_header(false) .delimiter(b'|') .file_extension(".tbl"); + let options = if self.sort { + // indicated that the file is already sorted by its first column to speed up the conversion + options + .file_sort_order(vec![vec![col(key_column_name).sort(true, false)]]) + } else { + options + }; let config = SessionConfig::new().with_batch_size(self.batch_size); let ctx = SessionContext::new_with_config(config); @@ -99,6 +110,11 @@ impl ConvertOpt { if partitions > 1 { csv = csv.repartition(Partitioning::RoundRobinBatch(partitions))? } + let csv = if self.sort { + csv.sort_by(vec![col(key_column_name)])? + } else { + csv + }; // create the physical plan let csv = csv.create_physical_plan().await?; diff --git a/benchmarks/src/tpch/run.rs b/benchmarks/src/tpch/run.rs index eb9db821db02f..752a5a1a6ba01 100644 --- a/benchmarks/src/tpch/run.rs +++ b/benchmarks/src/tpch/run.rs @@ -90,6 +90,11 @@ pub struct RunOpt { /// True by default. #[structopt(short = "j", long = "prefer_hash_join", default_value = "true")] prefer_hash_join: BoolDefaultTrue, + + /// Mark the first column of each table as sorted in ascending order. + /// The tables should have been created with the `--sort` option for this to have any effect. + #[structopt(short = "t", long = "sorted")] + sorted: bool, } const TPCH_QUERY_START_ID: usize = 1; @@ -275,20 +280,28 @@ impl RunOpt { } }; + let table_path = ListingTableUrl::parse(path)?; let options = ListingOptions::new(format) .with_file_extension(extension) .with_target_partitions(target_partitions) .with_collect_stat(state.config().collect_statistics()); - - let table_path = ListingTableUrl::parse(path)?; - let config = ListingTableConfig::new(table_path).with_listing_options(options); - - let config = match table_format { - "parquet" => config.infer_schema(&state).await?, - "tbl" => config.with_schema(Arc::new(get_tbl_tpch_table_schema(table))), - "csv" => config.with_schema(Arc::new(get_tpch_table_schema(table))), + let schema = match table_format { + "parquet" => options.infer_schema(&state, &table_path).await?, + "tbl" => Arc::new(get_tbl_tpch_table_schema(table)), + "csv" => Arc::new(get_tpch_table_schema(table)), _ => unreachable!(), }; + let options = if self.sorted { + let key_column_name = schema.fields()[0].name(); + options + .with_file_sort_order(vec![vec![col(key_column_name).sort(true, false)]]) + } else { + options + }; + + let config = ListingTableConfig::new(table_path) + .with_listing_options(options) + .with_schema(schema); Ok(Arc::new(ListingTable::try_new(config)?)) } @@ -357,6 +370,7 @@ mod tests { output_path: None, disable_statistics: false, prefer_hash_join: true, + sorted: false, }; opt.register_tables(&ctx).await?; let queries = get_query_sql(query)?; @@ -393,6 +407,7 @@ mod tests { output_path: None, disable_statistics: false, prefer_hash_join: true, + sorted: false, }; opt.register_tables(&ctx).await?; let queries = get_query_sql(query)?; diff --git a/datafusion-cli/Cargo.toml b/datafusion-cli/Cargo.toml index c70e3fc1caec5..e21c005cee5bf 100644 --- a/datafusion-cli/Cargo.toml +++ b/datafusion-cli/Cargo.toml @@ -39,7 +39,7 @@ arrow = { workspace = true } async-trait = { workspace = true } aws-config = "1.6.1" aws-credential-types = "1.2.0" -clap = { version = "4.5.34", features = ["derive", "cargo"] } +clap = { version = "4.5.36", features = ["derive", "cargo"] } datafusion = { workspace = true, features = [ "avro", "crypto_expressions", diff --git a/datafusion-cli/src/main.rs b/datafusion-cli/src/main.rs index e21006312d85a..dad2d15f01a11 100644 --- a/datafusion-cli/src/main.rs +++ b/datafusion-cli/src/main.rs @@ -25,6 +25,7 @@ use datafusion::error::{DataFusionError, Result}; use datafusion::execution::context::SessionConfig; use datafusion::execution::memory_pool::{FairSpillPool, GreedyMemoryPool, MemoryPool}; use datafusion::execution::runtime_env::RuntimeEnvBuilder; +use datafusion::execution::DiskManager; use datafusion::prelude::SessionContext; use datafusion_cli::catalog::DynamicObjectStoreCatalog; use datafusion_cli::functions::ParquetMetadataFunc; @@ -37,6 +38,9 @@ use datafusion_cli::{ }; use clap::Parser; +use datafusion::common::config_err; +use datafusion::config::ConfigOptions; +use datafusion::execution::disk_manager::DiskManagerConfig; use mimalloc::MiMalloc; #[global_allocator] @@ -123,6 +127,14 @@ struct Args { #[clap(long, help = "Enables console syntax highlighting")] color: bool, + + #[clap( + short = 'd', + long, + help = "Available disk space for spilling queries (e.g. '10g'), default to None (uses DataFusion's default value of '100g')", + value_parser(extract_disk_limit) + )] + disk_limit: Option, } #[tokio::main] @@ -150,11 +162,7 @@ async fn main_inner() -> Result<()> { env::set_current_dir(p).unwrap(); }; - let mut session_config = SessionConfig::from_env()?.with_information_schema(true); - - if let Some(batch_size) = args.batch_size { - session_config = session_config.with_batch_size(batch_size); - }; + let session_config = get_session_config(&args)?; let mut rt_builder = RuntimeEnvBuilder::new(); // set memory pool size @@ -167,6 +175,18 @@ async fn main_inner() -> Result<()> { rt_builder = rt_builder.with_memory_pool(pool) } + // set disk limit + if let Some(disk_limit) = args.disk_limit { + let disk_manager = DiskManager::try_new(DiskManagerConfig::NewOs)?; + + let disk_manager = Arc::try_unwrap(disk_manager) + .expect("DiskManager should be a single instance") + .with_max_temp_directory_size(disk_limit.try_into().unwrap())?; + + let disk_config = DiskManagerConfig::new_existing(Arc::new(disk_manager)); + rt_builder = rt_builder.with_disk_manager(disk_config); + } + let runtime_env = rt_builder.build_arc()?; // enable dynamic file query @@ -226,6 +246,30 @@ async fn main_inner() -> Result<()> { Ok(()) } +/// Get the session configuration based on the provided arguments +/// and environment settings. +fn get_session_config(args: &Args) -> Result { + // Read options from environment variables and merge with command line options + let mut config_options = ConfigOptions::from_env()?; + + if let Some(batch_size) = args.batch_size { + if batch_size == 0 { + return config_err!("batch_size must be greater than 0"); + } + config_options.execution.batch_size = batch_size; + }; + + // use easier to understand "tree" mode by default + // if the user hasn't specified an explain format in the environment + if env::var_os("DATAFUSION_EXPLAIN_FORMAT").is_none() { + config_options.explain.format = String::from("tree"); + } + + let session_config = + SessionConfig::from(config_options).with_information_schema(true); + Ok(session_config) +} + fn parse_valid_file(dir: &str) -> Result { if Path::new(dir).is_file() { Ok(dir.to_string()) @@ -278,7 +322,7 @@ impl ByteUnit { } } -fn extract_memory_pool_size(size: &str) -> Result { +fn parse_size_string(size: &str, label: &str) -> Result { static BYTE_SUFFIXES: LazyLock> = LazyLock::new(|| { let mut m = HashMap::new(); @@ -300,25 +344,33 @@ fn extract_memory_pool_size(size: &str) -> Result { let lower = size.to_lowercase(); if let Some(caps) = SUFFIX_REGEX.captures(&lower) { let num_str = caps.get(1).unwrap().as_str(); - let num = num_str.parse::().map_err(|_| { - format!("Invalid numeric value in memory pool size '{}'", size) - })?; + let num = num_str + .parse::() + .map_err(|_| format!("Invalid numeric value in {} '{}'", label, size))?; let suffix = caps.get(2).map(|m| m.as_str()).unwrap_or("b"); - let unit = &BYTE_SUFFIXES + let unit = BYTE_SUFFIXES .get(suffix) - .ok_or_else(|| format!("Invalid memory pool size '{}'", size))?; - let memory_pool_size = usize::try_from(unit.multiplier()) + .ok_or_else(|| format!("Invalid {} '{}'", label, size))?; + let total_bytes = usize::try_from(unit.multiplier()) .ok() .and_then(|multiplier| num.checked_mul(multiplier)) - .ok_or_else(|| format!("Memory pool size '{}' is too large", size))?; + .ok_or_else(|| format!("{} '{}' is too large", label, size))?; - Ok(memory_pool_size) + Ok(total_bytes) } else { - Err(format!("Invalid memory pool size '{}'", size)) + Err(format!("Invalid {} '{}'", label, size)) } } +pub fn extract_memory_pool_size(size: &str) -> Result { + parse_size_string(size, "memory pool size") +} + +pub fn extract_disk_limit(size: &str) -> Result { + parse_size_string(size, "disk limit") +} + #[cfg(test)] mod tests { use super::*; diff --git a/datafusion-cli/tests/cli_integration.rs b/datafusion-cli/tests/cli_integration.rs index a54a920e97bbf..9ac09955512b8 100644 --- a/datafusion-cli/tests/cli_integration.rs +++ b/datafusion-cli/tests/cli_integration.rs @@ -59,6 +59,16 @@ fn init() { "batch_size", ["--command", "show datafusion.execution.batch_size", "-q", "-b", "1"], )] +#[case::default_explain_plan( + "default_explain_plan", + // default explain format should be tree + ["--command", "EXPLAIN SELECT 123"], +)] +#[case::can_see_indent_format( + "can_see_indent_format", + // can choose the old explain format too + ["--command", "EXPLAIN FORMAT indent SELECT 123"], +)] #[test] fn cli_quick_test<'a>( #[case] snapshot_name: &'a str, @@ -74,6 +84,21 @@ fn cli_quick_test<'a>( assert_cmd_snapshot!(cmd); } +#[test] +fn cli_explain_environment_overrides() { + let mut settings = make_settings(); + settings.set_snapshot_suffix("explain_plan_environment_overrides"); + let _bound = settings.bind_to_scope(); + + let mut cmd = cli(); + + // should use the environment variable to override the default explain plan + cmd.env("DATAFUSION_EXPLAIN_FORMAT", "pgjson") + .args(["--command", "EXPLAIN SELECT 123"]); + + assert_cmd_snapshot!(cmd); +} + #[rstest] #[case("csv")] #[case("tsv")] diff --git a/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap b/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap new file mode 100644 index 0000000000000..6b3a247dd7b82 --- /dev/null +++ b/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap @@ -0,0 +1,44 @@ +--- +source: datafusion-cli/tests/cli_integration.rs +info: + program: datafusion-cli + args: + - "--command" + - EXPLAIN SELECT 123 + env: + DATAFUSION_EXPLAIN_FORMAT: pgjson +snapshot_kind: text +--- +success: true +exit_code: 0 +----- stdout ----- +[CLI_VERSION] ++--------------+-----------------------------------------+ +| plan_type | plan | ++--------------+-----------------------------------------+ +| logical_plan | [ | +| | { | +| | "Plan": { | +| | "Expressions": [ | +| | "Int64(123)" | +| | ], | +| | "Node Type": "Projection", | +| | "Output": [ | +| | "Int64(123)" | +| | ], | +| | "Plans": [ | +| | { | +| | "Node Type": "EmptyRelation", | +| | "Output": [], | +| | "Plans": [] | +| | } | +| | ] | +| | } | +| | } | +| | ] | ++--------------+-----------------------------------------+ +1 row(s) fetched. +[ELAPSED] + + +----- stderr ----- diff --git a/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap b/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap new file mode 100644 index 0000000000000..b2fb64709974e --- /dev/null +++ b/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap @@ -0,0 +1,27 @@ +--- +source: datafusion-cli/tests/cli_integration.rs +info: + program: datafusion-cli + args: + - "--command" + - EXPLAIN FORMAT indent SELECT 123 +snapshot_kind: text +--- +success: true +exit_code: 0 +----- stdout ----- +[CLI_VERSION] ++---------------+------------------------------------------+ +| plan_type | plan | ++---------------+------------------------------------------+ +| logical_plan | Projection: Int64(123) | +| | EmptyRelation | +| physical_plan | ProjectionExec: expr=[123 as Int64(123)] | +| | PlaceholderRowExec | +| | | ++---------------+------------------------------------------+ +2 row(s) fetched. +[ELAPSED] + + +----- stderr ----- diff --git a/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap b/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap new file mode 100644 index 0000000000000..46ee6be64f624 --- /dev/null +++ b/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap @@ -0,0 +1,31 @@ +--- +source: datafusion-cli/tests/cli_integration.rs +info: + program: datafusion-cli + args: + - "--command" + - EXPLAIN SELECT 123 +snapshot_kind: text +--- +success: true +exit_code: 0 +----- stdout ----- +[CLI_VERSION] ++---------------+-------------------------------+ +| plan_type | plan | ++---------------+-------------------------------+ +| physical_plan | ┌───────────────────────────┐ | +| | │ ProjectionExec │ | +| | │ -------------------- │ | +| | │ Int64(123): 123 │ | +| | └─────────────┬─────────────┘ | +| | ┌─────────────┴─────────────┐ | +| | │ PlaceholderRowExec │ | +| | └───────────────────────────┘ | +| | | ++---------------+-------------------------------+ +1 row(s) fetched. +[ELAPSED] + + +----- stderr ----- diff --git a/datafusion-examples/Cargo.toml b/datafusion-examples/Cargo.toml index f6b7d641d1264..2ba1673d97b99 100644 --- a/datafusion-examples/Cargo.toml +++ b/datafusion-examples/Cargo.toml @@ -62,6 +62,7 @@ bytes = { workspace = true } dashmap = { workspace = true } # note only use main datafusion crate for examples datafusion = { workspace = true, default-features = true } +datafusion-ffi = { workspace = true } datafusion-proto = { workspace = true } env_logger = { workspace = true } futures = { workspace = true } diff --git a/datafusion-examples/examples/advanced_parquet_index.rs b/datafusion-examples/examples/advanced_parquet_index.rs index b8c303e221618..03ef3d66f9d71 100644 --- a/datafusion-examples/examples/advanced_parquet_index.rs +++ b/datafusion-examples/examples/advanced_parquet_index.rs @@ -571,7 +571,9 @@ impl ParquetFileReaderFactory for CachedParquetFileReaderFactory { .to_string(); let object_store = Arc::clone(&self.object_store); - let mut inner = ParquetObjectReader::new(object_store, file_meta.object_meta); + let mut inner = + ParquetObjectReader::new(object_store, file_meta.object_meta.location) + .with_file_size(file_meta.object_meta.size); if let Some(hint) = metadata_size_hint { inner = inner.with_footer_size_hint(hint) @@ -599,7 +601,7 @@ struct ParquetReaderWithCache { impl AsyncFileReader for ParquetReaderWithCache { fn get_bytes( &mut self, - range: Range, + range: Range, ) -> BoxFuture<'_, datafusion::parquet::errors::Result> { println!("get_bytes: {} Reading range {:?}", self.filename, range); self.inner.get_bytes(range) @@ -607,7 +609,7 @@ impl AsyncFileReader for ParquetReaderWithCache { fn get_byte_ranges( &mut self, - ranges: Vec>, + ranges: Vec>, ) -> BoxFuture<'_, datafusion::parquet::errors::Result>> { println!( "get_byte_ranges: {} Reading ranges {:?}", @@ -618,6 +620,7 @@ impl AsyncFileReader for ParquetReaderWithCache { fn get_metadata( &mut self, + _options: Option<&ArrowReaderOptions>, ) -> BoxFuture<'_, datafusion::parquet::errors::Result>> { println!("get_metadata: {} returning cached metadata", self.filename); diff --git a/datafusion-examples/examples/parquet_index.rs b/datafusion-examples/examples/parquet_index.rs index 0b6bccc27b1d1..7d6ce4d86af1a 100644 --- a/datafusion-examples/examples/parquet_index.rs +++ b/datafusion-examples/examples/parquet_index.rs @@ -685,7 +685,7 @@ fn make_demo_file(path: impl AsRef, value_range: Range) -> Result<()> let num_values = value_range.len(); let file_names = - StringArray::from_iter_values(std::iter::repeat(&filename).take(num_values)); + StringArray::from_iter_values(std::iter::repeat_n(&filename, num_values)); let values = Int32Array::from_iter_values(value_range); let batch = RecordBatch::try_from_iter(vec![ ("file_name", Arc::new(file_names) as ArrayRef), diff --git a/datafusion-examples/examples/sql_dialect.rs b/datafusion-examples/examples/sql_dialect.rs index 12141847ca361..840faa63b1a48 100644 --- a/datafusion-examples/examples/sql_dialect.rs +++ b/datafusion-examples/examples/sql_dialect.rs @@ -17,10 +17,10 @@ use std::fmt::Display; -use datafusion::error::Result; +use datafusion::error::{DataFusionError, Result}; use datafusion::sql::{ parser::{CopyToSource, CopyToStatement, DFParser, DFParserBuilder, Statement}, - sqlparser::{keywords::Keyword, parser::ParserError, tokenizer::Token}, + sqlparser::{keywords::Keyword, tokenizer::Token}, }; /// This example demonstrates how to use the DFParser to parse a statement in a custom way @@ -62,7 +62,7 @@ impl<'a> MyParser<'a> { /// This is the entry point to our parser -- it handles `COPY` statements specially /// but otherwise delegates to the existing DataFusion parser. - pub fn parse_statement(&mut self) -> Result { + pub fn parse_statement(&mut self) -> Result { if self.is_copy() { self.df_parser.parser.next_token(); // COPY let df_statement = self.df_parser.parse_copy()?; diff --git a/datafusion-testing b/datafusion-testing index 243047b9dd682..e9f9e22ccf091 160000 --- a/datafusion-testing +++ b/datafusion-testing @@ -1 +1 @@ -Subproject commit 243047b9dd682be688628539c604daaddfe640f9 +Subproject commit e9f9e22ccf09145a7368f80fd6a871f11e2b4481 diff --git a/datafusion/catalog/src/lib.rs b/datafusion/catalog/src/lib.rs index f160bddd2b9c1..0394b05277dac 100644 --- a/datafusion/catalog/src/lib.rs +++ b/datafusion/catalog/src/lib.rs @@ -50,7 +50,7 @@ pub use catalog::*; pub use datafusion_session::Session; pub use dynamic_file::catalog::*; pub use memory::{ - MemoryCatalogProvider, MemoryCatalogProviderList, MemorySchemaProvider, + MemTable, MemoryCatalogProvider, MemoryCatalogProviderList, MemorySchemaProvider, }; pub use r#async::*; pub use schema::*; diff --git a/datafusion/catalog/src/memory/mod.rs b/datafusion/catalog/src/memory/mod.rs index 4c5cf1a9ae9de..541d25b3345b4 100644 --- a/datafusion/catalog/src/memory/mod.rs +++ b/datafusion/catalog/src/memory/mod.rs @@ -17,6 +17,12 @@ pub(crate) mod catalog; pub(crate) mod schema; +pub(crate) mod table; pub use catalog::*; pub use schema::*; +pub use table::*; + +// backward compatibility +pub use datafusion_datasource::memory::MemorySourceConfig; +pub use datafusion_datasource::source::DataSourceExec; diff --git a/datafusion/catalog/src/memory/table.rs b/datafusion/catalog/src/memory/table.rs new file mode 100644 index 0000000000000..81243e2c4889e --- /dev/null +++ b/datafusion/catalog/src/memory/table.rs @@ -0,0 +1,296 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! [`MemTable`] for querying `Vec` by DataFusion. + +use std::any::Any; +use std::collections::HashMap; +use std::fmt::Debug; +use std::sync::Arc; + +use crate::TableProvider; +use datafusion_common::error::Result; +use datafusion_expr::Expr; +use datafusion_expr::TableType; +use datafusion_physical_expr::create_physical_sort_exprs; +use datafusion_physical_plan::repartition::RepartitionExec; +use datafusion_physical_plan::{ + common, ExecutionPlan, ExecutionPlanProperties, Partitioning, +}; + +use arrow::datatypes::SchemaRef; +use arrow::record_batch::RecordBatch; +use datafusion_common::{not_impl_err, plan_err, Constraints, DFSchema, SchemaExt}; +use datafusion_common_runtime::JoinSet; +use datafusion_datasource::memory::MemSink; +use datafusion_datasource::memory::MemorySourceConfig; +use datafusion_datasource::sink::DataSinkExec; +use datafusion_datasource::source::DataSourceExec; +use datafusion_expr::dml::InsertOp; +use datafusion_expr::SortExpr; +use datafusion_session::Session; + +use async_trait::async_trait; +use futures::StreamExt; +use log::debug; +use parking_lot::Mutex; +use tokio::sync::RwLock; + +// backward compatibility +pub use datafusion_datasource::memory::PartitionData; + +/// In-memory data source for presenting a `Vec` as a +/// data source that can be queried by DataFusion. This allows data to +/// be pre-loaded into memory and then repeatedly queried without +/// incurring additional file I/O overhead. +#[derive(Debug)] +pub struct MemTable { + schema: SchemaRef, + // batches used to be pub(crate), but it's needed to be public for the tests + pub batches: Vec, + constraints: Constraints, + column_defaults: HashMap, + /// Optional pre-known sort order(s). Must be `SortExpr`s. + /// inserting data into this table removes the order + pub sort_order: Arc>>>, +} + +impl MemTable { + /// Create a new in-memory table from the provided schema and record batches + pub fn try_new(schema: SchemaRef, partitions: Vec>) -> Result { + for batches in partitions.iter().flatten() { + let batches_schema = batches.schema(); + if !schema.contains(&batches_schema) { + debug!( + "mem table schema does not contain batches schema. \ + Target_schema: {schema:?}. Batches Schema: {batches_schema:?}" + ); + return plan_err!("Mismatch between schema and batches"); + } + } + + Ok(Self { + schema, + batches: partitions + .into_iter() + .map(|e| Arc::new(RwLock::new(e))) + .collect::>(), + constraints: Constraints::empty(), + column_defaults: HashMap::new(), + sort_order: Arc::new(Mutex::new(vec![])), + }) + } + + /// Assign constraints + pub fn with_constraints(mut self, constraints: Constraints) -> Self { + self.constraints = constraints; + self + } + + /// Assign column defaults + pub fn with_column_defaults( + mut self, + column_defaults: HashMap, + ) -> Self { + self.column_defaults = column_defaults; + self + } + + /// Specify an optional pre-known sort order(s). Must be `SortExpr`s. + /// + /// If the data is not sorted by this order, DataFusion may produce + /// incorrect results. + /// + /// DataFusion may take advantage of this ordering to omit sorts + /// or use more efficient algorithms. + /// + /// Note that multiple sort orders are supported, if some are known to be + /// equivalent, + pub fn with_sort_order(self, mut sort_order: Vec>) -> Self { + std::mem::swap(self.sort_order.lock().as_mut(), &mut sort_order); + self + } + + /// Create a mem table by reading from another data source + pub async fn load( + t: Arc, + output_partitions: Option, + state: &dyn Session, + ) -> Result { + let schema = t.schema(); + let constraints = t.constraints(); + let exec = t.scan(state, None, &[], None).await?; + let partition_count = exec.output_partitioning().partition_count(); + + let mut join_set = JoinSet::new(); + + for part_idx in 0..partition_count { + let task = state.task_ctx(); + let exec = Arc::clone(&exec); + join_set.spawn(async move { + let stream = exec.execute(part_idx, task)?; + common::collect(stream).await + }); + } + + let mut data: Vec> = + Vec::with_capacity(exec.output_partitioning().partition_count()); + + while let Some(result) = join_set.join_next().await { + match result { + Ok(res) => data.push(res?), + Err(e) => { + if e.is_panic() { + std::panic::resume_unwind(e.into_panic()); + } else { + unreachable!(); + } + } + } + } + + let mut exec = DataSourceExec::new(Arc::new(MemorySourceConfig::try_new( + &data, + Arc::clone(&schema), + None, + )?)); + if let Some(cons) = constraints { + exec = exec.with_constraints(cons.clone()); + } + + if let Some(num_partitions) = output_partitions { + let exec = RepartitionExec::try_new( + Arc::new(exec), + Partitioning::RoundRobinBatch(num_partitions), + )?; + + // execute and collect results + let mut output_partitions = vec![]; + for i in 0..exec.properties().output_partitioning().partition_count() { + // execute this *output* partition and collect all batches + let task_ctx = state.task_ctx(); + let mut stream = exec.execute(i, task_ctx)?; + let mut batches = vec![]; + while let Some(result) = stream.next().await { + batches.push(result?); + } + output_partitions.push(batches); + } + + return MemTable::try_new(Arc::clone(&schema), output_partitions); + } + MemTable::try_new(Arc::clone(&schema), data) + } +} + +#[async_trait] +impl TableProvider for MemTable { + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> SchemaRef { + Arc::clone(&self.schema) + } + + fn constraints(&self) -> Option<&Constraints> { + Some(&self.constraints) + } + + fn table_type(&self) -> TableType { + TableType::Base + } + + async fn scan( + &self, + state: &dyn Session, + projection: Option<&Vec>, + _filters: &[Expr], + _limit: Option, + ) -> Result> { + let mut partitions = vec![]; + for arc_inner_vec in self.batches.iter() { + let inner_vec = arc_inner_vec.read().await; + partitions.push(inner_vec.clone()) + } + + let mut source = + MemorySourceConfig::try_new(&partitions, self.schema(), projection.cloned())?; + + let show_sizes = state.config_options().explain.show_sizes; + source = source.with_show_sizes(show_sizes); + + // add sort information if present + let sort_order = self.sort_order.lock(); + if !sort_order.is_empty() { + let df_schema = DFSchema::try_from(self.schema.as_ref().clone())?; + + let file_sort_order = sort_order + .iter() + .map(|sort_exprs| { + create_physical_sort_exprs( + sort_exprs, + &df_schema, + state.execution_props(), + ) + }) + .collect::>>()?; + source = source.try_with_sort_information(file_sort_order)?; + } + + Ok(DataSourceExec::from_data_source(source)) + } + + /// Returns an ExecutionPlan that inserts the execution results of a given [`ExecutionPlan`] into this [`MemTable`]. + /// + /// The [`ExecutionPlan`] must have the same schema as this [`MemTable`]. + /// + /// # Arguments + /// + /// * `state` - The [`SessionState`] containing the context for executing the plan. + /// * `input` - The [`ExecutionPlan`] to execute and insert. + /// + /// # Returns + /// + /// * A plan that returns the number of rows written. + /// + /// [`SessionState`]: https://docs.rs/datafusion/latest/datafusion/execution/session_state/struct.SessionState.html + async fn insert_into( + &self, + _state: &dyn Session, + input: Arc, + insert_op: InsertOp, + ) -> Result> { + // If we are inserting into the table, any sort order may be messed up so reset it here + *self.sort_order.lock() = vec![]; + + // Create a physical plan from the logical plan. + // Check that the schema of the plan matches the schema of this table. + self.schema() + .logically_equivalent_names_and_types(&input.schema())?; + + if insert_op != InsertOp::Append { + return not_impl_err!("{insert_op} not implemented for MemoryTable yet"); + } + let sink = MemSink::try_new(self.batches.clone(), Arc::clone(&self.schema))?; + Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None))) + } + + fn get_column_default(&self, column: &str) -> Option<&Expr> { + self.column_defaults.get(column) + } +} diff --git a/datafusion/common-runtime/src/common.rs b/datafusion/common-runtime/src/common.rs index 361f6af95cf13..e7aba1d455ee6 100644 --- a/datafusion/common-runtime/src/common.rs +++ b/datafusion/common-runtime/src/common.rs @@ -15,18 +15,25 @@ // specific language governing permissions and limitations // under the License. -use std::future::Future; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; -use crate::JoinSet; -use tokio::task::JoinError; +use tokio::task::{JoinError, JoinHandle}; + +use crate::trace_utils::{trace_block, trace_future}; /// Helper that provides a simple API to spawn a single task and join it. /// Provides guarantees of aborting on `Drop` to keep it cancel-safe. +/// Note that if the task was spawned with `spawn_blocking`, it will only be +/// aborted if it hasn't started yet. /// -/// Technically, it's just a wrapper of `JoinSet` (with size=1). +/// Technically, it's just a wrapper of a `JoinHandle` overriding drop. #[derive(Debug)] pub struct SpawnedTask { - inner: JoinSet, + inner: JoinHandle, } impl SpawnedTask { @@ -36,8 +43,9 @@ impl SpawnedTask { T: Send + 'static, R: Send, { - let mut inner = JoinSet::new(); - inner.spawn(task); + // Ok to use spawn here as SpawnedTask handles aborting/cancelling the task on Drop + #[allow(clippy::disallowed_methods)] + let inner = tokio::task::spawn(trace_future(task)); Self { inner } } @@ -47,22 +55,21 @@ impl SpawnedTask { T: Send + 'static, R: Send, { - let mut inner = JoinSet::new(); - inner.spawn_blocking(task); + // Ok to use spawn_blocking here as SpawnedTask handles aborting/cancelling the task on Drop + #[allow(clippy::disallowed_methods)] + let inner = tokio::task::spawn_blocking(trace_block(task)); Self { inner } } /// Joins the task, returning the result of join (`Result`). - pub async fn join(mut self) -> Result { - self.inner - .join_next() - .await - .expect("`SpawnedTask` instance always contains exactly 1 task") + /// Same as awaiting the spawned task, but left for backwards compatibility. + pub async fn join(self) -> Result { + self.await } /// Joins the task and unwinds the panic if it happens. pub async fn join_unwind(self) -> Result { - self.join().await.map_err(|e| { + self.await.map_err(|e| { // `JoinError` can be caused either by panic or cancellation. We have to handle panics: if e.is_panic() { std::panic::resume_unwind(e.into_panic()); @@ -77,17 +84,32 @@ impl SpawnedTask { } } +impl Future for SpawnedTask { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.inner).poll(cx) + } +} + +impl Drop for SpawnedTask { + fn drop(&mut self) { + self.inner.abort(); + } +} + #[cfg(test)] mod tests { use super::*; use std::future::{pending, Pending}; - use tokio::runtime::Runtime; + use tokio::{runtime::Runtime, sync::oneshot}; #[tokio::test] async fn runtime_shutdown() { let rt = Runtime::new().unwrap(); + #[allow(clippy::async_yields_async)] let task = rt .spawn(async { SpawnedTask::spawn(async { @@ -119,4 +141,36 @@ mod tests { .await .ok(); } + + #[tokio::test] + async fn cancel_not_started_task() { + let (sender, receiver) = oneshot::channel::(); + let task = SpawnedTask::spawn(async { + // Shouldn't be reached. + sender.send(42).unwrap(); + }); + + drop(task); + + // If the task was cancelled, the sender was also dropped, + // and awaiting the receiver should result in an error. + assert!(receiver.await.is_err()); + } + + #[tokio::test] + async fn cancel_ongoing_task() { + let (sender, mut receiver) = tokio::sync::mpsc::channel(1); + let task = SpawnedTask::spawn(async move { + sender.send(1).await.unwrap(); + // This line will never be reached because the channel has a buffer + // of 1. + sender.send(2).await.unwrap(); + }); + // Let the task start. + assert_eq!(receiver.recv().await.unwrap(), 1); + drop(task); + + // The sender was dropped so we receive `None`. + assert!(receiver.recv().await.is_none()); + } } diff --git a/datafusion/common/Cargo.toml b/datafusion/common/Cargo.toml index 39b47a96bccf3..d471e48be4e75 100644 --- a/datafusion/common/Cargo.toml +++ b/datafusion/common/Cargo.toml @@ -58,12 +58,12 @@ base64 = "0.22.1" half = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } -libc = "0.2.171" +libc = "0.2.172" log = { workspace = true } object_store = { workspace = true, optional = true } parquet = { workspace = true, optional = true, default-features = true } paste = "1.0.15" -pyo3 = { version = "0.23.5", optional = true } +pyo3 = { version = "0.24.2", optional = true } recursive = { workspace = true, optional = true } sqlparser = { workspace = true } tokio = { workspace = true } diff --git a/datafusion/common/src/config.rs b/datafusion/common/src/config.rs index b0f17630c910c..1e0f63d6d81ca 100644 --- a/datafusion/common/src/config.rs +++ b/datafusion/common/src/config.rs @@ -149,9 +149,17 @@ macro_rules! config_namespace { // $(#[allow(deprecated)])? { $(let value = $transform(value);)? // Apply transformation if specified - $(log::warn!($warn);)? // Log warning if specified #[allow(deprecated)] - self.$field_name.set(rem, value.as_ref()) + let ret = self.$field_name.set(rem, value.as_ref()); + + $(if !$warn.is_empty() { + let default: $field_type = $default; + #[allow(deprecated)] + if default != self.$field_name { + log::warn!($warn); + } + })? // Log warning if specified, and the value is not the default + ret } }, )* @@ -292,7 +300,7 @@ config_namespace! { /// concurrency. /// /// Defaults to the number of CPU cores on the system - pub target_partitions: usize, default = get_available_parallelism() + pub target_partitions: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism() /// The default time zone /// @@ -308,7 +316,7 @@ config_namespace! { /// This is mostly use to plan `UNION` children in parallel. /// /// Defaults to the number of CPU cores on the system - pub planning_concurrency: usize, default = get_available_parallelism() + pub planning_concurrency: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism() /// When set to true, skips verifying that the schema produced by /// planning the input of `LogicalPlan::Aggregate` exactly matches the @@ -451,6 +459,14 @@ config_namespace! { /// BLOB instead. pub binary_as_string: bool, default = false + /// (reading) If true, parquet reader will read columns of + /// physical type int96 as originating from a different resolution + /// than nanosecond. This is useful for reading data from systems like Spark + /// which stores microsecond resolution timestamps in an int96 allowing it + /// to write values with a larger date range than 64-bit timestamps with + /// nanosecond resolution. + pub coerce_int96: Option, transform = str::to_lowercase, default = None + // The following options affect writing to parquet files // and map to parquet::file::properties::WriterProperties @@ -723,6 +739,19 @@ config_namespace! { } } +impl ExecutionOptions { + /// Returns the correct parallelism based on the provided `value`. + /// If `value` is `"0"`, returns the default available parallelism, computed with + /// `get_available_parallelism`. Otherwise, returns `value`. + fn normalized_parallelism(value: &str) -> String { + if value.parse::() == Ok(0) { + get_available_parallelism().to_string() + } else { + value.to_owned() + } + } +} + /// A key value pair, with a corresponding description #[derive(Debug)] pub struct ConfigEntry { @@ -1999,8 +2028,8 @@ mod tests { use std::collections::HashMap; use crate::config::{ - ConfigEntry, ConfigExtension, ConfigFileType, ExtensionOptions, Extensions, - TableOptions, + ConfigEntry, ConfigExtension, ConfigField, ConfigFileType, ExtensionOptions, + Extensions, TableOptions, }; #[derive(Default, Debug, Clone)] @@ -2085,6 +2114,37 @@ mod tests { assert_eq!(table_config.csv.escape.unwrap() as char, '\''); } + #[test] + fn warning_only_not_default() { + use std::sync::atomic::AtomicUsize; + static COUNT: AtomicUsize = AtomicUsize::new(0); + use log::{Level, LevelFilter, Metadata, Record}; + struct SimpleLogger; + impl log::Log for SimpleLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Info + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + } + fn flush(&self) {} + } + log::set_logger(&SimpleLogger).unwrap(); + log::set_max_level(LevelFilter::Info); + let mut sql_parser_options = crate::config::SqlParserOptions::default(); + sql_parser_options + .set("enable_options_value_normalization", "false") + .unwrap(); + assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 0); + sql_parser_options + .set("enable_options_value_normalization", "true") + .unwrap(); + assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 1); + } + #[cfg(feature = "parquet")] #[test] fn parquet_table_options() { diff --git a/datafusion/common/src/dfschema.rs b/datafusion/common/src/dfschema.rs index 43d082f9dc936..66a26a18c0dc8 100644 --- a/datafusion/common/src/dfschema.rs +++ b/datafusion/common/src/dfschema.rs @@ -641,7 +641,7 @@ impl DFSchema { || (!DFSchema::datatype_is_semantically_equal( f1.data_type(), f2.data_type(), - ) && !can_cast_types(f2.data_type(), f1.data_type())) + )) { _plan_err!( "Schema mismatch: Expected field '{}' with type {:?}, \ @@ -659,9 +659,12 @@ impl DFSchema { } /// Checks if two [`DataType`]s are logically equal. This is a notably weaker constraint - /// than datatype_is_semantically_equal in that a Dictionary type is logically - /// equal to a plain V type, but not semantically equal. Dictionary is also - /// logically equal to Dictionary. + /// than datatype_is_semantically_equal in that different representations of same data can be + /// logically but not semantically equivalent. Semantically equivalent types are always also + /// logically equivalent. For example: + /// - a Dictionary type is logically equal to a plain V type + /// - a Dictionary is also logically equal to Dictionary + /// - Utf8 and Utf8View are logically equal pub fn datatype_is_logically_equal(dt1: &DataType, dt2: &DataType) -> bool { // check nested fields match (dt1, dt2) { @@ -711,12 +714,15 @@ impl DFSchema { .zip(iter2) .all(|((t1, f1), (t2, f2))| t1 == t2 && Self::field_is_logically_equal(f1, f2)) } - _ => dt1 == dt2, + // Utf8 and Utf8View are logically equivalent + (DataType::Utf8, DataType::Utf8View) => true, + (DataType::Utf8View, DataType::Utf8) => true, + _ => Self::datatype_is_semantically_equal(dt1, dt2), } } /// Returns true of two [`DataType`]s are semantically equal (same - /// name and type), ignoring both metadata and nullability. + /// name and type), ignoring both metadata and nullability, and decimal precision/scale. /// /// request to upstream: pub fn datatype_is_semantically_equal(dt1: &DataType, dt2: &DataType) -> bool { diff --git a/datafusion/common/src/file_options/parquet_writer.rs b/datafusion/common/src/file_options/parquet_writer.rs index 939cb5e1a3578..3e33466edf505 100644 --- a/datafusion/common/src/file_options/parquet_writer.rs +++ b/datafusion/common/src/file_options/parquet_writer.rs @@ -239,6 +239,7 @@ impl ParquetOptions { bloom_filter_on_read: _, // reads not used for writer props schema_force_view_types: _, binary_as_string: _, // not used for writer props + coerce_int96: _, // not used for writer props skip_arrow_metadata: _, } = self; @@ -516,6 +517,7 @@ mod tests { schema_force_view_types: defaults.schema_force_view_types, binary_as_string: defaults.binary_as_string, skip_arrow_metadata: defaults.skip_arrow_metadata, + coerce_int96: None, } } @@ -622,6 +624,7 @@ mod tests { schema_force_view_types: global_options_defaults.schema_force_view_types, binary_as_string: global_options_defaults.binary_as_string, skip_arrow_metadata: global_options_defaults.skip_arrow_metadata, + coerce_int96: None, }, column_specific_options, key_value_metadata, diff --git a/datafusion/common/src/functional_dependencies.rs b/datafusion/common/src/functional_dependencies.rs index 5f262d634af37..c4f2805f82856 100644 --- a/datafusion/common/src/functional_dependencies.rs +++ b/datafusion/common/src/functional_dependencies.rs @@ -47,11 +47,13 @@ impl Constraints { Constraints::new_unverified(vec![]) } - /// Create a new `Constraints` object from the given `constraints`. - /// Users should use the `empty` or `new_from_table_constraints` functions - /// for constructing `Constraints`. This constructor is for internal + /// Create a new [`Constraints`] object from the given `constraints`. + /// Users should use the [`Constraints::empty`] or [`SqlToRel::new_constraint_from_table_constraints`] functions + /// for constructing [`Constraints`]. This constructor is for internal /// purposes only and does not check whether the argument is valid. The user - /// is responsible for supplying a valid vector of `Constraint` objects. + /// is responsible for supplying a valid vector of [`Constraint`] objects. + /// + /// [`SqlToRel::new_constraint_from_table_constraints`]: https://docs.rs/datafusion/latest/datafusion/sql/planner/struct.SqlToRel.html#method.new_constraint_from_table_constraints pub fn new_unverified(constraints: Vec) -> Self { Self { inner: constraints } } diff --git a/datafusion/common/src/scalar/mod.rs b/datafusion/common/src/scalar/mod.rs index 2b758f4568760..b8d9aea810f03 100644 --- a/datafusion/common/src/scalar/mod.rs +++ b/datafusion/common/src/scalar/mod.rs @@ -27,7 +27,7 @@ use std::convert::Infallible; use std::fmt; use std::hash::Hash; use std::hash::Hasher; -use std::iter::repeat; +use std::iter::repeat_n; use std::mem::{size_of, size_of_val}; use std::str::FromStr; use std::sync::Arc; @@ -802,12 +802,14 @@ fn dict_from_scalar( let values_array = value.to_array_of_size(1)?; // Create a key array with `size` elements, each of 0 - let key_array: PrimitiveArray = repeat(if value.is_null() { - None - } else { - Some(K::default_value()) - }) - .take(size) + let key_array: PrimitiveArray = repeat_n( + if value.is_null() { + None + } else { + Some(K::default_value()) + }, + size, + ) .collect(); // create a new DictionaryArray @@ -2189,8 +2191,7 @@ impl ScalarValue { scale: i8, size: usize, ) -> Result { - Ok(repeat(value) - .take(size) + Ok(repeat_n(value, size) .collect::() .with_precision_and_scale(precision, scale)?) } @@ -2416,53 +2417,47 @@ impl ScalarValue { } ScalarValue::Utf8(e) => match e { Some(value) => { - Arc::new(StringArray::from_iter_values(repeat(value).take(size))) + Arc::new(StringArray::from_iter_values(repeat_n(value, size))) } None => new_null_array(&DataType::Utf8, size), }, ScalarValue::Utf8View(e) => match e { Some(value) => { - Arc::new(StringViewArray::from_iter_values(repeat(value).take(size))) + Arc::new(StringViewArray::from_iter_values(repeat_n(value, size))) } None => new_null_array(&DataType::Utf8View, size), }, ScalarValue::LargeUtf8(e) => match e { Some(value) => { - Arc::new(LargeStringArray::from_iter_values(repeat(value).take(size))) + Arc::new(LargeStringArray::from_iter_values(repeat_n(value, size))) } None => new_null_array(&DataType::LargeUtf8, size), }, ScalarValue::Binary(e) => match e { Some(value) => Arc::new( - repeat(Some(value.as_slice())) - .take(size) - .collect::(), + repeat_n(Some(value.as_slice()), size).collect::(), ), - None => { - Arc::new(repeat(None::<&str>).take(size).collect::()) - } + None => Arc::new(repeat_n(None::<&str>, size).collect::()), }, ScalarValue::BinaryView(e) => match e { Some(value) => Arc::new( - repeat(Some(value.as_slice())) - .take(size) - .collect::(), + repeat_n(Some(value.as_slice()), size).collect::(), ), None => { - Arc::new(repeat(None::<&str>).take(size).collect::()) + Arc::new(repeat_n(None::<&str>, size).collect::()) } }, ScalarValue::FixedSizeBinary(s, e) => match e { Some(value) => Arc::new( FixedSizeBinaryArray::try_from_sparse_iter_with_size( - repeat(Some(value.as_slice())).take(size), + repeat_n(Some(value.as_slice()), size), *s, ) .unwrap(), ), None => Arc::new( FixedSizeBinaryArray::try_from_sparse_iter_with_size( - repeat(None::<&[u8]>).take(size), + repeat_n(None::<&[u8]>, size), *s, ) .unwrap(), @@ -2470,15 +2465,11 @@ impl ScalarValue { }, ScalarValue::LargeBinary(e) => match e { Some(value) => Arc::new( - repeat(Some(value.as_slice())) - .take(size) - .collect::(), - ), - None => Arc::new( - repeat(None::<&str>) - .take(size) - .collect::(), + repeat_n(Some(value.as_slice()), size).collect::(), ), + None => { + Arc::new(repeat_n(None::<&str>, size).collect::()) + } }, ScalarValue::List(arr) => { Self::list_to_array_of_size(arr.as_ref() as &dyn Array, size)? @@ -2606,7 +2597,7 @@ impl ScalarValue { child_arrays.push(ar); new_fields.push(field.clone()); } - let type_ids = repeat(*v_id).take(size); + let type_ids = repeat_n(*v_id, size); let type_ids = ScalarBuffer::::from_iter(type_ids); let value_offsets = match mode { UnionMode::Sparse => None, @@ -2674,7 +2665,7 @@ impl ScalarValue { } fn list_to_array_of_size(arr: &dyn Array, size: usize) -> Result { - let arrays = repeat(arr).take(size).collect::>(); + let arrays = repeat_n(arr, size).collect::>(); let ret = match !arrays.is_empty() { true => arrow::compute::concat(arrays.as_slice())?, false => arr.slice(0, 0), @@ -3036,6 +3027,34 @@ impl ScalarValue { DataType::Timestamp(TimeUnit::Nanosecond, None), ) => ScalarValue::Int64(Some((float_ts * 1_000_000_000_f64).trunc() as i64)) .to_array()?, + ( + ScalarValue::Decimal128(Some(decimal_value), _, scale), + DataType::Timestamp(time_unit, None), + ) => { + let scale_factor = 10_i128.pow(*scale as u32); + let seconds = decimal_value / scale_factor; + let fraction = decimal_value % scale_factor; + + let timestamp_value = match time_unit { + TimeUnit::Second => ScalarValue::Int64(Some(seconds as i64)), + TimeUnit::Millisecond => { + let millis = seconds * 1_000 + (fraction * 1_000) / scale_factor; + ScalarValue::Int64(Some(millis as i64)) + } + TimeUnit::Microsecond => { + let micros = + seconds * 1_000_000 + (fraction * 1_000_000) / scale_factor; + ScalarValue::Int64(Some(micros as i64)) + } + TimeUnit::Nanosecond => { + let nanos = seconds * 1_000_000_000 + + (fraction * 1_000_000_000) / scale_factor; + ScalarValue::Int64(Some(nanos as i64)) + } + }; + + timestamp_value.to_array()? + } _ => self.to_array()?, }; diff --git a/datafusion/common/src/stats.rs b/datafusion/common/src/stats.rs index 5b841db53c5ee..807d885b3a4de 100644 --- a/datafusion/common/src/stats.rs +++ b/datafusion/common/src/stats.rs @@ -21,6 +21,7 @@ use std::fmt::{self, Debug, Display}; use crate::{Result, ScalarValue}; +use crate::error::_plan_err; use arrow::datatypes::{DataType, Schema, SchemaRef}; /// Represents a value with a degree of certainty. `Precision` is used to @@ -271,11 +272,25 @@ pub struct Statistics { pub num_rows: Precision, /// Total bytes of the table rows. pub total_byte_size: Precision, - /// Statistics on a column level. It contains a [`ColumnStatistics`] for - /// each field in the schema of the table to which the [`Statistics`] refer. + /// Statistics on a column level. + /// + /// It must contains a [`ColumnStatistics`] for each field in the schema of + /// the table to which the [`Statistics`] refer. pub column_statistics: Vec, } +impl Default for Statistics { + /// Returns a new [`Statistics`] instance with all fields set to unknown + /// and no columns. + fn default() -> Self { + Self { + num_rows: Precision::Absent, + total_byte_size: Precision::Absent, + column_statistics: vec![], + } + } +} + impl Statistics { /// Returns a [`Statistics`] instance for the given schema by assigning /// unknown statistics to each column in the schema. @@ -296,6 +311,24 @@ impl Statistics { .collect() } + /// Set the number of rows + pub fn with_num_rows(mut self, num_rows: Precision) -> Self { + self.num_rows = num_rows; + self + } + + /// Set the total size, in bytes + pub fn with_total_byte_size(mut self, total_byte_size: Precision) -> Self { + self.total_byte_size = total_byte_size; + self + } + + /// Add a column to the column statistics + pub fn add_column_statistics(mut self, column_stats: ColumnStatistics) -> Self { + self.column_statistics.push(column_stats); + self + } + /// If the exactness of a [`Statistics`] instance is lost, this function relaxes /// the exactness of all information by converting them [`Precision::Inexact`]. pub fn to_inexact(mut self) -> Self { @@ -351,7 +384,8 @@ impl Statistics { self } - /// Calculates the statistics after `fetch` and `skip` operations apply. + /// Calculates the statistics after applying `fetch` and `skip` operations. + /// /// Here, `self` denotes per-partition statistics. Use the `n_partitions` /// parameter to compute global statistics in a multi-partition setting. pub fn with_fetch( @@ -414,6 +448,100 @@ impl Statistics { self.total_byte_size = Precision::Absent; Ok(self) } + + /// Summarize zero or more statistics into a single `Statistics` instance. + /// + /// Returns an error if the statistics do not match the specified schemas. + pub fn try_merge_iter<'a, I>(items: I, schema: &Schema) -> Result + where + I: IntoIterator, + { + let mut items = items.into_iter(); + + let Some(init) = items.next() else { + return Ok(Statistics::new_unknown(schema)); + }; + items.try_fold(init.clone(), |acc: Statistics, item_stats: &Statistics| { + acc.try_merge(item_stats) + }) + } + + /// Merge this Statistics value with another Statistics value. + /// + /// Returns an error if the statistics do not match (different schemas). + /// + /// # Example + /// ``` + /// # use datafusion_common::{ColumnStatistics, ScalarValue, Statistics}; + /// # use arrow::datatypes::{Field, Schema, DataType}; + /// # use datafusion_common::stats::Precision; + /// let stats1 = Statistics::default() + /// .with_num_rows(Precision::Exact(1)) + /// .with_total_byte_size(Precision::Exact(2)) + /// .add_column_statistics(ColumnStatistics::new_unknown() + /// .with_null_count(Precision::Exact(3)) + /// .with_min_value(Precision::Exact(ScalarValue::from(4))) + /// .with_max_value(Precision::Exact(ScalarValue::from(5))) + /// ); + /// + /// let stats2 = Statistics::default() + /// .with_num_rows(Precision::Exact(10)) + /// .with_total_byte_size(Precision::Inexact(20)) + /// .add_column_statistics(ColumnStatistics::new_unknown() + /// // absent null count + /// .with_min_value(Precision::Exact(ScalarValue::from(40))) + /// .with_max_value(Precision::Exact(ScalarValue::from(50))) + /// ); + /// + /// let merged_stats = stats1.try_merge(&stats2).unwrap(); + /// let expected_stats = Statistics::default() + /// .with_num_rows(Precision::Exact(11)) + /// .with_total_byte_size(Precision::Inexact(22)) // inexact in stats2 --> inexact + /// .add_column_statistics( + /// ColumnStatistics::new_unknown() + /// .with_null_count(Precision::Absent) // missing from stats2 --> absent + /// .with_min_value(Precision::Exact(ScalarValue::from(4))) + /// .with_max_value(Precision::Exact(ScalarValue::from(50))) + /// ); + /// + /// assert_eq!(merged_stats, expected_stats) + /// ``` + pub fn try_merge(self, other: &Statistics) -> Result { + let Self { + mut num_rows, + mut total_byte_size, + mut column_statistics, + } = self; + + // Accumulate statistics for subsequent items + num_rows = num_rows.add(&other.num_rows); + total_byte_size = total_byte_size.add(&other.total_byte_size); + + if column_statistics.len() != other.column_statistics.len() { + return _plan_err!( + "Cannot merge statistics with different number of columns: {} vs {}", + column_statistics.len(), + other.column_statistics.len() + ); + } + + for (item_col_stats, col_stats) in other + .column_statistics + .iter() + .zip(column_statistics.iter_mut()) + { + col_stats.null_count = col_stats.null_count.add(&item_col_stats.null_count); + col_stats.max_value = col_stats.max_value.max(&item_col_stats.max_value); + col_stats.min_value = col_stats.min_value.min(&item_col_stats.min_value); + col_stats.sum_value = col_stats.sum_value.add(&item_col_stats.sum_value); + } + + Ok(Statistics { + num_rows, + total_byte_size, + column_statistics, + }) + } } /// Creates an estimate of the number of rows in the output using the given @@ -521,6 +649,36 @@ impl ColumnStatistics { } } + /// Set the null count + pub fn with_null_count(mut self, null_count: Precision) -> Self { + self.null_count = null_count; + self + } + + /// Set the max value + pub fn with_max_value(mut self, max_value: Precision) -> Self { + self.max_value = max_value; + self + } + + /// Set the min value + pub fn with_min_value(mut self, min_value: Precision) -> Self { + self.min_value = min_value; + self + } + + /// Set the sum value + pub fn with_sum_value(mut self, sum_value: Precision) -> Self { + self.sum_value = sum_value; + self + } + + /// Set the distinct count + pub fn with_distinct_count(mut self, distinct_count: Precision) -> Self { + self.distinct_count = distinct_count; + self + } + /// If the exactness of a [`ColumnStatistics`] instance is lost, this /// function relaxes the exactness of all information by converting them /// [`Precision::Inexact`]. @@ -537,6 +695,9 @@ impl ColumnStatistics { #[cfg(test)] mod tests { use super::*; + use crate::assert_contains; + use arrow::datatypes::Field; + use std::sync::Arc; #[test] fn test_get_value() { @@ -798,4 +959,193 @@ mod tests { distinct_count: Precision::Exact(100), } } + + #[test] + fn test_try_merge_basic() { + // Create a schema with two columns + let schema = Arc::new(Schema::new(vec![ + Field::new("col1", DataType::Int32, false), + Field::new("col2", DataType::Int32, false), + ])); + + // Create items with statistics + let stats1 = Statistics { + num_rows: Precision::Exact(10), + total_byte_size: Precision::Exact(100), + column_statistics: vec![ + ColumnStatistics { + null_count: Precision::Exact(1), + max_value: Precision::Exact(ScalarValue::Int32(Some(100))), + min_value: Precision::Exact(ScalarValue::Int32(Some(1))), + sum_value: Precision::Exact(ScalarValue::Int32(Some(500))), + distinct_count: Precision::Absent, + }, + ColumnStatistics { + null_count: Precision::Exact(2), + max_value: Precision::Exact(ScalarValue::Int32(Some(200))), + min_value: Precision::Exact(ScalarValue::Int32(Some(10))), + sum_value: Precision::Exact(ScalarValue::Int32(Some(1000))), + distinct_count: Precision::Absent, + }, + ], + }; + + let stats2 = Statistics { + num_rows: Precision::Exact(15), + total_byte_size: Precision::Exact(150), + column_statistics: vec![ + ColumnStatistics { + null_count: Precision::Exact(2), + max_value: Precision::Exact(ScalarValue::Int32(Some(120))), + min_value: Precision::Exact(ScalarValue::Int32(Some(-10))), + sum_value: Precision::Exact(ScalarValue::Int32(Some(600))), + distinct_count: Precision::Absent, + }, + ColumnStatistics { + null_count: Precision::Exact(3), + max_value: Precision::Exact(ScalarValue::Int32(Some(180))), + min_value: Precision::Exact(ScalarValue::Int32(Some(5))), + sum_value: Precision::Exact(ScalarValue::Int32(Some(1200))), + distinct_count: Precision::Absent, + }, + ], + }; + + let items = vec![stats1, stats2]; + + let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap(); + + // Verify the results + assert_eq!(summary_stats.num_rows, Precision::Exact(25)); // 10 + 15 + assert_eq!(summary_stats.total_byte_size, Precision::Exact(250)); // 100 + 150 + + // Verify column statistics + let col1_stats = &summary_stats.column_statistics[0]; + assert_eq!(col1_stats.null_count, Precision::Exact(3)); // 1 + 2 + assert_eq!( + col1_stats.max_value, + Precision::Exact(ScalarValue::Int32(Some(120))) + ); + assert_eq!( + col1_stats.min_value, + Precision::Exact(ScalarValue::Int32(Some(-10))) + ); + assert_eq!( + col1_stats.sum_value, + Precision::Exact(ScalarValue::Int32(Some(1100))) + ); // 500 + 600 + + let col2_stats = &summary_stats.column_statistics[1]; + assert_eq!(col2_stats.null_count, Precision::Exact(5)); // 2 + 3 + assert_eq!( + col2_stats.max_value, + Precision::Exact(ScalarValue::Int32(Some(200))) + ); + assert_eq!( + col2_stats.min_value, + Precision::Exact(ScalarValue::Int32(Some(5))) + ); + assert_eq!( + col2_stats.sum_value, + Precision::Exact(ScalarValue::Int32(Some(2200))) + ); // 1000 + 1200 + } + + #[test] + fn test_try_merge_mixed_precision() { + // Create a schema with one column + let schema = Arc::new(Schema::new(vec![Field::new( + "col1", + DataType::Int32, + false, + )])); + + // Create items with different precision levels + let stats1 = Statistics { + num_rows: Precision::Exact(10), + total_byte_size: Precision::Inexact(100), + column_statistics: vec![ColumnStatistics { + null_count: Precision::Exact(1), + max_value: Precision::Exact(ScalarValue::Int32(Some(100))), + min_value: Precision::Inexact(ScalarValue::Int32(Some(1))), + sum_value: Precision::Exact(ScalarValue::Int32(Some(500))), + distinct_count: Precision::Absent, + }], + }; + + let stats2 = Statistics { + num_rows: Precision::Inexact(15), + total_byte_size: Precision::Exact(150), + column_statistics: vec![ColumnStatistics { + null_count: Precision::Inexact(2), + max_value: Precision::Inexact(ScalarValue::Int32(Some(120))), + min_value: Precision::Exact(ScalarValue::Int32(Some(-10))), + sum_value: Precision::Absent, + distinct_count: Precision::Absent, + }], + }; + + let items = vec![stats1, stats2]; + + let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap(); + + assert_eq!(summary_stats.num_rows, Precision::Inexact(25)); + assert_eq!(summary_stats.total_byte_size, Precision::Inexact(250)); + + let col_stats = &summary_stats.column_statistics[0]; + assert_eq!(col_stats.null_count, Precision::Inexact(3)); + assert_eq!( + col_stats.max_value, + Precision::Inexact(ScalarValue::Int32(Some(120))) + ); + assert_eq!( + col_stats.min_value, + Precision::Inexact(ScalarValue::Int32(Some(-10))) + ); + assert!(matches!(col_stats.sum_value, Precision::Absent)); + } + + #[test] + fn test_try_merge_empty() { + let schema = Arc::new(Schema::new(vec![Field::new( + "col1", + DataType::Int32, + false, + )])); + + // Empty collection + let items: Vec = vec![]; + + let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap(); + + // Verify default values for empty collection + assert_eq!(summary_stats.num_rows, Precision::Absent); + assert_eq!(summary_stats.total_byte_size, Precision::Absent); + assert_eq!(summary_stats.column_statistics.len(), 1); + assert_eq!( + summary_stats.column_statistics[0].null_count, + Precision::Absent + ); + } + + #[test] + fn test_try_merge_mismatched_size() { + // Create a schema with one column + let schema = Arc::new(Schema::new(vec![Field::new( + "col1", + DataType::Int32, + false, + )])); + + // No column statistics + let stats1 = Statistics::default(); + + let stats2 = + Statistics::default().add_column_statistics(ColumnStatistics::new_unknown()); + + let items = vec![stats1, stats2]; + + let e = Statistics::try_merge_iter(&items, &schema).unwrap_err(); + assert_contains!(e.to_string(), "Error during planning: Cannot merge statistics with different number of columns: 0 vs 1"); + } } diff --git a/datafusion/common/src/utils/memory.rs b/datafusion/common/src/utils/memory.rs index ab73996fcd8b7..7ac081e0beb84 100644 --- a/datafusion/common/src/utils/memory.rs +++ b/datafusion/common/src/utils/memory.rs @@ -25,7 +25,7 @@ use std::mem::size_of; /// # Parameters /// - `num_elements`: The number of elements expected in the hash table. /// - `fixed_size`: A fixed overhead size associated with the collection -/// (e.g., HashSet or HashTable). +/// (e.g., HashSet or HashTable). /// - `T`: The type of elements stored in the hash table. /// /// # Details diff --git a/datafusion/core/Cargo.toml b/datafusion/core/Cargo.toml index 56698e4d7e255..edc0d34b539ac 100644 --- a/datafusion/core/Cargo.toml +++ b/datafusion/core/Cargo.toml @@ -125,7 +125,7 @@ datafusion-physical-optimizer = { workspace = true } datafusion-physical-plan = { workspace = true } datafusion-session = { workspace = true } datafusion-sql = { workspace = true } -flate2 = { version = "1.1.0", optional = true } +flate2 = { version = "1.1.1", optional = true } futures = { workspace = true } itertools = { workspace = true } log = { workspace = true } @@ -160,7 +160,7 @@ rand_distr = "0.4.3" regex = { workspace = true } rstest = { workspace = true } serde_json = { workspace = true } -sysinfo = "0.33.1" +sysinfo = "0.34.2" test-utils = { path = "../../test-utils" } tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot", "fs"] } diff --git a/datafusion/core/benches/aggregate_query_sql.rs b/datafusion/core/benches/aggregate_query_sql.rs index ebe94450c1f8d..057a0e1d1b54c 100644 --- a/datafusion/core/benches/aggregate_query_sql.rs +++ b/datafusion/core/benches/aggregate_query_sql.rs @@ -29,8 +29,7 @@ use parking_lot::Mutex; use std::sync::Arc; use tokio::runtime::Runtime; -fn query(ctx: Arc>, sql: &str) { - let rt = Runtime::new().unwrap(); +fn query(ctx: Arc>, rt: &Runtime, sql: &str) { let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -51,11 +50,13 @@ fn criterion_benchmark(c: &mut Criterion) { let array_len = 32768 * 2; // 2^16 let batch_size = 2048; // 2^11 let ctx = create_context(partitions_len, array_len, batch_size).unwrap(); + let rt = Runtime::new().unwrap(); c.bench_function("aggregate_query_no_group_by 15 12", |b| { b.iter(|| { query( ctx.clone(), + &rt, "SELECT MIN(f64), AVG(f64), COUNT(f64) \ FROM t", ) @@ -66,6 +67,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT MIN(f64), MAX(f64) \ FROM t", ) @@ -76,6 +78,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT COUNT(DISTINCT u64_wide) \ FROM t", ) @@ -86,6 +89,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT COUNT(DISTINCT u64_narrow) \ FROM t", ) @@ -96,6 +100,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT utf8, MIN(f64), AVG(f64), COUNT(f64) \ FROM t GROUP BY utf8", ) @@ -106,6 +111,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT utf8, MIN(f64), AVG(f64), COUNT(f64) \ FROM t \ WHERE f32 > 10 AND f32 < 20 GROUP BY utf8", @@ -117,6 +123,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT u64_narrow, MIN(f64), AVG(f64), COUNT(f64) \ FROM t GROUP BY u64_narrow", ) @@ -127,6 +134,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT u64_narrow, MIN(f64), AVG(f64), COUNT(f64) \ FROM t \ WHERE f32 > 10 AND f32 < 20 GROUP BY u64_narrow", @@ -138,6 +146,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT u64_wide, utf8, MIN(f64), AVG(f64), COUNT(f64) \ FROM t GROUP BY u64_wide, utf8", ) @@ -148,7 +157,8 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - "SELECT utf8, approx_percentile_cont(u64_wide, 0.5, 2500) \ + &rt, + "SELECT utf8, approx_percentile_cont(0.5, 2500) WITHIN GROUP (ORDER BY u64_wide) \ FROM t GROUP BY utf8", ) }) @@ -158,7 +168,8 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - "SELECT utf8, approx_percentile_cont(f32, 0.5, 2500) \ + &rt, + "SELECT utf8, approx_percentile_cont(0.5, 2500) WITHIN GROUP (ORDER BY f32) \ FROM t GROUP BY utf8", ) }) @@ -168,6 +179,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT MEDIAN(DISTINCT u64_wide), MEDIAN(DISTINCT u64_narrow) \ FROM t", ) @@ -178,6 +190,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT first_value(u64_wide order by f64, u64_narrow, utf8),\ last_value(u64_wide order by f64, u64_narrow, utf8) \ FROM t GROUP BY u64_narrow", @@ -189,6 +202,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT first_value(u64_wide ignore nulls order by f64, u64_narrow, utf8), \ last_value(u64_wide ignore nulls order by f64, u64_narrow, utf8) \ FROM t GROUP BY u64_narrow", @@ -200,6 +214,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT first_value(u64_wide order by f64), \ last_value(u64_wide order by f64) \ FROM t GROUP BY u64_narrow", diff --git a/datafusion/core/benches/csv_load.rs b/datafusion/core/benches/csv_load.rs index 2d42121ec9b25..3f984757466d5 100644 --- a/datafusion/core/benches/csv_load.rs +++ b/datafusion/core/benches/csv_load.rs @@ -32,8 +32,12 @@ use std::time::Duration; use test_utils::AccessLogGenerator; use tokio::runtime::Runtime; -fn load_csv(ctx: Arc>, path: &str, options: CsvReadOptions) { - let rt = Runtime::new().unwrap(); +fn load_csv( + ctx: Arc>, + rt: &Runtime, + path: &str, + options: CsvReadOptions, +) { let df = rt.block_on(ctx.lock().read_csv(path, options)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -61,6 +65,7 @@ fn generate_test_file() -> TestCsvFile { fn criterion_benchmark(c: &mut Criterion) { let ctx = create_context().unwrap(); + let rt = Runtime::new().unwrap(); let test_file = generate_test_file(); let mut group = c.benchmark_group("load csv testing"); @@ -70,6 +75,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { load_csv( ctx.clone(), + &rt, test_file.path().to_str().unwrap(), CsvReadOptions::default(), ) @@ -80,6 +86,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { load_csv( ctx.clone(), + &rt, test_file.path().to_str().unwrap(), CsvReadOptions::default().null_regex(Some("^NULL$|^$".to_string())), ) diff --git a/datafusion/core/benches/data_utils/mod.rs b/datafusion/core/benches/data_utils/mod.rs index 38f6a2c76df6d..fc5f8945c4392 100644 --- a/datafusion/core/benches/data_utils/mod.rs +++ b/datafusion/core/benches/data_utils/mod.rs @@ -19,7 +19,8 @@ use arrow::array::{ builder::{Int64Builder, StringBuilder}, - Float32Array, Float64Array, RecordBatch, StringArray, UInt64Array, + ArrayRef, Float32Array, Float64Array, RecordBatch, StringArray, StringViewBuilder, + UInt64Array, }; use arrow::datatypes::{DataType, Field, Schema, SchemaRef}; use datafusion::datasource::MemTable; @@ -158,6 +159,31 @@ pub fn create_record_batches( .collect::>() } +/// An enum that wraps either a regular StringBuilder or a GenericByteViewBuilder +/// so that both can be used interchangeably. +enum TraceIdBuilder { + Utf8(StringBuilder), + Utf8View(StringViewBuilder), +} + +impl TraceIdBuilder { + /// Append a value to the builder. + fn append_value(&mut self, value: &str) { + match self { + TraceIdBuilder::Utf8(builder) => builder.append_value(value), + TraceIdBuilder::Utf8View(builder) => builder.append_value(value), + } + } + + /// Finish building and return the ArrayRef. + fn finish(self) -> ArrayRef { + match self { + TraceIdBuilder::Utf8(mut builder) => Arc::new(builder.finish()), + TraceIdBuilder::Utf8View(mut builder) => Arc::new(builder.finish()), + } + } +} + /// Create time series data with `partition_cnt` partitions and `sample_cnt` rows per partition /// in ascending order, if `asc` is true, otherwise randomly sampled using a Pareto distribution #[allow(dead_code)] @@ -165,6 +191,7 @@ pub(crate) fn make_data( partition_cnt: i32, sample_cnt: i32, asc: bool, + use_view: bool, ) -> Result<(Arc, Vec>), DataFusionError> { // constants observed from trace data let simultaneous_group_cnt = 2000; @@ -177,11 +204,17 @@ pub(crate) fn make_data( let mut rng = rand::rngs::SmallRng::from_seed([0; 32]); // populate data - let schema = test_schema(); + let schema = test_schema(use_view); let mut partitions = vec![]; let mut cur_time = 16909000000000i64; for _ in 0..partition_cnt { - let mut id_builder = StringBuilder::new(); + // Choose the appropriate builder based on use_view. + let mut id_builder = if use_view { + TraceIdBuilder::Utf8View(StringViewBuilder::new()) + } else { + TraceIdBuilder::Utf8(StringBuilder::new()) + }; + let mut ts_builder = Int64Builder::new(); let gen_id = |rng: &mut rand::rngs::SmallRng| { rng.gen::<[u8; 16]>() @@ -230,10 +263,19 @@ pub(crate) fn make_data( Ok((schema, partitions)) } -/// The Schema used by make_data -fn test_schema() -> SchemaRef { - Arc::new(Schema::new(vec![ - Field::new("trace_id", DataType::Utf8, false), - Field::new("timestamp_ms", DataType::Int64, false), - ])) +/// Returns a Schema based on the use_view flag +fn test_schema(use_view: bool) -> SchemaRef { + if use_view { + // Return Utf8View schema + Arc::new(Schema::new(vec![ + Field::new("trace_id", DataType::Utf8View, false), + Field::new("timestamp_ms", DataType::Int64, false), + ])) + } else { + // Return regular Utf8 schema + Arc::new(Schema::new(vec![ + Field::new("trace_id", DataType::Utf8, false), + Field::new("timestamp_ms", DataType::Int64, false), + ])) + } } diff --git a/datafusion/core/benches/dataframe.rs b/datafusion/core/benches/dataframe.rs index 03078e05e1054..832553ebed82a 100644 --- a/datafusion/core/benches/dataframe.rs +++ b/datafusion/core/benches/dataframe.rs @@ -44,9 +44,7 @@ fn create_context(field_count: u32) -> datafusion_common::Result) { - let rt = Runtime::new().unwrap(); - +fn run(column_count: u32, ctx: Arc, rt: &Runtime) { criterion::black_box(rt.block_on(async { let mut data_frame = ctx.table("t").await.unwrap(); @@ -67,11 +65,13 @@ fn run(column_count: u32, ctx: Arc) { } fn criterion_benchmark(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + for column_count in [10, 100, 200, 500] { let ctx = create_context(column_count).unwrap(); c.bench_function(&format!("with_column_{column_count}"), |b| { - b.iter(|| run(column_count, ctx.clone())) + b.iter(|| run(column_count, ctx.clone(), &rt)) }); } } diff --git a/datafusion/core/benches/distinct_query_sql.rs b/datafusion/core/benches/distinct_query_sql.rs index c242798a56f00..c7056aab86897 100644 --- a/datafusion/core/benches/distinct_query_sql.rs +++ b/datafusion/core/benches/distinct_query_sql.rs @@ -33,8 +33,7 @@ use parking_lot::Mutex; use std::{sync::Arc, time::Duration}; use tokio::runtime::Runtime; -fn query(ctx: Arc>, sql: &str) { - let rt = Runtime::new().unwrap(); +fn query(ctx: Arc>, rt: &Runtime, sql: &str) { let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -55,6 +54,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { let array_len = 1 << 26; // 64 M let batch_size = 8192; let ctx = create_context(partitions_len, array_len, batch_size).unwrap(); + let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("custom-measurement-time"); group.measurement_time(Duration::from_secs(40)); @@ -63,6 +63,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10", ) }) @@ -72,6 +73,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 100", ) }) @@ -81,6 +83,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 1000", ) }) @@ -90,6 +93,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10000", ) }) @@ -99,6 +103,7 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT u64_narrow, u64_wide, utf8, f64 FROM t GROUP BY 1, 2, 3, 4 LIMIT 10", ) }) @@ -118,12 +123,9 @@ async fn distinct_with_limit( Ok(()) } -fn run(plan: Arc, ctx: Arc) { - let rt = Runtime::new().unwrap(); - criterion::black_box( - rt.block_on(async { distinct_with_limit(plan.clone(), ctx.clone()).await }), - ) - .unwrap(); +fn run(rt: &Runtime, plan: Arc, ctx: Arc) { + criterion::black_box(rt.block_on(distinct_with_limit(plan.clone(), ctx.clone()))) + .unwrap(); } pub async fn create_context_sampled_data( @@ -131,7 +133,8 @@ pub async fn create_context_sampled_data( partition_cnt: i32, sample_cnt: i32, ) -> Result<(Arc, Arc)> { - let (schema, parts) = make_data(partition_cnt, sample_cnt, false /* asc */).unwrap(); + let (schema, parts) = + make_data(partition_cnt, sample_cnt, false /* asc */, false).unwrap(); let mem_table = Arc::new(MemTable::try_new(schema, parts).unwrap()); // Create the DataFrame @@ -145,58 +148,47 @@ pub async fn create_context_sampled_data( fn criterion_benchmark_limited_distinct_sampled(c: &mut Criterion) { let rt = Runtime::new().unwrap(); - let limit = 10; let partitions = 100; let samples = 100_000; let sql = format!("select DISTINCT trace_id from traces group by trace_id limit {limit};"); - - let distinct_trace_id_100_partitions_100_000_samples_limit_100 = rt.block_on(async { - create_context_sampled_data(sql.as_str(), partitions, samples) - .await - .unwrap() - }); - c.bench_function( format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(), - |b| b.iter(|| run(distinct_trace_id_100_partitions_100_000_samples_limit_100.0.clone(), - distinct_trace_id_100_partitions_100_000_samples_limit_100.1.clone())), + |b| b.iter(|| { + let (plan, ctx) = rt.block_on( + create_context_sampled_data(sql.as_str(), partitions, samples) + ).unwrap(); + run(&rt, plan.clone(), ctx.clone()) + }), ); let partitions = 10; let samples = 1_000_000; let sql = format!("select DISTINCT trace_id from traces group by trace_id limit {limit};"); - - let distinct_trace_id_10_partitions_1_000_000_samples_limit_10 = rt.block_on(async { - create_context_sampled_data(sql.as_str(), partitions, samples) - .await - .unwrap() - }); - c.bench_function( format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(), - |b| b.iter(|| run(distinct_trace_id_10_partitions_1_000_000_samples_limit_10.0.clone(), - distinct_trace_id_10_partitions_1_000_000_samples_limit_10.1.clone())), + |b| b.iter(|| { + let (plan, ctx) = rt.block_on( + create_context_sampled_data(sql.as_str(), partitions, samples) + ).unwrap(); + run(&rt, plan.clone(), ctx.clone()) + }), ); let partitions = 1; let samples = 10_000_000; let sql = format!("select DISTINCT trace_id from traces group by trace_id limit {limit};"); - - let rt = Runtime::new().unwrap(); - let distinct_trace_id_1_partition_10_000_000_samples_limit_10 = rt.block_on(async { - create_context_sampled_data(sql.as_str(), partitions, samples) - .await - .unwrap() - }); - c.bench_function( format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(), - |b| b.iter(|| run(distinct_trace_id_1_partition_10_000_000_samples_limit_10.0.clone(), - distinct_trace_id_1_partition_10_000_000_samples_limit_10.1.clone())), + |b| b.iter(|| { + let (plan, ctx) = rt.block_on( + create_context_sampled_data(sql.as_str(), partitions, samples) + ).unwrap(); + run(&rt, plan.clone(), ctx.clone()) + }), ); } diff --git a/datafusion/core/benches/filter_query_sql.rs b/datafusion/core/benches/filter_query_sql.rs index 0e09ae09d7c2e..c82a1607184dc 100644 --- a/datafusion/core/benches/filter_query_sql.rs +++ b/datafusion/core/benches/filter_query_sql.rs @@ -27,9 +27,7 @@ use futures::executor::block_on; use std::sync::Arc; use tokio::runtime::Runtime; -async fn query(ctx: &SessionContext, sql: &str) { - let rt = Runtime::new().unwrap(); - +async fn query(ctx: &SessionContext, rt: &Runtime, sql: &str) { // execute the query let df = rt.block_on(ctx.sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); @@ -68,10 +66,11 @@ fn create_context(array_len: usize, batch_size: usize) -> Result fn criterion_benchmark(c: &mut Criterion) { let array_len = 524_288; // 2^19 let batch_size = 4096; // 2^12 + let rt = Runtime::new().unwrap(); c.bench_function("filter_array", |b| { let ctx = create_context(array_len, batch_size).unwrap(); - b.iter(|| block_on(query(&ctx, "select f32, f64 from t where f32 >= f64"))) + b.iter(|| block_on(query(&ctx, &rt, "select f32, f64 from t where f32 >= f64"))) }); c.bench_function("filter_scalar", |b| { @@ -79,6 +78,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { block_on(query( &ctx, + &rt, "select f32, f64 from t where f32 >= 250 and f64 > 250", )) }) @@ -89,6 +89,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { block_on(query( &ctx, + &rt, "select f32, f64 from t where f32 in (10, 20, 30, 40)", )) }) diff --git a/datafusion/core/benches/math_query_sql.rs b/datafusion/core/benches/math_query_sql.rs index 92c59d5066401..76824850c114c 100644 --- a/datafusion/core/benches/math_query_sql.rs +++ b/datafusion/core/benches/math_query_sql.rs @@ -36,9 +36,7 @@ use datafusion::datasource::MemTable; use datafusion::error::Result; use datafusion::execution::context::SessionContext; -fn query(ctx: Arc>, sql: &str) { - let rt = Runtime::new().unwrap(); - +fn query(ctx: Arc>, rt: &Runtime, sql: &str) { // execute the query let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); rt.block_on(df.collect()).unwrap(); @@ -81,29 +79,31 @@ fn criterion_benchmark(c: &mut Criterion) { let array_len = 1048576; // 2^20 let batch_size = 512; // 2^9 let ctx = create_context(array_len, batch_size).unwrap(); + let rt = Runtime::new().unwrap(); + c.bench_function("sqrt_20_9", |b| { - b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) }); let array_len = 1048576; // 2^20 let batch_size = 4096; // 2^12 let ctx = create_context(array_len, batch_size).unwrap(); c.bench_function("sqrt_20_12", |b| { - b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) }); let array_len = 4194304; // 2^22 let batch_size = 4096; // 2^12 let ctx = create_context(array_len, batch_size).unwrap(); c.bench_function("sqrt_22_12", |b| { - b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) }); let array_len = 4194304; // 2^22 let batch_size = 16384; // 2^14 let ctx = create_context(array_len, batch_size).unwrap(); c.bench_function("sqrt_22_14", |b| { - b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) }); } diff --git a/datafusion/core/benches/physical_plan.rs b/datafusion/core/benches/physical_plan.rs index aae1457ab9e6d..0a65c52f72def 100644 --- a/datafusion/core/benches/physical_plan.rs +++ b/datafusion/core/benches/physical_plan.rs @@ -42,6 +42,7 @@ use datafusion_physical_expr_common::sort_expr::LexOrdering; // as inputs. All record batches must have the same schema. fn sort_preserving_merge_operator( session_ctx: Arc, + rt: &Runtime, batches: Vec, sort: &[&str], ) { @@ -63,7 +64,6 @@ fn sort_preserving_merge_operator( .unwrap(); let merge = Arc::new(SortPreservingMergeExec::new(sort, exec)); let task_ctx = session_ctx.task_ctx(); - let rt = Runtime::new().unwrap(); rt.block_on(collect(merge, task_ctx)).unwrap(); } @@ -166,14 +166,16 @@ fn criterion_benchmark(c: &mut Criterion) { ]; let ctx = Arc::new(SessionContext::new()); + let rt = Runtime::new().unwrap(); + for (name, input) in benches { - let ctx_clone = ctx.clone(); - c.bench_function(name, move |b| { + c.bench_function(name, |b| { b.iter_batched( || input.clone(), |input| { sort_preserving_merge_operator( - ctx_clone.clone(), + ctx.clone(), + &rt, input, &["a", "b", "c", "d"], ); diff --git a/datafusion/core/benches/sort_limit_query_sql.rs b/datafusion/core/benches/sort_limit_query_sql.rs index cfd4b8bc4bba8..e535a018161f1 100644 --- a/datafusion/core/benches/sort_limit_query_sql.rs +++ b/datafusion/core/benches/sort_limit_query_sql.rs @@ -37,9 +37,7 @@ use datafusion::execution::context::SessionContext; use tokio::runtime::Runtime; -fn query(ctx: Arc>, sql: &str) { - let rt = Runtime::new().unwrap(); - +fn query(ctx: Arc>, rt: &Runtime, sql: &str) { // execute the query let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); rt.block_on(df.collect()).unwrap(); @@ -104,11 +102,14 @@ fn create_context() -> Arc> { } fn criterion_benchmark(c: &mut Criterion) { + let ctx = create_context(); + let rt = Runtime::new().unwrap(); + c.bench_function("sort_and_limit_by_int", |b| { - let ctx = create_context(); b.iter(|| { query( ctx.clone(), + &rt, "SELECT c1, c13, c6, c10 \ FROM aggregate_test_100 \ ORDER BY c6 @@ -118,10 +119,10 @@ fn criterion_benchmark(c: &mut Criterion) { }); c.bench_function("sort_and_limit_by_float", |b| { - let ctx = create_context(); b.iter(|| { query( ctx.clone(), + &rt, "SELECT c1, c13, c12 \ FROM aggregate_test_100 \ ORDER BY c13 @@ -131,10 +132,10 @@ fn criterion_benchmark(c: &mut Criterion) { }); c.bench_function("sort_and_limit_lex_by_int", |b| { - let ctx = create_context(); b.iter(|| { query( ctx.clone(), + &rt, "SELECT c1, c13, c6, c10 \ FROM aggregate_test_100 \ ORDER BY c6 DESC, c10 DESC @@ -144,10 +145,10 @@ fn criterion_benchmark(c: &mut Criterion) { }); c.bench_function("sort_and_limit_lex_by_string", |b| { - let ctx = create_context(); b.iter(|| { query( ctx.clone(), + &rt, "SELECT c1, c13, c6, c10 \ FROM aggregate_test_100 \ ORDER BY c1, c13 diff --git a/datafusion/core/benches/sql_planner.rs b/datafusion/core/benches/sql_planner.rs index 2d79778d4d42f..49cc830d58bc4 100644 --- a/datafusion/core/benches/sql_planner.rs +++ b/datafusion/core/benches/sql_planner.rs @@ -45,14 +45,12 @@ const BENCHMARKS_PATH_2: &str = "./benchmarks/"; const CLICKBENCH_DATA_PATH: &str = "data/hits_partitioned/"; /// Create a logical plan from the specified sql -fn logical_plan(ctx: &SessionContext, sql: &str) { - let rt = Runtime::new().unwrap(); +fn logical_plan(ctx: &SessionContext, rt: &Runtime, sql: &str) { criterion::black_box(rt.block_on(ctx.sql(sql)).unwrap()); } /// Create a physical ExecutionPlan (by way of logical plan) -fn physical_plan(ctx: &SessionContext, sql: &str) { - let rt = Runtime::new().unwrap(); +fn physical_plan(ctx: &SessionContext, rt: &Runtime, sql: &str) { criterion::black_box(rt.block_on(async { ctx.sql(sql) .await @@ -104,9 +102,8 @@ fn register_defs(ctx: SessionContext, defs: Vec) -> SessionContext { ctx } -fn register_clickbench_hits_table() -> SessionContext { +fn register_clickbench_hits_table(rt: &Runtime) -> SessionContext { let ctx = SessionContext::new(); - let rt = Runtime::new().unwrap(); // use an external table for clickbench benchmarks let path = @@ -128,7 +125,11 @@ fn register_clickbench_hits_table() -> SessionContext { /// Target of this benchmark: control that placeholders replacing does not get slower, /// if the query does not contain placeholders at all. -fn benchmark_with_param_values_many_columns(ctx: &SessionContext, b: &mut Bencher) { +fn benchmark_with_param_values_many_columns( + ctx: &SessionContext, + rt: &Runtime, + b: &mut Bencher, +) { const COLUMNS_NUM: usize = 200; let mut aggregates = String::new(); for i in 0..COLUMNS_NUM { @@ -140,7 +141,6 @@ fn benchmark_with_param_values_many_columns(ctx: &SessionContext, b: &mut Benche // SELECT max(attr0), ..., max(attrN) FROM t1. let query = format!("SELECT {} FROM t1", aggregates); let statement = ctx.state().sql_to_statement(&query, "Generic").unwrap(); - let rt = Runtime::new().unwrap(); let plan = rt.block_on(async { ctx.state().statement_to_plan(statement).await.unwrap() }); b.iter(|| { @@ -230,33 +230,35 @@ fn criterion_benchmark(c: &mut Criterion) { } let ctx = create_context(); + let rt = Runtime::new().unwrap(); // Test simplest // https://github.com/apache/datafusion/issues/5157 c.bench_function("logical_select_one_from_700", |b| { - b.iter(|| logical_plan(&ctx, "SELECT c1 FROM t700")) + b.iter(|| logical_plan(&ctx, &rt, "SELECT c1 FROM t700")) }); // Test simplest // https://github.com/apache/datafusion/issues/5157 c.bench_function("physical_select_one_from_700", |b| { - b.iter(|| physical_plan(&ctx, "SELECT c1 FROM t700")) + b.iter(|| physical_plan(&ctx, &rt, "SELECT c1 FROM t700")) }); // Test simplest c.bench_function("logical_select_all_from_1000", |b| { - b.iter(|| logical_plan(&ctx, "SELECT * FROM t1000")) + b.iter(|| logical_plan(&ctx, &rt, "SELECT * FROM t1000")) }); // Test simplest c.bench_function("physical_select_all_from_1000", |b| { - b.iter(|| physical_plan(&ctx, "SELECT * FROM t1000")) + b.iter(|| physical_plan(&ctx, &rt, "SELECT * FROM t1000")) }); c.bench_function("logical_trivial_join_low_numbered_columns", |b| { b.iter(|| { logical_plan( &ctx, + &rt, "SELECT t1.a2, t2.b2 \ FROM t1, t2 WHERE a1 = b1", ) @@ -267,6 +269,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { logical_plan( &ctx, + &rt, "SELECT t1.a99, t2.b99 \ FROM t1, t2 WHERE a199 = b199", ) @@ -277,6 +280,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { logical_plan( &ctx, + &rt, "SELECT t1.a99, MIN(t2.b1), MAX(t2.b199), AVG(t2.b123), COUNT(t2.b73) \ FROM t1 JOIN t2 ON t1.a199 = t2.b199 GROUP BY t1.a99", ) @@ -293,7 +297,7 @@ fn criterion_benchmark(c: &mut Criterion) { } let query = format!("SELECT {} FROM t1", aggregates); b.iter(|| { - physical_plan(&ctx, &query); + physical_plan(&ctx, &rt, &query); }); }); @@ -302,6 +306,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, + &rt, "SELECT t1.a7, t2.b8 \ FROM t1, t2 WHERE a7 = b7 \ ORDER BY a7", @@ -313,6 +318,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, + &rt, "SELECT t1.a7, t2.b8 \ FROM t1, t2 WHERE a7 < b7 \ ORDER BY a7", @@ -324,6 +330,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, + &rt, "SELECT ta.a9, tb.a10, tc.a11, td.a12, te.a13, tf.a14 \ FROM t1 AS ta, t1 AS tb, t1 AS tc, t1 AS td, t1 AS te, t1 AS tf \ WHERE ta.a9 = tb.a10 AND tb.a10 = tc.a11 AND tc.a11 = td.a12 AND \ @@ -336,6 +343,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, + &rt, "SELECT t1.a7 \ FROM t1 WHERE a7 = (SELECT b8 FROM t2)", ); @@ -346,6 +354,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, + &rt, "SELECT t1.a7 FROM t1 \ INTERSECT SELECT t2.b8 FROM t2", ); @@ -356,6 +365,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { logical_plan( &ctx, + &rt, "SELECT DISTINCT t1.a7 \ FROM t1, t2 WHERE t1.a7 = t2.b8", ); @@ -370,7 +380,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_sorted_union_orderby", |b| { // SELECT ... UNION ALL ... let query = union_orderby_query(20); - b.iter(|| physical_plan(&ctx, &query)) + b.iter(|| physical_plan(&ctx, &rt, &query)) }); // --- TPC-H --- @@ -393,7 +403,7 @@ fn criterion_benchmark(c: &mut Criterion) { let sql = std::fs::read_to_string(format!("{benchmarks_path}queries/{q}.sql")).unwrap(); c.bench_function(&format!("physical_plan_tpch_{}", q), |b| { - b.iter(|| physical_plan(&tpch_ctx, &sql)) + b.iter(|| physical_plan(&tpch_ctx, &rt, &sql)) }); } @@ -407,7 +417,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_plan_tpch_all", |b| { b.iter(|| { for sql in &all_tpch_sql_queries { - physical_plan(&tpch_ctx, sql) + physical_plan(&tpch_ctx, &rt, sql) } }) }); @@ -442,7 +452,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_plan_tpcds_all", |b| { b.iter(|| { for sql in &all_tpcds_sql_queries { - physical_plan(&tpcds_ctx, sql) + physical_plan(&tpcds_ctx, &rt, sql) } }) }); @@ -468,7 +478,7 @@ fn criterion_benchmark(c: &mut Criterion) { .map(|l| l.expect("Could not parse line")) .collect_vec(); - let clickbench_ctx = register_clickbench_hits_table(); + let clickbench_ctx = register_clickbench_hits_table(&rt); // for (i, sql) in clickbench_queries.iter().enumerate() { // c.bench_function(&format!("logical_plan_clickbench_q{}", i + 1), |b| { @@ -478,7 +488,7 @@ fn criterion_benchmark(c: &mut Criterion) { for (i, sql) in clickbench_queries.iter().enumerate() { c.bench_function(&format!("physical_plan_clickbench_q{}", i + 1), |b| { - b.iter(|| physical_plan(&clickbench_ctx, sql)) + b.iter(|| physical_plan(&clickbench_ctx, &rt, sql)) }); } @@ -493,13 +503,13 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_plan_clickbench_all", |b| { b.iter(|| { for sql in &clickbench_queries { - physical_plan(&clickbench_ctx, sql) + physical_plan(&clickbench_ctx, &rt, sql) } }) }); c.bench_function("with_param_values_many_columns", |b| { - benchmark_with_param_values_many_columns(&ctx, b); + benchmark_with_param_values_many_columns(&ctx, &rt, b); }); } diff --git a/datafusion/core/benches/struct_query_sql.rs b/datafusion/core/benches/struct_query_sql.rs index 3ef7292c66271..f9cc43d1ea2c5 100644 --- a/datafusion/core/benches/struct_query_sql.rs +++ b/datafusion/core/benches/struct_query_sql.rs @@ -27,9 +27,7 @@ use futures::executor::block_on; use std::sync::Arc; use tokio::runtime::Runtime; -async fn query(ctx: &SessionContext, sql: &str) { - let rt = Runtime::new().unwrap(); - +async fn query(ctx: &SessionContext, rt: &Runtime, sql: &str) { // execute the query let df = rt.block_on(ctx.sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); @@ -68,10 +66,11 @@ fn create_context(array_len: usize, batch_size: usize) -> Result fn criterion_benchmark(c: &mut Criterion) { let array_len = 524_288; // 2^19 let batch_size = 4096; // 2^12 + let ctx = create_context(array_len, batch_size).unwrap(); + let rt = Runtime::new().unwrap(); c.bench_function("struct", |b| { - let ctx = create_context(array_len, batch_size).unwrap(); - b.iter(|| block_on(query(&ctx, "select struct(f32, f64) from t"))) + b.iter(|| block_on(query(&ctx, &rt, "select struct(f32, f64) from t"))) }); } diff --git a/datafusion/core/benches/topk_aggregate.rs b/datafusion/core/benches/topk_aggregate.rs index 922cbd2b42292..cf3c7fa2e26fe 100644 --- a/datafusion/core/benches/topk_aggregate.rs +++ b/datafusion/core/benches/topk_aggregate.rs @@ -33,8 +33,9 @@ async fn create_context( sample_cnt: i32, asc: bool, use_topk: bool, + use_view: bool, ) -> Result<(Arc, Arc)> { - let (schema, parts) = make_data(partition_cnt, sample_cnt, asc).unwrap(); + let (schema, parts) = make_data(partition_cnt, sample_cnt, asc, use_view).unwrap(); let mem_table = Arc::new(MemTable::try_new(schema, parts).unwrap()); // Create the DataFrame @@ -55,8 +56,7 @@ async fn create_context( Ok((physical_plan, ctx.task_ctx())) } -fn run(plan: Arc, ctx: Arc, asc: bool) { - let rt = Runtime::new().unwrap(); +fn run(rt: &Runtime, plan: Arc, ctx: Arc, asc: bool) { criterion::black_box( rt.block_on(async { aggregate(plan.clone(), ctx.clone(), asc).await }), ) @@ -99,40 +99,37 @@ async fn aggregate( } fn criterion_benchmark(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); let limit = 10; let partitions = 10; let samples = 1_000_000; - let rt = Runtime::new().unwrap(); - let topk_real = rt.block_on(async { - create_context(limit, partitions, samples, false, true) - .await - .unwrap() - }); - let topk_asc = rt.block_on(async { - create_context(limit, partitions, samples, true, true) - .await - .unwrap() - }); - let real = rt.block_on(async { - create_context(limit, partitions, samples, false, false) - .await - .unwrap() - }); - let asc = rt.block_on(async { - create_context(limit, partitions, samples, true, false) - .await - .unwrap() - }); - c.bench_function( format!("aggregate {} time-series rows", partitions * samples).as_str(), - |b| b.iter(|| run(real.0.clone(), real.1.clone(), false)), + |b| { + b.iter(|| { + let real = rt.block_on(async { + create_context(limit, partitions, samples, false, false, false) + .await + .unwrap() + }); + run(&rt, real.0.clone(), real.1.clone(), false) + }) + }, ); c.bench_function( format!("aggregate {} worst-case rows", partitions * samples).as_str(), - |b| b.iter(|| run(asc.0.clone(), asc.1.clone(), true)), + |b| { + b.iter(|| { + let asc = rt.block_on(async { + create_context(limit, partitions, samples, true, false, false) + .await + .unwrap() + }); + run(&rt, asc.0.clone(), asc.1.clone(), true) + }) + }, ); c.bench_function( @@ -141,7 +138,16 @@ fn criterion_benchmark(c: &mut Criterion) { partitions * samples ) .as_str(), - |b| b.iter(|| run(topk_real.0.clone(), topk_real.1.clone(), false)), + |b| { + b.iter(|| { + let topk_real = rt.block_on(async { + create_context(limit, partitions, samples, false, true, false) + .await + .unwrap() + }); + run(&rt, topk_real.0.clone(), topk_real.1.clone(), false) + }) + }, ); c.bench_function( @@ -150,7 +156,54 @@ fn criterion_benchmark(c: &mut Criterion) { partitions * samples ) .as_str(), - |b| b.iter(|| run(topk_asc.0.clone(), topk_asc.1.clone(), true)), + |b| { + b.iter(|| { + let topk_asc = rt.block_on(async { + create_context(limit, partitions, samples, true, true, false) + .await + .unwrap() + }); + run(&rt, topk_asc.0.clone(), topk_asc.1.clone(), true) + }) + }, + ); + + // Utf8View schema,time-series rows + c.bench_function( + format!( + "top k={limit} aggregate {} time-series rows [Utf8View]", + partitions * samples + ) + .as_str(), + |b| { + b.iter(|| { + let topk_real = rt.block_on(async { + create_context(limit, partitions, samples, false, true, true) + .await + .unwrap() + }); + run(&rt, topk_real.0.clone(), topk_real.1.clone(), false) + }) + }, + ); + + // Utf8View schema,worst-case rows + c.bench_function( + format!( + "top k={limit} aggregate {} worst-case rows [Utf8View]", + partitions * samples + ) + .as_str(), + |b| { + b.iter(|| { + let topk_asc = rt.block_on(async { + create_context(limit, partitions, samples, true, true, true) + .await + .unwrap() + }); + run(&rt, topk_asc.0.clone(), topk_asc.1.clone(), true) + }) + }, ); } diff --git a/datafusion/core/benches/window_query_sql.rs b/datafusion/core/benches/window_query_sql.rs index 42a1e51be361a..a55d17a7c5dcf 100644 --- a/datafusion/core/benches/window_query_sql.rs +++ b/datafusion/core/benches/window_query_sql.rs @@ -29,8 +29,7 @@ use parking_lot::Mutex; use std::sync::Arc; use tokio::runtime::Runtime; -fn query(ctx: Arc>, sql: &str) { - let rt = Runtime::new().unwrap(); +fn query(ctx: Arc>, rt: &Runtime, sql: &str) { let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -51,11 +50,13 @@ fn criterion_benchmark(c: &mut Criterion) { let array_len = 1024 * 1024; let batch_size = 8 * 1024; let ctx = create_context(partitions_len, array_len, batch_size).unwrap(); + let rt = Runtime::new().unwrap(); c.bench_function("window empty over, aggregate functions", |b| { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ MAX(f64) OVER (), \ MIN(f32) OVER (), \ @@ -69,6 +70,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ FIRST_VALUE(f64) OVER (), \ LAST_VALUE(f32) OVER (), \ @@ -82,6 +84,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ MAX(f64) OVER (ORDER BY u64_narrow), \ MIN(f32) OVER (ORDER BY u64_narrow DESC), \ @@ -95,6 +98,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ FIRST_VALUE(f64) OVER (ORDER BY u64_narrow), \ LAST_VALUE(f32) OVER (ORDER BY u64_narrow DESC), \ @@ -108,6 +112,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_wide), \ MIN(f32) OVER (PARTITION BY u64_wide), \ @@ -123,6 +128,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_narrow), \ MIN(f32) OVER (PARTITION BY u64_narrow), \ @@ -137,6 +143,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_wide), \ LAST_VALUE(f32) OVER (PARTITION BY u64_wide), \ @@ -150,6 +157,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_narrow), \ LAST_VALUE(f32) OVER (PARTITION BY u64_narrow), \ @@ -165,6 +173,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_wide ORDER by f64), \ MIN(f32) OVER (PARTITION BY u64_wide ORDER by f64), \ @@ -181,6 +190,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_narrow ORDER by f64), \ MIN(f32) OVER (PARTITION BY u64_narrow ORDER by f64), \ @@ -197,6 +207,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_wide ORDER by f64), \ LAST_VALUE(f32) OVER (PARTITION BY u64_wide ORDER by f64), \ @@ -213,6 +224,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), + &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_narrow ORDER by f64), \ LAST_VALUE(f32) OVER (PARTITION BY u64_narrow ORDER by f64), \ diff --git a/datafusion/core/src/bin/print_runtime_config_docs.rs b/datafusion/core/src/bin/print_runtime_config_docs.rs new file mode 100644 index 0000000000000..f374a5acb78a0 --- /dev/null +++ b/datafusion/core/src/bin/print_runtime_config_docs.rs @@ -0,0 +1,23 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use datafusion_execution::runtime_env::RuntimeEnvBuilder; + +fn main() { + let docs = RuntimeEnvBuilder::generate_config_markdown(); + println!("{}", docs); +} diff --git a/datafusion/core/src/dataframe/mod.rs b/datafusion/core/src/dataframe/mod.rs index 9c27c7c5d3076..9a70f8f43fb61 100644 --- a/datafusion/core/src/dataframe/mod.rs +++ b/datafusion/core/src/dataframe/mod.rs @@ -685,6 +685,46 @@ impl DataFrame { }) } + /// Calculate the union of two [`DataFrame`]s using column names, preserving duplicate rows. + /// + /// The two [`DataFrame`]s are combined using column names rather than position, + /// filling missing columns with null. + /// + /// + /// # Example + /// ``` + /// # use datafusion::prelude::*; + /// # use datafusion::error::Result; + /// # use datafusion_common::assert_batches_sorted_eq; + /// # #[tokio::main] + /// # async fn main() -> Result<()> { + /// let ctx = SessionContext::new(); + /// let df = ctx.read_csv("tests/data/example.csv", CsvReadOptions::new()).await?; + /// let d2 = df.clone().select_columns(&["b", "c", "a"])?.with_column("d", lit("77"))?; + /// let df = df.union_by_name(d2)?; + /// let expected = vec![ + /// "+---+---+---+----+", + /// "| a | b | c | d |", + /// "+---+---+---+----+", + /// "| 1 | 2 | 3 | |", + /// "| 1 | 2 | 3 | 77 |", + /// "+---+---+---+----+" + /// ]; + /// # assert_batches_sorted_eq!(expected, &df.collect().await?); + /// # Ok(()) + /// # } + /// ``` + pub fn union_by_name(self, dataframe: DataFrame) -> Result { + let plan = LogicalPlanBuilder::from(self.plan) + .union_by_name(dataframe.plan)? + .build()?; + Ok(DataFrame { + session_state: self.session_state, + plan, + projection_requires_validation: true, + }) + } + /// Calculate the distinct union of two [`DataFrame`]s. /// /// The two [`DataFrame`]s must have exactly the same schema. Any duplicate @@ -724,6 +764,45 @@ impl DataFrame { }) } + /// Calculate the union of two [`DataFrame`]s using column names with all duplicated rows removed. + /// + /// The two [`DataFrame`]s are combined using column names rather than position, + /// filling missing columns with null. + /// + /// + /// # Example + /// ``` + /// # use datafusion::prelude::*; + /// # use datafusion::error::Result; + /// # use datafusion_common::assert_batches_sorted_eq; + /// # #[tokio::main] + /// # async fn main() -> Result<()> { + /// let ctx = SessionContext::new(); + /// let df = ctx.read_csv("tests/data/example.csv", CsvReadOptions::new()).await?; + /// let d2 = df.clone().select_columns(&["b", "c", "a"])?; + /// let df = df.union_by_name_distinct(d2)?; + /// let expected = vec![ + /// "+---+---+---+", + /// "| a | b | c |", + /// "+---+---+---+", + /// "| 1 | 2 | 3 |", + /// "+---+---+---+" + /// ]; + /// # assert_batches_sorted_eq!(expected, &df.collect().await?); + /// # Ok(()) + /// # } + /// ``` + pub fn union_by_name_distinct(self, dataframe: DataFrame) -> Result { + let plan = LogicalPlanBuilder::from(self.plan) + .union_by_name_distinct(dataframe.plan)? + .build()?; + Ok(DataFrame { + session_state: self.session_state, + plan, + projection_requires_validation: true, + }) + } + /// Return a new `DataFrame` with all duplicated rows removed. /// /// # Example diff --git a/datafusion/core/src/datasource/file_format/arrow.rs b/datafusion/core/src/datasource/file_format/arrow.rs index 6c7c9463cf3b7..7fc27453d1ad5 100644 --- a/datafusion/core/src/datasource/file_format/arrow.rs +++ b/datafusion/core/src/datasource/file_format/arrow.rs @@ -144,6 +144,7 @@ impl FileFormat for ArrowFormat { for object in objects { let r = store.as_ref().get(&object.location).await?; let schema = match r.payload { + #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(mut file, _) => { let reader = FileReader::try_new(&mut file, None)?; reader.schema() @@ -442,7 +443,7 @@ mod tests { let object_meta = ObjectMeta { location, last_modified: DateTime::default(), - size: usize::MAX, + size: u64::MAX, e_tag: None, version: None, }; @@ -485,7 +486,7 @@ mod tests { let object_meta = ObjectMeta { location, last_modified: DateTime::default(), - size: usize::MAX, + size: u64::MAX, e_tag: None, version: None, }; diff --git a/datafusion/core/src/datasource/file_format/avro.rs b/datafusion/core/src/datasource/file_format/avro.rs index a9516aad9e22d..3428d08a6ae52 100644 --- a/datafusion/core/src/datasource/file_format/avro.rs +++ b/datafusion/core/src/datasource/file_format/avro.rs @@ -382,6 +382,15 @@ mod tests { let testdata = test_util::arrow_test_data(); let store_root = format!("{testdata}/avro"); let format = AvroFormat {}; - scan_format(state, &format, &store_root, file_name, projection, limit).await + scan_format( + state, + &format, + None, + &store_root, + file_name, + projection, + limit, + ) + .await } } diff --git a/datafusion/core/src/datasource/file_format/csv.rs b/datafusion/core/src/datasource/file_format/csv.rs index 309458975ab6c..323bc28057d43 100644 --- a/datafusion/core/src/datasource/file_format/csv.rs +++ b/datafusion/core/src/datasource/file_format/csv.rs @@ -72,7 +72,7 @@ mod tests { #[derive(Debug)] struct VariableStream { bytes_to_repeat: Bytes, - max_iterations: usize, + max_iterations: u64, iterations_detected: Arc>, } @@ -103,14 +103,15 @@ mod tests { async fn get(&self, location: &Path) -> object_store::Result { let bytes = self.bytes_to_repeat.clone(); - let range = 0..bytes.len() * self.max_iterations; + let len = bytes.len() as u64; + let range = 0..len * self.max_iterations; let arc = self.iterations_detected.clone(); let stream = futures::stream::repeat_with(move || { let arc_inner = arc.clone(); *arc_inner.lock().unwrap() += 1; Ok(bytes.clone()) }) - .take(self.max_iterations) + .take(self.max_iterations as usize) .boxed(); Ok(GetResult { @@ -138,7 +139,7 @@ mod tests { async fn get_ranges( &self, _location: &Path, - _ranges: &[Range], + _ranges: &[Range], ) -> object_store::Result> { unimplemented!() } @@ -154,7 +155,7 @@ mod tests { fn list( &self, _prefix: Option<&Path>, - ) -> BoxStream<'_, object_store::Result> { + ) -> BoxStream<'static, object_store::Result> { unimplemented!() } @@ -179,7 +180,7 @@ mod tests { } impl VariableStream { - pub fn new(bytes_to_repeat: Bytes, max_iterations: usize) -> Self { + pub fn new(bytes_to_repeat: Bytes, max_iterations: u64) -> Self { Self { bytes_to_repeat, max_iterations, @@ -249,6 +250,7 @@ mod tests { let exec = scan_format( &state, &format, + None, root, "aggregate_test_100_with_nulls.csv", projection, @@ -299,6 +301,7 @@ mod tests { let exec = scan_format( &state, &format, + None, root, "aggregate_test_100_with_nulls.csv", projection, @@ -371,7 +374,7 @@ mod tests { let object_meta = ObjectMeta { location: Path::parse("/")?, last_modified: DateTime::default(), - size: usize::MAX, + size: u64::MAX, e_tag: None, version: None, }; @@ -429,7 +432,7 @@ mod tests { let object_meta = ObjectMeta { location: Path::parse("/")?, last_modified: DateTime::default(), - size: usize::MAX, + size: u64::MAX, e_tag: None, version: None, }; @@ -581,7 +584,7 @@ mod tests { ) -> Result> { let root = format!("{}/csv", arrow_test_data()); let format = CsvFormat::default().with_has_header(has_header); - scan_format(state, &format, &root, file_name, projection, limit).await + scan_format(state, &format, None, &root, file_name, projection, limit).await } #[tokio::test] diff --git a/datafusion/core/src/datasource/file_format/json.rs b/datafusion/core/src/datasource/file_format/json.rs index d533dcf7646da..a70a0f51d3307 100644 --- a/datafusion/core/src/datasource/file_format/json.rs +++ b/datafusion/core/src/datasource/file_format/json.rs @@ -149,7 +149,7 @@ mod tests { ) -> Result> { let filename = "tests/data/2.json"; let format = JsonFormat::default(); - scan_format(state, &format, ".", filename, projection, limit).await + scan_format(state, &format, None, ".", filename, projection, limit).await } #[tokio::test] diff --git a/datafusion/core/src/datasource/file_format/mod.rs b/datafusion/core/src/datasource/file_format/mod.rs index e921f0158e540..3a098301f14e3 100644 --- a/datafusion/core/src/datasource/file_format/mod.rs +++ b/datafusion/core/src/datasource/file_format/mod.rs @@ -36,19 +36,20 @@ pub use datafusion_datasource::write; #[cfg(test)] pub(crate) mod test_util { - use std::sync::Arc; - + use arrow_schema::SchemaRef; use datafusion_catalog::Session; use datafusion_common::Result; use datafusion_datasource::file_scan_config::FileScanConfigBuilder; use datafusion_datasource::{file_format::FileFormat, PartitionedFile}; use datafusion_execution::object_store::ObjectStoreUrl; + use std::sync::Arc; use crate::test::object_store::local_unpartitioned_file; pub async fn scan_format( state: &dyn Session, format: &dyn FileFormat, + schema: Option, store_root: &str, file_name: &str, projection: Option>, @@ -57,9 +58,13 @@ pub(crate) mod test_util { let store = Arc::new(object_store::local::LocalFileSystem::new()) as _; let meta = local_unpartitioned_file(format!("{store_root}/{file_name}")); - let file_schema = format - .infer_schema(state, &store, std::slice::from_ref(&meta)) - .await?; + let file_schema = if let Some(file_schema) = schema { + file_schema + } else { + format + .infer_schema(state, &store, std::slice::from_ref(&meta)) + .await? + }; let statistics = format .infer_stats(state, &store, file_schema.clone(), &meta) @@ -127,7 +132,7 @@ mod tests { .write_parquet(out_dir_url, DataFrameWriteOptions::new(), None) .await .expect_err("should fail because input file does not match inferred schema"); - assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4"); + assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'"); Ok(()) } } diff --git a/datafusion/core/src/datasource/file_format/parquet.rs b/datafusion/core/src/datasource/file_format/parquet.rs index 27a7e7ae3c061..7b8b99273f4ea 100644 --- a/datafusion/core/src/datasource/file_format/parquet.rs +++ b/datafusion/core/src/datasource/file_format/parquet.rs @@ -67,13 +67,13 @@ pub(crate) mod test_util { .into_iter() .zip(tmp_files.into_iter()) .map(|(batch, mut output)| { - let builder = parquet::file::properties::WriterProperties::builder(); - let props = if multi_page { - builder.set_data_page_row_count_limit(ROWS_PER_PAGE) - } else { - builder + let mut builder = parquet::file::properties::WriterProperties::builder(); + if multi_page { + builder = builder.set_data_page_row_count_limit(ROWS_PER_PAGE) } - .build(); + builder = builder.set_bloom_filter_enabled(true); + + let props = builder.build(); let mut writer = parquet::arrow::ArrowWriter::try_new( &mut output, @@ -331,7 +331,7 @@ mod tests { fn list( &self, _prefix: Option<&Path>, - ) -> BoxStream<'_, object_store::Result> { + ) -> BoxStream<'static, object_store::Result> { Box::pin(futures::stream::once(async { Err(object_store::Error::NotImplemented) })) @@ -408,7 +408,7 @@ mod tests { ))); // Use the file size as the hint so we can get the full metadata from the first fetch - let size_hint = meta[0].size; + let size_hint = meta[0].size as usize; fetch_parquet_metadata(store.upcast().as_ref(), &meta[0], Some(size_hint)) .await @@ -443,7 +443,7 @@ mod tests { ))); // Use the a size hint larger than the file size to make sure we don't panic - let size_hint = meta[0].size + 100; + let size_hint = (meta[0].size + 100) as usize; fetch_parquet_metadata(store.upcast().as_ref(), &meta[0], Some(size_hint)) .await @@ -1075,7 +1075,10 @@ mod tests { .map(|factory| factory.create(state, &Default::default()).unwrap()) .unwrap_or(Arc::new(ParquetFormat::new())); - scan_format(state, &*format, &testdata, file_name, projection, limit).await + scan_format( + state, &*format, None, &testdata, file_name, projection, limit, + ) + .await } /// Test that 0-byte files don't break while reading diff --git a/datafusion/core/src/datasource/listing/table.rs b/datafusion/core/src/datasource/listing/table.rs index 61eeb419a4800..a9834da92e5a4 100644 --- a/datafusion/core/src/datasource/listing/table.rs +++ b/datafusion/core/src/datasource/listing/table.rs @@ -17,18 +17,16 @@ //! The table implementation. -use std::collections::HashMap; -use std::{any::Any, str::FromStr, sync::Arc}; - use super::helpers::{expr_applicable_for_cols, pruned_partition_list}; use super::{ListingTableUrl, PartitionedFile}; +use std::collections::HashMap; +use std::{any::Any, str::FromStr, sync::Arc}; use crate::datasource::{ create_ordering, file_format::{ file_compression_type::FileCompressionType, FileFormat, FilePushdownSupport, }, - get_statistics_with_limit, physical_plan::FileSinkConfig, }; use crate::execution::context::SessionState; @@ -55,9 +53,11 @@ use datafusion_physical_expr::{ use async_trait::async_trait; use datafusion_catalog::Session; +use datafusion_common::stats::Precision; +use datafusion_datasource::compute_all_files_statistics; use datafusion_datasource::file_groups::FileGroup; use datafusion_physical_expr_common::sort_expr::LexRequirement; -use futures::{future, stream, StreamExt, TryStreamExt}; +use futures::{future, stream, Stream, StreamExt, TryStreamExt}; use itertools::Itertools; use object_store::ObjectStore; @@ -715,9 +715,13 @@ impl ListingOptions { #[derive(Debug)] pub struct ListingTable { table_paths: Vec, - /// File fields only + /// `file_schema` contains only the columns physically stored in the data files themselves. + /// - Represents the actual fields found in files like Parquet, CSV, etc. + /// - Used when reading the raw data from files file_schema: SchemaRef, - /// File fields + partition columns + /// `table_schema` combines `file_schema` + partition columns + /// - Partition columns are derived from directory paths (not stored in files) + /// - These are columns like "year=2022/month=01" in paths like `/data/year=2022/month=01/file.parquet` table_schema: SchemaRef, options: ListingOptions, definition: Option, @@ -795,7 +799,7 @@ impl ListingTable { /// If `None`, creates a new [`DefaultFileStatisticsCache`] scoped to this query. pub fn with_cache(mut self, cache: Option) -> Self { self.collected_statistics = - cache.unwrap_or(Arc::new(DefaultFileStatisticsCache::default())); + cache.unwrap_or_else(|| Arc::new(DefaultFileStatisticsCache::default())); self } @@ -874,15 +878,13 @@ impl TableProvider for ListingTable { filters.iter().cloned().partition(|filter| { can_be_evaluted_for_partition_pruning(&table_partition_col_names, filter) }); - // TODO (https://github.com/apache/datafusion/issues/11600) remove downcast_ref from here? - let session_state = state.as_any().downcast_ref::().unwrap(); // We should not limit the number of partitioned files to scan if there are filters and limit // at the same time. This is because the limit should be applied after the filters are applied. let statistic_file_limit = if filters.is_empty() { limit } else { None }; let (mut partitioned_file_lists, statistics) = self - .list_files_for_scan(session_state, &partition_filters, statistic_file_limit) + .list_files_for_scan(state, &partition_filters, statistic_file_limit) .await?; // if no files need to be read, return an `EmptyExec` @@ -898,10 +900,11 @@ impl TableProvider for ListingTable { .split_file_groups_by_statistics .then(|| { output_ordering.first().map(|output_ordering| { - FileScanConfig::split_groups_by_statistics( + FileScanConfig::split_groups_by_statistics_with_target_partitions( &self.table_schema, &partitioned_file_lists, output_ordering, + self.options.target_partitions, ) }) }) @@ -941,7 +944,7 @@ impl TableProvider for ListingTable { self.options .format .create_physical_plan( - session_state, + state, FileScanConfigBuilder::new( object_store_url, Arc::clone(&self.file_schema), @@ -1021,10 +1024,8 @@ impl TableProvider for ListingTable { // Get the object store for the table path. let store = state.runtime_env().object_store(table_path)?; - // TODO (https://github.com/apache/datafusion/issues/11600) remove downcast_ref from here? - let session_state = state.as_any().downcast_ref::().unwrap(); let file_list_stream = pruned_partition_list( - session_state, + state, store.as_ref(), table_path, &[], @@ -1072,7 +1073,7 @@ impl TableProvider for ListingTable { self.options() .format - .create_writer_physical_plan(input, session_state, config, order_requirements) + .create_writer_physical_plan(input, state, config, order_requirements) .await } @@ -1115,32 +1116,26 @@ impl ListingTable { let files = file_list .map(|part_file| async { let part_file = part_file?; - if self.options.collect_stat { - let statistics = - self.do_collect_statistics(ctx, &store, &part_file).await?; - Ok((part_file, statistics)) + let statistics = if self.options.collect_stat { + self.do_collect_statistics(ctx, &store, &part_file).await? } else { - Ok(( - part_file, - Arc::new(Statistics::new_unknown(&self.file_schema)), - )) - } + Arc::new(Statistics::new_unknown(&self.file_schema)) + }; + Ok(part_file.with_statistics(statistics)) }) .boxed() .buffer_unordered(ctx.config_options().execution.meta_fetch_concurrency); - let (files, statistics) = get_statistics_with_limit( - files, + let (file_group, inexact_stats) = + get_files_with_limit(files, limit, self.options.collect_stat).await?; + + let file_groups = file_group.split_files(self.options.target_partitions); + compute_all_files_statistics( + file_groups, self.schema(), - limit, self.options.collect_stat, + inexact_stats, ) - .await?; - - Ok(( - files.split_files(self.options.target_partitions), - statistics, - )) } /// Collects statistics for a given partitioned file. @@ -1182,6 +1177,82 @@ impl ListingTable { } } +/// Processes a stream of partitioned files and returns a `FileGroup` containing the files. +/// +/// This function collects files from the provided stream until either: +/// 1. The stream is exhausted +/// 2. The accumulated number of rows exceeds the provided `limit` (if specified) +/// +/// # Arguments +/// * `files` - A stream of `Result` items to process +/// * `limit` - An optional row count limit. If provided, the function will stop collecting files +/// once the accumulated number of rows exceeds this limit +/// * `collect_stats` - Whether to collect and accumulate statistics from the files +/// +/// # Returns +/// A `Result` containing a `FileGroup` with the collected files +/// and a boolean indicating whether the statistics are inexact. +/// +/// # Note +/// The function will continue processing files if statistics are not available or if the +/// limit is not provided. If `collect_stats` is false, statistics won't be accumulated +/// but files will still be collected. +async fn get_files_with_limit( + files: impl Stream>, + limit: Option, + collect_stats: bool, +) -> Result<(FileGroup, bool)> { + let mut file_group = FileGroup::default(); + // Fusing the stream allows us to call next safely even once it is finished. + let mut all_files = Box::pin(files.fuse()); + enum ProcessingState { + ReadingFiles, + ReachedLimit, + } + + let mut state = ProcessingState::ReadingFiles; + let mut num_rows = Precision::Absent; + + while let Some(file_result) = all_files.next().await { + // Early exit if we've already reached our limit + if matches!(state, ProcessingState::ReachedLimit) { + break; + } + + let file = file_result?; + + // Update file statistics regardless of state + if collect_stats { + if let Some(file_stats) = &file.statistics { + num_rows = if file_group.is_empty() { + // For the first file, just take its row count + file_stats.num_rows + } else { + // For subsequent files, accumulate the counts + num_rows.add(&file_stats.num_rows) + }; + } + } + + // Always add the file to our group + file_group.push(file); + + // Check if we've hit the limit (if one was specified) + if let Some(limit) = limit { + if let Precision::Exact(row_count) = num_rows { + if row_count > limit { + state = ProcessingState::ReachedLimit; + } + } + } + } + // If we still have files in the stream, it means that the limit kicked + // in, and the statistic could have been different had we processed the + // files in a different order. + let inexact_stats = all_files.next().await.is_some(); + Ok((file_group, inexact_stats)) +} + #[cfg(test)] mod tests { use super::*; diff --git a/datafusion/core/src/datasource/memory.rs b/datafusion/core/src/datasource/memory_test.rs similarity index 58% rename from datafusion/core/src/datasource/memory.rs rename to datafusion/core/src/datasource/memory_test.rs index 0288cd3e8bc7d..381000ab8ee1e 100644 --- a/datafusion/core/src/datasource/memory.rs +++ b/datafusion/core/src/datasource/memory_test.rs @@ -15,378 +15,25 @@ // specific language governing permissions and limitations // under the License. -//! [`MemTable`] for querying `Vec` by DataFusion. - -use std::any::Any; -use std::collections::HashMap; -use std::fmt::{self, Debug}; -use std::sync::Arc; - -use crate::datasource::{TableProvider, TableType}; -use crate::error::Result; -use crate::logical_expr::Expr; -use crate::physical_plan::repartition::RepartitionExec; -use crate::physical_plan::{ - common, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, - Partitioning, SendableRecordBatchStream, -}; -use crate::physical_planner::create_physical_sort_exprs; - -use arrow::datatypes::SchemaRef; -use arrow::record_batch::RecordBatch; -use datafusion_catalog::Session; -use datafusion_common::{not_impl_err, plan_err, Constraints, DFSchema, SchemaExt}; -use datafusion_common_runtime::JoinSet; -pub use datafusion_datasource::memory::MemorySourceConfig; -use datafusion_datasource::sink::{DataSink, DataSinkExec}; -pub use datafusion_datasource::source::DataSourceExec; -use datafusion_execution::TaskContext; -use datafusion_expr::dml::InsertOp; -use datafusion_expr::SortExpr; - -use async_trait::async_trait; -use futures::StreamExt; -use log::debug; -use parking_lot::Mutex; -use tokio::sync::RwLock; - -/// Type alias for partition data -pub type PartitionData = Arc>>; - -/// In-memory data source for presenting a `Vec` as a -/// data source that can be queried by DataFusion. This allows data to -/// be pre-loaded into memory and then repeatedly queried without -/// incurring additional file I/O overhead. -#[derive(Debug)] -pub struct MemTable { - schema: SchemaRef, - pub(crate) batches: Vec, - constraints: Constraints, - column_defaults: HashMap, - /// Optional pre-known sort order(s). Must be `SortExpr`s. - /// inserting data into this table removes the order - pub sort_order: Arc>>>, -} - -impl MemTable { - /// Create a new in-memory table from the provided schema and record batches - pub fn try_new(schema: SchemaRef, partitions: Vec>) -> Result { - for batches in partitions.iter().flatten() { - let batches_schema = batches.schema(); - if !schema.contains(&batches_schema) { - debug!( - "mem table schema does not contain batches schema. \ - Target_schema: {schema:?}. Batches Schema: {batches_schema:?}" - ); - return plan_err!("Mismatch between schema and batches"); - } - } - - Ok(Self { - schema, - batches: partitions - .into_iter() - .map(|e| Arc::new(RwLock::new(e))) - .collect::>(), - constraints: Constraints::empty(), - column_defaults: HashMap::new(), - sort_order: Arc::new(Mutex::new(vec![])), - }) - } - - /// Assign constraints - pub fn with_constraints(mut self, constraints: Constraints) -> Self { - self.constraints = constraints; - self - } - - /// Assign column defaults - pub fn with_column_defaults( - mut self, - column_defaults: HashMap, - ) -> Self { - self.column_defaults = column_defaults; - self - } - - /// Specify an optional pre-known sort order(s). Must be `SortExpr`s. - /// - /// If the data is not sorted by this order, DataFusion may produce - /// incorrect results. - /// - /// DataFusion may take advantage of this ordering to omit sorts - /// or use more efficient algorithms. - /// - /// Note that multiple sort orders are supported, if some are known to be - /// equivalent, - pub fn with_sort_order(self, mut sort_order: Vec>) -> Self { - std::mem::swap(self.sort_order.lock().as_mut(), &mut sort_order); - self - } - - /// Create a mem table by reading from another data source - pub async fn load( - t: Arc, - output_partitions: Option, - state: &dyn Session, - ) -> Result { - let schema = t.schema(); - let constraints = t.constraints(); - let exec = t.scan(state, None, &[], None).await?; - let partition_count = exec.output_partitioning().partition_count(); - - let mut join_set = JoinSet::new(); - - for part_idx in 0..partition_count { - let task = state.task_ctx(); - let exec = Arc::clone(&exec); - join_set.spawn(async move { - let stream = exec.execute(part_idx, task)?; - common::collect(stream).await - }); - } - - let mut data: Vec> = - Vec::with_capacity(exec.output_partitioning().partition_count()); - - while let Some(result) = join_set.join_next().await { - match result { - Ok(res) => data.push(res?), - Err(e) => { - if e.is_panic() { - std::panic::resume_unwind(e.into_panic()); - } else { - unreachable!(); - } - } - } - } - - let mut exec = DataSourceExec::new(Arc::new(MemorySourceConfig::try_new( - &data, - Arc::clone(&schema), - None, - )?)); - if let Some(cons) = constraints { - exec = exec.with_constraints(cons.clone()); - } - - if let Some(num_partitions) = output_partitions { - let exec = RepartitionExec::try_new( - Arc::new(exec), - Partitioning::RoundRobinBatch(num_partitions), - )?; - - // execute and collect results - let mut output_partitions = vec![]; - for i in 0..exec.properties().output_partitioning().partition_count() { - // execute this *output* partition and collect all batches - let task_ctx = state.task_ctx(); - let mut stream = exec.execute(i, task_ctx)?; - let mut batches = vec![]; - while let Some(result) = stream.next().await { - batches.push(result?); - } - output_partitions.push(batches); - } - - return MemTable::try_new(Arc::clone(&schema), output_partitions); - } - MemTable::try_new(Arc::clone(&schema), data) - } -} - -#[async_trait] -impl TableProvider for MemTable { - fn as_any(&self) -> &dyn Any { - self - } - - fn schema(&self) -> SchemaRef { - Arc::clone(&self.schema) - } - - fn constraints(&self) -> Option<&Constraints> { - Some(&self.constraints) - } - - fn table_type(&self) -> TableType { - TableType::Base - } - - async fn scan( - &self, - state: &dyn Session, - projection: Option<&Vec>, - _filters: &[Expr], - _limit: Option, - ) -> Result> { - let mut partitions = vec![]; - for arc_inner_vec in self.batches.iter() { - let inner_vec = arc_inner_vec.read().await; - partitions.push(inner_vec.clone()) - } - - let mut source = - MemorySourceConfig::try_new(&partitions, self.schema(), projection.cloned())?; - - let show_sizes = state.config_options().explain.show_sizes; - source = source.with_show_sizes(show_sizes); - - // add sort information if present - let sort_order = self.sort_order.lock(); - if !sort_order.is_empty() { - let df_schema = DFSchema::try_from(self.schema.as_ref().clone())?; - - let file_sort_order = sort_order - .iter() - .map(|sort_exprs| { - create_physical_sort_exprs( - sort_exprs, - &df_schema, - state.execution_props(), - ) - }) - .collect::>>()?; - source = source.try_with_sort_information(file_sort_order)?; - } - - Ok(DataSourceExec::from_data_source(source)) - } - - /// Returns an ExecutionPlan that inserts the execution results of a given [`ExecutionPlan`] into this [`MemTable`]. - /// - /// The [`ExecutionPlan`] must have the same schema as this [`MemTable`]. - /// - /// # Arguments - /// - /// * `state` - The [`SessionState`] containing the context for executing the plan. - /// * `input` - The [`ExecutionPlan`] to execute and insert. - /// - /// # Returns - /// - /// * A plan that returns the number of rows written. - /// - /// [`SessionState`]: crate::execution::context::SessionState - async fn insert_into( - &self, - _state: &dyn Session, - input: Arc, - insert_op: InsertOp, - ) -> Result> { - // If we are inserting into the table, any sort order may be messed up so reset it here - *self.sort_order.lock() = vec![]; - - // Create a physical plan from the logical plan. - // Check that the schema of the plan matches the schema of this table. - self.schema() - .logically_equivalent_names_and_types(&input.schema())?; - - if insert_op != InsertOp::Append { - return not_impl_err!("{insert_op} not implemented for MemoryTable yet"); - } - let sink = MemSink::try_new(self.batches.clone(), Arc::clone(&self.schema))?; - Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None))) - } - - fn get_column_default(&self, column: &str) -> Option<&Expr> { - self.column_defaults.get(column) - } -} - -/// Implements for writing to a [`MemTable`] -struct MemSink { - /// Target locations for writing data - batches: Vec, - schema: SchemaRef, -} - -impl Debug for MemSink { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("MemSink") - .field("num_partitions", &self.batches.len()) - .finish() - } -} - -impl DisplayAs for MemSink { - fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match t { - DisplayFormatType::Default | DisplayFormatType::Verbose => { - let partition_count = self.batches.len(); - write!(f, "MemoryTable (partitions={partition_count})") - } - DisplayFormatType::TreeRender => { - // TODO: collect info - write!(f, "") - } - } - } -} - -impl MemSink { - /// Creates a new [`MemSink`]. - /// - /// The caller is responsible for ensuring that there is at least one partition to insert into. - fn try_new(batches: Vec, schema: SchemaRef) -> Result { - if batches.is_empty() { - return plan_err!("Cannot insert into MemTable with zero partitions"); - } - Ok(Self { batches, schema }) - } -} - -#[async_trait] -impl DataSink for MemSink { - fn as_any(&self) -> &dyn Any { - self - } - - fn schema(&self) -> &SchemaRef { - &self.schema - } - - async fn write_all( - &self, - mut data: SendableRecordBatchStream, - _context: &Arc, - ) -> Result { - let num_partitions = self.batches.len(); - - // buffer up the data round robin style into num_partitions - - let mut new_batches = vec![vec![]; num_partitions]; - let mut i = 0; - let mut row_count = 0; - while let Some(batch) = data.next().await.transpose()? { - row_count += batch.num_rows(); - new_batches[i].push(batch); - i = (i + 1) % num_partitions; - } - - // write the outputs into the batches - for (target, mut batches) in self.batches.iter().zip(new_batches.into_iter()) { - // Append all the new batches in one go to minimize locking overhead - target.write().await.append(&mut batches); - } - - Ok(row_count as u64) - } -} - #[cfg(test)] mod tests { - use super::*; + use crate::datasource::MemTable; use crate::datasource::{provider_as_source, DefaultTableSource}; use crate::physical_plan::collect; use crate::prelude::SessionContext; - use arrow::array::{AsArray, Int32Array}; use arrow::datatypes::{DataType, Field, Schema, UInt64Type}; use arrow::error::ArrowError; - use datafusion_common::DataFusionError; + use arrow::record_batch::RecordBatch; + use arrow_schema::SchemaRef; + use datafusion_catalog::TableProvider; + use datafusion_common::{DataFusionError, Result}; + use datafusion_expr::dml::InsertOp; use datafusion_expr::LogicalPlanBuilder; + use futures::StreamExt; + use std::collections::HashMap; + use std::sync::Arc; #[tokio::test] async fn test_with_projection() -> Result<()> { diff --git a/datafusion/core/src/datasource/mod.rs b/datafusion/core/src/datasource/mod.rs index 35a451cbc803a..25a89644cd2a4 100644 --- a/datafusion/core/src/datasource/mod.rs +++ b/datafusion/core/src/datasource/mod.rs @@ -24,10 +24,9 @@ pub mod empty; pub mod file_format; pub mod listing; pub mod listing_table_factory; -pub mod memory; +mod memory_test; pub mod physical_plan; pub mod provider; -mod statistics; mod view_test; // backwards compatibility @@ -40,6 +39,7 @@ pub use crate::catalog::TableProvider; pub use crate::logical_expr::TableType; pub use datafusion_catalog::cte_worktable; pub use datafusion_catalog::default_table_source; +pub use datafusion_catalog::memory; pub use datafusion_catalog::stream; pub use datafusion_catalog::view; pub use datafusion_datasource::schema_adapter; @@ -47,7 +47,6 @@ pub use datafusion_datasource::sink; pub use datafusion_datasource::source; pub use datafusion_execution::object_store; pub use datafusion_physical_expr::create_ordering; -pub use statistics::get_statistics_with_limit; #[cfg(all(test, feature = "parquet"))] mod tests { @@ -107,7 +106,7 @@ mod tests { let meta = ObjectMeta { location, last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len() as usize, + size: metadata.len(), e_tag: None, version: None, }; diff --git a/datafusion/core/src/datasource/physical_plan/arrow_file.rs b/datafusion/core/src/datasource/physical_plan/arrow_file.rs index 5dcf4df73f57a..f0a1f94d87e1f 100644 --- a/datafusion/core/src/datasource/physical_plan/arrow_file.rs +++ b/datafusion/core/src/datasource/physical_plan/arrow_file.rs @@ -273,6 +273,7 @@ impl FileOpener for ArrowOpener { None => { let r = object_store.get(file_meta.location()).await?; match r.payload { + #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(file, _) => { let arrow_reader = arrow::ipc::reader::FileReader::try_new( file, projection, @@ -305,7 +306,7 @@ impl FileOpener for ArrowOpener { )?; // read footer according to footer_len let get_option = GetOptions { - range: Some(GetRange::Suffix(10 + footer_len)), + range: Some(GetRange::Suffix(10 + (footer_len as u64))), ..Default::default() }; let get_result = object_store @@ -332,9 +333,9 @@ impl FileOpener for ArrowOpener { .iter() .flatten() .map(|block| { - let block_len = block.bodyLength() as usize - + block.metaDataLength() as usize; - let block_offset = block.offset() as usize; + let block_len = + block.bodyLength() as u64 + block.metaDataLength() as u64; + let block_offset = block.offset() as u64; block_offset..block_offset + block_len }) .collect_vec(); @@ -354,9 +355,9 @@ impl FileOpener for ArrowOpener { .iter() .flatten() .filter(|block| { - let block_offset = block.offset() as usize; - block_offset >= range.start as usize - && block_offset < range.end as usize + let block_offset = block.offset() as u64; + block_offset >= range.start as u64 + && block_offset < range.end as u64 }) .copied() .collect_vec(); @@ -364,9 +365,9 @@ impl FileOpener for ArrowOpener { let recordbatch_ranges = recordbatches .iter() .map(|block| { - let block_len = block.bodyLength() as usize - + block.metaDataLength() as usize; - let block_offset = block.offset() as usize; + let block_len = + block.bodyLength() as u64 + block.metaDataLength() as u64; + let block_offset = block.offset() as u64; block_offset..block_offset + block_len }) .collect_vec(); diff --git a/datafusion/core/src/datasource/physical_plan/csv.rs b/datafusion/core/src/datasource/physical_plan/csv.rs index 5914924797dce..3ef4030134520 100644 --- a/datafusion/core/src/datasource/physical_plan/csv.rs +++ b/datafusion/core/src/datasource/physical_plan/csv.rs @@ -658,7 +658,7 @@ mod tests { ) .await .expect_err("should fail because input file does not match inferred schema"); - assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4"); + assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'"); Ok(()) } diff --git a/datafusion/core/src/datasource/physical_plan/json.rs b/datafusion/core/src/datasource/physical_plan/json.rs index 910c4316d9734..736248fbd95df 100644 --- a/datafusion/core/src/datasource/physical_plan/json.rs +++ b/datafusion/core/src/datasource/physical_plan/json.rs @@ -495,7 +495,7 @@ mod tests { .write_json(out_dir_url, DataFrameWriteOptions::new(), None) .await .expect_err("should fail because input file does not match inferred schema"); - assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4"); + assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'"); Ok(()) } diff --git a/datafusion/core/src/datasource/physical_plan/parquet.rs b/datafusion/core/src/datasource/physical_plan/parquet.rs index 9e1b2822e8540..e9bb8b0db3682 100644 --- a/datafusion/core/src/datasource/physical_plan/parquet.rs +++ b/datafusion/core/src/datasource/physical_plan/parquet.rs @@ -38,11 +38,12 @@ mod tests { use crate::prelude::{ParquetReadOptions, SessionConfig, SessionContext}; use crate::test::object_store::local_unpartitioned_file; use arrow::array::{ - ArrayRef, Date64Array, Int32Array, Int64Array, Int8Array, StringArray, + ArrayRef, AsArray, Date64Array, Int32Array, Int64Array, Int8Array, StringArray, StructArray, }; use arrow::datatypes::{DataType, Field, Fields, Schema, SchemaBuilder}; use arrow::record_batch::RecordBatch; + use arrow::util::pretty::pretty_format_batches; use arrow_schema::SchemaRef; use bytes::{BufMut, BytesMut}; use datafusion_common::config::TableParquetOptions; @@ -61,8 +62,9 @@ mod tests { use datafusion_execution::object_store::ObjectStoreUrl; use datafusion_expr::{col, lit, when, Expr}; use datafusion_physical_expr::planner::logical2physical; + use datafusion_physical_plan::analyze::AnalyzeExec; + use datafusion_physical_plan::collect; use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, MetricsSet}; - use datafusion_physical_plan::{collect, displayable}; use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties}; use chrono::{TimeZone, Utc}; @@ -81,10 +83,10 @@ mod tests { struct RoundTripResult { /// Data that was read back from ParquetFiles batches: Result>, + /// The EXPLAIN ANALYZE output + explain: Result, /// The physical plan that was created (that has statistics, etc) parquet_exec: Arc, - /// The ParquetSource that is used in plan - parquet_source: ParquetSource, } /// round-trip record batches by writing each individual RecordBatch to @@ -137,71 +139,109 @@ mod tests { self.round_trip(batches).await.batches } - /// run the test, returning the `RoundTripResult` - async fn round_trip(self, batches: Vec) -> RoundTripResult { - let Self { - projection, - schema, - predicate, - pushdown_predicate, - page_index_predicate, - } = self; - - let file_schema = match schema { - Some(schema) => schema, - None => Arc::new( - Schema::try_merge( - batches.iter().map(|b| b.schema().as_ref().clone()), - ) - .unwrap(), - ), - }; - // If testing with page_index_predicate, write parquet - // files with multiple pages - let multi_page = page_index_predicate; - let (meta, _files) = store_parquet(batches, multi_page).await.unwrap(); - let file_group = meta.into_iter().map(Into::into).collect(); - + fn build_file_source(&self, file_schema: SchemaRef) -> Arc { // set up predicate (this is normally done by a layer higher up) - let predicate = predicate.map(|p| logical2physical(&p, &file_schema)); + let predicate = self + .predicate + .as_ref() + .map(|p| logical2physical(p, &file_schema)); let mut source = ParquetSource::default(); if let Some(predicate) = predicate { source = source.with_predicate(Arc::clone(&file_schema), predicate); } - if pushdown_predicate { + if self.pushdown_predicate { source = source .with_pushdown_filters(true) .with_reorder_filters(true); } - if page_index_predicate { + if self.page_index_predicate { source = source.with_enable_page_index(true); } + Arc::new(source) + } + + fn build_parquet_exec( + &self, + file_schema: SchemaRef, + file_group: FileGroup, + source: Arc, + ) -> Arc { let base_config = FileScanConfigBuilder::new( ObjectStoreUrl::local_filesystem(), file_schema, - Arc::new(source.clone()), + source, ) .with_file_group(file_group) - .with_projection(projection) + .with_projection(self.projection.clone()) .build(); + DataSourceExec::from_data_source(base_config) + } + + /// run the test, returning the `RoundTripResult` + async fn round_trip(&self, batches: Vec) -> RoundTripResult { + let file_schema = match &self.schema { + Some(schema) => schema, + None => &Arc::new( + Schema::try_merge( + batches.iter().map(|b| b.schema().as_ref().clone()), + ) + .unwrap(), + ), + }; + let file_schema = Arc::clone(file_schema); + // If testing with page_index_predicate, write parquet + // files with multiple pages + let multi_page = self.page_index_predicate; + let (meta, _files) = store_parquet(batches, multi_page).await.unwrap(); + let file_group: FileGroup = meta.into_iter().map(Into::into).collect(); + + // build a ParquetExec to return the results + let parquet_source = self.build_file_source(file_schema.clone()); + let parquet_exec = self.build_parquet_exec( + file_schema.clone(), + file_group.clone(), + Arc::clone(&parquet_source), + ); + + let analyze_exec = Arc::new(AnalyzeExec::new( + false, + false, + // use a new ParquetSource to avoid sharing execution metrics + self.build_parquet_exec( + file_schema.clone(), + file_group.clone(), + self.build_file_source(file_schema.clone()), + ), + Arc::new(Schema::new(vec![ + Field::new("plan_type", DataType::Utf8, true), + Field::new("plan", DataType::Utf8, true), + ])), + )); let session_ctx = SessionContext::new(); let task_ctx = session_ctx.task_ctx(); - let parquet_exec = DataSourceExec::from_data_source(base_config.clone()); + let batches = collect( + Arc::clone(&parquet_exec) as Arc, + task_ctx.clone(), + ) + .await; + + let explain = collect(analyze_exec, task_ctx.clone()) + .await + .map(|batches| { + let batches = pretty_format_batches(&batches).unwrap(); + format!("{batches}") + }); + RoundTripResult { - batches: collect(parquet_exec.clone(), task_ctx).await, + batches, + explain, parquet_exec, - parquet_source: base_config - .file_source() - .as_any() - .downcast_ref::() - .unwrap() - .clone(), } } } @@ -1069,6 +1109,7 @@ mod tests { let parquet_exec = scan_format( &state, &ParquetFormat::default(), + None, &testdata, filename, Some(vec![0, 1, 2]), @@ -1101,6 +1142,92 @@ mod tests { Ok(()) } + #[tokio::test] + async fn parquet_exec_with_int96_from_spark() -> Result<()> { + // arrow-rs relies on the chrono library to convert between timestamps and strings, so + // instead compare as Int64. The underlying type should be a PrimitiveArray of Int64 + // anyway, so this should be a zero-copy non-modifying cast at the SchemaAdapter. + + let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)])); + let testdata = datafusion_common::test_util::parquet_test_data(); + let filename = "int96_from_spark.parquet"; + let session_ctx = SessionContext::new(); + let state = session_ctx.state(); + let task_ctx = state.task_ctx(); + + let time_units_and_expected = vec![ + ( + None, // Same as "ns" time_unit + Arc::new(Int64Array::from(vec![ + Some(1704141296123456000), // Reads as nanosecond fine (note 3 extra 0s) + Some(1704070800000000000), // Reads as nanosecond fine (note 3 extra 0s) + Some(-4852191831933722624), // Cannot be represented with nanos timestamp (year 9999) + Some(1735599600000000000), // Reads as nanosecond fine (note 3 extra 0s) + None, + Some(-4864435138808946688), // Cannot be represented with nanos timestamp (year 290000) + ])), + ), + ( + Some("ns".to_string()), + Arc::new(Int64Array::from(vec![ + Some(1704141296123456000), + Some(1704070800000000000), + Some(-4852191831933722624), + Some(1735599600000000000), + None, + Some(-4864435138808946688), + ])), + ), + ( + Some("us".to_string()), + Arc::new(Int64Array::from(vec![ + Some(1704141296123456), + Some(1704070800000000), + Some(253402225200000000), + Some(1735599600000000), + None, + Some(9089380393200000000), + ])), + ), + ]; + + for (time_unit, expected) in time_units_and_expected { + let parquet_exec = scan_format( + &state, + &ParquetFormat::default().with_coerce_int96(time_unit.clone()), + Some(schema.clone()), + &testdata, + filename, + Some(vec![0]), + None, + ) + .await + .unwrap(); + assert_eq!(parquet_exec.output_partitioning().partition_count(), 1); + + let mut results = parquet_exec.execute(0, task_ctx.clone())?; + let batch = results.next().await.unwrap()?; + + assert_eq!(6, batch.num_rows()); + assert_eq!(1, batch.num_columns()); + + assert_eq!(batch.num_columns(), 1); + let column = batch.column(0); + + assert_eq!(column.len(), expected.len()); + + column + .as_primitive::() + .iter() + .zip(expected.iter()) + .for_each(|(lhs, rhs)| { + assert_eq!(lhs, rhs); + }); + } + + Ok(()) + } + #[tokio::test] async fn parquet_exec_with_range() -> Result<()> { fn file_range(meta: &ObjectMeta, start: i64, end: i64) -> PartitionedFile { @@ -1375,26 +1502,6 @@ mod tests { create_batch(vec![("c1", c1.clone())]) } - /// Returns a int64 array with contents: - /// "[-1, 1, null, 2, 3, null, null]" - fn int64_batch() -> RecordBatch { - let contents: ArrayRef = Arc::new(Int64Array::from(vec![ - Some(-1), - Some(1), - None, - Some(2), - Some(3), - None, - None, - ])); - - create_batch(vec![ - ("a", contents.clone()), - ("b", contents.clone()), - ("c", contents.clone()), - ]) - } - #[tokio::test] async fn parquet_exec_metrics() { // batch1: c1(string) @@ -1454,110 +1561,17 @@ mod tests { .round_trip(vec![batch1]) .await; - // should have a pruning predicate - let pruning_predicate = rt.parquet_source.pruning_predicate(); - assert!(pruning_predicate.is_some()); - - // convert to explain plan form - let display = displayable(rt.parquet_exec.as_ref()) - .indent(true) - .to_string(); + let explain = rt.explain.unwrap(); - assert_contains!( - &display, - "pruning_predicate=c1_null_count@2 != row_count@3 AND (c1_min@0 != bar OR bar != c1_max@1)" - ); + // check that there was a pruning predicate -> row groups got pruned + assert_contains!(&explain, "predicate=c1@0 != bar"); - assert_contains!(&display, r#"predicate=c1@0 != bar"#); + // there's a single row group, but we can check that it matched + // if no pruning was done this would be 0 instead of 1 + assert_contains!(&explain, "row_groups_matched_statistics=1"); - assert_contains!(&display, "projection=[c1]"); - } - - #[tokio::test] - async fn parquet_exec_display_deterministic() { - // batches: a(int64), b(int64), c(int64) - let batches = int64_batch(); - - fn extract_required_guarantees(s: &str) -> Option<&str> { - s.split("required_guarantees=").nth(1) - } - - // Ensuring that the required_guarantees remain consistent across every display plan of the filter conditions - for _ in 0..100 { - // c = 1 AND b = 1 AND a = 1 - let filter0 = col("c") - .eq(lit(1)) - .and(col("b").eq(lit(1))) - .and(col("a").eq(lit(1))); - - let rt0 = RoundTrip::new() - .with_predicate(filter0) - .with_pushdown_predicate() - .round_trip(vec![batches.clone()]) - .await; - - let pruning_predicate = rt0.parquet_source.pruning_predicate(); - assert!(pruning_predicate.is_some()); - - let display0 = displayable(rt0.parquet_exec.as_ref()) - .indent(true) - .to_string(); - - let guarantees0: &str = extract_required_guarantees(&display0) - .expect("Failed to extract required_guarantees"); - // Compare only the required_guarantees part (Because the file_groups part will not be the same) - assert_eq!( - guarantees0.trim(), - "[a in (1), b in (1), c in (1)]", - "required_guarantees don't match" - ); - } - - // c = 1 AND a = 1 AND b = 1 - let filter1 = col("c") - .eq(lit(1)) - .and(col("a").eq(lit(1))) - .and(col("b").eq(lit(1))); - - let rt1 = RoundTrip::new() - .with_predicate(filter1) - .with_pushdown_predicate() - .round_trip(vec![batches.clone()]) - .await; - - // b = 1 AND a = 1 AND c = 1 - let filter2 = col("b") - .eq(lit(1)) - .and(col("a").eq(lit(1))) - .and(col("c").eq(lit(1))); - - let rt2 = RoundTrip::new() - .with_predicate(filter2) - .with_pushdown_predicate() - .round_trip(vec![batches]) - .await; - - // should have a pruning predicate - let pruning_predicate = rt1.parquet_source.pruning_predicate(); - assert!(pruning_predicate.is_some()); - let pruning_predicate = rt2.parquet_source.predicate(); - assert!(pruning_predicate.is_some()); - - // convert to explain plan form - let display1 = displayable(rt1.parquet_exec.as_ref()) - .indent(true) - .to_string(); - let display2 = displayable(rt2.parquet_exec.as_ref()) - .indent(true) - .to_string(); - - let guarantees1 = extract_required_guarantees(&display1) - .expect("Failed to extract required_guarantees"); - let guarantees2 = extract_required_guarantees(&display2) - .expect("Failed to extract required_guarantees"); - - // Compare only the required_guarantees part (Because the predicate part will not be the same) - assert_eq!(guarantees1, guarantees2, "required_guarantees don't match"); + // check the projection + assert_contains!(&explain, "projection=[c1]"); } #[tokio::test] @@ -1581,16 +1595,19 @@ mod tests { .await; // Should not contain a pruning predicate (since nothing can be pruned) - let pruning_predicate = rt.parquet_source.pruning_predicate(); - assert!( - pruning_predicate.is_none(), - "Still had pruning predicate: {pruning_predicate:?}" - ); + let explain = rt.explain.unwrap(); - // but does still has a pushdown down predicate - let predicate = rt.parquet_source.predicate(); - let filter_phys = logical2physical(&filter, rt.parquet_exec.schema().as_ref()); - assert_eq!(predicate.unwrap().to_string(), filter_phys.to_string()); + // When both matched and pruned are 0, it means that the pruning predicate + // was not used at all. + assert_contains!(&explain, "row_groups_matched_statistics=0"); + assert_contains!(&explain, "row_groups_pruned_statistics=0"); + + // But pushdown predicate should be present + assert_contains!( + &explain, + "predicate=CASE WHEN c1@0 != bar THEN true ELSE false END" + ); + assert_contains!(&explain, "pushdown_rows_pruned=5"); } #[tokio::test] @@ -1616,8 +1633,14 @@ mod tests { .await; // Should have a pruning predicate - let pruning_predicate = rt.parquet_source.pruning_predicate(); - assert!(pruning_predicate.is_some()); + let explain = rt.explain.unwrap(); + assert_contains!( + &explain, + "predicate=c1@0 = foo AND CASE WHEN c1@0 != bar THEN true ELSE false END" + ); + + // And bloom filters should have been evaluated + assert_contains!(&explain, "row_groups_pruned_bloom_filter=1"); } /// Returns the sum of all the metrics with the specified name @@ -1850,13 +1873,13 @@ mod tests { path: &str, store: Arc, batch: RecordBatch, - ) -> usize { + ) -> u64 { let mut writer = ArrowWriter::try_new(BytesMut::new().writer(), batch.schema(), None).unwrap(); writer.write(&batch).unwrap(); writer.flush().unwrap(); let bytes = writer.into_inner().unwrap().into_inner().freeze(); - let total_size = bytes.len(); + let total_size = bytes.len() as u64; let path = Path::from(path); let payload = object_store::PutPayload::from_bytes(bytes); store diff --git a/datafusion/core/src/datasource/statistics.rs b/datafusion/core/src/datasource/statistics.rs deleted file mode 100644 index cf283ecee0bf7..0000000000000 --- a/datafusion/core/src/datasource/statistics.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::mem; -use std::sync::Arc; - -use futures::{Stream, StreamExt}; - -use crate::arrow::datatypes::SchemaRef; -use crate::error::Result; -use crate::physical_plan::{ColumnStatistics, Statistics}; -use datafusion_common::stats::Precision; -use datafusion_common::ScalarValue; -use datafusion_datasource::file_groups::FileGroup; - -use super::listing::PartitionedFile; - -/// Get all files as well as the file level summary statistics (no statistic for partition columns). -/// If the optional `limit` is provided, includes only sufficient files. Needed to read up to -/// `limit` number of rows. `collect_stats` is passed down from the configuration parameter on -/// `ListingTable`. If it is false we only construct bare statistics and skip a potentially expensive -/// call to `multiunzip` for constructing file level summary statistics. -pub async fn get_statistics_with_limit( - all_files: impl Stream)>>, - file_schema: SchemaRef, - limit: Option, - collect_stats: bool, -) -> Result<(FileGroup, Statistics)> { - let mut result_files = FileGroup::default(); - // These statistics can be calculated as long as at least one file provides - // useful information. If none of the files provides any information, then - // they will end up having `Precision::Absent` values. Throughout calculations, - // missing values will be imputed as: - // - zero for summations, and - // - neutral element for extreme points. - let size = file_schema.fields().len(); - let mut col_stats_set = vec![ColumnStatistics::default(); size]; - let mut num_rows = Precision::::Absent; - let mut total_byte_size = Precision::::Absent; - - // Fusing the stream allows us to call next safely even once it is finished. - let mut all_files = Box::pin(all_files.fuse()); - - if let Some(first_file) = all_files.next().await { - let (mut file, file_stats) = first_file?; - file.statistics = Some(file_stats.as_ref().clone()); - result_files.push(file); - - // First file, we set them directly from the file statistics. - num_rows = file_stats.num_rows; - total_byte_size = file_stats.total_byte_size; - for (index, file_column) in - file_stats.column_statistics.clone().into_iter().enumerate() - { - col_stats_set[index].null_count = file_column.null_count; - col_stats_set[index].max_value = file_column.max_value; - col_stats_set[index].min_value = file_column.min_value; - col_stats_set[index].sum_value = file_column.sum_value; - } - - // If the number of rows exceeds the limit, we can stop processing - // files. This only applies when we know the number of rows. It also - // currently ignores tables that have no statistics regarding the - // number of rows. - let conservative_num_rows = match num_rows { - Precision::Exact(nr) => nr, - _ => usize::MIN, - }; - if conservative_num_rows <= limit.unwrap_or(usize::MAX) { - while let Some(current) = all_files.next().await { - let (mut file, file_stats) = current?; - file.statistics = Some(file_stats.as_ref().clone()); - result_files.push(file); - if !collect_stats { - continue; - } - - // We accumulate the number of rows, total byte size and null - // counts across all the files in question. If any file does not - // provide any information or provides an inexact value, we demote - // the statistic precision to inexact. - num_rows = add_row_stats(file_stats.num_rows, num_rows); - - total_byte_size = - add_row_stats(file_stats.total_byte_size, total_byte_size); - - for (file_col_stats, col_stats) in file_stats - .column_statistics - .iter() - .zip(col_stats_set.iter_mut()) - { - let ColumnStatistics { - null_count: file_nc, - max_value: file_max, - min_value: file_min, - sum_value: file_sum, - distinct_count: _, - } = file_col_stats; - - col_stats.null_count = add_row_stats(*file_nc, col_stats.null_count); - set_max_if_greater(file_max, &mut col_stats.max_value); - set_min_if_lesser(file_min, &mut col_stats.min_value); - col_stats.sum_value = file_sum.add(&col_stats.sum_value); - } - - // If the number of rows exceeds the limit, we can stop processing - // files. This only applies when we know the number of rows. It also - // currently ignores tables that have no statistics regarding the - // number of rows. - if num_rows.get_value().unwrap_or(&usize::MIN) - > &limit.unwrap_or(usize::MAX) - { - break; - } - } - } - }; - - let mut statistics = Statistics { - num_rows, - total_byte_size, - column_statistics: col_stats_set, - }; - if all_files.next().await.is_some() { - // If we still have files in the stream, it means that the limit kicked - // in, and the statistic could have been different had we processed the - // files in a different order. - statistics = statistics.to_inexact() - } - - Ok((result_files, statistics)) -} - -fn add_row_stats( - file_num_rows: Precision, - num_rows: Precision, -) -> Precision { - match (file_num_rows, &num_rows) { - (Precision::Absent, _) => num_rows.to_inexact(), - (lhs, Precision::Absent) => lhs.to_inexact(), - (lhs, rhs) => lhs.add(rhs), - } -} - -/// If the given value is numerically greater than the original maximum value, -/// return the new maximum value with appropriate exactness information. -fn set_max_if_greater( - max_nominee: &Precision, - max_value: &mut Precision, -) { - match (&max_value, max_nominee) { - (Precision::Exact(val1), Precision::Exact(val2)) if val1 < val2 => { - *max_value = max_nominee.clone(); - } - (Precision::Exact(val1), Precision::Inexact(val2)) - | (Precision::Inexact(val1), Precision::Inexact(val2)) - | (Precision::Inexact(val1), Precision::Exact(val2)) - if val1 < val2 => - { - *max_value = max_nominee.clone().to_inexact(); - } - (Precision::Exact(_), Precision::Absent) => { - let exact_max = mem::take(max_value); - *max_value = exact_max.to_inexact(); - } - (Precision::Absent, Precision::Exact(_)) => { - *max_value = max_nominee.clone().to_inexact(); - } - (Precision::Absent, Precision::Inexact(_)) => { - *max_value = max_nominee.clone(); - } - _ => {} - } -} - -/// If the given value is numerically lesser than the original minimum value, -/// return the new minimum value with appropriate exactness information. -fn set_min_if_lesser( - min_nominee: &Precision, - min_value: &mut Precision, -) { - match (&min_value, min_nominee) { - (Precision::Exact(val1), Precision::Exact(val2)) if val1 > val2 => { - *min_value = min_nominee.clone(); - } - (Precision::Exact(val1), Precision::Inexact(val2)) - | (Precision::Inexact(val1), Precision::Inexact(val2)) - | (Precision::Inexact(val1), Precision::Exact(val2)) - if val1 > val2 => - { - *min_value = min_nominee.clone().to_inexact(); - } - (Precision::Exact(_), Precision::Absent) => { - let exact_min = mem::take(min_value); - *min_value = exact_min.to_inexact(); - } - (Precision::Absent, Precision::Exact(_)) => { - *min_value = min_nominee.clone().to_inexact(); - } - (Precision::Absent, Precision::Inexact(_)) => { - *min_value = min_nominee.clone(); - } - _ => {} - } -} diff --git a/datafusion/core/src/execution/context/mod.rs b/datafusion/core/src/execution/context/mod.rs index fc110a0699df2..0bb91536da3ca 100644 --- a/datafusion/core/src/execution/context/mod.rs +++ b/datafusion/core/src/execution/context/mod.rs @@ -35,7 +35,11 @@ use crate::{ }, datasource::{provider_as_source, MemTable, ViewTable}, error::{DataFusionError, Result}, - execution::{options::ArrowReadOptions, runtime_env::RuntimeEnv, FunctionRegistry}, + execution::{ + options::ArrowReadOptions, + runtime_env::{RuntimeEnv, RuntimeEnvBuilder}, + FunctionRegistry, + }, logical_expr::AggregateUDF, logical_expr::ScalarUDF, logical_expr::{ @@ -1036,13 +1040,73 @@ impl SessionContext { variable, value, .. } = stmt; - let mut state = self.state.write(); - state.config_mut().options_mut().set(&variable, &value)?; - drop(state); + // Check if this is a runtime configuration + if variable.starts_with("datafusion.runtime.") { + self.set_runtime_variable(&variable, &value)?; + } else { + let mut state = self.state.write(); + state.config_mut().options_mut().set(&variable, &value)?; + drop(state); + } self.return_empty_dataframe() } + fn set_runtime_variable(&self, variable: &str, value: &str) -> Result<()> { + let key = variable.strip_prefix("datafusion.runtime.").unwrap(); + + match key { + "memory_limit" => { + let memory_limit = Self::parse_memory_limit(value)?; + + let mut state = self.state.write(); + let mut builder = + RuntimeEnvBuilder::from_runtime_env(state.runtime_env()); + builder = builder.with_memory_limit(memory_limit, 1.0); + *state = SessionStateBuilder::from(state.clone()) + .with_runtime_env(Arc::new(builder.build()?)) + .build(); + } + _ => { + return Err(DataFusionError::Plan(format!( + "Unknown runtime configuration: {}", + variable + ))) + } + } + Ok(()) + } + + /// Parse memory limit from string to number of bytes + /// Supports formats like '1.5G', '100M', '512K' + /// + /// # Examples + /// ``` + /// use datafusion::execution::context::SessionContext; + /// + /// assert_eq!(SessionContext::parse_memory_limit("1M").unwrap(), 1024 * 1024); + /// assert_eq!(SessionContext::parse_memory_limit("1.5G").unwrap(), (1.5 * 1024.0 * 1024.0 * 1024.0) as usize); + /// ``` + pub fn parse_memory_limit(limit: &str) -> Result { + let (number, unit) = limit.split_at(limit.len() - 1); + let number: f64 = number.parse().map_err(|_| { + DataFusionError::Plan(format!( + "Failed to parse number from memory limit '{}'", + limit + )) + })?; + + match unit { + "K" => Ok((number * 1024.0) as usize), + "M" => Ok((number * 1024.0 * 1024.0) as usize), + "G" => Ok((number * 1024.0 * 1024.0 * 1024.0) as usize), + _ => Err(DataFusionError::Plan(format!( + "Unsupported unit '{}' in memory limit '{}'", + unit, limit + ))), + } + } + async fn create_custom_table( &self, cmd: &CreateExternalTable, @@ -1833,7 +1897,6 @@ mod tests { use crate::test; use crate::test_util::{plan_and_collect, populate_csv_partitions}; use arrow::datatypes::{DataType, TimeUnit}; - use std::env; use std::error::Error; use std::path::PathBuf; diff --git a/datafusion/core/src/execution/session_state.rs b/datafusion/core/src/execution/session_state.rs index 28f599304f8c8..597700bf8be3d 100644 --- a/datafusion/core/src/execution/session_state.rs +++ b/datafusion/core/src/execution/session_state.rs @@ -1348,28 +1348,30 @@ impl SessionStateBuilder { } = self; let config = config.unwrap_or_default(); - let runtime_env = runtime_env.unwrap_or(Arc::new(RuntimeEnv::default())); + let runtime_env = runtime_env.unwrap_or_else(|| Arc::new(RuntimeEnv::default())); let mut state = SessionState { - session_id: session_id.unwrap_or(Uuid::new_v4().to_string()), + session_id: session_id.unwrap_or_else(|| Uuid::new_v4().to_string()), analyzer: analyzer.unwrap_or_default(), expr_planners: expr_planners.unwrap_or_default(), type_planner, optimizer: optimizer.unwrap_or_default(), physical_optimizers: physical_optimizers.unwrap_or_default(), - query_planner: query_planner.unwrap_or(Arc::new(DefaultQueryPlanner {})), - catalog_list: catalog_list - .unwrap_or(Arc::new(MemoryCatalogProviderList::new()) - as Arc), + query_planner: query_planner + .unwrap_or_else(|| Arc::new(DefaultQueryPlanner {})), + catalog_list: catalog_list.unwrap_or_else(|| { + Arc::new(MemoryCatalogProviderList::new()) as Arc + }), table_functions: table_functions.unwrap_or_default(), scalar_functions: HashMap::new(), aggregate_functions: HashMap::new(), window_functions: HashMap::new(), serializer_registry: serializer_registry - .unwrap_or(Arc::new(EmptySerializerRegistry)), + .unwrap_or_else(|| Arc::new(EmptySerializerRegistry)), file_formats: HashMap::new(), - table_options: table_options - .unwrap_or(TableOptions::default_from_session_config(config.options())), + table_options: table_options.unwrap_or_else(|| { + TableOptions::default_from_session_config(config.options()) + }), config, execution_props: execution_props.unwrap_or_default(), table_factories: table_factories.unwrap_or_default(), diff --git a/datafusion/core/src/lib.rs b/datafusion/core/src/lib.rs index cc510bc81f1a8..928efd533ca44 100644 --- a/datafusion/core/src/lib.rs +++ b/datafusion/core/src/lib.rs @@ -22,7 +22,18 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] // Make sure fast / cheap clones on Arc are explicit: // https://github.com/apache/datafusion/issues/11143 -#![cfg_attr(not(test), deny(clippy::clone_on_ref_ptr))] +// +// Eliminate unnecessary function calls(some may be not cheap) due to `xxx_or` +// for performance. Also avoid abusing `xxx_or_else` for readability: +// https://github.com/apache/datafusion/issues/15802 +#![cfg_attr( + not(test), + deny( + clippy::clone_on_ref_ptr, + clippy::or_fun_call, + clippy::unnecessary_lazy_evaluations + ) +)] #![warn(missing_docs, clippy::needless_borrow)] //! [DataFusion] is an extensible query engine written in Rust that @@ -872,6 +883,12 @@ doc_comment::doctest!( user_guide_configs ); +#[cfg(doctest)] +doc_comment::doctest!( + "../../../docs/source/user-guide/runtime_configs.md", + user_guide_runtime_configs +); + #[cfg(doctest)] doc_comment::doctest!( "../../../docs/source/user-guide/crate-configuration.md", @@ -1021,8 +1038,8 @@ doc_comment::doctest!( #[cfg(doctest)] doc_comment::doctest!( - "../../../docs/source/user-guide/sql/write_options.md", - user_guide_sql_write_options + "../../../docs/source/user-guide/sql/format_options.md", + user_guide_sql_format_options ); #[cfg(doctest)] diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs index f1a99a7714ac4..be24206c676c6 100644 --- a/datafusion/core/src/physical_planner.rs +++ b/datafusion/core/src/physical_planner.rs @@ -81,7 +81,7 @@ use datafusion_expr::{ WindowFrameBound, WriteOp, }; use datafusion_physical_expr::aggregate::{AggregateExprBuilder, AggregateFunctionExpr}; -use datafusion_physical_expr::expressions::Literal; +use datafusion_physical_expr::expressions::{Column, Literal}; use datafusion_physical_expr::LexOrdering; use datafusion_physical_optimizer::PhysicalOptimizerRule; use datafusion_physical_plan::execution_plan::InvariantLevel; @@ -1023,18 +1023,12 @@ impl DefaultPhysicalPlanner { // Collect left & right field indices, the field indices are sorted in ascending order let left_field_indices = cols .iter() - .filter_map(|c| match left_df_schema.index_of_column(c) { - Ok(idx) => Some(idx), - _ => None, - }) + .filter_map(|c| left_df_schema.index_of_column(c).ok()) .sorted() .collect::>(); let right_field_indices = cols .iter() - .filter_map(|c| match right_df_schema.index_of_column(c) { - Ok(idx) => Some(idx), - _ => None, - }) + .filter_map(|c| right_df_schema.index_of_column(c).ok()) .sorted() .collect::>(); @@ -2006,7 +2000,8 @@ impl DefaultPhysicalPlanner { input: &Arc, expr: &[Expr], ) -> Result> { - let input_schema = input.as_ref().schema(); + let input_logical_schema = input.as_ref().schema(); + let input_physical_schema = input_exec.schema(); let physical_exprs = expr .iter() .map(|e| { @@ -2025,7 +2020,7 @@ impl DefaultPhysicalPlanner { // This depends on the invariant that logical schema field index MUST match // with physical schema field index. let physical_name = if let Expr::Column(col) = e { - match input_schema.index_of_column(col) { + match input_logical_schema.index_of_column(col) { Ok(idx) => { // index physical field using logical field index Ok(input_exec.schema().field(idx).name().to_string()) @@ -2038,10 +2033,14 @@ impl DefaultPhysicalPlanner { physical_name(e) }; - tuple_err(( - self.create_physical_expr(e, input_schema, session_state), - physical_name, - )) + let physical_expr = + self.create_physical_expr(e, input_logical_schema, session_state); + + // Check for possible column name mismatches + let final_physical_expr = + maybe_fix_physical_column_name(physical_expr, &input_physical_schema); + + tuple_err((final_physical_expr, physical_name)) }) .collect::>>()?; @@ -2061,6 +2060,40 @@ fn tuple_err(value: (Result, Result)) -> Result<(T, R)> { } } +// Handle the case where the name of a physical column expression does not match the corresponding physical input fields names. +// Physical column names are derived from the physical schema, whereas physical column expressions are derived from the logical column names. +// +// This is a special case that applies only to column expressions. Logical plans may slightly modify column names by appending a suffix (e.g., using ':'), +// to avoid duplicates—since DFSchemas do not allow duplicate names. For example: `count(Int64(1)):1`. +fn maybe_fix_physical_column_name( + expr: Result>, + input_physical_schema: &SchemaRef, +) -> Result> { + if let Ok(e) = &expr { + if let Some(column) = e.as_any().downcast_ref::() { + let physical_field = input_physical_schema.field(column.index()); + let expr_col_name = column.name(); + let physical_name = physical_field.name(); + + if physical_name != expr_col_name { + // handle edge cases where the physical_name contains ':'. + let colon_count = physical_name.matches(':').count(); + let mut splits = expr_col_name.match_indices(':'); + let split_pos = splits.nth(colon_count); + + if let Some((idx, _)) = split_pos { + let base_name = &expr_col_name[..idx]; + if base_name == physical_name { + let updated_column = Column::new(physical_name, column.index()); + return Ok(Arc::new(updated_column)); + } + } + } + } + } + expr +} + struct OptimizationInvariantChecker<'a> { rule: &'a Arc, } @@ -2656,6 +2689,30 @@ mod tests { } } + #[tokio::test] + async fn test_maybe_fix_colon_in_physical_name() { + // The physical schema has a field name with a colon + let schema = Schema::new(vec![Field::new("metric:avg", DataType::Int32, false)]); + let schema_ref: SchemaRef = Arc::new(schema); + + // What might happen after deduplication + let logical_col_name = "metric:avg:1"; + let expr_with_suffix = + Arc::new(Column::new(logical_col_name, 0)) as Arc; + let expr_result = Ok(expr_with_suffix); + + // Call function under test + let fixed_expr = + maybe_fix_physical_column_name(expr_result, &schema_ref).unwrap(); + + // Downcast back to Column so we can check the name + let col = fixed_expr + .as_any() + .downcast_ref::() + .expect("Column"); + + assert_eq!(col.name(), "metric:avg"); + } struct ErrorExtensionPlanner {} #[async_trait] diff --git a/datafusion/core/src/test/object_store.rs b/datafusion/core/src/test/object_store.rs index e1328770cabdd..8b19658bb1473 100644 --- a/datafusion/core/src/test/object_store.rs +++ b/datafusion/core/src/test/object_store.rs @@ -66,7 +66,7 @@ pub fn local_unpartitioned_file(path: impl AsRef) -> ObjectMeta ObjectMeta { location, last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len() as usize, + size: metadata.len(), e_tag: None, version: None, } @@ -166,7 +166,7 @@ impl ObjectStore for BlockingObjectStore { fn list( &self, prefix: Option<&Path>, - ) -> BoxStream<'_, object_store::Result> { + ) -> BoxStream<'static, object_store::Result> { self.inner.list(prefix) } diff --git a/datafusion/core/src/test_util/parquet.rs b/datafusion/core/src/test_util/parquet.rs index 084554eecbdb0..f5753af64d93f 100644 --- a/datafusion/core/src/test_util/parquet.rs +++ b/datafusion/core/src/test_util/parquet.rs @@ -102,7 +102,7 @@ impl TestParquetFile { println!("Generated test dataset with {num_rows} rows"); - let size = std::fs::metadata(&path)?.len() as usize; + let size = std::fs::metadata(&path)?.len(); let mut canonical_path = path.canonicalize()?; diff --git a/datafusion/core/tests/core_integration.rs b/datafusion/core/tests/core_integration.rs index 9bcb9e41f86a9..250538b133703 100644 --- a/datafusion/core/tests/core_integration.rs +++ b/datafusion/core/tests/core_integration.rs @@ -51,6 +51,9 @@ mod serde; /// Run all tests that are found in the `catalog` directory mod catalog; +/// Run all tests that are found in the `tracing` directory +mod tracing; + #[cfg(test)] #[ctor::ctor] fn init() { diff --git a/datafusion/core/tests/dataframe/dataframe_functions.rs b/datafusion/core/tests/dataframe/dataframe_functions.rs index c763d4c8de2d6..40590d74ad910 100644 --- a/datafusion/core/tests/dataframe/dataframe_functions.rs +++ b/datafusion/core/tests/dataframe/dataframe_functions.rs @@ -384,7 +384,7 @@ async fn test_fn_approx_median() -> Result<()> { #[tokio::test] async fn test_fn_approx_percentile_cont() -> Result<()> { - let expr = approx_percentile_cont(col("b"), lit(0.5), None); + let expr = approx_percentile_cont(col("b").sort(true, false), lit(0.5), None); let df = create_test_table().await?; let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; @@ -392,11 +392,26 @@ async fn test_fn_approx_percentile_cont() -> Result<()> { assert_snapshot!( batches_to_string(&batches), @r" - +---------------------------------------------+ - | approx_percentile_cont(test.b,Float64(0.5)) | - +---------------------------------------------+ - | 10 | - +---------------------------------------------+ + +---------------------------------------------------------------------------+ + | approx_percentile_cont(Float64(0.5)) WITHIN GROUP [test.b ASC NULLS LAST] | + +---------------------------------------------------------------------------+ + | 10 | + +---------------------------------------------------------------------------+ + "); + + let expr = approx_percentile_cont(col("b").sort(false, false), lit(0.1), None); + + let df = create_test_table().await?; + let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; + + assert_snapshot!( + batches_to_string(&batches), + @r" + +----------------------------------------------------------------------------+ + | approx_percentile_cont(Float64(0.1)) WITHIN GROUP [test.b DESC NULLS LAST] | + +----------------------------------------------------------------------------+ + | 100 | + +----------------------------------------------------------------------------+ "); // the arg2 parameter is a complex expr, but it can be evaluated to the literal value @@ -405,23 +420,59 @@ async fn test_fn_approx_percentile_cont() -> Result<()> { None::<&str>, "arg_2".to_string(), )); - let expr = approx_percentile_cont(col("b"), alias_expr, None); + let expr = approx_percentile_cont(col("b").sort(true, false), alias_expr, None); let df = create_test_table().await?; let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; assert_snapshot!( batches_to_string(&batches), @r" - +--------------------------------------+ - | approx_percentile_cont(test.b,arg_2) | - +--------------------------------------+ - | 10 | - +--------------------------------------+ + +--------------------------------------------------------------------+ + | approx_percentile_cont(arg_2) WITHIN GROUP [test.b ASC NULLS LAST] | + +--------------------------------------------------------------------+ + | 10 | + +--------------------------------------------------------------------+ + " + ); + + let alias_expr = Expr::Alias(Alias::new( + cast(lit(0.1), DataType::Float32), + None::<&str>, + "arg_2".to_string(), + )); + let expr = approx_percentile_cont(col("b").sort(false, false), alias_expr, None); + let df = create_test_table().await?; + let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; + + assert_snapshot!( + batches_to_string(&batches), + @r" + +---------------------------------------------------------------------+ + | approx_percentile_cont(arg_2) WITHIN GROUP [test.b DESC NULLS LAST] | + +---------------------------------------------------------------------+ + | 100 | + +---------------------------------------------------------------------+ " ); // with number of centroids set - let expr = approx_percentile_cont(col("b"), lit(0.5), Some(lit(2))); + let expr = approx_percentile_cont(col("b").sort(true, false), lit(0.5), Some(lit(2))); + + let df = create_test_table().await?; + let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; + + assert_snapshot!( + batches_to_string(&batches), + @r" + +------------------------------------------------------------------------------------+ + | approx_percentile_cont(Float64(0.5),Int32(2)) WITHIN GROUP [test.b ASC NULLS LAST] | + +------------------------------------------------------------------------------------+ + | 30 | + +------------------------------------------------------------------------------------+ + "); + + let expr = + approx_percentile_cont(col("b").sort(false, false), lit(0.1), Some(lit(2))); let df = create_test_table().await?; let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; @@ -429,11 +480,11 @@ async fn test_fn_approx_percentile_cont() -> Result<()> { assert_snapshot!( batches_to_string(&batches), @r" - +------------------------------------------------------+ - | approx_percentile_cont(test.b,Float64(0.5),Int32(2)) | - +------------------------------------------------------+ - | 30 | - +------------------------------------------------------+ + +-------------------------------------------------------------------------------------+ + | approx_percentile_cont(Float64(0.1),Int32(2)) WITHIN GROUP [test.b DESC NULLS LAST] | + +-------------------------------------------------------------------------------------+ + | 69 | + +-------------------------------------------------------------------------------------+ "); Ok(()) diff --git a/datafusion/core/tests/dataframe/mod.rs b/datafusion/core/tests/dataframe/mod.rs index b5923269ab8ba..1855a512048d6 100644 --- a/datafusion/core/tests/dataframe/mod.rs +++ b/datafusion/core/tests/dataframe/mod.rs @@ -5206,6 +5206,40 @@ fn union_fields() -> UnionFields { .collect() } +#[tokio::test] +async fn union_literal_is_null_and_not_null() -> Result<()> { + let str_array_1 = StringArray::from(vec![None::]); + let str_array_2 = StringArray::from(vec![Some("a")]); + + let batch_1 = + RecordBatch::try_from_iter(vec![("arr", Arc::new(str_array_1) as ArrayRef)])?; + let batch_2 = + RecordBatch::try_from_iter(vec![("arr", Arc::new(str_array_2) as ArrayRef)])?; + + let ctx = SessionContext::new(); + ctx.register_batch("union_batch_1", batch_1)?; + ctx.register_batch("union_batch_2", batch_2)?; + + let df1 = ctx.table("union_batch_1").await?; + let df2 = ctx.table("union_batch_2").await?; + + let batches = df1.union(df2)?.collect().await?; + let schema = batches[0].schema(); + + for batch in batches { + // Verify schema is the same for all batches + if !schema.contains(&batch.schema()) { + return Err(DataFusionError::Internal(format!( + "Schema mismatch. Previously had\n{:#?}\n\nGot:\n{:#?}", + &schema, + batch.schema() + ))); + } + } + + Ok(()) +} + #[tokio::test] async fn sparse_union_is_null() { // union of [{A=1}, {A=}, {B=3.2}, {B=}, {C="a"}, {C=}] @@ -5477,6 +5511,64 @@ async fn boolean_dictionary_as_filter() { ); } +#[tokio::test] +async fn test_union_by_name() -> Result<()> { + let df = create_test_table("test") + .await? + .select(vec![col("a"), col("b"), lit(1).alias("c")])? + .alias("table_alias")?; + + let df2 = df.clone().select_columns(&["c", "b", "a"])?; + let result = df.union_by_name(df2)?.sort_by(vec![col("a"), col("b")])?; + + assert_snapshot!( + batches_to_sort_string(&result.collect().await?), + @r" + +-----------+-----+---+ + | a | b | c | + +-----------+-----+---+ + | 123AbcDef | 100 | 1 | + | 123AbcDef | 100 | 1 | + | CBAdef | 10 | 1 | + | CBAdef | 10 | 1 | + | abc123 | 10 | 1 | + | abc123 | 10 | 1 | + | abcDEF | 1 | 1 | + | abcDEF | 1 | 1 | + +-----------+-----+---+ + " + ); + Ok(()) +} + +#[tokio::test] +async fn test_union_by_name_distinct() -> Result<()> { + let df = create_test_table("test") + .await? + .select(vec![col("a"), col("b"), lit(1).alias("c")])? + .alias("table_alias")?; + + let df2 = df.clone().select_columns(&["c", "b", "a"])?; + let result = df + .union_by_name_distinct(df2)? + .sort_by(vec![col("a"), col("b")])?; + + assert_snapshot!( + batches_to_sort_string(&result.collect().await?), + @r" + +-----------+-----+---+ + | a | b | c | + +-----------+-----+---+ + | 123AbcDef | 100 | 1 | + | CBAdef | 10 | 1 | + | abc123 | 10 | 1 | + | abcDEF | 1 | 1 | + +-----------+-----+---+ + " + ); + Ok(()) +} + #[tokio::test] async fn test_alias() -> Result<()> { let df = create_test_table("test") diff --git a/datafusion/core/tests/execution/logical_plan.rs b/datafusion/core/tests/execution/logical_plan.rs index b30636ddf6a81..fdee6fd5dbbce 100644 --- a/datafusion/core/tests/execution/logical_plan.rs +++ b/datafusion/core/tests/execution/logical_plan.rs @@ -19,15 +19,19 @@ //! create them and depend on them. Test executable semantics of logical plans. use arrow::array::Int64Array; -use arrow::datatypes::{DataType, Field}; +use arrow::datatypes::{DataType, Field, Schema}; +use datafusion::datasource::{provider_as_source, ViewTable}; use datafusion::execution::session_state::SessionStateBuilder; -use datafusion_common::{Column, DFSchema, Result, ScalarValue, Spans}; +use datafusion_common::{Column, DFSchema, DFSchemaRef, Result, ScalarValue, Spans}; use datafusion_execution::TaskContext; use datafusion_expr::expr::{AggregateFunction, AggregateFunctionParams}; use datafusion_expr::logical_plan::{LogicalPlan, Values}; -use datafusion_expr::{Aggregate, AggregateUDF, Expr}; +use datafusion_expr::{ + Aggregate, AggregateUDF, EmptyRelation, Expr, LogicalPlanBuilder, UNNAMED_TABLE, +}; use datafusion_functions_aggregate::count::Count; use datafusion_physical_plan::collect; +use insta::assert_snapshot; use std::collections::HashMap; use std::fmt::Debug; use std::ops::Deref; @@ -96,3 +100,37 @@ where }; element } + +#[test] +fn inline_scan_projection_test() -> Result<()> { + let name = UNNAMED_TABLE; + let column = "a"; + + let schema = Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ]); + let projection = vec![schema.index_of(column)?]; + + let provider = ViewTable::new( + LogicalPlan::EmptyRelation(EmptyRelation { + produce_one_row: false, + schema: DFSchemaRef::new(DFSchema::try_from(schema)?), + }), + None, + ); + let source = provider_as_source(Arc::new(provider)); + + let plan = LogicalPlanBuilder::scan(name, source, Some(projection))?.build()?; + + assert_snapshot!( + format!("{plan}"), + @r" + SubqueryAlias: ?table? + Projection: a + EmptyRelation + " + ); + + Ok(()) +} diff --git a/datafusion/core/tests/expr_api/simplification.rs b/datafusion/core/tests/expr_api/simplification.rs index 7bb21725ef401..34e0487f312fb 100644 --- a/datafusion/core/tests/expr_api/simplification.rs +++ b/datafusion/core/tests/expr_api/simplification.rs @@ -547,9 +547,9 @@ fn test_simplify_with_cycle_count( }; let simplifier = ExprSimplifier::new(info); let (simplified_expr, count) = simplifier - .simplify_with_cycle_count(input_expr.clone()) + .simplify_with_cycle_count_transformed(input_expr.clone()) .expect("successfully evaluated"); - + let simplified_expr = simplified_expr.data; assert_eq!( simplified_expr, expected_expr, "Mismatch evaluating {input_expr}\n Expected:{expected_expr}\n Got:{simplified_expr}" diff --git a/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs b/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs index dcf477135a377..ff3b66986ced9 100644 --- a/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs +++ b/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs @@ -18,16 +18,17 @@ use std::sync::Arc; use crate::fuzz_cases::aggregation_fuzzer::{ - AggregationFuzzerBuilder, ColumnDescr, DatasetGeneratorConfig, QueryBuilder, + AggregationFuzzerBuilder, DatasetGeneratorConfig, QueryBuilder, }; -use arrow::array::{types::Int64Type, Array, ArrayRef, AsArray, Int64Array, RecordBatch}; -use arrow::compute::{concat_batches, SortOptions}; -use arrow::datatypes::{ - DataType, IntervalUnit, TimeUnit, DECIMAL128_MAX_PRECISION, DECIMAL128_MAX_SCALE, - DECIMAL256_MAX_PRECISION, DECIMAL256_MAX_SCALE, +use arrow::array::{ + types::Int64Type, Array, ArrayRef, AsArray, Int32Array, Int64Array, RecordBatch, + StringArray, }; +use arrow::compute::{concat_batches, SortOptions}; +use arrow::datatypes::DataType; use arrow::util::pretty::pretty_format_batches; +use arrow_schema::{Field, Schema, SchemaRef}; use datafusion::common::Result; use datafusion::datasource::memory::MemorySourceConfig; use datafusion::datasource::source::DataSourceExec; @@ -42,14 +43,20 @@ use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion, TreeNodeVisitor} use datafusion_common::HashMap; use datafusion_common_runtime::JoinSet; use datafusion_functions_aggregate::sum::sum_udaf; -use datafusion_physical_expr::expressions::col; +use datafusion_physical_expr::expressions::{col, lit, Column}; use datafusion_physical_expr::PhysicalSortExpr; use datafusion_physical_expr_common::sort_expr::LexOrdering; use datafusion_physical_plan::InputOrderMode; use test_utils::{add_empty_batches, StringBatchGenerator}; +use datafusion_execution::memory_pool::FairSpillPool; +use datafusion_execution::runtime_env::RuntimeEnvBuilder; +use datafusion_execution::TaskContext; +use datafusion_physical_plan::metrics::MetricValue; use rand::rngs::StdRng; -use rand::{thread_rng, Rng, SeedableRng}; +use rand::{random, thread_rng, Rng, SeedableRng}; + +use super::record_batch_generator::get_supported_types_columns; // ======================================================================== // The new aggregation fuzz tests based on [`AggregationFuzzer`] @@ -113,6 +120,32 @@ async fn test_first_val() { .await; } +#[tokio::test(flavor = "multi_thread")] +async fn test_last_val() { + let mut data_gen_config = baseline_config(); + + for i in 0..data_gen_config.columns.len() { + if data_gen_config.columns[i].get_max_num_distinct().is_none() { + data_gen_config.columns[i] = data_gen_config.columns[i] + .clone() + // Minimize the chance of identical values in the order by columns to make the test more stable + .with_max_num_distinct(usize::MAX); + } + } + + let query_builder = QueryBuilder::new() + .with_table_name("fuzz_table") + .with_aggregate_function("last_value") + .with_aggregate_arguments(data_gen_config.all_columns()) + .set_group_by_columns(data_gen_config.all_columns()); + + AggregationFuzzerBuilder::from(data_gen_config) + .add_query_builder(query_builder) + .build() + .run() + .await; +} + #[tokio::test(flavor = "multi_thread")] async fn test_max() { let data_gen_config = baseline_config(); @@ -201,81 +234,7 @@ async fn test_median() { /// 1. structured types fn baseline_config() -> DatasetGeneratorConfig { let mut rng = thread_rng(); - let columns = vec![ - ColumnDescr::new("i8", DataType::Int8), - ColumnDescr::new("i16", DataType::Int16), - ColumnDescr::new("i32", DataType::Int32), - ColumnDescr::new("i64", DataType::Int64), - ColumnDescr::new("u8", DataType::UInt8), - ColumnDescr::new("u16", DataType::UInt16), - ColumnDescr::new("u32", DataType::UInt32), - ColumnDescr::new("u64", DataType::UInt64), - ColumnDescr::new("date32", DataType::Date32), - ColumnDescr::new("date64", DataType::Date64), - ColumnDescr::new("time32_s", DataType::Time32(TimeUnit::Second)), - ColumnDescr::new("time32_ms", DataType::Time32(TimeUnit::Millisecond)), - ColumnDescr::new("time64_us", DataType::Time64(TimeUnit::Microsecond)), - ColumnDescr::new("time64_ns", DataType::Time64(TimeUnit::Nanosecond)), - // `None` is passed in here however when generating the array, it will generate - // random timezones. - ColumnDescr::new("timestamp_s", DataType::Timestamp(TimeUnit::Second, None)), - ColumnDescr::new( - "timestamp_ms", - DataType::Timestamp(TimeUnit::Millisecond, None), - ), - ColumnDescr::new( - "timestamp_us", - DataType::Timestamp(TimeUnit::Microsecond, None), - ), - ColumnDescr::new( - "timestamp_ns", - DataType::Timestamp(TimeUnit::Nanosecond, None), - ), - ColumnDescr::new("float32", DataType::Float32), - ColumnDescr::new("float64", DataType::Float64), - ColumnDescr::new( - "interval_year_month", - DataType::Interval(IntervalUnit::YearMonth), - ), - ColumnDescr::new( - "interval_day_time", - DataType::Interval(IntervalUnit::DayTime), - ), - ColumnDescr::new( - "interval_month_day_nano", - DataType::Interval(IntervalUnit::MonthDayNano), - ), - // begin decimal columns - ColumnDescr::new("decimal128", { - // Generate valid precision and scale for Decimal128 randomly. - let precision: u8 = rng.gen_range(1..=DECIMAL128_MAX_PRECISION); - // It's safe to cast `precision` to i8 type directly. - let scale: i8 = rng.gen_range( - i8::MIN..=std::cmp::min(precision as i8, DECIMAL128_MAX_SCALE), - ); - DataType::Decimal128(precision, scale) - }), - ColumnDescr::new("decimal256", { - // Generate valid precision and scale for Decimal256 randomly. - let precision: u8 = rng.gen_range(1..=DECIMAL256_MAX_PRECISION); - // It's safe to cast `precision` to i8 type directly. - let scale: i8 = rng.gen_range( - i8::MIN..=std::cmp::min(precision as i8, DECIMAL256_MAX_SCALE), - ); - DataType::Decimal256(precision, scale) - }), - // begin string columns - ColumnDescr::new("utf8", DataType::Utf8), - ColumnDescr::new("largeutf8", DataType::LargeUtf8), - ColumnDescr::new("utf8view", DataType::Utf8View), - // low cardinality columns - ColumnDescr::new("u8_low", DataType::UInt8).with_max_num_distinct(10), - ColumnDescr::new("utf8_low", DataType::Utf8).with_max_num_distinct(10), - ColumnDescr::new("bool", DataType::Boolean), - ColumnDescr::new("binary", DataType::Binary), - ColumnDescr::new("large_binary", DataType::LargeBinary), - ColumnDescr::new("binaryview", DataType::BinaryView), - ]; + let columns = get_supported_types_columns(rng.gen()); let min_num_rows = 512; let max_num_rows = 1024; @@ -663,3 +622,134 @@ fn extract_result_counts(results: Vec) -> HashMap, i } output } + +fn assert_spill_count_metric(expect_spill: bool, single_aggregate: Arc) { + if let Some(metrics_set) = single_aggregate.metrics() { + let mut spill_count = 0; + + // Inspect metrics for SpillCount + for metric in metrics_set.iter() { + if let MetricValue::SpillCount(count) = metric.value() { + spill_count = count.value(); + break; + } + } + + if expect_spill && spill_count == 0 { + panic!("Expected spill but SpillCount metric not found or SpillCount was 0."); + } else if !expect_spill && spill_count > 0 { + panic!("Expected no spill but found SpillCount metric with value greater than 0."); + } + } else { + panic!("No metrics returned from the operator; cannot verify spilling."); + } +} + +// Fix for https://github.com/apache/datafusion/issues/15530 +#[tokio::test] +async fn test_single_mode_aggregate_with_spill() -> Result<()> { + let scan_schema = Arc::new(Schema::new(vec![ + Field::new("col_0", DataType::Int64, true), + Field::new("col_1", DataType::Utf8, true), + Field::new("col_2", DataType::Utf8, true), + Field::new("col_3", DataType::Utf8, true), + Field::new("col_4", DataType::Utf8, true), + Field::new("col_5", DataType::Int32, true), + Field::new("col_6", DataType::Utf8, true), + Field::new("col_7", DataType::Utf8, true), + Field::new("col_8", DataType::Utf8, true), + ])); + + let group_by = PhysicalGroupBy::new_single(vec![ + (Arc::new(Column::new("col_1", 1)), "col_1".to_string()), + (Arc::new(Column::new("col_7", 7)), "col_7".to_string()), + (Arc::new(Column::new("col_0", 0)), "col_0".to_string()), + (Arc::new(Column::new("col_8", 8)), "col_8".to_string()), + ]); + + fn generate_int64_array() -> ArrayRef { + Arc::new(Int64Array::from_iter_values( + (0..1024).map(|_| random::()), + )) + } + fn generate_int32_array() -> ArrayRef { + Arc::new(Int32Array::from_iter_values( + (0..1024).map(|_| random::()), + )) + } + + fn generate_string_array() -> ArrayRef { + Arc::new(StringArray::from( + (0..1024) + .map(|_| -> String { + thread_rng() + .sample_iter::(rand::distributions::Standard) + .take(5) + .collect() + }) + .collect::>(), + )) + } + + fn generate_record_batch(schema: &SchemaRef) -> Result { + RecordBatch::try_new( + Arc::clone(schema), + vec![ + generate_int64_array(), + generate_string_array(), + generate_string_array(), + generate_string_array(), + generate_string_array(), + generate_int32_array(), + generate_string_array(), + generate_string_array(), + generate_string_array(), + ], + ) + .map_err(|err| err.into()) + } + + let aggregate_expressions = vec![Arc::new( + AggregateExprBuilder::new(sum_udaf(), vec![lit(1i64)]) + .schema(Arc::clone(&scan_schema)) + .alias("SUM(1i64)") + .build()?, + )]; + + let batches = (0..5) + .map(|_| generate_record_batch(&scan_schema)) + .collect::>>()?; + + let plan: Arc = + MemorySourceConfig::try_new_exec(&[batches], Arc::clone(&scan_schema), None) + .unwrap(); + + let single_aggregate = Arc::new(AggregateExec::try_new( + AggregateMode::Single, + group_by, + aggregate_expressions.clone(), + vec![None; aggregate_expressions.len()], + plan, + Arc::clone(&scan_schema), + )?); + + let memory_pool = Arc::new(FairSpillPool::new(250000)); + let task_ctx = Arc::new( + TaskContext::default() + .with_session_config(SessionConfig::new().with_batch_size(248)) + .with_runtime(Arc::new( + RuntimeEnvBuilder::new() + .with_memory_pool(memory_pool) + .build()?, + )), + ); + + datafusion_physical_plan::common::collect( + single_aggregate.execute(0, Arc::clone(&task_ctx))?, + ) + .await?; + + assert_spill_count_metric(true, single_aggregate); + + Ok(()) +} diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs index 8a8aa180b3c44..3c9fe2917251c 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs @@ -43,7 +43,7 @@ use crate::fuzz_cases::aggregation_fuzzer::data_generator::Dataset; /// - `skip_partial parameters` /// - hint `sorted` or not /// - `spilling` or not (TODO, I think a special `MemoryPool` may be needed -/// to support this) +/// to support this) /// pub struct SessionContextGenerator { /// Current testing dataset diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs index d61835a0804ed..82bfe199234ef 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs @@ -15,34 +15,15 @@ // specific language governing permissions and limitations // under the License. -use std::sync::Arc; - -use arrow::array::{ArrayRef, RecordBatch}; -use arrow::datatypes::{ - BinaryType, BinaryViewType, BooleanType, ByteArrayType, ByteViewType, DataType, - Date32Type, Date64Type, Decimal128Type, Decimal256Type, Field, Float32Type, - Float64Type, Int16Type, Int32Type, Int64Type, Int8Type, IntervalDayTimeType, - IntervalMonthDayNanoType, IntervalUnit, IntervalYearMonthType, LargeBinaryType, - LargeUtf8Type, Schema, StringViewType, Time32MillisecondType, Time32SecondType, - Time64MicrosecondType, Time64NanosecondType, TimeUnit, TimestampMicrosecondType, - TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, UInt16Type, - UInt32Type, UInt64Type, UInt8Type, Utf8Type, -}; -use datafusion_common::{arrow_datafusion_err, DataFusionError, Result}; +use arrow::array::RecordBatch; +use arrow::datatypes::DataType; +use datafusion_common::Result; use datafusion_physical_expr::{expressions::col, PhysicalSortExpr}; use datafusion_physical_expr_common::sort_expr::LexOrdering; use datafusion_physical_plan::sorts::sort::sort_batch; -use rand::{ - rngs::{StdRng, ThreadRng}, - thread_rng, Rng, SeedableRng, -}; -use test_utils::{ - array_gen::{ - BinaryArrayGenerator, BooleanArrayGenerator, DecimalArrayGenerator, - PrimitiveArrayGenerator, StringArrayGenerator, - }, - stagger_batch, -}; +use test_utils::stagger_batch; + +use crate::fuzz_cases::record_batch_generator::{ColumnDescr, RecordBatchGenerator}; /// Config for Dataset generator /// @@ -52,12 +33,12 @@ use test_utils::{ /// when you call `generate` function /// /// - `rows_num_range`, the number of rows in the datasets will be randomly generated -/// within this range +/// within this range /// /// - `sort_keys`, if `sort_keys` are defined, when you call the `generate` function, the generator -/// will generate one `base dataset` firstly. Then the `base dataset` will be sorted -/// based on each `sort_key` respectively. And finally `len(sort_keys) + 1` datasets -/// will be returned +/// will generate one `base dataset` firstly. Then the `base dataset` will be sorted +/// based on each `sort_key` respectively. And finally `len(sort_keys) + 1` datasets +/// will be returned /// #[derive(Debug, Clone)] pub struct DatasetGeneratorConfig { @@ -154,7 +135,7 @@ impl DatasetGenerator { } } - pub fn generate(&self) -> Result> { + pub fn generate(&mut self) -> Result> { let mut datasets = Vec::with_capacity(self.sort_keys_set.len() + 1); // Generate the base batch (unsorted) @@ -204,553 +185,6 @@ impl Dataset { } } -#[derive(Debug, Clone)] -pub struct ColumnDescr { - /// Column name - name: String, - - /// Data type of this column - column_type: DataType, - - /// The maximum number of distinct values in this column. - /// - /// See [`ColumnDescr::with_max_num_distinct`] for more information - max_num_distinct: Option, -} - -impl ColumnDescr { - #[inline] - pub fn new(name: &str, column_type: DataType) -> Self { - Self { - name: name.to_string(), - column_type, - max_num_distinct: None, - } - } - - pub fn get_max_num_distinct(&self) -> Option { - self.max_num_distinct - } - - /// set the maximum number of distinct values in this column - /// - /// If `None`, the number of distinct values is randomly selected between 1 - /// and the number of rows. - pub fn with_max_num_distinct(mut self, num_distinct: usize) -> Self { - self.max_num_distinct = Some(num_distinct); - self - } -} - -/// Record batch generator -struct RecordBatchGenerator { - min_rows_nun: usize, - - max_rows_num: usize, - - columns: Vec, - - candidate_null_pcts: Vec, -} - -macro_rules! generate_string_array { - ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE: ident) => {{ - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - let max_len = $BATCH_GEN_RNG.gen_range(1..50); - - let mut generator = StringArrayGenerator { - max_len, - num_strings: $NUM_ROWS, - num_distinct_strings: $MAX_NUM_DISTINCT, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - match $ARROW_TYPE::DATA_TYPE { - DataType::Utf8 => generator.gen_data::(), - DataType::LargeUtf8 => generator.gen_data::(), - DataType::Utf8View => generator.gen_string_view(), - _ => unreachable!(), - } - }}; -} - -macro_rules! generate_decimal_array { - ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT: expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $PRECISION: ident, $SCALE: ident, $ARROW_TYPE: ident) => {{ - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - - let mut generator = DecimalArrayGenerator { - precision: $PRECISION, - scale: $SCALE, - num_decimals: $NUM_ROWS, - num_distinct_decimals: $MAX_NUM_DISTINCT, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - generator.gen_data::<$ARROW_TYPE>() - }}; -} - -// Generating `BooleanArray` due to it being a special type in Arrow (bit-packed) -macro_rules! generate_boolean_array { - ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE: ident) => {{ - // Select a null percentage from the candidate percentages - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - - let num_distinct_booleans = if $MAX_NUM_DISTINCT >= 2 { 2 } else { 1 }; - - let mut generator = BooleanArrayGenerator { - num_booleans: $NUM_ROWS, - num_distinct_booleans, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - generator.gen_data::<$ARROW_TYPE>() - }}; -} - -macro_rules! generate_primitive_array { - ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE:ident) => {{ - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - - let mut generator = PrimitiveArrayGenerator { - num_primitives: $NUM_ROWS, - num_distinct_primitives: $MAX_NUM_DISTINCT, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - generator.gen_data::<$ARROW_TYPE>() - }}; -} - -macro_rules! generate_binary_array { - ( - $SELF:ident, - $NUM_ROWS:ident, - $MAX_NUM_DISTINCT:expr, - $BATCH_GEN_RNG:ident, - $ARRAY_GEN_RNG:ident, - $ARROW_TYPE:ident - ) => {{ - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - - let max_len = $BATCH_GEN_RNG.gen_range(1..100); - - let mut generator = BinaryArrayGenerator { - max_len, - num_binaries: $NUM_ROWS, - num_distinct_binaries: $MAX_NUM_DISTINCT, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - match $ARROW_TYPE::DATA_TYPE { - DataType::Binary => generator.gen_data::(), - DataType::LargeBinary => generator.gen_data::(), - DataType::BinaryView => generator.gen_binary_view(), - _ => unreachable!(), - } - }}; -} - -impl RecordBatchGenerator { - fn new(min_rows_nun: usize, max_rows_num: usize, columns: Vec) -> Self { - let candidate_null_pcts = vec![0.0, 0.01, 0.1, 0.5]; - - Self { - min_rows_nun, - max_rows_num, - columns, - candidate_null_pcts, - } - } - - fn generate(&self) -> Result { - let mut rng = thread_rng(); - let num_rows = rng.gen_range(self.min_rows_nun..=self.max_rows_num); - let array_gen_rng = StdRng::from_seed(rng.gen()); - - // Build arrays - let mut arrays = Vec::with_capacity(self.columns.len()); - for col in self.columns.iter() { - let array = self.generate_array_of_type( - col, - num_rows, - &mut rng, - array_gen_rng.clone(), - ); - arrays.push(array); - } - - // Build schema - let fields = self - .columns - .iter() - .map(|col| Field::new(col.name.clone(), col.column_type.clone(), true)) - .collect::>(); - let schema = Arc::new(Schema::new(fields)); - - RecordBatch::try_new(schema, arrays).map_err(|e| arrow_datafusion_err!(e)) - } - - fn generate_array_of_type( - &self, - col: &ColumnDescr, - num_rows: usize, - batch_gen_rng: &mut ThreadRng, - array_gen_rng: StdRng, - ) -> ArrayRef { - let num_distinct = if num_rows > 1 { - batch_gen_rng.gen_range(1..num_rows) - } else { - num_rows - }; - // cap to at most the num_distinct values - let max_num_distinct = col - .max_num_distinct - .map(|max| num_distinct.min(max)) - .unwrap_or(num_distinct); - - match col.column_type { - DataType::Int8 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int8Type - ) - } - DataType::Int16 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int16Type - ) - } - DataType::Int32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int32Type - ) - } - DataType::Int64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int64Type - ) - } - DataType::UInt8 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt8Type - ) - } - DataType::UInt16 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt16Type - ) - } - DataType::UInt32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt32Type - ) - } - DataType::UInt64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt64Type - ) - } - DataType::Float32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Float32Type - ) - } - DataType::Float64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Float64Type - ) - } - DataType::Date32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Date32Type - ) - } - DataType::Date64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Date64Type - ) - } - DataType::Time32(TimeUnit::Second) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time32SecondType - ) - } - DataType::Time32(TimeUnit::Millisecond) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time32MillisecondType - ) - } - DataType::Time64(TimeUnit::Microsecond) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time64MicrosecondType - ) - } - DataType::Time64(TimeUnit::Nanosecond) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time64NanosecondType - ) - } - DataType::Interval(IntervalUnit::YearMonth) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - IntervalYearMonthType - ) - } - DataType::Interval(IntervalUnit::DayTime) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - IntervalDayTimeType - ) - } - DataType::Interval(IntervalUnit::MonthDayNano) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - IntervalMonthDayNanoType - ) - } - DataType::Timestamp(TimeUnit::Second, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampSecondType - ) - } - DataType::Timestamp(TimeUnit::Millisecond, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampMillisecondType - ) - } - DataType::Timestamp(TimeUnit::Microsecond, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampMicrosecondType - ) - } - DataType::Timestamp(TimeUnit::Nanosecond, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampNanosecondType - ) - } - DataType::Binary => { - generate_binary_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - BinaryType - ) - } - DataType::LargeBinary => { - generate_binary_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - LargeBinaryType - ) - } - DataType::BinaryView => { - generate_binary_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - BinaryViewType - ) - } - DataType::Decimal128(precision, scale) => { - generate_decimal_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - precision, - scale, - Decimal128Type - ) - } - DataType::Decimal256(precision, scale) => { - generate_decimal_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - precision, - scale, - Decimal256Type - ) - } - DataType::Utf8 => { - generate_string_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Utf8Type - ) - } - DataType::LargeUtf8 => { - generate_string_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - LargeUtf8Type - ) - } - DataType::Utf8View => { - generate_string_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - StringViewType - ) - } - DataType::Boolean => { - generate_boolean_array! { - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - BooleanType - } - } - _ => { - panic!("Unsupported data generator type: {}", col.column_type) - } - } - } -} - #[cfg(test)] mod test { use arrow::array::UInt32Array; @@ -777,7 +211,7 @@ mod test { sort_keys_set: vec![vec!["b".to_string()]], }; - let gen = DatasetGenerator::new(config); + let mut gen = DatasetGenerator::new(config); let datasets = gen.generate().unwrap(); // Should Generate 2 datasets diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs index bb24fb554d65a..53e9288ab4af6 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs @@ -164,7 +164,7 @@ struct QueryGroup { impl AggregationFuzzer { /// Run the fuzzer, printing an error and panicking if any of the tasks fail - pub async fn run(&self) { + pub async fn run(&mut self) { let res = self.run_inner().await; if let Err(e) = res { @@ -176,7 +176,7 @@ impl AggregationFuzzer { } } - async fn run_inner(&self) -> Result<()> { + async fn run_inner(&mut self) -> Result<()> { let mut join_set = JoinSet::new(); let mut rng = thread_rng(); @@ -270,7 +270,7 @@ impl AggregationFuzzer { /// - `sql`, the selected test sql /// /// - `dataset_ref`, the input dataset, store it for error reported when found -/// the inconsistency between the one for `ctx` and `expected results`. +/// the inconsistency between the one for `ctx` and `expected results`. /// struct AggregationFuzzTestTask { /// Generated session context in current test case @@ -503,7 +503,9 @@ impl QueryBuilder { let distinct = if *is_distinct { "DISTINCT " } else { "" }; alias_gen += 1; - let (order_by, null_opt) = if function_name.eq("first_value") { + let (order_by, null_opt) = if function_name.eq("first_value") + || function_name.eq("last_value") + { ( self.order_by(&order_by_black_list), /* Among the order by columns, at most one group by column can be included to avoid all order by column values being identical */ self.null_opt(), diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs index 1e42ac1f4b30b..bfb3bb096326f 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs @@ -44,7 +44,8 @@ mod context_generator; mod data_generator; mod fuzzer; -pub use data_generator::{ColumnDescr, DatasetGeneratorConfig}; +pub use crate::fuzz_cases::record_batch_generator::ColumnDescr; +pub use data_generator::DatasetGeneratorConfig; pub use fuzzer::*; #[derive(Debug)] diff --git a/datafusion/core/tests/fuzz_cases/mod.rs b/datafusion/core/tests/fuzz_cases/mod.rs index d5511e2970f4d..8ccc2a5bc1310 100644 --- a/datafusion/core/tests/fuzz_cases/mod.rs +++ b/datafusion/core/tests/fuzz_cases/mod.rs @@ -20,6 +20,7 @@ mod distinct_count_string_fuzz; mod join_fuzz; mod merge_fuzz; mod sort_fuzz; +mod sort_query_fuzz; mod aggregation_fuzzer; mod equivalence; @@ -29,3 +30,6 @@ mod pruning; mod limit_fuzz; mod sort_preserving_repartition_fuzz; mod window_fuzz; + +// Utility modules +mod record_batch_generator; diff --git a/datafusion/core/tests/fuzz_cases/record_batch_generator.rs b/datafusion/core/tests/fuzz_cases/record_batch_generator.rs new file mode 100644 index 0000000000000..9a62a6397d822 --- /dev/null +++ b/datafusion/core/tests/fuzz_cases/record_batch_generator.rs @@ -0,0 +1,644 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::sync::Arc; + +use arrow::array::{ArrayRef, RecordBatch}; +use arrow::datatypes::{ + BooleanType, DataType, Date32Type, Date64Type, Decimal128Type, Decimal256Type, Field, + Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type, + IntervalDayTimeType, IntervalMonthDayNanoType, IntervalUnit, IntervalYearMonthType, + Schema, Time32MillisecondType, Time32SecondType, Time64MicrosecondType, + Time64NanosecondType, TimeUnit, TimestampMicrosecondType, TimestampMillisecondType, + TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, + UInt8Type, +}; +use arrow_schema::{ + DECIMAL128_MAX_PRECISION, DECIMAL128_MAX_SCALE, DECIMAL256_MAX_PRECISION, + DECIMAL256_MAX_SCALE, +}; +use datafusion_common::{arrow_datafusion_err, DataFusionError, Result}; +use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng}; +use test_utils::array_gen::{ + BinaryArrayGenerator, BooleanArrayGenerator, DecimalArrayGenerator, + PrimitiveArrayGenerator, StringArrayGenerator, +}; + +/// Columns that are supported by the record batch generator +/// The RNG is used to generate the precision and scale for the decimal columns, thread +/// RNG is not used because this is used in fuzzing and deterministic results are preferred +pub fn get_supported_types_columns(rng_seed: u64) -> Vec { + let mut rng = StdRng::seed_from_u64(rng_seed); + vec![ + ColumnDescr::new("i8", DataType::Int8), + ColumnDescr::new("i16", DataType::Int16), + ColumnDescr::new("i32", DataType::Int32), + ColumnDescr::new("i64", DataType::Int64), + ColumnDescr::new("u8", DataType::UInt8), + ColumnDescr::new("u16", DataType::UInt16), + ColumnDescr::new("u32", DataType::UInt32), + ColumnDescr::new("u64", DataType::UInt64), + ColumnDescr::new("date32", DataType::Date32), + ColumnDescr::new("date64", DataType::Date64), + ColumnDescr::new("time32_s", DataType::Time32(TimeUnit::Second)), + ColumnDescr::new("time32_ms", DataType::Time32(TimeUnit::Millisecond)), + ColumnDescr::new("time64_us", DataType::Time64(TimeUnit::Microsecond)), + ColumnDescr::new("time64_ns", DataType::Time64(TimeUnit::Nanosecond)), + ColumnDescr::new("timestamp_s", DataType::Timestamp(TimeUnit::Second, None)), + ColumnDescr::new( + "timestamp_ms", + DataType::Timestamp(TimeUnit::Millisecond, None), + ), + ColumnDescr::new( + "timestamp_us", + DataType::Timestamp(TimeUnit::Microsecond, None), + ), + ColumnDescr::new( + "timestamp_ns", + DataType::Timestamp(TimeUnit::Nanosecond, None), + ), + ColumnDescr::new("float32", DataType::Float32), + ColumnDescr::new("float64", DataType::Float64), + ColumnDescr::new( + "interval_year_month", + DataType::Interval(IntervalUnit::YearMonth), + ), + ColumnDescr::new( + "interval_day_time", + DataType::Interval(IntervalUnit::DayTime), + ), + ColumnDescr::new( + "interval_month_day_nano", + DataType::Interval(IntervalUnit::MonthDayNano), + ), + ColumnDescr::new("decimal128", { + let precision: u8 = rng.gen_range(1..=DECIMAL128_MAX_PRECISION); + let scale: i8 = rng.gen_range( + i8::MIN..=std::cmp::min(precision as i8, DECIMAL128_MAX_SCALE), + ); + DataType::Decimal128(precision, scale) + }), + ColumnDescr::new("decimal256", { + let precision: u8 = rng.gen_range(1..=DECIMAL256_MAX_PRECISION); + let scale: i8 = rng.gen_range( + i8::MIN..=std::cmp::min(precision as i8, DECIMAL256_MAX_SCALE), + ); + DataType::Decimal256(precision, scale) + }), + ColumnDescr::new("utf8", DataType::Utf8), + ColumnDescr::new("largeutf8", DataType::LargeUtf8), + ColumnDescr::new("utf8view", DataType::Utf8View), + ColumnDescr::new("u8_low", DataType::UInt8).with_max_num_distinct(10), + ColumnDescr::new("utf8_low", DataType::Utf8).with_max_num_distinct(10), + ColumnDescr::new("bool", DataType::Boolean), + ColumnDescr::new("binary", DataType::Binary), + ColumnDescr::new("large_binary", DataType::LargeBinary), + ColumnDescr::new("binaryview", DataType::BinaryView), + ] +} + +#[derive(Debug, Clone)] +pub struct ColumnDescr { + /// Column name + pub name: String, + + /// Data type of this column + pub column_type: DataType, + + /// The maximum number of distinct values in this column. + /// + /// See [`ColumnDescr::with_max_num_distinct`] for more information + max_num_distinct: Option, +} + +impl ColumnDescr { + #[inline] + pub fn new(name: &str, column_type: DataType) -> Self { + Self { + name: name.to_string(), + column_type, + max_num_distinct: None, + } + } + + pub fn get_max_num_distinct(&self) -> Option { + self.max_num_distinct + } + + /// set the maximum number of distinct values in this column + /// + /// If `None`, the number of distinct values is randomly selected between 1 + /// and the number of rows. + pub fn with_max_num_distinct(mut self, num_distinct: usize) -> Self { + self.max_num_distinct = Some(num_distinct); + self + } +} + +/// Record batch generator +pub struct RecordBatchGenerator { + pub min_rows_num: usize, + + pub max_rows_num: usize, + + pub columns: Vec, + + pub candidate_null_pcts: Vec, + + /// If a seed is provided when constructing the generator, it will be used to + /// create `rng` and the pseudo-randomly generated batches will be deterministic. + /// Otherwise, `rng` will be initialized using `thread_rng()` and the batches + /// generated will be different each time. + rng: StdRng, +} + +macro_rules! generate_decimal_array { + ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT: expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $PRECISION: ident, $SCALE: ident, $ARROW_TYPE: ident) => {{ + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + + let mut generator = DecimalArrayGenerator { + precision: $PRECISION, + scale: $SCALE, + num_decimals: $NUM_ROWS, + num_distinct_decimals: $MAX_NUM_DISTINCT, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + generator.gen_data::<$ARROW_TYPE>() + }}; +} + +// Generating `BooleanArray` due to it being a special type in Arrow (bit-packed) +macro_rules! generate_boolean_array { + ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE: ident) => {{ + // Select a null percentage from the candidate percentages + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + + let num_distinct_booleans = if $MAX_NUM_DISTINCT >= 2 { 2 } else { 1 }; + + let mut generator = BooleanArrayGenerator { + num_booleans: $NUM_ROWS, + num_distinct_booleans, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + generator.gen_data::<$ARROW_TYPE>() + }}; +} + +macro_rules! generate_primitive_array { + ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE:ident) => {{ + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + + let mut generator = PrimitiveArrayGenerator { + num_primitives: $NUM_ROWS, + num_distinct_primitives: $MAX_NUM_DISTINCT, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + generator.gen_data::<$ARROW_TYPE>() + }}; +} + +impl RecordBatchGenerator { + /// Create a new `RecordBatchGenerator` with a random seed. The generated + /// batches will be different each time. + pub fn new( + min_rows_nun: usize, + max_rows_num: usize, + columns: Vec, + ) -> Self { + let candidate_null_pcts = vec![0.0, 0.01, 0.1, 0.5]; + + Self { + min_rows_num: min_rows_nun, + max_rows_num, + columns, + candidate_null_pcts, + rng: StdRng::from_rng(thread_rng()).unwrap(), + } + } + + /// Set a seed for the generator. The pseudo-randomly generated batches will be + /// deterministic for the same seed. + pub fn with_seed(mut self, seed: u64) -> Self { + self.rng = StdRng::seed_from_u64(seed); + self + } + + pub fn generate(&mut self) -> Result { + let num_rows = self.rng.gen_range(self.min_rows_num..=self.max_rows_num); + let array_gen_rng = StdRng::from_seed(self.rng.gen()); + let mut batch_gen_rng = StdRng::from_seed(self.rng.gen()); + let columns = self.columns.clone(); + + // Build arrays + let mut arrays = Vec::with_capacity(columns.len()); + for col in columns.iter() { + let array = self.generate_array_of_type( + col, + num_rows, + &mut batch_gen_rng, + array_gen_rng.clone(), + ); + arrays.push(array); + } + + // Build schema + let fields = self + .columns + .iter() + .map(|col| Field::new(col.name.clone(), col.column_type.clone(), true)) + .collect::>(); + let schema = Arc::new(Schema::new(fields)); + + RecordBatch::try_new(schema, arrays).map_err(|e| arrow_datafusion_err!(e)) + } + + fn generate_array_of_type( + &mut self, + col: &ColumnDescr, + num_rows: usize, + batch_gen_rng: &mut StdRng, + array_gen_rng: StdRng, + ) -> ArrayRef { + let num_distinct = if num_rows > 1 { + batch_gen_rng.gen_range(1..num_rows) + } else { + num_rows + }; + // cap to at most the num_distinct values + let max_num_distinct = col + .max_num_distinct + .map(|max| num_distinct.min(max)) + .unwrap_or(num_distinct); + + match col.column_type { + DataType::Int8 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int8Type + ) + } + DataType::Int16 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int16Type + ) + } + DataType::Int32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int32Type + ) + } + DataType::Int64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int64Type + ) + } + DataType::UInt8 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt8Type + ) + } + DataType::UInt16 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt16Type + ) + } + DataType::UInt32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt32Type + ) + } + DataType::UInt64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt64Type + ) + } + DataType::Float32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Float32Type + ) + } + DataType::Float64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Float64Type + ) + } + DataType::Date32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Date32Type + ) + } + DataType::Date64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Date64Type + ) + } + DataType::Time32(TimeUnit::Second) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time32SecondType + ) + } + DataType::Time32(TimeUnit::Millisecond) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time32MillisecondType + ) + } + DataType::Time64(TimeUnit::Microsecond) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time64MicrosecondType + ) + } + DataType::Time64(TimeUnit::Nanosecond) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time64NanosecondType + ) + } + DataType::Interval(IntervalUnit::YearMonth) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + IntervalYearMonthType + ) + } + DataType::Interval(IntervalUnit::DayTime) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + IntervalDayTimeType + ) + } + DataType::Interval(IntervalUnit::MonthDayNano) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + IntervalMonthDayNanoType + ) + } + DataType::Timestamp(TimeUnit::Second, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampSecondType + ) + } + DataType::Timestamp(TimeUnit::Millisecond, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampMillisecondType + ) + } + DataType::Timestamp(TimeUnit::Microsecond, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampMicrosecondType + ) + } + DataType::Timestamp(TimeUnit::Nanosecond, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampNanosecondType + ) + } + DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => { + let null_pct_idx = + batch_gen_rng.gen_range(0..self.candidate_null_pcts.len()); + let null_pct = self.candidate_null_pcts[null_pct_idx]; + let max_len = batch_gen_rng.gen_range(1..50); + + let mut generator = StringArrayGenerator { + max_len, + num_strings: num_rows, + num_distinct_strings: max_num_distinct, + null_pct, + rng: array_gen_rng, + }; + + match col.column_type { + DataType::Utf8 => generator.gen_data::(), + DataType::LargeUtf8 => generator.gen_data::(), + DataType::Utf8View => generator.gen_string_view(), + _ => unreachable!(), + } + } + DataType::Binary | DataType::LargeBinary | DataType::BinaryView => { + let null_pct_idx = + batch_gen_rng.gen_range(0..self.candidate_null_pcts.len()); + let null_pct = self.candidate_null_pcts[null_pct_idx]; + let max_len = batch_gen_rng.gen_range(1..100); + + let mut generator = BinaryArrayGenerator { + max_len, + num_binaries: num_rows, + num_distinct_binaries: max_num_distinct, + null_pct, + rng: array_gen_rng, + }; + + match col.column_type { + DataType::Binary => generator.gen_data::(), + DataType::LargeBinary => generator.gen_data::(), + DataType::BinaryView => generator.gen_binary_view(), + _ => unreachable!(), + } + } + DataType::Decimal128(precision, scale) => { + generate_decimal_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + precision, + scale, + Decimal128Type + ) + } + DataType::Decimal256(precision, scale) => { + generate_decimal_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + precision, + scale, + Decimal256Type + ) + } + DataType::Boolean => { + generate_boolean_array! { + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + BooleanType + } + } + _ => { + panic!("Unsupported data generator type: {}", col.column_type) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generator_with_fixed_seed_deterministic() { + let mut gen1 = RecordBatchGenerator::new( + 16, + 32, + vec![ + ColumnDescr::new("a", DataType::Utf8), + ColumnDescr::new("b", DataType::UInt32), + ], + ) + .with_seed(310104); + + let mut gen2 = RecordBatchGenerator::new( + 16, + 32, + vec![ + ColumnDescr::new("a", DataType::Utf8), + ColumnDescr::new("b", DataType::UInt32), + ], + ) + .with_seed(310104); + + let batch1 = gen1.generate().unwrap(); + let batch2 = gen2.generate().unwrap(); + + let batch1_formatted = format!("{:?}", batch1); + let batch2_formatted = format!("{:?}", batch2); + + assert_eq!(batch1_formatted, batch2_formatted); + } +} diff --git a/datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs b/datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs new file mode 100644 index 0000000000000..1319d4817326d --- /dev/null +++ b/datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs @@ -0,0 +1,625 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Fuzz Test for various corner cases sorting RecordBatches exceeds available memory and should spill + +use std::cmp::min; +use std::sync::Arc; + +use arrow::array::RecordBatch; +use arrow_schema::SchemaRef; +use datafusion::datasource::MemTable; +use datafusion::prelude::{SessionConfig, SessionContext}; +use datafusion_common::{instant::Instant, Result}; +use datafusion_execution::memory_pool::{ + human_readable_size, MemoryPool, UnboundedMemoryPool, +}; +use datafusion_expr::display_schema; +use datafusion_physical_plan::spill::get_record_batch_memory_size; +use rand::seq::SliceRandom; +use std::time::Duration; + +use datafusion_execution::{ + disk_manager::DiskManagerConfig, memory_pool::FairSpillPool, + runtime_env::RuntimeEnvBuilder, +}; +use rand::Rng; +use rand::{rngs::StdRng, SeedableRng}; + +use crate::fuzz_cases::aggregation_fuzzer::check_equality_of_batches; + +use super::aggregation_fuzzer::ColumnDescr; +use super::record_batch_generator::{get_supported_types_columns, RecordBatchGenerator}; + +/// Entry point for executing the sort query fuzzer. +/// +/// Now memory limiting is disabled by default. See TODOs in `SortQueryFuzzer`. +#[tokio::test(flavor = "multi_thread")] +async fn sort_query_fuzzer_runner() { + let random_seed = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs(); + let test_generator = SortFuzzerTestGenerator::new( + 2000, + 3, + "sort_fuzz_table".to_string(), + get_supported_types_columns(random_seed), + false, + random_seed, + ); + let mut fuzzer = SortQueryFuzzer::new(random_seed) + // Configs for how many random query to test + .with_max_rounds(Some(5)) + .with_queries_per_round(4) + .with_config_variations_per_query(5) + // Will stop early if the time limit is reached + .with_time_limit(Duration::from_secs(5)) + .with_test_generator(test_generator); + + fuzzer.run().await.unwrap(); +} + +/// SortQueryFuzzer holds the runner configuration for executing sort query fuzz tests. The fuzzing details are managed inside `SortFuzzerTestGenerator`. +/// +/// It defines: +/// - `max_rounds`: Maximum number of rounds to run (or None to run until `time_limit`). +/// - `queries_per_round`: Number of different queries to run in each round. +/// - `config_variations_per_query`: Number of different configurations to test per query. +/// - `time_limit`: Time limit for the entire fuzzer execution. +/// +/// TODO: The following improvements are blocked on https://github.com/apache/datafusion/issues/14748: +/// 1. Support generating queries with arbitrary number of ORDER BY clauses +/// Currently limited to be smaller than number of projected columns +/// 2. Enable special type columns like utf8_low to be used in ORDER BY clauses +/// 3. Enable memory limiting functionality in the fuzzer runner +pub struct SortQueryFuzzer { + test_gen: SortFuzzerTestGenerator, + /// Random number generator for the runner, used to generate seeds for inner components. + /// Seeds for each choice (query, config, etc.) are printed out for reproducibility. + runner_rng: StdRng, + + // ======================================================================== + // Runner configurations + // ======================================================================== + /// For each round, a new dataset is generated. If `None`, keep running until + /// the time limit is reached + max_rounds: Option, + /// How many different queries to run in each round + queries_per_round: usize, + /// For each query, how many different configurations to try and make sure their + /// results are consistent + config_variations_per_query: usize, + /// The time limit for the entire sort query fuzzer execution. + time_limit: Option, +} + +impl SortQueryFuzzer { + pub fn new(seed: u64) -> Self { + let max_rounds = Some(2); + let queries_per_round = 3; + let config_variations_per_query = 5; + let time_limit = None; + + // Filtered out one column due to a known bug https://github.com/apache/datafusion/issues/14748 + // TODO: Remove this once the bug is fixed + let candidate_columns = get_supported_types_columns(seed) + .into_iter() + .filter(|col| { + col.name != "utf8_low" + && col.name != "utf8view" + && col.name != "binaryview" + }) + .collect::>(); + + let test_gen = SortFuzzerTestGenerator::new( + 10000, + 4, + "sort_fuzz_table".to_string(), + candidate_columns, + false, + seed, + ); + + Self { + max_rounds, + queries_per_round, + config_variations_per_query, + time_limit, + test_gen, + runner_rng: StdRng::seed_from_u64(seed), + } + } + + pub fn with_test_generator(mut self, test_gen: SortFuzzerTestGenerator) -> Self { + self.test_gen = test_gen; + self + } + + pub fn with_max_rounds(mut self, max_rounds: Option) -> Self { + self.max_rounds = max_rounds; + self + } + + pub fn with_queries_per_round(mut self, queries_per_round: usize) -> Self { + self.queries_per_round = queries_per_round; + self + } + + pub fn with_config_variations_per_query( + mut self, + config_variations_per_query: usize, + ) -> Self { + self.config_variations_per_query = config_variations_per_query; + self + } + + pub fn with_time_limit(mut self, time_limit: Duration) -> Self { + self.time_limit = Some(time_limit); + self + } + + fn should_stop_due_to_time_limit( + &self, + start_time: Instant, + n_round: usize, + n_query: usize, + ) -> bool { + if let Some(time_limit) = self.time_limit { + if Instant::now().duration_since(start_time) > time_limit { + println!( + "[SortQueryFuzzer] Time limit reached: {} queries ({} random configs each) in {} rounds", + n_round * self.queries_per_round + n_query, + self.config_variations_per_query, + n_round + ); + return true; + } + } + false + } + + pub async fn run(&mut self) -> Result<()> { + let start_time = Instant::now(); + + // Execute until either`max_rounds` or `time_limit` is reached + let max_rounds = self.max_rounds.unwrap_or(usize::MAX); + for round in 0..max_rounds { + let init_seed = self.runner_rng.gen(); + for query_i in 0..self.queries_per_round { + let query_seed = self.runner_rng.gen(); + let mut expected_results: Option> = None; // use first config's result as the expected result + for config_i in 0..self.config_variations_per_query { + if self.should_stop_due_to_time_limit(start_time, round, query_i) { + return Ok(()); + } + + let config_seed = self.runner_rng.gen(); + + println!( + "[SortQueryFuzzer] Round {}, Query {} (Config {})", + round, query_i, config_i + ); + println!(" Seeds:"); + println!(" init_seed = {}", init_seed); + println!(" query_seed = {}", query_seed); + println!(" config_seed = {}", config_seed); + + let results = self + .test_gen + .fuzzer_run(init_seed, query_seed, config_seed) + .await?; + println!("\n"); // Seperator between tested runs + + if expected_results.is_none() { + expected_results = Some(results); + } else if let Some(ref expected) = expected_results { + // `fuzzer_run` might append `LIMIT k` to either the + // expected or actual query. The number of results is + // checked inside `fuzzer_run()`. Here we only check + // that the first k rows of each result are consistent. + check_equality_of_batches(expected, &results).unwrap(); + } else { + unreachable!(); + } + } + } + } + Ok(()) + } +} + +/// Struct to generate and manage a random dataset for fuzz testing. +/// It is able to re-run the failed test cases by setting the same seed printed out. +/// See the unit tests for examples. +/// +/// To use this struct: +/// 1. Call `init_partitioned_staggered_batches` to generate a random dataset. +/// 2. Use `generate_random_query` to create a random SQL query. +/// 3. Use `generate_random_config` to create a random configuration. +/// 4. Run the fuzzer check with the generated query and configuration. +pub struct SortFuzzerTestGenerator { + /// The total number of rows for the registered table + num_rows: usize, + /// Max number of partitions for the registered table + max_partitions: usize, + /// The name of the registered table + table_name: String, + /// The selected columns from all available candidate columns to be used for + /// this dataset + selected_columns: Vec, + /// If true, will randomly generate a memory limit for the query. Otherwise + /// the query will run under the context with unlimited memory. + set_memory_limit: bool, + + /// States related to the randomly generated dataset. `None` if not initialized + /// by calling `init_partitioned_staggered_batches()` + dataset_state: Option, +} + +/// Struct to hold states related to the randomly generated dataset +pub struct DatasetState { + /// Dataset to construct the partitioned memory table. Outer vector is the + /// partitions, inner vector is staggered batches within the same partition. + partitioned_staggered_batches: Vec>, + /// Number of rows in the whole dataset + dataset_size: usize, + /// The approximate number of rows of a batch (staggered batches will be generated + /// with random number of rows between 1 and `approx_batch_size`) + approx_batch_num_rows: usize, + /// The schema of the dataset + schema: SchemaRef, + /// The memory size of the whole dataset + mem_size: usize, +} + +impl SortFuzzerTestGenerator { + /// Randomly pick a subset of `candidate_columns` to be used for this dataset + pub fn new( + num_rows: usize, + max_partitions: usize, + table_name: String, + candidate_columns: Vec, + set_memory_limit: bool, + rng_seed: u64, + ) -> Self { + let mut rng = StdRng::seed_from_u64(rng_seed); + let min_ncol = min(candidate_columns.len(), 5); + let max_ncol = min(candidate_columns.len(), 10); + let amount = rng.gen_range(min_ncol..=max_ncol); + let selected_columns = candidate_columns + .choose_multiple(&mut rng, amount) + .cloned() + .collect(); + + Self { + num_rows, + max_partitions, + table_name, + selected_columns, + set_memory_limit, + dataset_state: None, + } + } + + /// The outer vector is the partitions, the inner vector is the chunked batches + /// within each partition. + /// The partition number is determined by `self.max_partitions`. + /// The chunked batch length is a random number between 1 and `self.num_rows` / + /// 100 (make sure a single batch won't exceed memory budget for external sort + /// executions) + /// + /// Hack: If we want the query to run under certain degree of parallelism, the + /// memory table should be generated with more partitions, due to https://github.com/apache/datafusion/issues/15088 + fn init_partitioned_staggered_batches(&mut self, rng_seed: u64) { + let mut rng = StdRng::seed_from_u64(rng_seed); + let num_partitions = rng.gen_range(1..=self.max_partitions); + + let max_batch_size = self.num_rows / num_partitions / 50; + let target_partition_size = self.num_rows / num_partitions; + + let mut partitions = Vec::new(); + let mut schema = None; + for _ in 0..num_partitions { + let mut partition = Vec::new(); + let mut num_rows = 0; + + // For each partition, generate random batches until there is about enough + // rows for the specified total number of rows + while num_rows < target_partition_size { + // Generate a random batch of size between 1 and max_batch_size + + // Let edge case (1-row batch) more common + let (min_nrow, max_nrow) = if rng.gen_bool(0.1) { + (1, 3) + } else { + (1, max_batch_size) + }; + + let mut record_batch_generator = RecordBatchGenerator::new( + min_nrow, + max_nrow, + self.selected_columns.clone(), + ) + .with_seed(rng.gen()); + + let record_batch = record_batch_generator.generate().unwrap(); + num_rows += record_batch.num_rows(); + + if schema.is_none() { + schema = Some(record_batch.schema()); + println!(" Dataset schema:"); + println!(" {}", display_schema(schema.as_ref().unwrap())); + } + + partition.push(record_batch); + } + + partitions.push(partition); + } + + // After all partitions are created, optionally make one partition have 0/1 batch + if num_partitions > 2 && rng.gen_bool(0.1) { + let partition_index = rng.gen_range(0..num_partitions); + if rng.gen_bool(0.5) { + // 0 batch + partitions[partition_index] = Vec::new(); + } else { + // 1 batch, keep the old first batch + let first_batch = partitions[partition_index].first().cloned(); + if let Some(batch) = first_batch { + partitions[partition_index] = vec![batch]; + } + } + } + + // Init self fields + let mem_size: usize = partitions + .iter() + .map(|partition| { + partition + .iter() + .map(get_record_batch_memory_size) + .sum::() + }) + .sum(); + + let dataset_size = partitions + .iter() + .map(|partition| { + partition + .iter() + .map(|batch| batch.num_rows()) + .sum::() + }) + .sum::(); + + let approx_batch_num_rows = max_batch_size; + + self.dataset_state = Some(DatasetState { + partitioned_staggered_batches: partitions, + dataset_size, + approx_batch_num_rows, + schema: schema.unwrap(), + mem_size, + }); + } + + /// Generates a random SQL query string and an optional limit value. + /// Returns a tuple containing the query string and an optional limit. + pub fn generate_random_query(&self, rng_seed: u64) -> (String, Option) { + let mut rng = StdRng::seed_from_u64(rng_seed); + + let num_columns = rng.gen_range(1..=3).min(self.selected_columns.len()); + let selected_columns: Vec<_> = self + .selected_columns + .choose_multiple(&mut rng, num_columns) + .collect(); + + let mut order_by_clauses = Vec::new(); + for col in selected_columns { + let mut clause = col.name.clone(); + if rng.gen_bool(0.5) { + let order = if rng.gen_bool(0.5) { "ASC" } else { "DESC" }; + clause.push_str(&format!(" {}", order)); + } + if rng.gen_bool(0.5) { + let nulls = if rng.gen_bool(0.5) { + "NULLS FIRST" + } else { + "NULLS LAST" + }; + clause.push_str(&format!(" {}", nulls)); + } + order_by_clauses.push(clause); + } + + let dataset_size = self.dataset_state.as_ref().unwrap().dataset_size; + + let limit = if rng.gen_bool(0.2) { + // Prefer edge cases for k like 1, dataset_size, etc. + Some(if rng.gen_bool(0.5) { + let edge_cases = + [1, 2, 3, dataset_size - 1, dataset_size, dataset_size + 1]; + *edge_cases.choose(&mut rng).unwrap() + } else { + rng.gen_range(1..=dataset_size) + }) + } else { + None + }; + + let limit_clause = limit.map_or(String::new(), |l| format!(" LIMIT {}", l)); + + let query = format!( + "SELECT * FROM {} ORDER BY {}{}", + self.table_name, + order_by_clauses.join(", "), + limit_clause + ); + + (query, limit) + } + + pub fn generate_random_config( + &self, + rng_seed: u64, + with_memory_limit: bool, + ) -> Result { + let mut rng = StdRng::seed_from_u64(rng_seed); + let init_state = self.dataset_state.as_ref().unwrap(); + let dataset_size = init_state.mem_size; + let num_partitions = init_state.partitioned_staggered_batches.len(); + + // 30% to 200% of the dataset size (if `with_memory_limit` is false, config + // will use the default unbounded pool to override it later) + let memory_limit = rng.gen_range( + (dataset_size as f64 * 0.5) as usize..=(dataset_size as f64 * 2.0) as usize, + ); + // 10% to 20% of the per-partition memory limit size + let per_partition_mem_limit = memory_limit / num_partitions; + let sort_spill_reservation_bytes = rng.gen_range( + (per_partition_mem_limit as f64 * 0.2) as usize + ..=(per_partition_mem_limit as f64 * 0.3) as usize, + ); + + // 1 to 3 times of the approx batch size. Setting this to a very large nvalue + // will cause external sort to fail. + let sort_in_place_threshold_bytes = if with_memory_limit { + // For memory-limited query, setting `sort_in_place_threshold_bytes` too + // large will cause failure. + 0 + } else { + let dataset_size = self.dataset_state.as_ref().unwrap().dataset_size; + rng.gen_range(0..=dataset_size * 2_usize) + }; + + // Set up strings for printing + let memory_limit_str = if with_memory_limit { + human_readable_size(memory_limit) + } else { + "Unbounded".to_string() + }; + let per_partition_limit_str = if with_memory_limit { + human_readable_size(per_partition_mem_limit) + } else { + "Unbounded".to_string() + }; + + println!(" Config: "); + println!(" Dataset size: {}", human_readable_size(dataset_size)); + println!(" Number of partitions: {}", num_partitions); + println!(" Batch size: {}", init_state.approx_batch_num_rows / 2); + println!(" Memory limit: {}", memory_limit_str); + println!( + " Per partition memory limit: {}", + per_partition_limit_str + ); + println!( + " Sort spill reservation bytes: {}", + human_readable_size(sort_spill_reservation_bytes) + ); + println!( + " Sort in place threshold bytes: {}", + human_readable_size(sort_in_place_threshold_bytes) + ); + + let config = SessionConfig::new() + .with_target_partitions(num_partitions) + .with_batch_size(init_state.approx_batch_num_rows / 2) + .with_sort_spill_reservation_bytes(sort_spill_reservation_bytes) + .with_sort_in_place_threshold_bytes(sort_in_place_threshold_bytes); + + let memory_pool: Arc = if with_memory_limit { + Arc::new(FairSpillPool::new(memory_limit)) + } else { + Arc::new(UnboundedMemoryPool::default()) + }; + + let runtime = RuntimeEnvBuilder::new() + .with_memory_pool(memory_pool) + .with_disk_manager(DiskManagerConfig::NewOs) + .build_arc()?; + + let ctx = SessionContext::new_with_config_rt(config, runtime); + + let dataset = &init_state.partitioned_staggered_batches; + let schema = &init_state.schema; + + let provider = MemTable::try_new(schema.clone(), dataset.clone())?; + ctx.register_table("sort_fuzz_table", Arc::new(provider))?; + + Ok(ctx) + } + + async fn fuzzer_run( + &mut self, + dataset_seed: u64, + query_seed: u64, + config_seed: u64, + ) -> Result> { + self.init_partitioned_staggered_batches(dataset_seed); + let (query_str, limit) = self.generate_random_query(query_seed); + println!(" Query:"); + println!(" {}", query_str); + + // ==== Execute the query ==== + + // Only enable memory limits if: + // 1. Query does not contain LIMIT (since topK does not support external execution) + // 2. Memory limiting is enabled in the test generator config + let with_mem_limit = !query_str.contains("LIMIT") && self.set_memory_limit; + + let ctx = self.generate_random_config(config_seed, with_mem_limit)?; + let df = ctx.sql(&query_str).await.unwrap(); + let results = df.collect().await.unwrap(); + + // ==== Check the result size is consistent with the limit ==== + let result_num_rows = results.iter().map(|batch| batch.num_rows()).sum::(); + let dataset_size = self.dataset_state.as_ref().unwrap().dataset_size; + + if let Some(limit) = limit { + let expected_num_rows = min(limit, dataset_size); + assert_eq!(result_num_rows, expected_num_rows); + } + + Ok(results) + } +} + +#[cfg(test)] +mod test { + use super::*; + + /// Given the same seed, the result should be the same + #[tokio::test] + async fn test_sort_query_fuzzer_deterministic() { + let gen_seed = 310104; + let mut test_generator = SortFuzzerTestGenerator::new( + 2000, + 3, + "sort_fuzz_table".to_string(), + get_supported_types_columns(gen_seed), + false, + gen_seed, + ); + + let res1 = test_generator.fuzzer_run(1, 2, 3).await.unwrap(); + let res2 = test_generator.fuzzer_run(1, 2, 3).await.unwrap(); + check_equality_of_batches(&res1, &res2).unwrap(); + } +} diff --git a/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs b/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs index bdf30c140afff..7b157b707a6de 100644 --- a/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs +++ b/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs @@ -18,7 +18,7 @@ use datafusion_common_runtime::SpawnedTask; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use sysinfo::System; +use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System}; use tokio::time::{interval, Duration}; use datafusion::prelude::{SessionConfig, SessionContext}; @@ -62,7 +62,11 @@ where loop { interval.tick().await; - sys.refresh_all(); + sys.refresh_processes_specifics( + ProcessesToUpdate::Some(&[pid]), + true, + ProcessRefreshKind::nothing().with_memory(), + ); if let Some(process) = sys.process(pid) { let rss_bytes = process.memory(); max_rss_clone @@ -116,8 +120,8 @@ where /// # Example /// /// utils::validate_query_with_memory_limits( -/// 40_000_000 * 2, -/// Some(40_000_000), +/// 40_000_000 * 2, +/// Some(40_000_000), /// "SELECT * FROM generate_series(1, 100000000) AS t(i) ORDER BY i", /// "SELECT * FROM generate_series(1, 10000000) AS t(i) ORDER BY i" /// ); diff --git a/datafusion/core/tests/memory_limit/mod.rs b/datafusion/core/tests/memory_limit/mod.rs index dd5acc8d8908a..01342d1604fca 100644 --- a/datafusion/core/tests/memory_limit/mod.rs +++ b/datafusion/core/tests/memory_limit/mod.rs @@ -44,11 +44,14 @@ use datafusion_common::{assert_contains, Result}; use datafusion_execution::memory_pool::{ FairSpillPool, GreedyMemoryPool, MemoryPool, TrackConsumersPool, }; -use datafusion_execution::TaskContext; +use datafusion_execution::runtime_env::RuntimeEnv; +use datafusion_execution::{DiskManager, TaskContext}; use datafusion_expr::{Expr, TableType}; use datafusion_physical_expr::{LexOrdering, PhysicalSortExpr}; use datafusion_physical_optimizer::join_selection::JoinSelection; use datafusion_physical_optimizer::PhysicalOptimizerRule; +use datafusion_physical_plan::collect as collect_batches; +use datafusion_physical_plan::common::collect; use datafusion_physical_plan::spill::get_record_batch_memory_size; use rand::Rng; use test_utils::AccessLogGenerator; @@ -493,6 +496,125 @@ async fn test_in_mem_buffer_almost_full() { let _ = df.collect().await.unwrap(); } +/// External sort should be able to run if there is very little pre-reserved memory +/// for merge (set configuration sort_spill_reservation_bytes to 0). +#[tokio::test] +async fn test_external_sort_zero_merge_reservation() { + let config = SessionConfig::new() + .with_sort_spill_reservation_bytes(0) + .with_target_partitions(14); + let runtime = RuntimeEnvBuilder::new() + .with_memory_pool(Arc::new(FairSpillPool::new(10 * 1024 * 1024))) + .build_arc() + .unwrap(); + + let ctx = SessionContext::new_with_config_rt(config, runtime); + + let query = "select * from generate_series(1,10000000) as t1(v1) order by v1;"; + let df = ctx.sql(query).await.unwrap(); + + let physical_plan = df.create_physical_plan().await.unwrap(); + let task_ctx = Arc::new(TaskContext::from(&ctx.state())); + let stream = physical_plan.execute(0, task_ctx).unwrap(); + + // Ensures execution succeed + let _result = collect(stream).await; + + // Ensures the query spilled during execution + let metrics = physical_plan.metrics().unwrap(); + let spill_count = metrics.spill_count().unwrap(); + assert!(spill_count > 0); +} + +// Tests for disk limit (`max_temp_directory_size` in `DiskManager`) +// ------------------------------------------------------------------ + +// Create a new `SessionContext` with speicified disk limit and memory pool limit +async fn setup_context( + disk_limit: u64, + memory_pool_limit: usize, +) -> Result { + let disk_manager = DiskManager::try_new(DiskManagerConfig::NewOs)?; + + let disk_manager = Arc::try_unwrap(disk_manager) + .expect("DiskManager should be a single instance") + .with_max_temp_directory_size(disk_limit)?; + + let runtime = RuntimeEnvBuilder::new() + .with_memory_pool(Arc::new(FairSpillPool::new(memory_pool_limit))) + .build_arc() + .unwrap(); + + let runtime = Arc::new(RuntimeEnv { + memory_pool: runtime.memory_pool.clone(), + disk_manager: Arc::new(disk_manager), + cache_manager: runtime.cache_manager.clone(), + object_store_registry: runtime.object_store_registry.clone(), + }); + + let config = SessionConfig::new() + .with_sort_spill_reservation_bytes(64 * 1024) // 256KB + .with_sort_in_place_threshold_bytes(0) + .with_batch_size(64) // To reduce test memory usage + .with_target_partitions(1); + + Ok(SessionContext::new_with_config_rt(config, runtime)) +} + +/// If the spilled bytes exceed the disk limit, the query should fail +/// (specified by `max_temp_directory_size` in `DiskManager`) +#[tokio::test] +async fn test_disk_spill_limit_reached() -> Result<()> { + let ctx = setup_context(1024 * 1024, 1024 * 1024).await?; // 1MB disk limit, 1MB memory limit + + let df = ctx + .sql("select * from generate_series(1, 1000000000000) as t1(v1) order by v1") + .await + .unwrap(); + + let err = df.collect().await.unwrap_err(); + assert_contains!( + err.to_string(), + "The used disk space during the spilling process has exceeded the allowable limit" + ); + + Ok(()) +} + +/// External query should succeed, if the spilled bytes is less than the disk limit +/// Also verify that after the query is finished, all the disk usage accounted by +/// tempfiles are cleaned up. +#[tokio::test] +async fn test_disk_spill_limit_not_reached() -> Result<()> { + let disk_spill_limit = 1024 * 1024; // 1MB + let ctx = setup_context(disk_spill_limit, 128 * 1024).await?; // 1MB disk limit, 128KB memory limit + + let df = ctx + .sql("select * from generate_series(1, 10000) as t1(v1) order by v1") + .await + .unwrap(); + let plan = df.create_physical_plan().await.unwrap(); + + let task_ctx = ctx.task_ctx(); + let _ = collect_batches(Arc::clone(&plan), task_ctx) + .await + .expect("Query execution failed"); + + let spill_count = plan.metrics().unwrap().spill_count().unwrap(); + let spilled_bytes = plan.metrics().unwrap().spilled_bytes().unwrap(); + + println!("spill count {}, spill bytes {}", spill_count, spilled_bytes); + assert!(spill_count > 0); + assert!((spilled_bytes as u64) < disk_spill_limit); + + // Verify that all temporary files have been properly cleaned up by checking + // that the total disk usage tracked by the disk manager is zero + let current_disk_usage = ctx.runtime_env().disk_manager.used_disk_space(); + assert_eq!(current_disk_usage, 0); + + Ok(()) +} + /// Run the query with the specified memory limit, /// and verifies the expected errors are returned #[derive(Clone, Debug)] @@ -741,11 +863,10 @@ impl Scenario { single_row_batches, } => { use datafusion::physical_expr::expressions::col; - let batches: Vec> = std::iter::repeat(maybe_split_batches( - dict_batches(), - *single_row_batches, - )) - .take(*partitions) + let batches: Vec> = std::iter::repeat_n( + maybe_split_batches(dict_batches(), *single_row_batches), + *partitions, + ) .collect(); let schema = batches[0][0].schema(); diff --git a/datafusion/core/tests/parquet/custom_reader.rs b/datafusion/core/tests/parquet/custom_reader.rs index ce5c0d720174d..761a78a29fd3a 100644 --- a/datafusion/core/tests/parquet/custom_reader.rs +++ b/datafusion/core/tests/parquet/custom_reader.rs @@ -44,6 +44,7 @@ use insta::assert_snapshot; use object_store::memory::InMemory; use object_store::path::Path; use object_store::{ObjectMeta, ObjectStore}; +use parquet::arrow::arrow_reader::ArrowReaderOptions; use parquet::arrow::async_reader::AsyncFileReader; use parquet::arrow::ArrowWriter; use parquet::errors::ParquetError; @@ -186,7 +187,7 @@ async fn store_parquet_in_memory( location: Path::parse(format!("file-{offset}.parquet")) .expect("creating path"), last_modified: chrono::DateTime::from(SystemTime::now()), - size: buf.len(), + size: buf.len() as u64, e_tag: None, version: None, }; @@ -218,9 +219,10 @@ struct ParquetFileReader { impl AsyncFileReader for ParquetFileReader { fn get_bytes( &mut self, - range: Range, + range: Range, ) -> BoxFuture<'_, parquet::errors::Result> { - self.metrics.bytes_scanned.add(range.end - range.start); + let bytes_scanned = range.end - range.start; + self.metrics.bytes_scanned.add(bytes_scanned as usize); self.store .get_range(&self.meta.location, range) @@ -232,6 +234,7 @@ impl AsyncFileReader for ParquetFileReader { fn get_metadata( &mut self, + _options: Option<&ArrowReaderOptions>, ) -> BoxFuture<'_, parquet::errors::Result>> { Box::pin(async move { let metadata = fetch_parquet_metadata( diff --git a/datafusion/core/tests/parquet/mod.rs b/datafusion/core/tests/parquet/mod.rs index f45eacce18df5..87a5ed33f127d 100644 --- a/datafusion/core/tests/parquet/mod.rs +++ b/datafusion/core/tests/parquet/mod.rs @@ -611,7 +611,7 @@ fn make_bytearray_batch( large_binary_values: Vec<&[u8]>, ) -> RecordBatch { let num_rows = string_values.len(); - let name: StringArray = std::iter::repeat(Some(name)).take(num_rows).collect(); + let name: StringArray = std::iter::repeat_n(Some(name), num_rows).collect(); let service_string: StringArray = string_values.iter().map(Some).collect(); let service_binary: BinaryArray = binary_values.iter().map(Some).collect(); let service_fixedsize: FixedSizeBinaryArray = fixedsize_values @@ -659,7 +659,7 @@ fn make_bytearray_batch( /// name | service.name fn make_names_batch(name: &str, service_name_values: Vec<&str>) -> RecordBatch { let num_rows = service_name_values.len(); - let name: StringArray = std::iter::repeat(Some(name)).take(num_rows).collect(); + let name: StringArray = std::iter::repeat_n(Some(name), num_rows).collect(); let service_name: StringArray = service_name_values.iter().map(Some).collect(); let schema = Schema::new(vec![ @@ -698,7 +698,7 @@ fn make_int_batches_with_null( Int8Array::from_iter( v8.into_iter() .map(Some) - .chain(std::iter::repeat(None).take(null_values)), + .chain(std::iter::repeat_n(None, null_values)), ) .to_data(), ), @@ -706,7 +706,7 @@ fn make_int_batches_with_null( Int16Array::from_iter( v16.into_iter() .map(Some) - .chain(std::iter::repeat(None).take(null_values)), + .chain(std::iter::repeat_n(None, null_values)), ) .to_data(), ), @@ -714,7 +714,7 @@ fn make_int_batches_with_null( Int32Array::from_iter( v32.into_iter() .map(Some) - .chain(std::iter::repeat(None).take(null_values)), + .chain(std::iter::repeat_n(None, null_values)), ) .to_data(), ), @@ -722,7 +722,7 @@ fn make_int_batches_with_null( Int64Array::from_iter( v64.into_iter() .map(Some) - .chain(std::iter::repeat(None).take(null_values)), + .chain(std::iter::repeat_n(None, null_values)), ) .to_data(), ), diff --git a/datafusion/core/tests/parquet/page_pruning.rs b/datafusion/core/tests/parquet/page_pruning.rs index 7006bf083eeed..f693485cbe018 100644 --- a/datafusion/core/tests/parquet/page_pruning.rs +++ b/datafusion/core/tests/parquet/page_pruning.rs @@ -52,7 +52,7 @@ async fn get_parquet_exec(state: &SessionState, filter: Expr) -> DataSourceExec let meta = ObjectMeta { location, last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len() as usize, + size: metadata.len(), e_tag: None, version: None, }; diff --git a/datafusion/core/tests/physical_optimizer/enforce_distribution.rs b/datafusion/core/tests/physical_optimizer/enforce_distribution.rs index 9898f6204e880..5e182cb93b39c 100644 --- a/datafusion/core/tests/physical_optimizer/enforce_distribution.rs +++ b/datafusion/core/tests/physical_optimizer/enforce_distribution.rs @@ -52,6 +52,7 @@ use datafusion_physical_plan::aggregates::{ AggregateExec, AggregateMode, PhysicalGroupBy, }; use datafusion_physical_plan::coalesce_batches::CoalesceBatchesExec; +use datafusion_physical_plan::coalesce_partitions::CoalescePartitionsExec; use datafusion_physical_plan::execution_plan::ExecutionPlan; use datafusion_physical_plan::expressions::col; use datafusion_physical_plan::filter::FilterExec; @@ -3471,3 +3472,47 @@ fn optimize_away_unnecessary_repartition2() -> Result<()> { Ok(()) } + +#[test] +fn test_replace_order_preserving_variants_with_fetch() -> Result<()> { + // Create a base plan + let parquet_exec = parquet_exec(); + + let sort_expr = PhysicalSortExpr { + expr: Arc::new(Column::new("id", 0)), + options: SortOptions::default(), + }; + + let ordering = LexOrdering::new(vec![sort_expr]); + + // Create a SortPreservingMergeExec with fetch=5 + let spm_exec = Arc::new( + SortPreservingMergeExec::new(ordering, parquet_exec.clone()).with_fetch(Some(5)), + ); + + // Create distribution context + let dist_context = DistributionContext::new( + spm_exec, + true, + vec![DistributionContext::new(parquet_exec, false, vec![])], + ); + + // Apply the function + let result = replace_order_preserving_variants(dist_context)?; + + // Verify the plan was transformed to CoalescePartitionsExec + result + .plan + .as_any() + .downcast_ref::() + .expect("Expected CoalescePartitionsExec"); + + // Verify fetch was preserved + assert_eq!( + result.plan.fetch(), + Some(5), + "Fetch value was not preserved after transformation" + ); + + Ok(()) +} diff --git a/datafusion/core/tests/physical_optimizer/enforce_sorting.rs b/datafusion/core/tests/physical_optimizer/enforce_sorting.rs index 4d2c875d3f1d4..052db454ef3f5 100644 --- a/datafusion/core/tests/physical_optimizer/enforce_sorting.rs +++ b/datafusion/core/tests/physical_optimizer/enforce_sorting.rs @@ -1652,7 +1652,7 @@ async fn test_remove_unnecessary_sort7() -> Result<()> { ) as Arc; let expected_input = [ - "SortExec: TopK(fetch=2), expr=[non_nullable_col@1 ASC], preserve_partitioning=[false]", + "SortExec: TopK(fetch=2), expr=[non_nullable_col@1 ASC], preserve_partitioning=[false], sort_prefix=[non_nullable_col@1 ASC]", " SortExec: expr=[non_nullable_col@1 ASC, nullable_col@0 ASC], preserve_partitioning=[false]", " DataSourceExec: partitions=1, partition_sizes=[0]", ]; @@ -3440,3 +3440,38 @@ fn test_handles_multiple_orthogonal_sorts() -> Result<()> { Ok(()) } + +#[test] +fn test_parallelize_sort_preserves_fetch() -> Result<()> { + // Create a schema + let schema = create_test_schema3()?; + let parquet_exec = parquet_exec(&schema); + let coalesced = Arc::new(CoalescePartitionsExec::new(parquet_exec.clone())); + let top_coalesced = CoalescePartitionsExec::new(coalesced.clone()) + .with_fetch(Some(10)) + .unwrap(); + + let requirements = PlanWithCorrespondingCoalescePartitions::new( + top_coalesced.clone(), + true, + vec![PlanWithCorrespondingCoalescePartitions::new( + coalesced, + true, + vec![PlanWithCorrespondingCoalescePartitions::new( + parquet_exec, + false, + vec![], + )], + )], + ); + + let res = parallelize_sorts(requirements)?; + + // Verify fetch was preserved + assert_eq!( + res.data.plan.fetch(), + Some(10), + "Fetch value was not preserved after transformation" + ); + Ok(()) +} diff --git a/datafusion/core/tests/physical_optimizer/mod.rs b/datafusion/core/tests/physical_optimizer/mod.rs index 7d5d07715eebc..6643e7fd59b7a 100644 --- a/datafusion/core/tests/physical_optimizer/mod.rs +++ b/datafusion/core/tests/physical_optimizer/mod.rs @@ -25,6 +25,7 @@ mod join_selection; mod limit_pushdown; mod limited_distinct_aggregation; mod projection_pushdown; +mod push_down_filter; mod replace_with_order_preserving_variants; mod sanity_checker; mod test_utils; diff --git a/datafusion/core/tests/physical_optimizer/push_down_filter.rs b/datafusion/core/tests/physical_optimizer/push_down_filter.rs new file mode 100644 index 0000000000000..b19144f1bcffe --- /dev/null +++ b/datafusion/core/tests/physical_optimizer/push_down_filter.rs @@ -0,0 +1,542 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::sync::{Arc, OnceLock}; +use std::{ + any::Any, + fmt::{Display, Formatter}, +}; + +use arrow::datatypes::{DataType, Field, Schema, SchemaRef}; +use datafusion::{ + datasource::object_store::ObjectStoreUrl, + logical_expr::Operator, + physical_plan::{ + expressions::{BinaryExpr, Column, Literal}, + PhysicalExpr, + }, + scalar::ScalarValue, +}; +use datafusion_common::{config::ConfigOptions, Statistics}; +use datafusion_common::{internal_err, Result}; +use datafusion_datasource::file_scan_config::FileScanConfigBuilder; +use datafusion_datasource::source::DataSourceExec; +use datafusion_datasource::{ + file::FileSource, file_scan_config::FileScanConfig, file_stream::FileOpener, +}; +use datafusion_expr::test::function_stub::count_udaf; +use datafusion_physical_expr::expressions::col; +use datafusion_physical_expr::{ + aggregate::AggregateExprBuilder, conjunction, Partitioning, +}; +use datafusion_physical_expr_common::physical_expr::fmt_sql; +use datafusion_physical_optimizer::push_down_filter::PushdownFilter; +use datafusion_physical_optimizer::PhysicalOptimizerRule; +use datafusion_physical_plan::filter_pushdown::{ + filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, + FilterPushdownSupport, +}; +use datafusion_physical_plan::{ + aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy}, + coalesce_batches::CoalesceBatchesExec, + filter::FilterExec, + repartition::RepartitionExec, +}; +use datafusion_physical_plan::{ + displayable, metrics::ExecutionPlanMetricsSet, DisplayFormatType, ExecutionPlan, +}; + +use object_store::ObjectStore; + +/// A placeholder data source that accepts filter pushdown +#[derive(Clone, Default)] +struct TestSource { + support: bool, + predicate: Option>, + statistics: Option, +} + +impl TestSource { + fn new(support: bool) -> Self { + Self { + support, + predicate: None, + statistics: None, + } + } +} + +impl FileSource for TestSource { + fn create_file_opener( + &self, + _object_store: Arc, + _base_config: &FileScanConfig, + _partition: usize, + ) -> Arc { + todo!("should not be called") + } + + fn as_any(&self) -> &dyn Any { + todo!("should not be called") + } + + fn with_batch_size(&self, _batch_size: usize) -> Arc { + todo!("should not be called") + } + + fn with_schema(&self, _schema: SchemaRef) -> Arc { + todo!("should not be called") + } + + fn with_projection(&self, _config: &FileScanConfig) -> Arc { + todo!("should not be called") + } + + fn with_statistics(&self, statistics: Statistics) -> Arc { + Arc::new(TestSource { + statistics: Some(statistics), + ..self.clone() + }) + } + + fn metrics(&self) -> &ExecutionPlanMetricsSet { + todo!("should not be called") + } + + fn statistics(&self) -> Result { + Ok(self + .statistics + .as_ref() + .expect("statistics not set") + .clone()) + } + + fn file_type(&self) -> &str { + "test" + } + + fn fmt_extra(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result { + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + let support = format!(", pushdown_supported={}", self.support); + + let predicate_string = self + .predicate + .as_ref() + .map(|p| format!(", predicate={p}")) + .unwrap_or_default(); + + write!(f, "{}{}", support, predicate_string) + } + DisplayFormatType::TreeRender => { + if let Some(predicate) = &self.predicate { + writeln!(f, "pushdown_supported={}", fmt_sql(predicate.as_ref()))?; + writeln!(f, "predicate={}", fmt_sql(predicate.as_ref()))?; + } + Ok(()) + } + } + } + + fn try_pushdown_filters( + &self, + mut fd: FilterDescription, + config: &ConfigOptions, + ) -> Result>> { + if self.support && config.execution.parquet.pushdown_filters { + if let Some(internal) = self.predicate.as_ref() { + fd.filters.push(Arc::clone(internal)); + } + let all_filters = fd.take_description(); + + Ok(FilterPushdownResult { + support: FilterPushdownSupport::Supported { + child_descriptions: vec![], + op: Arc::new(TestSource { + support: true, + predicate: Some(conjunction(all_filters)), + statistics: self.statistics.clone(), // should be updated in reality + }), + revisit: false, + }, + remaining_description: FilterDescription::empty(), + }) + } else { + Ok(filter_pushdown_not_supported(fd)) + } + } +} + +fn test_scan(support: bool) -> Arc { + let schema = schema(); + let source = Arc::new(TestSource::new(support)); + let base_config = FileScanConfigBuilder::new( + ObjectStoreUrl::parse("test://").unwrap(), + Arc::clone(schema), + source, + ) + .build(); + DataSourceExec::from_data_source(base_config) +} + +#[test] +fn test_pushdown_into_scan() { + let scan = test_scan(true); + let predicate = col_lit_predicate("a", "foo", schema()); + let plan = Arc::new(FilterExec::try_new(predicate, scan).unwrap()); + + // expect the predicate to be pushed down into the DataSource + insta::assert_snapshot!( + OptimizationTest::new(plan, PushdownFilter{}, true), + @r" + OptimizationTest: + input: + - FilterExec: a@0 = foo + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo + " + ); +} + +/// Show that we can use config options to determine how to do pushdown. +#[test] +fn test_pushdown_into_scan_with_config_options() { + let scan = test_scan(true); + let predicate = col_lit_predicate("a", "foo", schema()); + let plan = Arc::new(FilterExec::try_new(predicate, scan).unwrap()) as _; + + let mut cfg = ConfigOptions::default(); + insta::assert_snapshot!( + OptimizationTest::new( + Arc::clone(&plan), + PushdownFilter {}, + false + ), + @r" + OptimizationTest: + input: + - FilterExec: a@0 = foo + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - FilterExec: a@0 = foo + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + " + ); + + cfg.execution.parquet.pushdown_filters = true; + insta::assert_snapshot!( + OptimizationTest::new( + plan, + PushdownFilter {}, + true + ), + @r" + OptimizationTest: + input: + - FilterExec: a@0 = foo + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo + " + ); +} + +#[test] +fn test_filter_collapse() { + // filter should be pushed down into the parquet scan with two filters + let scan = test_scan(true); + let predicate1 = col_lit_predicate("a", "foo", schema()); + let filter1 = Arc::new(FilterExec::try_new(predicate1, scan).unwrap()); + let predicate2 = col_lit_predicate("b", "bar", schema()); + let plan = Arc::new(FilterExec::try_new(predicate2, filter1).unwrap()); + + insta::assert_snapshot!( + OptimizationTest::new(plan, PushdownFilter{}, true), + @r" + OptimizationTest: + input: + - FilterExec: b@1 = bar + - FilterExec: a@0 = foo + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=b@1 = bar AND a@0 = foo + " + ); +} + +#[test] +fn test_filter_with_projection() { + let scan = test_scan(true); + let projection = vec![1, 0]; + let predicate = col_lit_predicate("a", "foo", schema()); + let plan = Arc::new( + FilterExec::try_new(predicate, Arc::clone(&scan)) + .unwrap() + .with_projection(Some(projection)) + .unwrap(), + ); + + // expect the predicate to be pushed down into the DataSource but the FilterExec to be converted to ProjectionExec + insta::assert_snapshot!( + OptimizationTest::new(plan, PushdownFilter{}, true), + @r" + OptimizationTest: + input: + - FilterExec: a@0 = foo, projection=[b@1, a@0] + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - ProjectionExec: expr=[b@1 as b, a@0 as a] + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo + ", + ); + + // add a test where the filter is on a column that isn't included in the output + let projection = vec![1]; + let predicate = col_lit_predicate("a", "foo", schema()); + let plan = Arc::new( + FilterExec::try_new(predicate, scan) + .unwrap() + .with_projection(Some(projection)) + .unwrap(), + ); + insta::assert_snapshot!( + OptimizationTest::new(plan, PushdownFilter{},true), + @r" + OptimizationTest: + input: + - FilterExec: a@0 = foo, projection=[b@1] + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - ProjectionExec: expr=[b@1 as b] + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo + " + ); +} + +#[test] +fn test_push_down_through_transparent_nodes() { + // expect the predicate to be pushed down into the DataSource + let scan = test_scan(true); + let coalesce = Arc::new(CoalesceBatchesExec::new(scan, 1)); + let predicate = col_lit_predicate("a", "foo", schema()); + let filter = Arc::new(FilterExec::try_new(predicate, coalesce).unwrap()); + let repartition = Arc::new( + RepartitionExec::try_new(filter, Partitioning::RoundRobinBatch(1)).unwrap(), + ); + let predicate = col_lit_predicate("b", "bar", schema()); + let plan = Arc::new(FilterExec::try_new(predicate, repartition).unwrap()); + + // expect the predicate to be pushed down into the DataSource + insta::assert_snapshot!( + OptimizationTest::new(plan, PushdownFilter{},true), + @r" + OptimizationTest: + input: + - FilterExec: b@1 = bar + - RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=0 + - FilterExec: a@0 = foo + - CoalesceBatchesExec: target_batch_size=1 + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=0 + - CoalesceBatchesExec: target_batch_size=1 + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=b@1 = bar AND a@0 = foo + " + ); +} + +#[test] +fn test_no_pushdown_through_aggregates() { + // There are 2 important points here: + // 1. The outer filter **is not** pushed down at all because we haven't implemented pushdown support + // yet for AggregateExec. + // 2. The inner filter **is** pushed down into the DataSource. + let scan = test_scan(true); + + let coalesce = Arc::new(CoalesceBatchesExec::new(scan, 10)); + + let filter = Arc::new( + FilterExec::try_new(col_lit_predicate("a", "foo", schema()), coalesce).unwrap(), + ); + + let aggregate_expr = + vec![ + AggregateExprBuilder::new(count_udaf(), vec![col("a", schema()).unwrap()]) + .schema(Arc::clone(schema())) + .alias("cnt") + .build() + .map(Arc::new) + .unwrap(), + ]; + let group_by = PhysicalGroupBy::new_single(vec![ + (col("a", schema()).unwrap(), "a".to_string()), + (col("b", schema()).unwrap(), "b".to_string()), + ]); + let aggregate = Arc::new( + AggregateExec::try_new( + AggregateMode::Final, + group_by, + aggregate_expr.clone(), + vec![None], + filter, + Arc::clone(schema()), + ) + .unwrap(), + ); + + let coalesce = Arc::new(CoalesceBatchesExec::new(aggregate, 100)); + + let predicate = col_lit_predicate("b", "bar", schema()); + let plan = Arc::new(FilterExec::try_new(predicate, coalesce).unwrap()); + + // expect the predicate to be pushed down into the DataSource + insta::assert_snapshot!( + OptimizationTest::new(plan, PushdownFilter{}, true), + @r" + OptimizationTest: + input: + - FilterExec: b@1 = bar + - CoalesceBatchesExec: target_batch_size=100 + - AggregateExec: mode=Final, gby=[a@0 as a, b@1 as b], aggr=[cnt], ordering_mode=PartiallySorted([0]) + - FilterExec: a@0 = foo + - CoalesceBatchesExec: target_batch_size=10 + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true + output: + Ok: + - FilterExec: b@1 = bar + - CoalesceBatchesExec: target_batch_size=100 + - AggregateExec: mode=Final, gby=[a@0 as a, b@1 as b], aggr=[cnt] + - CoalesceBatchesExec: target_batch_size=10 + - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo + " + ); +} + +/// Schema: +/// a: String +/// b: String +/// c: f64 +static TEST_SCHEMA: OnceLock = OnceLock::new(); + +fn schema() -> &'static SchemaRef { + TEST_SCHEMA.get_or_init(|| { + let fields = vec![ + Field::new("a", DataType::Utf8, false), + Field::new("b", DataType::Utf8, false), + Field::new("c", DataType::Float64, false), + ]; + Arc::new(Schema::new(fields)) + }) +} + +/// Returns a predicate that is a binary expression col = lit +fn col_lit_predicate( + column_name: &str, + scalar_value: impl Into, + schema: &Schema, +) -> Arc { + let scalar_value = scalar_value.into(); + Arc::new(BinaryExpr::new( + Arc::new(Column::new_with_schema(column_name, schema).unwrap()), + Operator::Eq, + Arc::new(Literal::new(scalar_value)), + )) +} + +/// A harness for testing physical optimizers. +/// +/// You can use this to test the output of a physical optimizer rule using insta snapshots +#[derive(Debug)] +pub struct OptimizationTest { + input: Vec, + output: Result, String>, +} + +impl OptimizationTest { + pub fn new( + input_plan: Arc, + opt: O, + allow_pushdown_filters: bool, + ) -> Self + where + O: PhysicalOptimizerRule, + { + let mut parquet_pushdown_config = ConfigOptions::default(); + parquet_pushdown_config.execution.parquet.pushdown_filters = + allow_pushdown_filters; + + let input = format_execution_plan(&input_plan); + let input_schema = input_plan.schema(); + + let output_result = opt.optimize(input_plan, &parquet_pushdown_config); + let output = output_result + .and_then(|plan| { + if opt.schema_check() && (plan.schema() != input_schema) { + internal_err!( + "Schema mismatch:\n\nBefore:\n{:?}\n\nAfter:\n{:?}", + input_schema, + plan.schema() + ) + } else { + Ok(plan) + } + }) + .map(|plan| format_execution_plan(&plan)) + .map_err(|e| e.to_string()); + + Self { input, output } + } +} + +impl Display for OptimizationTest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + writeln!(f, "OptimizationTest:")?; + writeln!(f, " input:")?; + for line in &self.input { + writeln!(f, " - {line}")?; + } + writeln!(f, " output:")?; + match &self.output { + Ok(output) => { + writeln!(f, " Ok:")?; + for line in output { + writeln!(f, " - {line}")?; + } + } + Err(err) => { + writeln!(f, " Err: {err}")?; + } + } + Ok(()) + } +} + +pub fn format_execution_plan(plan: &Arc) -> Vec { + format_lines(&displayable(plan.as_ref()).indent(false).to_string()) +} + +fn format_lines(s: &str) -> Vec { + s.trim().split('\n').map(|s| s.to_string()).collect() +} diff --git a/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs b/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs index 58eb866c590cc..eb517c42b0ebb 100644 --- a/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs +++ b/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs @@ -18,7 +18,8 @@ use std::sync::Arc; use crate::physical_optimizer::test_utils::{ - check_integrity, sort_preserving_merge_exec, stream_exec_ordered_with_projection, + check_integrity, create_test_schema3, sort_preserving_merge_exec, + stream_exec_ordered_with_projection, }; use datafusion::prelude::SessionContext; @@ -40,13 +41,14 @@ use datafusion_physical_plan::{ }; use datafusion::datasource::source::DataSourceExec; use datafusion_common::tree_node::{TransformedResult, TreeNode}; -use datafusion_common::Result; +use datafusion_common::{assert_contains, Result}; use datafusion_expr::{JoinType, Operator}; use datafusion_physical_expr::expressions::{self, col, Column}; use datafusion_physical_expr::PhysicalSortExpr; -use datafusion_physical_optimizer::enforce_sorting::replace_with_order_preserving_variants::{replace_with_order_preserving_variants, OrderPreservationContext}; +use datafusion_physical_optimizer::enforce_sorting::replace_with_order_preserving_variants::{plan_with_order_preserving_variants, replace_with_order_preserving_variants, OrderPreservationContext}; use datafusion_common::config::ConfigOptions; +use crate::physical_optimizer::enforce_sorting::parquet_exec_sorted; use object_store::memory::InMemory; use object_store::ObjectStore; use rstest::rstest; @@ -1259,3 +1261,52 @@ fn memory_exec_sorted( )) }) } + +#[test] +fn test_plan_with_order_preserving_variants_preserves_fetch() -> Result<()> { + // Create a schema + let schema = create_test_schema3()?; + let parquet_sort_exprs = vec![crate::physical_optimizer::test_utils::sort_expr( + "a", &schema, + )]; + let parquet_exec = parquet_exec_sorted(&schema, parquet_sort_exprs); + let coalesced = CoalescePartitionsExec::new(parquet_exec.clone()) + .with_fetch(Some(10)) + .unwrap(); + + // Test sort's fetch is greater than coalesce fetch, return error because it's not reasonable + let requirements = OrderPreservationContext::new( + coalesced.clone(), + false, + vec![OrderPreservationContext::new( + parquet_exec.clone(), + false, + vec![], + )], + ); + let res = plan_with_order_preserving_variants(requirements, false, true, Some(15)); + assert_contains!(res.unwrap_err().to_string(), "CoalescePartitionsExec fetch [10] should be greater than or equal to SortExec fetch [15]"); + + // Test sort is without fetch, expected to get the fetch value from the coalesced + let requirements = OrderPreservationContext::new( + coalesced.clone(), + false, + vec![OrderPreservationContext::new( + parquet_exec.clone(), + false, + vec![], + )], + ); + let res = plan_with_order_preserving_variants(requirements, false, true, None)?; + assert_eq!(res.plan.fetch(), Some(10),); + + // Test sort's fetch is less than coalesces fetch, expected to get the fetch value from the sort + let requirements = OrderPreservationContext::new( + coalesced, + false, + vec![OrderPreservationContext::new(parquet_exec, false, vec![])], + ); + let res = plan_with_order_preserving_variants(requirements, false, true, Some(5))?; + assert_eq!(res.plan.fetch(), Some(5),); + Ok(()) +} diff --git a/datafusion/core/tests/sql/mod.rs b/datafusion/core/tests/sql/mod.rs index 579049692e7dc..2a5597b9fb7ee 100644 --- a/datafusion/core/tests/sql/mod.rs +++ b/datafusion/core/tests/sql/mod.rs @@ -63,6 +63,7 @@ pub mod create_drop; pub mod explain_analyze; pub mod joins; mod path_partition; +mod runtime_config; pub mod select; mod sql_api; diff --git a/datafusion/core/tests/sql/path_partition.rs b/datafusion/core/tests/sql/path_partition.rs index bf8466d849f25..fa6c7432413f1 100644 --- a/datafusion/core/tests/sql/path_partition.rs +++ b/datafusion/core/tests/sql/path_partition.rs @@ -712,7 +712,7 @@ impl ObjectStore for MirroringObjectStore { let meta = ObjectMeta { location: location.clone(), last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len() as usize, + size: metadata.len(), e_tag: None, version: None, }; @@ -728,14 +728,15 @@ impl ObjectStore for MirroringObjectStore { async fn get_range( &self, location: &Path, - range: Range, + range: Range, ) -> object_store::Result { self.files.iter().find(|x| *x == location).unwrap(); let path = std::path::PathBuf::from(&self.mirrored_file); let mut file = File::open(path).unwrap(); - file.seek(SeekFrom::Start(range.start as u64)).unwrap(); + file.seek(SeekFrom::Start(range.start)).unwrap(); let to_read = range.end - range.start; + let to_read: usize = to_read.try_into().unwrap(); let mut data = Vec::with_capacity(to_read); let read = file.take(to_read as u64).read_to_end(&mut data).unwrap(); assert_eq!(read, to_read); @@ -750,9 +751,10 @@ impl ObjectStore for MirroringObjectStore { fn list( &self, prefix: Option<&Path>, - ) -> BoxStream<'_, object_store::Result> { + ) -> BoxStream<'static, object_store::Result> { let prefix = prefix.cloned().unwrap_or_default(); - Box::pin(stream::iter(self.files.iter().filter_map( + let size = self.file_size; + Box::pin(stream::iter(self.files.clone().into_iter().filter_map( move |location| { // Don't return for exact prefix match let filter = location @@ -762,9 +764,9 @@ impl ObjectStore for MirroringObjectStore { filter.then(|| { Ok(ObjectMeta { - location: location.clone(), + location, last_modified: Utc.timestamp_nanos(0), - size: self.file_size as usize, + size, e_tag: None, version: None, }) @@ -802,7 +804,7 @@ impl ObjectStore for MirroringObjectStore { let object = ObjectMeta { location: k.clone(), last_modified: Utc.timestamp_nanos(0), - size: self.file_size as usize, + size: self.file_size, e_tag: None, version: None, }; diff --git a/datafusion/core/tests/sql/runtime_config.rs b/datafusion/core/tests/sql/runtime_config.rs new file mode 100644 index 0000000000000..18e07bb61ed94 --- /dev/null +++ b/datafusion/core/tests/sql/runtime_config.rs @@ -0,0 +1,166 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Tests for runtime configuration SQL interface + +use std::sync::Arc; + +use datafusion::execution::context::SessionContext; +use datafusion::execution::context::TaskContext; +use datafusion_physical_plan::common::collect; + +#[tokio::test] +async fn test_memory_limit_with_spill() { + let ctx = SessionContext::new(); + + ctx.sql("SET datafusion.runtime.memory_limit = '1M'") + .await + .unwrap() + .collect() + .await + .unwrap(); + + ctx.sql("SET datafusion.execution.sort_spill_reservation_bytes = 0") + .await + .unwrap() + .collect() + .await + .unwrap(); + + let query = "select * from generate_series(1,10000000) as t1(v1) order by v1;"; + let df = ctx.sql(query).await.unwrap(); + + let plan = df.create_physical_plan().await.unwrap(); + let task_ctx = Arc::new(TaskContext::from(&ctx.state())); + let stream = plan.execute(0, task_ctx).unwrap(); + + let _results = collect(stream).await; + let metrics = plan.metrics().unwrap(); + let spill_count = metrics.spill_count().unwrap(); + assert!(spill_count > 0, "Expected spills but none occurred"); +} + +#[tokio::test] +async fn test_no_spill_with_adequate_memory() { + let ctx = SessionContext::new(); + + ctx.sql("SET datafusion.runtime.memory_limit = '10M'") + .await + .unwrap() + .collect() + .await + .unwrap(); + ctx.sql("SET datafusion.execution.sort_spill_reservation_bytes = 0") + .await + .unwrap() + .collect() + .await + .unwrap(); + + let query = "select * from generate_series(1,100000) as t1(v1) order by v1;"; + let df = ctx.sql(query).await.unwrap(); + + let plan = df.create_physical_plan().await.unwrap(); + let task_ctx = Arc::new(TaskContext::from(&ctx.state())); + let stream = plan.execute(0, task_ctx).unwrap(); + + let _results = collect(stream).await; + let metrics = plan.metrics().unwrap(); + let spill_count = metrics.spill_count().unwrap(); + assert_eq!(spill_count, 0, "Expected no spills but some occurred"); +} + +#[tokio::test] +async fn test_multiple_configs() { + let ctx = SessionContext::new(); + + ctx.sql("SET datafusion.runtime.memory_limit = '100M'") + .await + .unwrap() + .collect() + .await + .unwrap(); + ctx.sql("SET datafusion.execution.batch_size = '2048'") + .await + .unwrap() + .collect() + .await + .unwrap(); + + let query = "select * from generate_series(1,100000) as t1(v1) order by v1;"; + let result = ctx.sql(query).await.unwrap().collect().await; + + assert!(result.is_ok(), "Should not fail due to memory limit"); + + let state = ctx.state(); + let batch_size = state.config().options().execution.batch_size; + assert_eq!(batch_size, 2048); +} + +#[tokio::test] +async fn test_memory_limit_enforcement() { + let ctx = SessionContext::new(); + + ctx.sql("SET datafusion.runtime.memory_limit = '1M'") + .await + .unwrap() + .collect() + .await + .unwrap(); + + let query = "select * from generate_series(1,100000) as t1(v1) order by v1;"; + let result = ctx.sql(query).await.unwrap().collect().await; + + assert!(result.is_err(), "Should fail due to memory limit"); + + ctx.sql("SET datafusion.runtime.memory_limit = '100M'") + .await + .unwrap() + .collect() + .await + .unwrap(); + + let result = ctx.sql(query).await.unwrap().collect().await; + + assert!(result.is_ok(), "Should not fail due to memory limit"); +} + +#[tokio::test] +async fn test_invalid_memory_limit() { + let ctx = SessionContext::new(); + + let result = ctx + .sql("SET datafusion.runtime.memory_limit = '100X'") + .await; + + assert!(result.is_err()); + let error_message = result.unwrap_err().to_string(); + assert!(error_message.contains("Unsupported unit 'X'")); +} + +#[tokio::test] +async fn test_unknown_runtime_config() { + let ctx = SessionContext::new(); + + let result = ctx + .sql("SET datafusion.runtime.unknown_config = 'value'") + .await; + + assert!(result.is_err()); + let error_message = result.unwrap_err().to_string(); + assert!(error_message.contains("Unknown runtime configuration")); +} diff --git a/datafusion/core/tests/sql/sql_api.rs b/datafusion/core/tests/sql/sql_api.rs index 034d6fa23d9cb..ec086bcc50c76 100644 --- a/datafusion/core/tests/sql/sql_api.rs +++ b/datafusion/core/tests/sql/sql_api.rs @@ -19,6 +19,23 @@ use datafusion::prelude::*; use tempfile::TempDir; +#[tokio::test] +async fn test_window_function() { + let ctx = SessionContext::new(); + let df = ctx + .sql( + r#"SELECT + t1.v1, + SUM(t1.v1) OVER w + 1 + FROM + generate_series(1, 10000) AS t1(v1) + WINDOW + w AS (ORDER BY t1.v1 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW);"#, + ) + .await; + assert!(df.is_ok()); +} + #[tokio::test] async fn unsupported_ddl_returns_error() { // Verify SessionContext::with_sql_options errors appropriately diff --git a/datafusion/core/tests/tracing/asserting_tracer.rs b/datafusion/core/tests/tracing/asserting_tracer.rs new file mode 100644 index 0000000000000..292e066e5f121 --- /dev/null +++ b/datafusion/core/tests/tracing/asserting_tracer.rs @@ -0,0 +1,142 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::any::Any; +use std::collections::VecDeque; +use std::ops::Deref; +use std::sync::{Arc, LazyLock}; + +use datafusion_common::{HashMap, HashSet}; +use datafusion_common_runtime::{set_join_set_tracer, JoinSetTracer}; +use futures::future::BoxFuture; +use tokio::sync::{Mutex, MutexGuard}; + +/// Initializes the global join set tracer with the asserting tracer. +/// Call this function before spawning any tasks that should be traced. +pub fn init_asserting_tracer() { + set_join_set_tracer(ASSERTING_TRACER.deref()) + .expect("Failed to initialize asserting tracer"); +} + +/// Verifies that the current task has a traceable ancestry back to "root". +/// +/// The function performs a breadth-first search (BFS) in the global spawn graph: +/// - It starts at the current task and follows parent links. +/// - If it reaches the "root" task, the ancestry is valid. +/// - If a task is missing from the graph, it panics. +/// +/// Note: Tokio task IDs are unique only while a task is active. +/// Once a task completes, its ID may be reused. +pub async fn assert_traceability() { + // Acquire the spawn graph lock. + let spawn_graph = acquire_spawn_graph().await; + + // Start BFS with the current task. + let mut tasks_to_check = VecDeque::from(vec![current_task()]); + + while let Some(task_id) = tasks_to_check.pop_front() { + if task_id == "root" { + // Ancestry reached the root. + continue; + } + // Obtain parent tasks, panicking if the task is not present. + let parents = spawn_graph + .get(&task_id) + .expect("Task ID not found in spawn graph"); + // Queue each parent for checking. + for parent in parents { + tasks_to_check.push_back(parent.clone()); + } + } +} + +/// Tracer that maintains a graph of task ancestry for tracing purposes. +/// +/// For each task, it records a set of parent task IDs to ensure that every +/// asynchronous task can be traced back to "root". +struct AssertingTracer { + /// An asynchronous map from task IDs to their parent task IDs. + spawn_graph: Arc>>>, +} + +/// Lazily initialized global instance of `AssertingTracer`. +static ASSERTING_TRACER: LazyLock = LazyLock::new(AssertingTracer::new); + +impl AssertingTracer { + /// Creates a new `AssertingTracer` with an empty spawn graph. + fn new() -> Self { + Self { + spawn_graph: Arc::default(), + } + } +} + +/// Returns the current task's ID as a string, or "root" if unavailable. +/// +/// Tokio guarantees task IDs are unique only among active tasks, +/// so completed tasks may have their IDs reused. +fn current_task() -> String { + tokio::task::try_id() + .map(|id| format!("{id}")) + .unwrap_or_else(|| "root".to_string()) +} + +/// Asynchronously locks and returns the spawn graph. +/// +/// The returned guard allows inspection or modification of task ancestry. +async fn acquire_spawn_graph<'a>() -> MutexGuard<'a, HashMap>> { + ASSERTING_TRACER.spawn_graph.lock().await +} + +/// Registers the current task as a child of `parent_id` in the spawn graph. +async fn register_task(parent_id: String) { + acquire_spawn_graph() + .await + .entry(current_task()) + .or_insert_with(HashSet::new) + .insert(parent_id); +} + +impl JoinSetTracer for AssertingTracer { + /// Wraps an asynchronous future to record its parent task before execution. + fn trace_future( + &self, + fut: BoxFuture<'static, Box>, + ) -> BoxFuture<'static, Box> { + // Capture the parent task ID. + let parent_id = current_task(); + Box::pin(async move { + // Register the parent-child relationship. + register_task(parent_id).await; + // Execute the wrapped future. + fut.await + }) + } + + /// Wraps a blocking closure to record its parent task before execution. + fn trace_block( + &self, + f: Box Box + Send>, + ) -> Box Box + Send> { + let parent_id = current_task(); + Box::new(move || { + // Synchronously record the task relationship. + futures::executor::block_on(register_task(parent_id)); + f() + }) + } +} diff --git a/datafusion/core/tests/tracing/mod.rs b/datafusion/core/tests/tracing/mod.rs new file mode 100644 index 0000000000000..787dd9f4f3cbc --- /dev/null +++ b/datafusion/core/tests/tracing/mod.rs @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! # JoinSetTracer Integration Tests +//! +//! These are smoke tests that verify `JoinSetTracer` can be correctly injected into DataFusion. +//! +//! They run a SQL query that reads Parquet data and performs an aggregation, +//! which causes DataFusion to spawn multiple tasks. +//! The object store is wrapped to assert that every task can be traced back to the root. +//! +//! These tests don't cover all edge cases, but they should fail if changes to +//! DataFusion's task spawning break tracing. + +mod asserting_tracer; +mod traceable_object_store; + +use asserting_tracer::init_asserting_tracer; +use datafusion::datasource::file_format::parquet::ParquetFormat; +use datafusion::datasource::listing::ListingOptions; +use datafusion::prelude::*; +use datafusion::test_util::parquet_test_data; +use datafusion_common::assert_contains; +use datafusion_common_runtime::SpawnedTask; +use log::info; +use object_store::local::LocalFileSystem; +use std::sync::Arc; +use traceable_object_store::traceable_object_store; +use url::Url; + +/// Combined test that first verifies the query panics when no tracer is registered, +/// then initializes the tracer and confirms the query runs successfully. +/// +/// Using a single test function prevents global tracer leakage between tests. +#[tokio::test(flavor = "multi_thread", worker_threads = 8)] +async fn test_tracer_injection() { + // Without initializing the tracer, run the query. + // Spawn the query in a separate task so we can catch its panic. + info!("Running query without tracer"); + // The absence of the tracer should cause the task to panic inside the `TraceableObjectStore`. + let untraced_result = SpawnedTask::spawn(run_query()).join().await; + if let Err(e) = untraced_result { + // Check if the error message contains the expected error. + assert!(e.is_panic(), "Expected a panic, but got: {:?}", e); + assert_contains!(e.to_string(), "Task ID not found in spawn graph"); + info!("Caught expected panic: {}", e); + } else { + panic!("Expected the task to panic, but it completed successfully"); + }; + + // Initialize the asserting tracer and run the query. + info!("Initializing tracer and re-running query"); + init_asserting_tracer(); + SpawnedTask::spawn(run_query()).join().await.unwrap(); // Should complete without panics or errors. +} + +/// Executes a sample task-spawning SQL query using a traceable object store. +async fn run_query() { + info!("Starting query execution"); + + // Create a new session context + let ctx = SessionContext::new(); + + // Get the test data directory + let test_data = parquet_test_data(); + + // Define a Parquet file format with pruning enabled + let file_format = ParquetFormat::default().with_enable_pruning(true); + + // Set listing options for the parquet file with a specific extension + let listing_options = ListingOptions::new(Arc::new(file_format)) + .with_file_extension("alltypes_tiny_pages_plain.parquet"); + + // Wrap the local file system in a traceable object store to verify task traceability. + let local_fs = Arc::new(LocalFileSystem::new()); + let traceable_store = traceable_object_store(local_fs); + + // Register the traceable object store with a test URL. + let url = Url::parse("test://").unwrap(); + ctx.register_object_store(&url, traceable_store.clone()); + + // Register a listing table from the test data directory. + let table_path = format!("test://{}/", test_data); + ctx.register_listing_table("alltypes", &table_path, listing_options, None, None) + .await + .expect("Failed to register table"); + + // Define and execute an SQL query against the registered table, which should + // spawn multiple tasks due to the aggregation and parquet file read. + let sql = "SELECT COUNT(*), string_col FROM alltypes GROUP BY string_col"; + let result_batches = ctx.sql(sql).await.unwrap().collect().await.unwrap(); + + info!("Query complete: {} batches returned", result_batches.len()); +} diff --git a/datafusion/core/tests/tracing/traceable_object_store.rs b/datafusion/core/tests/tracing/traceable_object_store.rs new file mode 100644 index 0000000000000..dfcafc3a63da1 --- /dev/null +++ b/datafusion/core/tests/tracing/traceable_object_store.rs @@ -0,0 +1,125 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//! Object store implementation used for testing + +use crate::tracing::asserting_tracer::assert_traceability; +use futures::stream::BoxStream; +use object_store::{ + path::Path, GetOptions, GetResult, ListResult, MultipartUpload, ObjectMeta, + ObjectStore, PutMultipartOpts, PutOptions, PutPayload, PutResult, +}; +use std::fmt::{Debug, Display, Formatter}; +use std::sync::Arc; + +/// Returns an `ObjectStore` that asserts it can trace its calls back to the root tokio task. +pub fn traceable_object_store( + object_store: Arc, +) -> Arc { + Arc::new(TraceableObjectStore::new(object_store)) +} + +/// An object store that asserts it can trace all its calls back to the root tokio task. +#[derive(Debug)] +struct TraceableObjectStore { + inner: Arc, +} + +impl TraceableObjectStore { + fn new(inner: Arc) -> Self { + Self { inner } + } +} + +impl Display for TraceableObjectStore { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.inner, f) + } +} + +/// All trait methods are forwarded to the inner object store, +/// after asserting they can trace their calls back to the root tokio task. +#[async_trait::async_trait] +impl ObjectStore for TraceableObjectStore { + async fn put_opts( + &self, + location: &Path, + payload: PutPayload, + opts: PutOptions, + ) -> object_store::Result { + assert_traceability().await; + self.inner.put_opts(location, payload, opts).await + } + + async fn put_multipart_opts( + &self, + location: &Path, + opts: PutMultipartOpts, + ) -> object_store::Result> { + assert_traceability().await; + self.inner.put_multipart_opts(location, opts).await + } + + async fn get_opts( + &self, + location: &Path, + options: GetOptions, + ) -> object_store::Result { + assert_traceability().await; + self.inner.get_opts(location, options).await + } + + async fn head(&self, location: &Path) -> object_store::Result { + assert_traceability().await; + self.inner.head(location).await + } + + async fn delete(&self, location: &Path) -> object_store::Result<()> { + assert_traceability().await; + self.inner.delete(location).await + } + + fn list( + &self, + prefix: Option<&Path>, + ) -> BoxStream<'static, object_store::Result> { + futures::executor::block_on(assert_traceability()); + self.inner.list(prefix) + } + + async fn list_with_delimiter( + &self, + prefix: Option<&Path>, + ) -> object_store::Result { + assert_traceability().await; + self.inner.list_with_delimiter(prefix).await + } + + async fn copy(&self, from: &Path, to: &Path) -> object_store::Result<()> { + assert_traceability().await; + self.inner.copy(from, to).await + } + + async fn copy_if_not_exists( + &self, + from: &Path, + to: &Path, + ) -> object_store::Result<()> { + assert_traceability().await; + self.inner.copy_if_not_exists(from, to).await + } +} diff --git a/datafusion/core/tests/user_defined/user_defined_window_functions.rs b/datafusion/core/tests/user_defined/user_defined_window_functions.rs index 28394f0b9dfaf..7c56507acd451 100644 --- a/datafusion/core/tests/user_defined/user_defined_window_functions.rs +++ b/datafusion/core/tests/user_defined/user_defined_window_functions.rs @@ -633,7 +633,7 @@ fn odd_count(arr: &Int64Array) -> i64 { /// returns an array of num_rows that has the number of odd values in `arr` fn odd_count_arr(arr: &Int64Array, num_rows: usize) -> ArrayRef { - let array: Int64Array = std::iter::repeat(odd_count(arr)).take(num_rows).collect(); + let array: Int64Array = std::iter::repeat_n(odd_count(arr), num_rows).collect(); Arc::new(array) } diff --git a/datafusion/datasource-csv/src/source.rs b/datafusion/datasource-csv/src/source.rs index 6db4d18703204..f5d45cd3fc881 100644 --- a/datafusion/datasource-csv/src/source.rs +++ b/datafusion/datasource-csv/src/source.rs @@ -704,6 +704,7 @@ impl FileOpener for CsvOpener { let result = store.get_opts(file_meta.location(), options).await?; match result.payload { + #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(mut file, _) => { let is_whole_file_scanned = file_meta.range.is_none(); let decoder = if is_whole_file_scanned { diff --git a/datafusion/datasource-json/src/file_format.rs b/datafusion/datasource-json/src/file_format.rs index a6c52312e4127..8d0515804fc7b 100644 --- a/datafusion/datasource-json/src/file_format.rs +++ b/datafusion/datasource-json/src/file_format.rs @@ -209,6 +209,7 @@ impl FileFormat for JsonFormat { let r = store.as_ref().get(&object.location).await?; let schema = match r.payload { + #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(file, _) => { let decoder = file_compression_type.convert_read(file)?; let mut reader = BufReader::new(decoder); diff --git a/datafusion/datasource-json/src/source.rs b/datafusion/datasource-json/src/source.rs index f1adccf9ded7d..ee96d050966d6 100644 --- a/datafusion/datasource-json/src/source.rs +++ b/datafusion/datasource-json/src/source.rs @@ -355,6 +355,7 @@ impl FileOpener for JsonOpener { let result = store.get_opts(file_meta.location(), options).await?; match result.payload { + #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(mut file, _) => { let bytes = match file_meta.range { None => file_compression_type.convert_read(file)?, diff --git a/datafusion/datasource-parquet/src/file_format.rs b/datafusion/datasource-parquet/src/file_format.rs index 1d9a67fd2eb6d..ee4db50a6eda5 100644 --- a/datafusion/datasource-parquet/src/file_format.rs +++ b/datafusion/datasource-parquet/src/file_format.rs @@ -24,9 +24,18 @@ use std::ops::Range; use std::sync::Arc; use arrow::array::RecordBatch; +use arrow::datatypes::{Fields, Schema, SchemaRef, TimeUnit}; +use datafusion_datasource::file_compression_type::FileCompressionType; +use datafusion_datasource::file_sink_config::{FileSink, FileSinkConfig}; +use datafusion_datasource::write::{create_writer, get_writer_schema, SharedBuffer}; + +use datafusion_datasource::file_format::{ + FileFormat, FileFormatFactory, FilePushdownSupport, +}; +use datafusion_datasource::write::demux::DemuxedStreamReceiver; + use arrow::compute::sum; use arrow::datatypes::{DataType, Field, FieldRef}; -use arrow::datatypes::{Fields, Schema, SchemaRef}; use datafusion_common::config::{ConfigField, ConfigFileType, TableParquetOptions}; use datafusion_common::parsers::CompressionTypeVariant; use datafusion_common::stats::Precision; @@ -38,15 +47,8 @@ use datafusion_common::{HashMap, Statistics}; use datafusion_common_runtime::{JoinSet, SpawnedTask}; use datafusion_datasource::display::FileGroupDisplay; use datafusion_datasource::file::FileSource; -use datafusion_datasource::file_compression_type::FileCompressionType; -use datafusion_datasource::file_format::{ - FileFormat, FileFormatFactory, FilePushdownSupport, -}; use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder}; -use datafusion_datasource::file_sink_config::{FileSink, FileSinkConfig}; use datafusion_datasource::sink::{DataSink, DataSinkExec}; -use datafusion_datasource::write::demux::DemuxedStreamReceiver; -use datafusion_datasource::write::{create_writer, get_writer_schema, SharedBuffer}; use datafusion_execution::memory_pool::{MemoryConsumer, MemoryPool, MemoryReservation}; use datafusion_execution::{SendableRecordBatchStream, TaskContext}; use datafusion_expr::dml::InsertOp; @@ -59,7 +61,7 @@ use datafusion_physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan}; use datafusion_session::Session; use crate::can_expr_be_pushed_down_with_schemas; -use crate::source::ParquetSource; +use crate::source::{parse_coerce_int96_string, ParquetSource}; use async_trait::async_trait; use bytes::Bytes; use datafusion_datasource::source::DataSourceExec; @@ -76,11 +78,13 @@ use parquet::arrow::arrow_writer::{ }; use parquet::arrow::async_reader::MetadataFetch; use parquet::arrow::{parquet_to_arrow_schema, ArrowSchemaConverter, AsyncArrowWriter}; +use parquet::basic::Type; use parquet::errors::ParquetError; use parquet::file::metadata::{ParquetMetaData, ParquetMetaDataReader, RowGroupMetaData}; use parquet::file::properties::{WriterProperties, WriterPropertiesBuilder}; use parquet::file::writer::SerializedFileWriter; use parquet::format::FileMetaData; +use parquet::schema::types::SchemaDescriptor; use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::sync::mpsc::{self, Receiver, Sender}; @@ -268,6 +272,15 @@ impl ParquetFormat { self.options.global.binary_as_string = binary_as_string; self } + + pub fn coerce_int96(&self) -> Option { + self.options.global.coerce_int96.clone() + } + + pub fn with_coerce_int96(mut self, time_unit: Option) -> Self { + self.options.global.coerce_int96 = time_unit; + self + } } /// Clears all metadata (Schema level and field level) on an iterator @@ -291,9 +304,10 @@ async fn fetch_schema_with_location( store: &dyn ObjectStore, file: &ObjectMeta, metadata_size_hint: Option, + coerce_int96: Option, ) -> Result<(Path, Schema)> { let loc_path = file.location.clone(); - let schema = fetch_schema(store, file, metadata_size_hint).await?; + let schema = fetch_schema(store, file, metadata_size_hint, coerce_int96).await?; Ok((loc_path, schema)) } @@ -324,12 +338,17 @@ impl FileFormat for ParquetFormat { store: &Arc, objects: &[ObjectMeta], ) -> Result { + let coerce_int96 = match self.coerce_int96() { + Some(time_unit) => Some(parse_coerce_int96_string(time_unit.as_str())?), + None => None, + }; let mut schemas: Vec<_> = futures::stream::iter(objects) .map(|object| { fetch_schema_with_location( store.as_ref(), object, self.metadata_size_hint(), + coerce_int96, ) }) .boxed() // Workaround https://github.com/rust-lang/rust/issues/64552 @@ -569,6 +588,46 @@ pub fn apply_file_schema_type_coercions( )) } +/// Coerces the file schema's Timestamps to the provided TimeUnit if Parquet schema contains INT96. +pub fn coerce_int96_to_resolution( + parquet_schema: &SchemaDescriptor, + file_schema: &Schema, + time_unit: &TimeUnit, +) -> Option { + let mut transform = false; + let parquet_fields: HashMap<_, _> = parquet_schema + .columns() + .iter() + .map(|f| { + let dt = f.physical_type(); + if dt.eq(&Type::INT96) { + transform = true; + } + (f.name(), dt) + }) + .collect(); + + if !transform { + return None; + } + + let transformed_fields: Vec> = file_schema + .fields + .iter() + .map(|field| match parquet_fields.get(field.name().as_str()) { + Some(Type::INT96) => { + field_with_new_type(field, DataType::Timestamp(*time_unit, None)) + } + _ => Arc::clone(field), + }) + .collect(); + + Some(Schema::new_with_metadata( + transformed_fields, + file_schema.metadata.clone(), + )) +} + /// Coerces the file schema if the table schema uses a view type. #[deprecated( since = "47.0.0", @@ -735,10 +794,7 @@ impl<'a> ObjectStoreFetch<'a> { } impl MetadataFetch for ObjectStoreFetch<'_> { - fn fetch( - &mut self, - range: Range, - ) -> BoxFuture<'_, Result> { + fn fetch(&mut self, range: Range) -> BoxFuture<'_, Result> { async { self.store .get_range(&self.meta.location, range) @@ -775,6 +831,7 @@ async fn fetch_schema( store: &dyn ObjectStore, file: &ObjectMeta, metadata_size_hint: Option, + coerce_int96: Option, ) -> Result { let metadata = fetch_parquet_metadata(store, file, metadata_size_hint).await?; let file_metadata = metadata.file_metadata(); @@ -782,6 +839,11 @@ async fn fetch_schema( file_metadata.schema_descr(), file_metadata.key_value_metadata(), )?; + let schema = coerce_int96 + .and_then(|time_unit| { + coerce_int96_to_resolution(file_metadata.schema_descr(), &schema, &time_unit) + }) + .unwrap_or(schema); Ok(schema) } diff --git a/datafusion/datasource-parquet/src/opener.rs b/datafusion/datasource-parquet/src/opener.rs index 732fef47d5a75..cfe8213f86e4b 100644 --- a/datafusion/datasource-parquet/src/opener.rs +++ b/datafusion/datasource-parquet/src/opener.rs @@ -22,25 +22,27 @@ use std::sync::Arc; use crate::page_filter::PagePruningAccessPlanFilter; use crate::row_group_filter::RowGroupAccessPlanFilter; use crate::{ - apply_file_schema_type_coercions, row_filter, should_enable_page_index, - ParquetAccessPlan, ParquetFileMetrics, ParquetFileReaderFactory, + apply_file_schema_type_coercions, coerce_int96_to_resolution, row_filter, + should_enable_page_index, ParquetAccessPlan, ParquetFileMetrics, + ParquetFileReaderFactory, }; use datafusion_datasource::file_meta::FileMeta; use datafusion_datasource::file_stream::{FileOpenFuture, FileOpener}; use datafusion_datasource::schema_adapter::SchemaAdapterFactory; -use arrow::datatypes::SchemaRef; +use arrow::datatypes::{SchemaRef, TimeUnit}; use arrow::error::ArrowError; use datafusion_common::{exec_err, Result}; use datafusion_physical_expr_common::physical_expr::PhysicalExpr; use datafusion_physical_optimizer::pruning::PruningPredicate; -use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet; +use datafusion_physical_plan::metrics::{Count, ExecutionPlanMetricsSet, MetricBuilder}; use futures::{StreamExt, TryStreamExt}; use log::debug; use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions}; use parquet::arrow::async_reader::AsyncFileReader; use parquet::arrow::{ParquetRecordBatchStreamBuilder, ProjectionMask}; +use parquet::file::metadata::ParquetMetaDataReader; /// Implements [`FileOpener`] for a parquet file pub(super) struct ParquetOpener { @@ -54,10 +56,6 @@ pub(super) struct ParquetOpener { pub limit: Option, /// Optional predicate to apply during the scan pub predicate: Option>, - /// Optional pruning predicate applied to row group statistics - pub pruning_predicate: Option>, - /// Optional pruning predicate applied to data page statistics - pub page_pruning_predicate: Option>, /// Schema of the output table pub table_schema: SchemaRef, /// Optional hint for how large the initial request to read parquet metadata @@ -80,6 +78,10 @@ pub(super) struct ParquetOpener { pub enable_bloom_filter: bool, /// Schema adapter factory pub schema_adapter_factory: Arc, + /// Should row group pruning be applied + pub enable_row_group_stats_pruning: bool, + /// Coerce INT96 timestamps to specific TimeUnit + pub coerce_int96: Option, } impl FileOpener for ParquetOpener { @@ -92,7 +94,7 @@ impl FileOpener for ParquetOpener { let metadata_size_hint = file_meta.metadata_size_hint.or(self.metadata_size_hint); - let mut reader: Box = + let mut async_file_reader: Box = self.parquet_file_reader_factory.create_reader( self.partition_index, file_meta, @@ -109,47 +111,100 @@ impl FileOpener for ParquetOpener { .schema_adapter_factory .create(projected_schema, Arc::clone(&self.table_schema)); let predicate = self.predicate.clone(); - let pruning_predicate = self.pruning_predicate.clone(); - let page_pruning_predicate = self.page_pruning_predicate.clone(); let table_schema = Arc::clone(&self.table_schema); let reorder_predicates = self.reorder_filters; let pushdown_filters = self.pushdown_filters; - let enable_page_index = should_enable_page_index( - self.enable_page_index, - &self.page_pruning_predicate, - ); + let coerce_int96 = self.coerce_int96; let enable_bloom_filter = self.enable_bloom_filter; + let enable_row_group_stats_pruning = self.enable_row_group_stats_pruning; let limit = self.limit; - Ok(Box::pin(async move { - let options = ArrowReaderOptions::new().with_page_index(enable_page_index); + let predicate_creation_errors = MetricBuilder::new(&self.metrics) + .global_counter("num_predicate_creation_errors"); + + let enable_page_index = self.enable_page_index; + Ok(Box::pin(async move { + // Don't load the page index yet. Since it is not stored inline in + // the footer, loading the page index if it is not needed will do + // unecessary I/O. We decide later if it is needed to evaluate the + // pruning predicates. Thus default to not requesting if from the + // underlying reader. + let mut options = ArrowReaderOptions::new().with_page_index(false); let mut metadata_timer = file_metrics.metadata_load_time.timer(); - let metadata = - ArrowReaderMetadata::load_async(&mut reader, options.clone()).await?; - let mut schema = Arc::clone(metadata.schema()); - // read with view types - if let Some(merged) = apply_file_schema_type_coercions(&table_schema, &schema) + // Begin by loading the metadata from the underlying reader (note + // the returned metadata may actually include page indexes as some + // readers may return page indexes even when not requested -- for + // example when they are cached) + let mut reader_metadata = + ArrowReaderMetadata::load_async(&mut async_file_reader, options.clone()) + .await?; + + // Note about schemas: we are actually dealing with **3 different schemas** here: + // - The table schema as defined by the TableProvider. This is what the user sees, what they get when they `SELECT * FROM table`, etc. + // - The "virtual" file schema: this is the table schema minus any hive partition columns and projections. This is what the file schema is coerced to. + // - The physical file schema: this is the schema as defined by the parquet file. This is what the parquet file actually contains. + let mut physical_file_schema = Arc::clone(reader_metadata.schema()); + + // The schema loaded from the file may not be the same as the + // desired schema (for example if we want to instruct the parquet + // reader to read strings using Utf8View instead). Update if necessary + if let Some(merged) = + apply_file_schema_type_coercions(&table_schema, &physical_file_schema) { - schema = Arc::new(merged); + physical_file_schema = Arc::new(merged); + options = options.with_schema(Arc::clone(&physical_file_schema)); + reader_metadata = ArrowReaderMetadata::try_new( + Arc::clone(reader_metadata.metadata()), + options.clone(), + )?; } - let options = ArrowReaderOptions::new() - .with_page_index(enable_page_index) - .with_schema(Arc::clone(&schema)); - let metadata = - ArrowReaderMetadata::try_new(Arc::clone(metadata.metadata()), options)?; + if coerce_int96.is_some() { + if let Some(merged) = coerce_int96_to_resolution( + reader_metadata.parquet_schema(), + &physical_file_schema, + &(coerce_int96.unwrap()), + ) { + physical_file_schema = Arc::new(merged); + options = options.with_schema(Arc::clone(&physical_file_schema)); + reader_metadata = ArrowReaderMetadata::try_new( + Arc::clone(reader_metadata.metadata()), + options.clone(), + )?; + } + } - metadata_timer.stop(); + // Build predicates for this specific file + let (pruning_predicate, page_pruning_predicate) = build_pruning_predicates( + &predicate, + &physical_file_schema, + &predicate_creation_errors, + ); - let mut builder = - ParquetRecordBatchStreamBuilder::new_with_metadata(reader, metadata); + // The page index is not stored inline in the parquet footer so the + // code above may not have read the page index structures yet. If we + // need them for reading and they aren't yet loaded, we need to load them now. + if should_enable_page_index(enable_page_index, &page_pruning_predicate) { + reader_metadata = load_page_index( + reader_metadata, + &mut async_file_reader, + // Since we're manually loading the page index the option here should not matter but we pass it in for consistency + options.with_page_index(true), + ) + .await?; + } - let file_schema = Arc::clone(builder.schema()); + metadata_timer.stop(); + + let mut builder = ParquetRecordBatchStreamBuilder::new_with_metadata( + async_file_reader, + reader_metadata, + ); let (schema_mapping, adapted_projections) = - schema_adapter.map_schema(&file_schema)?; + schema_adapter.map_schema(&physical_file_schema)?; let mask = ProjectionMask::roots( builder.parquet_schema(), @@ -160,7 +215,7 @@ impl FileOpener for ParquetOpener { if let Some(predicate) = pushdown_filters.then_some(predicate).flatten() { let row_filter = row_filter::build_row_filter( &predicate, - &file_schema, + &physical_file_schema, &table_schema, builder.metadata(), reorder_predicates, @@ -197,18 +252,20 @@ impl FileOpener for ParquetOpener { } // If there is a predicate that can be evaluated against the metadata if let Some(predicate) = predicate.as_ref() { - row_groups.prune_by_statistics( - &file_schema, - builder.parquet_schema(), - rg_metadata, - predicate, - &file_metrics, - ); + if enable_row_group_stats_pruning { + row_groups.prune_by_statistics( + &physical_file_schema, + builder.parquet_schema(), + rg_metadata, + predicate, + &file_metrics, + ); + } if enable_bloom_filter && !row_groups.is_empty() { row_groups .prune_by_bloom_filters( - &file_schema, + &physical_file_schema, &mut builder, predicate, &file_metrics, @@ -226,7 +283,7 @@ impl FileOpener for ParquetOpener { if let Some(p) = page_pruning_predicate { access_plan = p.prune_plan_with_page_index( access_plan, - &file_schema, + &physical_file_schema, builder.parquet_schema(), file_metadata.as_ref(), &file_metrics, @@ -295,3 +352,91 @@ fn create_initial_plan( // default to scanning all row groups Ok(ParquetAccessPlan::new_all(row_group_count)) } + +/// Build a pruning predicate from an optional predicate expression. +/// If the predicate is None or the predicate cannot be converted to a pruning +/// predicate, return None. +/// If there is an error creating the pruning predicate it is recorded by incrementing +/// the `predicate_creation_errors` counter. +pub(crate) fn build_pruning_predicate( + predicate: Arc, + file_schema: &SchemaRef, + predicate_creation_errors: &Count, +) -> Option> { + match PruningPredicate::try_new(predicate, Arc::clone(file_schema)) { + Ok(pruning_predicate) => { + if !pruning_predicate.always_true() { + return Some(Arc::new(pruning_predicate)); + } + } + Err(e) => { + debug!("Could not create pruning predicate for: {e}"); + predicate_creation_errors.add(1); + } + } + None +} + +/// Build a page pruning predicate from an optional predicate expression. +/// If the predicate is None or the predicate cannot be converted to a page pruning +/// predicate, return None. +pub(crate) fn build_page_pruning_predicate( + predicate: &Arc, + file_schema: &SchemaRef, +) -> Arc { + Arc::new(PagePruningAccessPlanFilter::new( + predicate, + Arc::clone(file_schema), + )) +} + +fn build_pruning_predicates( + predicate: &Option>, + file_schema: &SchemaRef, + predicate_creation_errors: &Count, +) -> ( + Option>, + Option>, +) { + let Some(predicate) = predicate.as_ref() else { + return (None, None); + }; + let pruning_predicate = build_pruning_predicate( + Arc::clone(predicate), + file_schema, + predicate_creation_errors, + ); + let page_pruning_predicate = build_page_pruning_predicate(predicate, file_schema); + (pruning_predicate, Some(page_pruning_predicate)) +} + +/// Returns a `ArrowReaderMetadata` with the page index loaded, loading +/// it from the underlying `AsyncFileReader` if necessary. +async fn load_page_index( + reader_metadata: ArrowReaderMetadata, + input: &mut T, + options: ArrowReaderOptions, +) -> Result { + let parquet_metadata = reader_metadata.metadata(); + let missing_column_index = parquet_metadata.column_index().is_none(); + let missing_offset_index = parquet_metadata.offset_index().is_none(); + // You may ask yourself: why are we even checking if the page index is already loaded here? + // Didn't we explicitly *not* load it above? + // Well it's possible that a custom implementation of `AsyncFileReader` gives you + // the page index even if you didn't ask for it (e.g. because it's cached) + // so it's important to check that here to avoid extra work. + if missing_column_index || missing_offset_index { + let m = Arc::try_unwrap(Arc::clone(parquet_metadata)) + .unwrap_or_else(|e| e.as_ref().clone()); + let mut reader = + ParquetMetaDataReader::new_with_metadata(m).with_page_indexes(true); + reader.load_page_index(input).await?; + let new_parquet_metadata = reader.finish()?; + let new_arrow_reader = + ArrowReaderMetadata::try_new(Arc::new(new_parquet_metadata), options)?; + Ok(new_arrow_reader) + } else { + // No need to load the page index again, just return the existing metadata + Ok(reader_metadata) + } +} diff --git a/datafusion/datasource-parquet/src/page_filter.rs b/datafusion/datasource-parquet/src/page_filter.rs index ef832d808647c..148527998ab53 100644 --- a/datafusion/datasource-parquet/src/page_filter.rs +++ b/datafusion/datasource-parquet/src/page_filter.rs @@ -249,9 +249,9 @@ impl PagePruningAccessPlanFilter { } if let Some(overall_selection) = overall_selection { - if overall_selection.selects_any() { - let rows_skipped = rows_skipped(&overall_selection); - let rows_selected = rows_selected(&overall_selection); + let rows_selected = overall_selection.row_count(); + if rows_selected > 0 { + let rows_skipped = overall_selection.skipped_row_count(); trace!("Overall selection from predicate skipped {rows_skipped}, selected {rows_selected}: {overall_selection:?}"); total_skip += rows_skipped; total_select += rows_selected; @@ -280,22 +280,6 @@ impl PagePruningAccessPlanFilter { } } -/// returns the number of rows skipped in the selection -/// TODO should this be upstreamed to RowSelection? -fn rows_skipped(selection: &RowSelection) -> usize { - selection - .iter() - .fold(0, |acc, x| if x.skip { acc + x.row_count } else { acc }) -} - -/// returns the number of rows not skipped in the selection -/// TODO should this be upstreamed to RowSelection? -fn rows_selected(selection: &RowSelection) -> usize { - selection - .iter() - .fold(0, |acc, x| if x.skip { acc } else { acc + x.row_count }) -} - fn update_selection( current_selection: Option, row_selection: RowSelection, diff --git a/datafusion/datasource-parquet/src/reader.rs b/datafusion/datasource-parquet/src/reader.rs index 5924a5b5038fc..27ec843c1991d 100644 --- a/datafusion/datasource-parquet/src/reader.rs +++ b/datafusion/datasource-parquet/src/reader.rs @@ -18,19 +18,19 @@ //! [`ParquetFileReaderFactory`] and [`DefaultParquetFileReaderFactory`] for //! low level control of parquet file readers +use crate::ParquetFileMetrics; use bytes::Bytes; use datafusion_datasource::file_meta::FileMeta; use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet; use futures::future::BoxFuture; use object_store::ObjectStore; +use parquet::arrow::arrow_reader::ArrowReaderOptions; use parquet::arrow::async_reader::{AsyncFileReader, ParquetObjectReader}; use parquet::file::metadata::ParquetMetaData; use std::fmt::Debug; use std::ops::Range; use std::sync::Arc; -use crate::ParquetFileMetrics; - /// Interface for reading parquet files. /// /// The combined implementations of [`ParquetFileReaderFactory`] and @@ -96,28 +96,30 @@ pub(crate) struct ParquetFileReader { impl AsyncFileReader for ParquetFileReader { fn get_bytes( &mut self, - range: Range, + range: Range, ) -> BoxFuture<'_, parquet::errors::Result> { - self.file_metrics.bytes_scanned.add(range.end - range.start); + let bytes_scanned = range.end - range.start; + self.file_metrics.bytes_scanned.add(bytes_scanned as usize); self.inner.get_bytes(range) } fn get_byte_ranges( &mut self, - ranges: Vec>, + ranges: Vec>, ) -> BoxFuture<'_, parquet::errors::Result>> where Self: Send, { - let total = ranges.iter().map(|r| r.end - r.start).sum(); - self.file_metrics.bytes_scanned.add(total); + let total: u64 = ranges.iter().map(|r| r.end - r.start).sum(); + self.file_metrics.bytes_scanned.add(total as usize); self.inner.get_byte_ranges(ranges) } - fn get_metadata( - &mut self, - ) -> BoxFuture<'_, parquet::errors::Result>> { - self.inner.get_metadata() + fn get_metadata<'a>( + &'a mut self, + options: Option<&'a ArrowReaderOptions>, + ) -> BoxFuture<'a, parquet::errors::Result>> { + self.inner.get_metadata(options) } } @@ -135,7 +137,8 @@ impl ParquetFileReaderFactory for DefaultParquetFileReaderFactory { metrics, ); let store = Arc::clone(&self.store); - let mut inner = ParquetObjectReader::new(store, file_meta.object_meta); + let mut inner = ParquetObjectReader::new(store, file_meta.object_meta.location) + .with_file_size(file_meta.object_meta.size); if let Some(hint) = metadata_size_hint { inner = inner.with_footer_size_hint(hint) diff --git a/datafusion/datasource-parquet/src/row_filter.rs b/datafusion/datasource-parquet/src/row_filter.rs index da6bf114d71dd..2d2993c29a6f2 100644 --- a/datafusion/datasource-parquet/src/row_filter.rs +++ b/datafusion/datasource-parquet/src/row_filter.rs @@ -449,7 +449,7 @@ fn columns_sorted(_columns: &[usize], _metadata: &ParquetMetaData) -> Result, - file_schema: &SchemaRef, + physical_file_schema: &SchemaRef, table_schema: &SchemaRef, metadata: &ParquetMetaData, reorder_predicates: bool, @@ -470,7 +470,7 @@ pub fn build_row_filter( .map(|expr| { FilterCandidateBuilder::new( Arc::clone(expr), - Arc::clone(file_schema), + Arc::clone(physical_file_schema), Arc::clone(table_schema), Arc::clone(schema_adapter_factory), ) diff --git a/datafusion/datasource-parquet/src/row_group_filter.rs b/datafusion/datasource-parquet/src/row_group_filter.rs index 9d5f9fa16b6eb..13418cdeee223 100644 --- a/datafusion/datasource-parquet/src/row_group_filter.rs +++ b/datafusion/datasource-parquet/src/row_group_filter.rs @@ -1513,7 +1513,7 @@ mod tests { let object_meta = ObjectMeta { location: object_store::path::Path::parse(file_name).expect("creating path"), last_modified: chrono::DateTime::from(std::time::SystemTime::now()), - size: data.len(), + size: data.len() as u64, e_tag: None, version: None, }; @@ -1526,8 +1526,11 @@ mod tests { let metrics = ExecutionPlanMetricsSet::new(); let file_metrics = ParquetFileMetrics::new(0, object_meta.location.as_ref(), &metrics); + let inner = ParquetObjectReader::new(Arc::new(in_memory), object_meta.location) + .with_file_size(object_meta.size); + let reader = ParquetFileReader { - inner: ParquetObjectReader::new(Arc::new(in_memory), object_meta), + inner, file_metrics: file_metrics.clone(), }; let mut builder = ParquetRecordBatchStreamBuilder::new(reader).await.unwrap(); diff --git a/datafusion/datasource-parquet/src/source.rs b/datafusion/datasource-parquet/src/source.rs index 66d4d313d5a61..e15f5243cd27e 100644 --- a/datafusion/datasource-parquet/src/source.rs +++ b/datafusion/datasource-parquet/src/source.rs @@ -17,9 +17,12 @@ //! ParquetSource implementation for reading parquet files use std::any::Any; +use std::fmt::Debug; use std::fmt::Formatter; use std::sync::Arc; +use crate::opener::build_page_pruning_predicate; +use crate::opener::build_pruning_predicate; use crate::opener::ParquetOpener; use crate::page_filter::PagePruningAccessPlanFilter; use crate::DefaultParquetFileReaderFactory; @@ -29,9 +32,9 @@ use datafusion_datasource::schema_adapter::{ DefaultSchemaAdapterFactory, SchemaAdapterFactory, }; -use arrow::datatypes::{Schema, SchemaRef}; +use arrow::datatypes::{Schema, SchemaRef, TimeUnit}; use datafusion_common::config::TableParquetOptions; -use datafusion_common::Statistics; +use datafusion_common::{DataFusionError, Statistics}; use datafusion_datasource::file::FileSource; use datafusion_datasource::file_scan_config::FileScanConfig; use datafusion_physical_expr_common::physical_expr::fmt_sql; @@ -41,7 +44,6 @@ use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder}; use datafusion_physical_plan::DisplayFormatType; use itertools::Itertools; -use log::debug; use object_store::ObjectStore; /// Execution plan for reading one or more Parquet files. @@ -316,24 +318,10 @@ impl ParquetSource { conf = conf.with_metrics(metrics); conf.predicate = Some(Arc::clone(&predicate)); - match PruningPredicate::try_new(Arc::clone(&predicate), Arc::clone(&file_schema)) - { - Ok(pruning_predicate) => { - if !pruning_predicate.always_true() { - conf.pruning_predicate = Some(Arc::new(pruning_predicate)); - } - } - Err(e) => { - debug!("Could not create pruning predicate for: {e}"); - predicate_creation_errors.add(1); - } - }; - - let page_pruning_predicate = Arc::new(PagePruningAccessPlanFilter::new( - &predicate, - Arc::clone(&file_schema), - )); - conf.page_pruning_predicate = Some(page_pruning_predicate); + conf.page_pruning_predicate = + Some(build_page_pruning_predicate(&predicate, &file_schema)); + conf.pruning_predicate = + build_pruning_predicate(predicate, &file_schema, &predicate_creation_errors); conf } @@ -348,16 +336,6 @@ impl ParquetSource { self.predicate.as_ref() } - /// Optional reference to this parquet scan's pruning predicate - pub fn pruning_predicate(&self) -> Option<&Arc> { - self.pruning_predicate.as_ref() - } - - /// Optional reference to this parquet scan's page pruning predicate - pub fn page_pruning_predicate(&self) -> Option<&Arc> { - self.page_pruning_predicate.as_ref() - } - /// return the optional file reader factory pub fn parquet_file_reader_factory( &self, @@ -460,6 +438,24 @@ impl ParquetSource { } } +/// Parses datafusion.common.config.ParquetOptions.coerce_int96 String to a arrow_schema.datatype.TimeUnit +pub(crate) fn parse_coerce_int96_string( + str_setting: &str, +) -> datafusion_common::Result { + let str_setting_lower: &str = &str_setting.to_lowercase(); + + match str_setting_lower { + "ns" => Ok(TimeUnit::Nanosecond), + "us" => Ok(TimeUnit::Microsecond), + "ms" => Ok(TimeUnit::Millisecond), + "s" => Ok(TimeUnit::Second), + _ => Err(DataFusionError::Configuration(format!( + "Unknown or unsupported parquet coerce_int96: \ + {str_setting}. Valid values are: ns, us, ms, and s." + ))), + } +} + impl FileSource for ParquetSource { fn create_file_opener( &self, @@ -480,6 +476,13 @@ impl FileSource for ParquetSource { Arc::new(DefaultParquetFileReaderFactory::new(object_store)) as _ }); + let coerce_int96 = self + .table_parquet_options + .global + .coerce_int96 + .as_ref() + .map(|time_unit| parse_coerce_int96_string(time_unit.as_str()).unwrap()); + Arc::new(ParquetOpener { partition_index: partition, projection: Arc::from(projection), @@ -488,8 +491,6 @@ impl FileSource for ParquetSource { .expect("Batch size must set before creating ParquetOpener"), limit: base_config.limit, predicate: self.predicate.clone(), - pruning_predicate: self.pruning_predicate.clone(), - page_pruning_predicate: self.page_pruning_predicate.clone(), table_schema: Arc::clone(&base_config.file_schema), metadata_size_hint: self.metadata_size_hint, metrics: self.metrics().clone(), @@ -498,7 +499,9 @@ impl FileSource for ParquetSource { reorder_filters: self.reorder_filters(), enable_page_index: self.enable_page_index(), enable_bloom_filter: self.bloom_filter_on_read(), + enable_row_group_stats_pruning: self.table_parquet_options.global.pruning, schema_adapter_factory, + coerce_int96, }) } @@ -537,11 +540,10 @@ impl FileSource for ParquetSource { .expect("projected_statistics must be set"); // When filters are pushed down, we have no way of knowing the exact statistics. // Note that pruning predicate is also a kind of filter pushdown. - // (bloom filters use `pruning_predicate` too) - if self.pruning_predicate().is_some() - || self.page_pruning_predicate().is_some() - || (self.predicate().is_some() && self.pushdown_filters()) - { + // (bloom filters use `pruning_predicate` too). + // Because filter pushdown may happen dynamically as long as there is a predicate + // if we have *any* predicate applied, we can't guarantee the statistics are exact. + if self.predicate().is_some() { Ok(statistics.to_inexact()) } else { Ok(statistics) @@ -560,7 +562,8 @@ impl FileSource for ParquetSource { .map(|p| format!(", predicate={p}")) .unwrap_or_default(); let pruning_predicate_string = self - .pruning_predicate() + .pruning_predicate + .as_ref() .map(|pre| { let mut guarantees = pre .literal_guarantees() diff --git a/datafusion/datasource/Cargo.toml b/datafusion/datasource/Cargo.toml index 2132272b5768d..1088efc268c9b 100644 --- a/datafusion/datasource/Cargo.toml +++ b/datafusion/datasource/Cargo.toml @@ -56,7 +56,7 @@ datafusion-physical-expr = { workspace = true } datafusion-physical-expr-common = { workspace = true } datafusion-physical-plan = { workspace = true } datafusion-session = { workspace = true } -flate2 = { version = "1.0.24", optional = true } +flate2 = { version = "1.1.1", optional = true } futures = { workspace = true } glob = "0.3.0" itertools = { workspace = true } @@ -72,6 +72,7 @@ xz2 = { version = "0.1", optional = true, features = ["static"] } zstd = { version = "0.13", optional = true, default-features = false } [dev-dependencies] +criterion = { workspace = true } tempfile = { workspace = true } [lints] @@ -80,3 +81,7 @@ workspace = true [lib] name = "datafusion_datasource" path = "src/mod.rs" + +[[bench]] +name = "split_groups_by_statistics" +harness = false diff --git a/datafusion/datasource/benches/split_groups_by_statistics.rs b/datafusion/datasource/benches/split_groups_by_statistics.rs new file mode 100644 index 0000000000000..f7c5e1b44ae00 --- /dev/null +++ b/datafusion/datasource/benches/split_groups_by_statistics.rs @@ -0,0 +1,108 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use arrow::datatypes::{DataType, Field, Schema}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use datafusion_datasource::file_scan_config::FileScanConfig; +use datafusion_datasource::{generate_test_files, verify_sort_integrity}; +use datafusion_physical_expr::PhysicalSortExpr; +use datafusion_physical_expr_common::sort_expr::LexOrdering; +use std::sync::Arc; +use std::time::Duration; + +pub fn compare_split_groups_by_statistics_algorithms(c: &mut Criterion) { + let file_schema = Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Float64, + false, + )])); + + let sort_expr = PhysicalSortExpr { + expr: Arc::new(datafusion_physical_expr::expressions::Column::new( + "value", 0, + )), + options: arrow::compute::SortOptions::default(), + }; + let sort_ordering = LexOrdering::from(vec![sort_expr]); + + // Small, medium, large number of files + let file_counts = [10, 100, 1000]; + let overlap_factors = [0.0, 0.2, 0.5, 0.8]; // No, low, medium, high overlap + + let target_partitions: [usize; 4] = [4, 8, 16, 32]; + + let mut group = c.benchmark_group("split_groups"); + group.measurement_time(Duration::from_secs(10)); + + for &num_files in &file_counts { + for &overlap in &overlap_factors { + let file_groups = generate_test_files(num_files, overlap); + // Benchmark original algorithm + group.bench_with_input( + BenchmarkId::new( + "original", + format!("files={},overlap={:.1}", num_files, overlap), + ), + &( + file_groups.clone(), + file_schema.clone(), + sort_ordering.clone(), + ), + |b, (fg, schema, order)| { + let mut result = Vec::new(); + b.iter(|| { + result = + FileScanConfig::split_groups_by_statistics(schema, fg, order) + .unwrap(); + }); + assert!(verify_sort_integrity(&result)); + }, + ); + + // Benchmark new algorithm with different target partitions + for &tp in &target_partitions { + group.bench_with_input( + BenchmarkId::new( + format!("v2_partitions={}", tp), + format!("files={},overlap={:.1}", num_files, overlap), + ), + &( + file_groups.clone(), + file_schema.clone(), + sort_ordering.clone(), + tp, + ), + |b, (fg, schema, order, target)| { + let mut result = Vec::new(); + b.iter(|| { + result = FileScanConfig::split_groups_by_statistics_with_target_partitions( + schema, fg, order, *target, + ) + .unwrap(); + }); + assert!(verify_sort_integrity(&result)); + }, + ); + } + } + } + + group.finish(); +} + +criterion_group!(benches, compare_split_groups_by_statistics_algorithms); +criterion_main!(benches); diff --git a/datafusion/datasource/src/file.rs b/datafusion/datasource/src/file.rs index 0066f39801a1b..835285b21e38a 100644 --- a/datafusion/datasource/src/file.rs +++ b/datafusion/datasource/src/file.rs @@ -26,8 +26,12 @@ use crate::file_groups::FileGroupPartitioner; use crate::file_scan_config::FileScanConfig; use crate::file_stream::FileOpener; use arrow::datatypes::SchemaRef; -use datafusion_common::Statistics; +use datafusion_common::config::ConfigOptions; +use datafusion_common::{Result, Statistics}; use datafusion_physical_expr::LexOrdering; +use datafusion_physical_plan::filter_pushdown::{ + filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, +}; use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet; use datafusion_physical_plan::DisplayFormatType; @@ -57,7 +61,7 @@ pub trait FileSource: Send + Sync { /// Return execution plan metrics fn metrics(&self) -> &ExecutionPlanMetricsSet; /// Return projected statistics - fn statistics(&self) -> datafusion_common::Result; + fn statistics(&self) -> Result; /// String representation of file source such as "csv", "json", "parquet" fn file_type(&self) -> &str; /// Format FileType specific information @@ -75,7 +79,7 @@ pub trait FileSource: Send + Sync { repartition_file_min_size: usize, output_ordering: Option, config: &FileScanConfig, - ) -> datafusion_common::Result> { + ) -> Result> { if config.file_compression_type.is_compressed() || config.new_lines_in_values { return Ok(None); } @@ -93,4 +97,16 @@ pub trait FileSource: Send + Sync { } Ok(None) } + + /// Try to push down filters into this FileSource. + /// See [`ExecutionPlan::try_pushdown_filters`] for more details. + /// + /// [`ExecutionPlan::try_pushdown_filters`]: datafusion_physical_plan::ExecutionPlan::try_pushdown_filters + fn try_pushdown_filters( + &self, + fd: FilterDescription, + _config: &ConfigOptions, + ) -> Result>> { + Ok(filter_pushdown_not_supported(fd)) + } } diff --git a/datafusion/datasource/src/file_groups.rs b/datafusion/datasource/src/file_groups.rs index 5fe3e25eaa1fe..15c86427ed00a 100644 --- a/datafusion/datasource/src/file_groups.rs +++ b/datafusion/datasource/src/file_groups.rs @@ -25,6 +25,7 @@ use std::collections::BinaryHeap; use std::iter::repeat_with; use std::mem; use std::ops::{Index, IndexMut}; +use std::sync::Arc; /// Repartition input files into `target_partitions` partitions, if total file size exceed /// `repartition_file_min_size` @@ -223,10 +224,11 @@ impl FileGroupPartitioner { return None; } - let target_partition_size = (total_size as usize).div_ceil(target_partitions); + let target_partition_size = + (total_size as u64).div_ceil(target_partitions as u64); let current_partition_index: usize = 0; - let current_partition_size: usize = 0; + let current_partition_size: u64 = 0; // Partition byte range evenly for all `PartitionedFile`s let repartitioned_files = flattened_files @@ -368,7 +370,7 @@ pub struct FileGroup { /// The files in this group files: Vec, /// Optional statistics for the data across all files in the group - statistics: Option, + statistics: Option>, } impl FileGroup { @@ -386,7 +388,7 @@ impl FileGroup { } /// Set the statistics for this group - pub fn with_statistics(mut self, statistics: Statistics) -> Self { + pub fn with_statistics(mut self, statistics: Arc) -> Self { self.statistics = Some(statistics); self } @@ -418,6 +420,11 @@ impl FileGroup { self.files.push(file); } + /// Get the statistics for this group + pub fn statistics(&self) -> Option<&Statistics> { + self.statistics.as_deref() + } + /// Partition the list of files into `n` groups pub fn split_files(mut self, n: usize) -> Vec { if self.is_empty() { @@ -491,15 +498,15 @@ struct ToRepartition { /// the index from which the original file will be taken source_index: usize, /// the size of the original file - file_size: usize, + file_size: u64, /// indexes of which group(s) will this be distributed to (including `source_index`) new_groups: Vec, } impl ToRepartition { - // how big will each file range be when this file is read in its new groups? - fn range_size(&self) -> usize { - self.file_size / self.new_groups.len() + /// How big will each file range be when this file is read in its new groups? + fn range_size(&self) -> u64 { + self.file_size / (self.new_groups.len() as u64) } } diff --git a/datafusion/datasource/src/file_scan_config.rs b/datafusion/datasource/src/file_scan_config.rs index 5172dafb1f91e..fb756cc11fbbc 100644 --- a/datafusion/datasource/src/file_scan_config.rs +++ b/datafusion/datasource/src/file_scan_config.rs @@ -23,6 +23,16 @@ use std::{ fmt::Result as FmtResult, marker::PhantomData, sync::Arc, }; +use crate::file_groups::FileGroup; +use crate::{ + display::FileGroupsDisplay, + file::FileSource, + file_compression_type::FileCompressionType, + file_stream::FileStream, + source::{DataSource, DataSourceExec}, + statistics::MinMaxStatistics, + PartitionedFile, +}; use arrow::{ array::{ ArrayData, ArrayRef, BufferBuilder, DictionaryArray, RecordBatch, @@ -31,7 +41,9 @@ use arrow::{ buffer::Buffer, datatypes::{ArrowNativeType, DataType, Field, Schema, SchemaRef, UInt16Type}, }; -use datafusion_common::{exec_err, ColumnStatistics, Constraints, Result, Statistics}; +use datafusion_common::{ + config::ConfigOptions, exec_err, ColumnStatistics, Constraints, Result, Statistics, +}; use datafusion_common::{DataFusionError, ScalarValue}; use datafusion_execution::{ object_store::ObjectStoreUrl, SendableRecordBatchStream, TaskContext, @@ -40,6 +52,10 @@ use datafusion_physical_expr::{ expressions::Column, EquivalenceProperties, LexOrdering, Partitioning, PhysicalSortExpr, }; +use datafusion_physical_plan::filter_pushdown::{ + filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, + FilterPushdownSupport, +}; use datafusion_physical_plan::{ display::{display_orderings, ProjectSchemaDisplay}, metrics::ExecutionPlanMetricsSet, @@ -48,17 +64,6 @@ use datafusion_physical_plan::{ }; use log::{debug, warn}; -use crate::file_groups::FileGroup; -use crate::{ - display::FileGroupsDisplay, - file::FileSource, - file_compression_type::FileCompressionType, - file_stream::FileStream, - source::{DataSource, DataSourceExec}, - statistics::MinMaxStatistics, - PartitionedFile, -}; - /// The base configurations for a [`DataSourceExec`], the a physical plan for /// any given file format. /// @@ -138,6 +143,9 @@ pub struct FileScanConfig { /// Schema before `projection` is applied. It contains the all columns that may /// appear in the files. It does not include table partition columns /// that may be added. + /// Note that this is **not** the schema of the physical files. + /// This is the schema that the physical file schema will be + /// mapped onto, and the schema that the [`DataSourceExec`] will return. pub file_schema: SchemaRef, /// List of files to be processed, grouped into partitions /// @@ -151,9 +159,6 @@ pub struct FileScanConfig { pub file_groups: Vec, /// Table constraints pub constraints: Constraints, - /// Estimated overall statistics of the files, taking `filters` into account. - /// Defaults to [`Statistics::new_unknown`]. - pub statistics: Statistics, /// Columns on which to project the data. Indexes that are higher than the /// number of columns of `file_schema` refer to `table_partition_cols`. pub projection: Option>, @@ -227,6 +232,10 @@ pub struct FileScanConfig { #[derive(Clone)] pub struct FileScanConfigBuilder { object_store_url: ObjectStoreUrl, + /// Table schema before any projections or partition columns are applied. + /// This schema is used to read the files, but is **not** necessarily the schema of the physical files. + /// Rather this is the schema that the physical file schema will be mapped onto, and the schema that the + /// [`DataSourceExec`] will return. file_schema: SchemaRef, file_source: Arc, @@ -412,7 +421,6 @@ impl FileScanConfigBuilder { table_partition_cols, constraints, file_groups, - statistics, output_ordering, file_compression_type, new_lines_in_values, @@ -426,9 +434,9 @@ impl From for FileScanConfigBuilder { Self { object_store_url: config.object_store_url, file_schema: config.file_schema, - file_source: config.file_source, + file_source: Arc::::clone(&config.file_source), file_groups: config.file_groups, - statistics: Some(config.statistics), + statistics: config.file_source.statistics().ok(), output_ordering: config.output_ordering, file_compression_type: Some(config.file_compression_type), new_lines_in_values: Some(config.new_lines_in_values), @@ -471,7 +479,8 @@ impl DataSource for FileScanConfig { fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult { match t { DisplayFormatType::Default | DisplayFormatType::Verbose => { - let (schema, _, _, orderings) = self.project(); + let schema = self.projected_schema(); + let orderings = get_projected_output_ordering(self, &schema); write!(f, "file_groups=")?; FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?; @@ -584,6 +593,46 @@ impl DataSource for FileScanConfig { ) as _ })) } + + fn try_pushdown_filters( + &self, + fd: FilterDescription, + config: &ConfigOptions, + ) -> Result>> { + let FilterPushdownResult { + support, + remaining_description, + } = self.file_source.try_pushdown_filters(fd, config)?; + + match support { + FilterPushdownSupport::Supported { + child_descriptions, + op, + revisit, + } => { + let new_data_source = Arc::new( + FileScanConfigBuilder::from(self.clone()) + .with_source(op) + .build(), + ); + + debug_assert!(child_descriptions.is_empty()); + debug_assert!(!revisit); + + Ok(FilterPushdownResult { + support: FilterPushdownSupport::Supported { + child_descriptions, + op: new_data_source, + revisit, + }, + remaining_description, + }) + } + FilterPushdownSupport::NotSupported => { + Ok(filter_pushdown_not_supported(remaining_description)) + } + } + } } impl FileScanConfig { @@ -610,7 +659,6 @@ impl FileScanConfig { file_schema, file_groups: vec![], constraints: Constraints::empty(), - statistics, projection: None, limit: None, table_partition_cols: vec![], @@ -625,7 +673,8 @@ impl FileScanConfig { /// Set the file source #[deprecated(since = "47.0.0", note = "use FileScanConfigBuilder instead")] pub fn with_source(mut self, file_source: Arc) -> Self { - self.file_source = file_source.with_statistics(self.statistics.clone()); + self.file_source = + file_source.with_statistics(Statistics::new_unknown(&self.file_schema)); self } @@ -639,7 +688,6 @@ impl FileScanConfig { /// Set the statistics of the files #[deprecated(since = "47.0.0", note = "use FileScanConfigBuilder instead")] pub fn with_statistics(mut self, statistics: Statistics) -> Self { - self.statistics = statistics.clone(); self.file_source = self.file_source.with_statistics(statistics); self } @@ -653,11 +701,8 @@ impl FileScanConfig { } } - fn projected_stats(&self) -> Statistics { - let statistics = self - .file_source - .statistics() - .unwrap_or(self.statistics.clone()); + pub fn projected_stats(&self) -> Statistics { + let statistics = self.file_source.statistics().unwrap(); let table_cols_stats = self .projection_indices() @@ -680,7 +725,7 @@ impl FileScanConfig { } } - fn projected_schema(&self) -> Arc { + pub fn projected_schema(&self) -> Arc { let table_fields: Vec<_> = self .projection_indices() .into_iter() @@ -700,7 +745,7 @@ impl FileScanConfig { )) } - fn projected_constraints(&self) -> Constraints { + pub fn projected_constraints(&self) -> Constraints { let indexes = self.projection_indices(); self.constraints @@ -804,7 +849,7 @@ impl FileScanConfig { return ( Arc::clone(&self.file_schema), self.constraints.clone(), - self.statistics.clone(), + self.file_source.statistics().unwrap().clone(), self.output_ordering.clone(), ); } @@ -858,6 +903,96 @@ impl FileScanConfig { }) } + /// Splits file groups into new groups based on statistics to enable efficient parallel processing. + /// + /// The method distributes files across a target number of partitions while ensuring + /// files within each partition maintain sort order based on their min/max statistics. + /// + /// The algorithm works by: + /// 1. Takes files sorted by minimum values + /// 2. For each file: + /// - Finds eligible groups (empty or where file's min > group's last max) + /// - Selects the smallest eligible group + /// - Creates a new group if needed + /// + /// # Parameters + /// * `table_schema`: Schema containing information about the columns + /// * `file_groups`: The original file groups to split + /// * `sort_order`: The lexicographical ordering to maintain within each group + /// * `target_partitions`: The desired number of output partitions + /// + /// # Returns + /// A new set of file groups, where files within each group are non-overlapping with respect to + /// their min/max statistics and maintain the specified sort order. + pub fn split_groups_by_statistics_with_target_partitions( + table_schema: &SchemaRef, + file_groups: &[FileGroup], + sort_order: &LexOrdering, + target_partitions: usize, + ) -> Result> { + if target_partitions == 0 { + return Err(DataFusionError::Internal( + "target_partitions must be greater than 0".to_string(), + )); + } + + let flattened_files = file_groups + .iter() + .flat_map(FileGroup::iter) + .collect::>(); + + if flattened_files.is_empty() { + return Ok(vec![]); + } + + let statistics = MinMaxStatistics::new_from_files( + sort_order, + table_schema, + None, + flattened_files.iter().copied(), + )?; + + let indices_sorted_by_min = statistics.min_values_sorted(); + + // Initialize with target_partitions empty groups + let mut file_groups_indices: Vec> = vec![vec![]; target_partitions]; + + for (idx, min) in indices_sorted_by_min { + if let Some((_, group)) = file_groups_indices + .iter_mut() + .enumerate() + .filter(|(_, group)| { + group.is_empty() + || min + > statistics + .max(*group.last().expect("groups should not be empty")) + }) + .min_by_key(|(_, group)| group.len()) + { + group.push(idx); + } else { + // Create a new group if no existing group fits + file_groups_indices.push(vec![idx]); + } + } + + // Remove any empty groups + file_groups_indices.retain(|group| !group.is_empty()); + + // Assemble indices back into groups of PartitionedFiles + Ok(file_groups_indices + .into_iter() + .map(|file_group_indices| { + FileGroup::new( + file_group_indices + .into_iter() + .map(|idx| flattened_files[idx].clone()) + .collect(), + ) + }) + .collect()) + } + /// Attempts to do a bin-packing on files into file groups, such that any two files /// in a file group are ordered and non-overlapping with respect to their statistics. /// It will produce the smallest number of file groups possible. @@ -949,7 +1084,11 @@ impl Debug for FileScanConfig { write!(f, "FileScanConfig {{")?; write!(f, "object_store_url={:?}, ", self.object_store_url)?; - write!(f, "statistics={:?}, ", self.statistics)?; + write!( + f, + "statistics={:?}, ", + self.file_source.statistics().unwrap() + )?; DisplayAs::fmt_as(self, DisplayFormatType::Verbose, f)?; write!(f, "}}") @@ -958,7 +1097,8 @@ impl Debug for FileScanConfig { impl DisplayAs for FileScanConfig { fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult { - let (schema, _, _, orderings) = self.project(); + let schema = self.projected_schema(); + let orderings = get_projected_output_ordering(self, &schema); write!(f, "file_groups=")?; FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?; @@ -1377,7 +1517,10 @@ pub fn wrap_partition_value_in_dict(val: ScalarValue) -> ScalarValue { #[cfg(test)] mod tests { - use crate::{test_util::MockSource, tests::aggr_test_schema}; + use crate::{ + generate_test_files, test_util::MockSource, tests::aggr_test_schema, + verify_sort_integrity, + }; use super::*; use arrow::{ @@ -1468,7 +1611,7 @@ mod tests { ); // verify the proj_schema includes the last column and exactly the same the field it is defined - let (proj_schema, _, _, _) = conf.project(); + let proj_schema = conf.projected_schema(); assert_eq!(proj_schema.fields().len(), file_schema.fields().len() + 1); assert_eq!( *proj_schema.field(file_schema.fields().len()), @@ -1574,7 +1717,7 @@ mod tests { assert_eq!(source_statistics, statistics); assert_eq!(source_statistics.column_statistics.len(), 3); - let (proj_schema, ..) = conf.project(); + let proj_schema = conf.projected_schema(); // created a projector for that projected schema let mut proj = PartitionColumnProjector::new( proj_schema, @@ -2000,7 +2143,7 @@ mod tests { }, partition_values: vec![ScalarValue::from(file.date)], range: None, - statistics: Some(Statistics { + statistics: Some(Arc::new(Statistics { num_rows: Precision::Absent, total_byte_size: Precision::Absent, column_statistics: file @@ -2020,7 +2163,7 @@ mod tests { .unwrap_or_default() }) .collect::>(), - }), + })), extensions: None, metadata_size_hint: None, } @@ -2161,13 +2304,24 @@ mod tests { assert!(config.constraints.is_empty()); // Verify statistics are set to unknown - assert_eq!(config.statistics.num_rows, Precision::Absent); - assert_eq!(config.statistics.total_byte_size, Precision::Absent); assert_eq!( - config.statistics.column_statistics.len(), + config.file_source.statistics().unwrap().num_rows, + Precision::Absent + ); + assert_eq!( + config.file_source.statistics().unwrap().total_byte_size, + Precision::Absent + ); + assert_eq!( + config + .file_source + .statistics() + .unwrap() + .column_statistics + .len(), file_schema.fields().len() ); - for stat in config.statistics.column_statistics { + for stat in config.file_source.statistics().unwrap().column_statistics { assert_eq!(stat.distinct_count, Precision::Absent); assert_eq!(stat.min_value, Precision::Absent); assert_eq!(stat.max_value, Precision::Absent); @@ -2222,4 +2376,163 @@ mod tests { assert_eq!(new_config.constraints, Constraints::default()); assert!(new_config.new_lines_in_values); } + + #[test] + fn test_split_groups_by_statistics_with_target_partitions() -> Result<()> { + use datafusion_common::DFSchema; + use datafusion_expr::{col, execution_props::ExecutionProps}; + + let schema = Arc::new(Schema::new(vec![Field::new( + "value", + DataType::Float64, + false, + )])); + + // Setup sort expression + let exec_props = ExecutionProps::new(); + let df_schema = DFSchema::try_from_qualified_schema("test", schema.as_ref())?; + let sort_expr = vec![col("value").sort(true, false)]; + + let physical_sort_exprs: Vec<_> = sort_expr + .iter() + .map(|expr| create_physical_sort_expr(expr, &df_schema, &exec_props).unwrap()) + .collect(); + + let sort_ordering = LexOrdering::from(physical_sort_exprs); + + // Test case parameters + struct TestCase { + name: String, + file_count: usize, + overlap_factor: f64, + target_partitions: usize, + expected_partition_count: usize, + } + + let test_cases = vec![ + // Basic cases + TestCase { + name: "no_overlap_10_files_4_partitions".to_string(), + file_count: 10, + overlap_factor: 0.0, + target_partitions: 4, + expected_partition_count: 4, + }, + TestCase { + name: "medium_overlap_20_files_5_partitions".to_string(), + file_count: 20, + overlap_factor: 0.5, + target_partitions: 5, + expected_partition_count: 5, + }, + TestCase { + name: "high_overlap_30_files_3_partitions".to_string(), + file_count: 30, + overlap_factor: 0.8, + target_partitions: 3, + expected_partition_count: 7, + }, + // Edge cases + TestCase { + name: "fewer_files_than_partitions".to_string(), + file_count: 3, + overlap_factor: 0.0, + target_partitions: 10, + expected_partition_count: 3, // Should only create as many partitions as files + }, + TestCase { + name: "single_file".to_string(), + file_count: 1, + overlap_factor: 0.0, + target_partitions: 5, + expected_partition_count: 1, // Should create only one partition + }, + TestCase { + name: "empty_files".to_string(), + file_count: 0, + overlap_factor: 0.0, + target_partitions: 3, + expected_partition_count: 0, // Empty result for empty input + }, + ]; + + for case in test_cases { + println!("Running test case: {}", case.name); + + // Generate files using bench utility function + let file_groups = generate_test_files(case.file_count, case.overlap_factor); + + // Call the function under test + let result = + FileScanConfig::split_groups_by_statistics_with_target_partitions( + &schema, + &file_groups, + &sort_ordering, + case.target_partitions, + )?; + + // Verify results + println!( + "Created {} partitions (target was {})", + result.len(), + case.target_partitions + ); + + // Check partition count + assert_eq!( + result.len(), + case.expected_partition_count, + "Case '{}': Unexpected partition count", + case.name + ); + + // Verify sort integrity + assert!( + verify_sort_integrity(&result), + "Case '{}': Files within partitions are not properly ordered", + case.name + ); + + // Distribution check for partitions + if case.file_count > 1 && case.expected_partition_count > 1 { + let group_sizes: Vec = result.iter().map(FileGroup::len).collect(); + let max_size = *group_sizes.iter().max().unwrap(); + let min_size = *group_sizes.iter().min().unwrap(); + + // Check partition balancing - difference shouldn't be extreme + let avg_files_per_partition = + case.file_count as f64 / case.expected_partition_count as f64; + assert!( + (max_size as f64) < 2.0 * avg_files_per_partition, + "Case '{}': Unbalanced distribution. Max partition size {} exceeds twice the average {}", + case.name, + max_size, + avg_files_per_partition + ); + + println!( + "Distribution - min files: {}, max files: {}", + min_size, max_size + ); + } + } + + // Test error case: zero target partitions + let empty_groups: Vec = vec![]; + let err = FileScanConfig::split_groups_by_statistics_with_target_partitions( + &schema, + &empty_groups, + &sort_ordering, + 0, + ) + .unwrap_err(); + + assert!( + err.to_string() + .contains("target_partitions must be greater than 0"), + "Expected error for zero target partitions" + ); + + Ok(()) + } } diff --git a/datafusion/datasource/src/file_sink_config.rs b/datafusion/datasource/src/file_sink_config.rs index 465167fea9546..2968bd1ee0449 100644 --- a/datafusion/datasource/src/file_sink_config.rs +++ b/datafusion/datasource/src/file_sink_config.rs @@ -89,6 +89,7 @@ pub trait FileSink: DataSink { /// The base configurations to provide when creating a physical plan for /// writing to any given file format. +#[derive(Debug, Clone)] pub struct FileSinkConfig { /// The unresolved URL specified by the user pub original_url: String, diff --git a/datafusion/datasource/src/file_stream.rs b/datafusion/datasource/src/file_stream.rs index 1caefc3277aca..1dc53bd6b9319 100644 --- a/datafusion/datasource/src/file_stream.rs +++ b/datafusion/datasource/src/file_stream.rs @@ -78,7 +78,7 @@ impl FileStream { file_opener: Arc, metrics: &ExecutionPlanMetricsSet, ) -> Result { - let (projected_schema, ..) = config.project(); + let projected_schema = config.projected_schema(); let pc_projector = PartitionColumnProjector::new( Arc::clone(&projected_schema), &config diff --git a/datafusion/datasource/src/memory.rs b/datafusion/datasource/src/memory.rs index f2e36672cd5c9..6d0e16ef4b916 100644 --- a/datafusion/datasource/src/memory.rs +++ b/datafusion/datasource/src/memory.rs @@ -19,9 +19,12 @@ use std::any::Any; use std::fmt; +use std::fmt::Debug; use std::sync::Arc; +use crate::sink::DataSink; use crate::source::{DataSource, DataSourceExec}; +use async_trait::async_trait; use datafusion_physical_plan::execution_plan::{Boundedness, EmissionType}; use datafusion_physical_plan::memory::MemoryStream; use datafusion_physical_plan::projection::{ @@ -42,6 +45,8 @@ use datafusion_physical_expr::equivalence::ProjectionMapping; use datafusion_physical_expr::expressions::Column; use datafusion_physical_expr::utils::collect_columns; use datafusion_physical_expr::{EquivalenceProperties, LexOrdering}; +use futures::StreamExt; +use tokio::sync::RwLock; /// Execution plan for reading in-memory batches of data #[derive(Clone)] @@ -62,7 +67,7 @@ pub struct MemoryExec { } #[allow(unused, deprecated)] -impl fmt::Debug for MemoryExec { +impl Debug for MemoryExec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt_as(DisplayFormatType::Default, f) } @@ -720,6 +725,91 @@ impl MemorySourceConfig { } } +/// Type alias for partition data +pub type PartitionData = Arc>>; + +/// Implements for writing to a [`MemTable`] +/// +/// [`MemTable`]: +pub struct MemSink { + /// Target locations for writing data + batches: Vec, + schema: SchemaRef, +} + +impl Debug for MemSink { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MemSink") + .field("num_partitions", &self.batches.len()) + .finish() + } +} + +impl DisplayAs for MemSink { + fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + let partition_count = self.batches.len(); + write!(f, "MemoryTable (partitions={partition_count})") + } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } + } + } +} + +impl MemSink { + /// Creates a new [`MemSink`]. + /// + /// The caller is responsible for ensuring that there is at least one partition to insert into. + pub fn try_new(batches: Vec, schema: SchemaRef) -> Result { + if batches.is_empty() { + return plan_err!("Cannot insert into MemTable with zero partitions"); + } + Ok(Self { batches, schema }) + } +} + +#[async_trait] +impl DataSink for MemSink { + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> &SchemaRef { + &self.schema + } + + async fn write_all( + &self, + mut data: SendableRecordBatchStream, + _context: &Arc, + ) -> Result { + let num_partitions = self.batches.len(); + + // buffer up the data round robin style into num_partitions + + let mut new_batches = vec![vec![]; num_partitions]; + let mut i = 0; + let mut row_count = 0; + while let Some(batch) = data.next().await.transpose()? { + row_count += batch.num_rows(); + new_batches[i].push(batch); + i = (i + 1) % num_partitions; + } + + // write the outputs into the batches + for (target, mut batches) in self.batches.iter().zip(new_batches.into_iter()) { + // Append all the new batches in one go to minimize locking overhead + target.write().await.append(&mut batches); + } + + Ok(row_count as u64) + } +} + #[cfg(test)] mod memory_source_tests { use std::sync::Arc; diff --git a/datafusion/datasource/src/mod.rs b/datafusion/datasource/src/mod.rs index fb119d1b3d2db..3e44851d145b8 100644 --- a/datafusion/datasource/src/mod.rs +++ b/datafusion/datasource/src/mod.rs @@ -44,23 +44,28 @@ pub mod source; mod statistics; #[cfg(test)] -mod test_util; +pub mod test_util; pub mod url; pub mod write; +pub use self::url::ListingTableUrl; +use crate::file_groups::FileGroup; use chrono::TimeZone; -use datafusion_common::Result; +use datafusion_common::stats::Precision; +use datafusion_common::{exec_datafusion_err, ColumnStatistics, Result}; use datafusion_common::{ScalarValue, Statistics}; use file_meta::FileMeta; use futures::{Stream, StreamExt}; use object_store::{path::Path, ObjectMeta}; use object_store::{GetOptions, GetRange, ObjectStore}; +// Remove when add_row_stats is remove +#[allow(deprecated)] +pub use statistics::add_row_stats; +pub use statistics::compute_all_files_statistics; use std::ops::Range; use std::pin::Pin; use std::sync::Arc; -pub use self::url::ListingTableUrl; - /// Stream of files get listed from object store pub type PartitionedFileStream = Pin> + Send + Sync + 'static>>; @@ -106,7 +111,7 @@ pub struct PartitionedFile { /// /// DataFusion relies on these statistics for planning (in particular to sort file groups), /// so if they are incorrect, incorrect answers may result. - pub statistics: Option, + pub statistics: Option>, /// An optional field for user defined per object metadata pub extensions: Option>, /// The estimated size of the parquet metadata, in bytes @@ -120,7 +125,7 @@ impl PartitionedFile { object_meta: ObjectMeta { location: Path::from(path.into()), last_modified: chrono::Utc.timestamp_nanos(0), - size: size as usize, + size, e_tag: None, version: None, }, @@ -138,7 +143,7 @@ impl PartitionedFile { object_meta: ObjectMeta { location: Path::from(path), last_modified: chrono::Utc.timestamp_nanos(0), - size: size as usize, + size, e_tag: None, version: None, }, @@ -186,6 +191,12 @@ impl PartitionedFile { self.extensions = Some(extensions); self } + + // Update the statistics for this file. + pub fn with_statistics(mut self, statistics: Arc) -> Self { + self.statistics = Some(statistics); + self + } } impl From for PartitionedFile { @@ -215,7 +226,7 @@ impl From for PartitionedFile { /// Indicates that the range calculation determined no further action is /// necessary, possibly because the calculated range is empty or invalid. pub enum RangeCalculation { - Range(Option>), + Range(Option>), TerminateEarly, } @@ -241,7 +252,12 @@ pub async fn calculate_range( match file_meta.range { None => Ok(RangeCalculation::Range(None)), Some(FileRange { start, end }) => { - let (start, end) = (start as usize, end as usize); + let start: u64 = start.try_into().map_err(|_| { + exec_datafusion_err!("Expect start range to fit in u64, got {start}") + })?; + let end: u64 = end.try_into().map_err(|_| { + exec_datafusion_err!("Expect end range to fit in u64, got {end}") + })?; let start_delta = if start != 0 { find_first_newline(store, location, start - 1, file_size, newline).await? @@ -280,10 +296,10 @@ pub async fn calculate_range( async fn find_first_newline( object_store: &Arc, location: &Path, - start: usize, - end: usize, + start: u64, + end: u64, newline: u8, -) -> Result { +) -> Result { let options = GetOptions { range: Some(GetRange::Bounded(start..end)), ..Default::default() @@ -296,15 +312,125 @@ async fn find_first_newline( while let Some(chunk) = result_stream.next().await.transpose()? { if let Some(position) = chunk.iter().position(|&byte| byte == newline) { + let position = position as u64; return Ok(index + position); } - index += chunk.len(); + index += chunk.len() as u64; } Ok(index) } +/// Generates test files with min-max statistics in different overlap patterns. +/// +/// Used by tests and benchmarks. +/// +/// # Overlap Factors +/// +/// The `overlap_factor` parameter controls how much the value ranges in generated test files overlap: +/// - `0.0`: No overlap between files (completely disjoint ranges) +/// - `0.2`: Low overlap (20% of the range size overlaps with adjacent files) +/// - `0.5`: Medium overlap (50% of ranges overlap) +/// - `0.8`: High overlap (80% of ranges overlap between files) +/// +/// # Examples +/// +/// With 5 files and different overlap factors showing `[min, max]` ranges: +/// +/// overlap_factor = 0.0 (no overlap): +/// +/// File 0: [0, 20] +/// File 1: [20, 40] +/// File 2: [40, 60] +/// File 3: [60, 80] +/// File 4: [80, 100] +/// +/// overlap_factor = 0.5 (50% overlap): +/// +/// File 0: [0, 40] +/// File 1: [20, 60] +/// File 2: [40, 80] +/// File 3: [60, 100] +/// File 4: [80, 120] +/// +/// overlap_factor = 0.8 (80% overlap): +/// +/// File 0: [0, 100] +/// File 1: [20, 120] +/// File 2: [40, 140] +/// File 3: [60, 160] +/// File 4: [80, 180] +pub fn generate_test_files(num_files: usize, overlap_factor: f64) -> Vec { + let mut files = Vec::with_capacity(num_files); + if num_files == 0 { + return vec![]; + } + let range_size = if overlap_factor == 0.0 { + 100 / num_files as i64 + } else { + (100.0 / (overlap_factor * num_files as f64)).max(1.0) as i64 + }; + + for i in 0..num_files { + let base = (i as f64 * range_size as f64 * (1.0 - overlap_factor)) as i64; + let min = base as f64; + let max = (base + range_size) as f64; + + let file = PartitionedFile { + object_meta: ObjectMeta { + location: Path::from(format!("file_{}.parquet", i)), + last_modified: chrono::Utc::now(), + size: 1000, + e_tag: None, + version: None, + }, + partition_values: vec![], + range: None, + statistics: Some(Arc::new(Statistics { + num_rows: Precision::Exact(100), + total_byte_size: Precision::Exact(1000), + column_statistics: vec![ColumnStatistics { + null_count: Precision::Exact(0), + max_value: Precision::Exact(ScalarValue::Float64(Some(max))), + min_value: Precision::Exact(ScalarValue::Float64(Some(min))), + sum_value: Precision::Absent, + distinct_count: Precision::Absent, + }], + })), + extensions: None, + metadata_size_hint: None, + }; + files.push(file); + } + + vec![FileGroup::new(files)] +} + +// Helper function to verify that files within each group maintain sort order +/// Used by tests and benchmarks +pub fn verify_sort_integrity(file_groups: &[FileGroup]) -> bool { + for group in file_groups { + let files = group.iter().collect::>(); + for i in 1..files.len() { + let prev_file = files[i - 1]; + let curr_file = files[i]; + + // Check if the min value of current file is greater than max value of previous file + if let (Some(prev_stats), Some(curr_stats)) = + (&prev_file.statistics, &curr_file.statistics) + { + let prev_max = &prev_stats.column_statistics[0].max_value; + let curr_min = &curr_stats.column_statistics[0].min_value; + if curr_min.get_value().unwrap() <= prev_max.get_value().unwrap() { + return false; + } + } + } + } + true +} + #[cfg(test)] mod tests { use super::ListingTableUrl; diff --git a/datafusion/datasource/src/schema_adapter.rs b/datafusion/datasource/src/schema_adapter.rs index 4164cda8cba11..eafddecd05f50 100644 --- a/datafusion/datasource/src/schema_adapter.rs +++ b/datafusion/datasource/src/schema_adapter.rs @@ -42,7 +42,7 @@ pub trait SchemaAdapterFactory: Debug + Send + Sync + 'static { /// Arguments: /// /// * `projected_table_schema`: The schema for the table, projected to - /// include only the fields being output (projected) by the this mapping. + /// include only the fields being output (projected) by the this mapping. /// /// * `table_schema`: The entire table schema for the table fn create( diff --git a/datafusion/datasource/src/source.rs b/datafusion/datasource/src/source.rs index 6c9122ce1ac10..2d6ea1a8b3915 100644 --- a/datafusion/datasource/src/source.rs +++ b/datafusion/datasource/src/source.rs @@ -31,10 +31,14 @@ use datafusion_physical_plan::{ use crate::file_scan_config::FileScanConfig; use datafusion_common::config::ConfigOptions; -use datafusion_common::{Constraints, Statistics}; +use datafusion_common::{Constraints, Result, Statistics}; use datafusion_execution::{SendableRecordBatchStream, TaskContext}; use datafusion_physical_expr::{EquivalenceProperties, Partitioning}; use datafusion_physical_expr_common::sort_expr::LexOrdering; +use datafusion_physical_plan::filter_pushdown::{ + filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, + FilterPushdownSupport, +}; /// Common behaviors in Data Sources for both from Files and Memory. /// @@ -51,7 +55,7 @@ pub trait DataSource: Send + Sync + Debug { &self, partition: usize, context: Arc, - ) -> datafusion_common::Result; + ) -> Result; fn as_any(&self) -> &dyn Any; /// Format this source for display in explain plans fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result; @@ -62,13 +66,13 @@ pub trait DataSource: Send + Sync + Debug { _target_partitions: usize, _repartition_file_min_size: usize, _output_ordering: Option, - ) -> datafusion_common::Result>> { + ) -> Result>> { Ok(None) } fn output_partitioning(&self) -> Partitioning; fn eq_properties(&self) -> EquivalenceProperties; - fn statistics(&self) -> datafusion_common::Result; + fn statistics(&self) -> Result; /// Return a copy of this DataSource with a new fetch limit fn with_fetch(&self, _limit: Option) -> Option>; fn fetch(&self) -> Option; @@ -78,7 +82,16 @@ pub trait DataSource: Send + Sync + Debug { fn try_swapping_with_projection( &self, _projection: &ProjectionExec, - ) -> datafusion_common::Result>>; + ) -> Result>>; + /// Try to push down filters into this DataSource. + /// See [`ExecutionPlan::try_pushdown_filters`] for more details. + fn try_pushdown_filters( + &self, + fd: FilterDescription, + _config: &ConfigOptions, + ) -> Result>> { + Ok(filter_pushdown_not_supported(fd)) + } } /// [`ExecutionPlan`] handles different file formats like JSON, CSV, AVRO, ARROW, PARQUET @@ -131,7 +144,7 @@ impl ExecutionPlan for DataSourceExec { fn with_new_children( self: Arc, _: Vec>, - ) -> datafusion_common::Result> { + ) -> Result> { Ok(self) } @@ -139,7 +152,7 @@ impl ExecutionPlan for DataSourceExec { &self, target_partitions: usize, config: &ConfigOptions, - ) -> datafusion_common::Result>> { + ) -> Result>> { let data_source = self.data_source.repartitioned( target_partitions, config.optimizer.repartition_file_min_size, @@ -163,7 +176,7 @@ impl ExecutionPlan for DataSourceExec { &self, partition: usize, context: Arc, - ) -> datafusion_common::Result { + ) -> Result { self.data_source.open(partition, context) } @@ -171,7 +184,7 @@ impl ExecutionPlan for DataSourceExec { Some(self.data_source.metrics().clone_inner()) } - fn statistics(&self) -> datafusion_common::Result { + fn statistics(&self) -> Result { self.data_source.statistics() } @@ -189,9 +202,45 @@ impl ExecutionPlan for DataSourceExec { fn try_swapping_with_projection( &self, projection: &ProjectionExec, - ) -> datafusion_common::Result>> { + ) -> Result>> { self.data_source.try_swapping_with_projection(projection) } + + fn try_pushdown_filters( + &self, + fd: FilterDescription, + config: &ConfigOptions, + ) -> Result>> { + let FilterPushdownResult { + support, + remaining_description, + } = self.data_source.try_pushdown_filters(fd, config)?; + + match support { + FilterPushdownSupport::Supported { + child_descriptions, + op, + revisit, + } => { + let new_exec = Arc::new(DataSourceExec::new(op)); + + debug_assert!(child_descriptions.is_empty()); + debug_assert!(!revisit); + + Ok(FilterPushdownResult { + support: FilterPushdownSupport::Supported { + child_descriptions, + op: new_exec, + revisit, + }, + remaining_description, + }) + } + FilterPushdownSupport::NotSupported => { + Ok(filter_pushdown_not_supported(remaining_description)) + } + } + } } impl DataSourceExec { @@ -254,3 +303,13 @@ impl DataSourceExec { }) } } + +/// Create a new `DataSourceExec` from a `DataSource` +impl From for DataSourceExec +where + S: DataSource + 'static, +{ + fn from(source: S) -> Self { + Self::new(Arc::new(source)) + } +} diff --git a/datafusion/datasource/src/statistics.rs b/datafusion/datasource/src/statistics.rs index cd002a96683a5..8a04d77b273d4 100644 --- a/datafusion/datasource/src/statistics.rs +++ b/datafusion/datasource/src/statistics.rs @@ -20,8 +20,10 @@ //! Currently, this module houses code to sort file groups if they are non-overlapping with //! respect to the required sort order. See [`MinMaxStatistics`] +use futures::{Stream, StreamExt}; use std::sync::Arc; +use crate::file_groups::FileGroup; use crate::PartitionedFile; use arrow::array::RecordBatch; @@ -30,9 +32,11 @@ use arrow::{ compute::SortColumn, row::{Row, Rows}, }; +use datafusion_common::stats::Precision; use datafusion_common::{plan_datafusion_err, plan_err, DataFusionError, Result}; use datafusion_physical_expr::{expressions::Column, PhysicalSortExpr}; use datafusion_physical_expr_common::sort_expr::LexOrdering; +use datafusion_physical_plan::{ColumnStatistics, Statistics}; /// A normalized representation of file min/max statistics that allows for efficient sorting & comparison. /// The min/max values are ordered by [`Self::sort_order`]. @@ -281,3 +285,213 @@ fn sort_columns_from_physical_sort_exprs( .map(|expr| expr.expr.as_any().downcast_ref::()) .collect::>>() } + +/// Get all files as well as the file level summary statistics (no statistic for partition columns). +/// If the optional `limit` is provided, includes only sufficient files. Needed to read up to +/// `limit` number of rows. `collect_stats` is passed down from the configuration parameter on +/// `ListingTable`. If it is false we only construct bare statistics and skip a potentially expensive +/// call to `multiunzip` for constructing file level summary statistics. +#[deprecated( + since = "47.0.0", + note = "Please use `get_files_with_limit` and `compute_all_files_statistics` instead" +)] +#[allow(unused)] +pub async fn get_statistics_with_limit( + all_files: impl Stream)>>, + file_schema: SchemaRef, + limit: Option, + collect_stats: bool, +) -> Result<(FileGroup, Statistics)> { + let mut result_files = FileGroup::default(); + // These statistics can be calculated as long as at least one file provides + // useful information. If none of the files provides any information, then + // they will end up having `Precision::Absent` values. Throughout calculations, + // missing values will be imputed as: + // - zero for summations, and + // - neutral element for extreme points. + let size = file_schema.fields().len(); + let mut col_stats_set = vec![ColumnStatistics::default(); size]; + let mut num_rows = Precision::::Absent; + let mut total_byte_size = Precision::::Absent; + + // Fusing the stream allows us to call next safely even once it is finished. + let mut all_files = Box::pin(all_files.fuse()); + + if let Some(first_file) = all_files.next().await { + let (mut file, file_stats) = first_file?; + file.statistics = Some(Arc::clone(&file_stats)); + result_files.push(file); + + // First file, we set them directly from the file statistics. + num_rows = file_stats.num_rows; + total_byte_size = file_stats.total_byte_size; + for (index, file_column) in + file_stats.column_statistics.clone().into_iter().enumerate() + { + col_stats_set[index].null_count = file_column.null_count; + col_stats_set[index].max_value = file_column.max_value; + col_stats_set[index].min_value = file_column.min_value; + col_stats_set[index].sum_value = file_column.sum_value; + } + + // If the number of rows exceeds the limit, we can stop processing + // files. This only applies when we know the number of rows. It also + // currently ignores tables that have no statistics regarding the + // number of rows. + let conservative_num_rows = match num_rows { + Precision::Exact(nr) => nr, + _ => usize::MIN, + }; + if conservative_num_rows <= limit.unwrap_or(usize::MAX) { + while let Some(current) = all_files.next().await { + let (mut file, file_stats) = current?; + file.statistics = Some(Arc::clone(&file_stats)); + result_files.push(file); + if !collect_stats { + continue; + } + + // We accumulate the number of rows, total byte size and null + // counts across all the files in question. If any file does not + // provide any information or provides an inexact value, we demote + // the statistic precision to inexact. + num_rows = num_rows.add(&file_stats.num_rows); + + total_byte_size = total_byte_size.add(&file_stats.total_byte_size); + + for (file_col_stats, col_stats) in file_stats + .column_statistics + .iter() + .zip(col_stats_set.iter_mut()) + { + let ColumnStatistics { + null_count: file_nc, + max_value: file_max, + min_value: file_min, + sum_value: file_sum, + distinct_count: _, + } = file_col_stats; + + col_stats.null_count = col_stats.null_count.add(file_nc); + col_stats.max_value = col_stats.max_value.max(file_max); + col_stats.min_value = col_stats.min_value.min(file_min); + col_stats.sum_value = col_stats.sum_value.add(file_sum); + } + + // If the number of rows exceeds the limit, we can stop processing + // files. This only applies when we know the number of rows. It also + // currently ignores tables that have no statistics regarding the + // number of rows. + if num_rows.get_value().unwrap_or(&usize::MIN) + > &limit.unwrap_or(usize::MAX) + { + break; + } + } + } + }; + + let mut statistics = Statistics { + num_rows, + total_byte_size, + column_statistics: col_stats_set, + }; + if all_files.next().await.is_some() { + // If we still have files in the stream, it means that the limit kicked + // in, and the statistic could have been different had we processed the + // files in a different order. + statistics = statistics.to_inexact() + } + + Ok((result_files, statistics)) +} + +/// Computes the summary statistics for a group of files(`FileGroup` level's statistics). +/// +/// This function combines statistics from all files in the file group to create +/// summary statistics. It handles the following aspects: +/// - Merges row counts and byte sizes across files +/// - Computes column-level statistics like min/max values +/// - Maintains appropriate precision information (exact, inexact, absent) +/// +/// # Parameters +/// * `file_group` - The group of files to process +/// * `file_schema` - Schema of the files +/// * `collect_stats` - Whether to collect statistics (if false, returns original file group) +/// +/// # Returns +/// A new file group with summary statistics attached +pub fn compute_file_group_statistics( + file_group: FileGroup, + file_schema: SchemaRef, + collect_stats: bool, +) -> Result { + if !collect_stats { + return Ok(file_group); + } + + let file_group_stats = file_group.iter().filter_map(|file| { + let stats = file.statistics.as_ref()?; + Some(stats.as_ref()) + }); + let statistics = Statistics::try_merge_iter(file_group_stats, &file_schema)?; + + Ok(file_group.with_statistics(Arc::new(statistics))) +} + +/// Computes statistics for all files across multiple file groups. +/// +/// This function: +/// 1. Computes statistics for each individual file group +/// 2. Summary statistics across all file groups +/// 3. Optionally marks statistics as inexact +/// +/// # Parameters +/// * `file_groups` - Vector of file groups to process +/// * `table_schema` - Schema of the table +/// * `collect_stats` - Whether to collect statistics +/// * `inexact_stats` - Whether to mark the resulting statistics as inexact +/// +/// # Returns +/// A tuple containing: +/// * The processed file groups with their individual statistics attached +/// * The summary statistics across all file groups, aka all files summary statistics +pub fn compute_all_files_statistics( + file_groups: Vec, + table_schema: SchemaRef, + collect_stats: bool, + inexact_stats: bool, +) -> Result<(Vec, Statistics)> { + let file_groups_with_stats = file_groups + .into_iter() + .map(|file_group| { + compute_file_group_statistics( + file_group, + Arc::clone(&table_schema), + collect_stats, + ) + }) + .collect::>>()?; + + // Then summary statistics across all file groups + let file_groups_statistics = file_groups_with_stats + .iter() + .filter_map(|file_group| file_group.statistics()); + + let mut statistics = + Statistics::try_merge_iter(file_groups_statistics, &table_schema)?; + + if inexact_stats { + statistics = statistics.to_inexact() + } + + Ok((file_groups_with_stats, statistics)) +} + +#[deprecated(since = "47.0.0", note = "Use Statistics::add")] +pub fn add_row_stats( + file_num_rows: Precision, + num_rows: Precision, +) -> Precision { + file_num_rows.add(&num_rows) +} diff --git a/datafusion/datasource/src/url.rs b/datafusion/datasource/src/url.rs index 2dbcfa2ef1fae..bddfdbcc06d13 100644 --- a/datafusion/datasource/src/url.rs +++ b/datafusion/datasource/src/url.rs @@ -209,10 +209,10 @@ impl ListingTableUrl { /// assert_eq!(url.file_extension(), None); /// ``` pub fn file_extension(&self) -> Option<&str> { - if let Some(segments) = self.url.path_segments() { - if let Some(last_segment) = segments.last() { + if let Some(mut segments) = self.url.path_segments() { + if let Some(last_segment) = segments.next_back() { if last_segment.contains(".") && !last_segment.ends_with(".") { - return last_segment.split('.').last(); + return last_segment.split('.').next_back(); } } } diff --git a/datafusion/datasource/src/write/demux.rs b/datafusion/datasource/src/write/demux.rs index fc2e5daf92b66..49c3a64d24aa8 100644 --- a/datafusion/datasource/src/write/demux.rs +++ b/datafusion/datasource/src/write/demux.rs @@ -28,8 +28,8 @@ use datafusion_common::error::Result; use datafusion_physical_plan::SendableRecordBatchStream; use arrow::array::{ - builder::UInt64Builder, cast::AsArray, downcast_dictionary_array, RecordBatch, - StringArray, StructArray, + builder::UInt64Builder, cast::AsArray, downcast_dictionary_array, ArrayAccessor, + RecordBatch, StringArray, StructArray, }; use arrow::datatypes::{DataType, Schema}; use datafusion_common::cast::{ @@ -482,10 +482,8 @@ fn compute_partition_keys_by_row<'a>( .ok_or(exec_datafusion_err!("it is not yet supported to write to hive partitions with datatype {}", dtype))?; - for val in array.values() { - partition_values.push( - Cow::from(val.ok_or(exec_datafusion_err!("Cannot partition by null value for column {}", col))?), - ); + for i in 0..rb.num_rows() { + partition_values.push(Cow::from(array.value(i))); } }, _ => unreachable!(), diff --git a/datafusion/execution/Cargo.toml b/datafusion/execution/Cargo.toml index 8f642f3384d2e..20e507e98b68a 100644 --- a/datafusion/execution/Cargo.toml +++ b/datafusion/execution/Cargo.toml @@ -44,7 +44,7 @@ datafusion-common = { workspace = true, default-features = true } datafusion-expr = { workspace = true } futures = { workspace = true } log = { workspace = true } -object_store = { workspace = true } +object_store = { workspace = true, features = ["fs"] } parking_lot = { workspace = true } rand = { workspace = true } tempfile = { workspace = true } diff --git a/datafusion/execution/src/config.rs b/datafusion/execution/src/config.rs index 53646dc5b468e..1e00a1ce4725e 100644 --- a/datafusion/execution/src/config.rs +++ b/datafusion/execution/src/config.rs @@ -193,9 +193,11 @@ impl SessionConfig { /// /// [`target_partitions`]: datafusion_common::config::ExecutionOptions::target_partitions pub fn with_target_partitions(mut self, n: usize) -> Self { - // partition count must be greater than zero - assert!(n > 0); - self.options.execution.target_partitions = n; + self.options.execution.target_partitions = if n == 0 { + datafusion_common::config::ExecutionOptions::default().target_partitions + } else { + n + }; self } diff --git a/datafusion/execution/src/disk_manager.rs b/datafusion/execution/src/disk_manager.rs index caa62eefe14c7..2b21a6dbf175f 100644 --- a/datafusion/execution/src/disk_manager.rs +++ b/datafusion/execution/src/disk_manager.rs @@ -17,14 +17,21 @@ //! [`DiskManager`]: Manages files generated during query execution -use datafusion_common::{resources_datafusion_err, DataFusionError, Result}; +use datafusion_common::{ + config_err, resources_datafusion_err, resources_err, DataFusionError, Result, +}; use log::debug; use parking_lot::Mutex; use rand::{thread_rng, Rng}; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use tempfile::{Builder, NamedTempFile, TempDir}; +use crate::memory_pool::human_readable_size; + +const DEFAULT_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; // 100GB + /// Configuration for temporary disk access #[derive(Debug, Clone)] pub enum DiskManagerConfig { @@ -75,6 +82,12 @@ pub struct DiskManager { /// If `Some(vec![])` a new OS specified temporary directory will be created /// If `None` an error will be returned (configured not to spill) local_dirs: Mutex>>>, + /// The maximum amount of data (in bytes) stored inside the temporary directories. + /// Default to 100GB + max_temp_directory_size: u64, + /// Used disk space in the temporary directories. Now only spilled data for + /// external executors are counted. + used_disk_space: Arc, } impl DiskManager { @@ -84,6 +97,8 @@ impl DiskManager { DiskManagerConfig::Existing(manager) => Ok(manager), DiskManagerConfig::NewOs => Ok(Arc::new(Self { local_dirs: Mutex::new(Some(vec![])), + max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE, + used_disk_space: Arc::new(AtomicU64::new(0)), })), DiskManagerConfig::NewSpecified(conf_dirs) => { let local_dirs = create_local_dirs(conf_dirs)?; @@ -93,14 +108,38 @@ impl DiskManager { ); Ok(Arc::new(Self { local_dirs: Mutex::new(Some(local_dirs)), + max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE, + used_disk_space: Arc::new(AtomicU64::new(0)), })) } DiskManagerConfig::Disabled => Ok(Arc::new(Self { local_dirs: Mutex::new(None), + max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE, + used_disk_space: Arc::new(AtomicU64::new(0)), })), } } + pub fn with_max_temp_directory_size( + mut self, + max_temp_directory_size: u64, + ) -> Result { + // If the disk manager is disabled and `max_temp_directory_size` is not 0, + // this operation is not meaningful, fail early. + if self.local_dirs.lock().is_none() && max_temp_directory_size != 0 { + return config_err!( + "Cannot set max temp directory size for a disk manager that spilling is disabled" + ); + } + + self.max_temp_directory_size = max_temp_directory_size; + Ok(self) + } + + pub fn used_disk_space(&self) -> u64 { + self.used_disk_space.load(Ordering::Relaxed) + } + /// Return true if this disk manager supports creating temporary /// files. If this returns false, any call to `create_tmp_file` /// will error. @@ -113,7 +152,7 @@ impl DiskManager { /// If the file can not be created for some reason, returns an /// error message referencing the request description pub fn create_tmp_file( - &self, + self: &Arc, request_description: &str, ) -> Result { let mut guard = self.local_dirs.lock(); @@ -142,18 +181,31 @@ impl DiskManager { tempfile: Builder::new() .tempfile_in(local_dirs[dir_index].as_ref()) .map_err(DataFusionError::IoError)?, + current_file_disk_usage: 0, + disk_manager: Arc::clone(self), }) } } /// A wrapper around a [`NamedTempFile`] that also contains -/// a reference to its parent temporary directory +/// a reference to its parent temporary directory. +/// +/// # Note +/// After any modification to the underlying file (e.g., writing data to it), the caller +/// must invoke [`Self::update_disk_usage`] to update the global disk usage counter. +/// This ensures the disk manager can properly enforce usage limits configured by +/// [`DiskManager::with_max_temp_directory_size`]. #[derive(Debug)] pub struct RefCountedTempFile { /// The reference to the directory in which temporary files are created to ensure /// it is not cleaned up prior to the NamedTempFile _parent_temp_dir: Arc, tempfile: NamedTempFile, + /// Tracks the current disk usage of this temporary file. See + /// [`Self::update_disk_usage`] for more details. + current_file_disk_usage: u64, + /// The disk manager that created and manages this temporary file + disk_manager: Arc, } impl RefCountedTempFile { @@ -164,6 +216,50 @@ impl RefCountedTempFile { pub fn inner(&self) -> &NamedTempFile { &self.tempfile } + + /// Updates the global disk usage counter after modifications to the underlying file. + /// + /// # Errors + /// - Returns an error if the global disk usage exceeds the configured limit. + pub fn update_disk_usage(&mut self) -> Result<()> { + // Get new file size from OS + let metadata = self.tempfile.as_file().metadata()?; + let new_disk_usage = metadata.len(); + + // Update the global disk usage by: + // 1. Subtracting the old file size from the global counter + self.disk_manager + .used_disk_space + .fetch_sub(self.current_file_disk_usage, Ordering::Relaxed); + // 2. Adding the new file size to the global counter + self.disk_manager + .used_disk_space + .fetch_add(new_disk_usage, Ordering::Relaxed); + + // 3. Check if the updated global disk usage exceeds the configured limit + let global_disk_usage = self.disk_manager.used_disk_space.load(Ordering::Relaxed); + if global_disk_usage > self.disk_manager.max_temp_directory_size { + return resources_err!( + "The used disk space during the spilling process has exceeded the allowable limit of {}. Try increasing the `max_temp_directory_size` in the disk manager configuration.", + human_readable_size(self.disk_manager.max_temp_directory_size as usize) + ); + } + + // 4. Update the local file size tracking + self.current_file_disk_usage = new_disk_usage; + + Ok(()) + } +} + +/// When the temporary file is dropped, subtract its disk usage from the disk manager's total +impl Drop for RefCountedTempFile { + fn drop(&mut self) { + // Subtract the current file's disk usage from the global counter + self.disk_manager + .used_disk_space + .fetch_sub(self.current_file_disk_usage, Ordering::Relaxed); + } } /// Setup local dirs by creating one new dir in each of the given dirs diff --git a/datafusion/execution/src/memory_pool/mod.rs b/datafusion/execution/src/memory_pool/mod.rs index 71d40aeab53c7..19e509d263ea2 100644 --- a/datafusion/execution/src/memory_pool/mod.rs +++ b/datafusion/execution/src/memory_pool/mod.rs @@ -19,7 +19,8 @@ //! help with allocation accounting. use datafusion_common::{internal_err, Result}; -use std::{cmp::Ordering, sync::Arc}; +use std::hash::{Hash, Hasher}; +use std::{cmp::Ordering, sync::atomic, sync::Arc}; mod pool; pub mod proxy { @@ -140,30 +141,101 @@ pub trait MemoryPool: Send + Sync + std::fmt::Debug { /// Return the total amount of memory reserved fn reserved(&self) -> usize; + + /// Return the memory limit of the pool + /// + /// The default implementation of `MemoryPool::memory_limit` + /// will return `MemoryLimit::Unknown`. + /// If you are using your custom memory pool, but have the requirement to + /// know the memory usage limit of the pool, please implement this method + /// to return it(`Memory::Finite(limit)`). + fn memory_limit(&self) -> MemoryLimit { + MemoryLimit::Unknown + } +} + +/// Memory limit of `MemoryPool` +pub enum MemoryLimit { + Infinite, + /// Bounded memory limit in bytes. + Finite(usize), + Unknown, } /// A memory consumer is a named allocation traced by a particular /// [`MemoryReservation`] in a [`MemoryPool`]. All allocations are registered to /// a particular `MemoryConsumer`; /// +/// Each `MemoryConsumer` is identifiable by a process-unique id, and is therefor not cloneable, +/// If you want a clone of a `MemoryConsumer`, you should look into [`MemoryConsumer::clone_with_new_id`], +/// but note that this `MemoryConsumer` may be treated as a separate entity based on the used pool, +/// and is only guaranteed to share the name and inner properties. +/// /// For help with allocation accounting, see the [`proxy`] module. /// /// [proxy]: datafusion_common::utils::proxy -#[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[derive(Debug)] pub struct MemoryConsumer { name: String, can_spill: bool, + id: usize, +} + +impl PartialEq for MemoryConsumer { + fn eq(&self, other: &Self) -> bool { + let is_same_id = self.id == other.id; + + #[cfg(debug_assertions)] + if is_same_id { + assert_eq!(self.name, other.name); + assert_eq!(self.can_spill, other.can_spill); + } + + is_same_id + } +} + +impl Eq for MemoryConsumer {} + +impl Hash for MemoryConsumer { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.name.hash(state); + self.can_spill.hash(state); + } } impl MemoryConsumer { + fn new_unique_id() -> usize { + static ID: atomic::AtomicUsize = atomic::AtomicUsize::new(0); + ID.fetch_add(1, atomic::Ordering::Relaxed) + } + /// Create a new empty [`MemoryConsumer`] that can be grown using [`MemoryReservation`] pub fn new(name: impl Into) -> Self { Self { name: name.into(), can_spill: false, + id: Self::new_unique_id(), + } + } + + /// Returns a clone of this [`MemoryConsumer`] with a new unique id, + /// which can be registered with a [`MemoryPool`], + /// This new consumer is separate from the original. + pub fn clone_with_new_id(&self) -> Self { + Self { + name: self.name.clone(), + can_spill: self.can_spill, + id: Self::new_unique_id(), } } + /// Return the unique id of this [`MemoryConsumer`] + pub fn id(&self) -> usize { + self.id + } + /// Set whether this allocation can be spilled to disk pub fn with_can_spill(self, can_spill: bool) -> Self { Self { can_spill, ..self } @@ -349,7 +421,7 @@ pub mod units { pub const KB: u64 = 1 << 10; } -/// Present size in human readable form +/// Present size in human-readable form pub fn human_readable_size(size: usize) -> String { use units::*; @@ -374,6 +446,15 @@ pub fn human_readable_size(size: usize) -> String { mod tests { use super::*; + #[test] + fn test_id_uniqueness() { + let mut ids = std::collections::HashSet::new(); + for _ in 0..100 { + let consumer = MemoryConsumer::new("test"); + assert!(ids.insert(consumer.id())); // Ensures unique insertion + } + } + #[test] fn test_memory_pool_underflow() { let pool = Arc::new(GreedyMemoryPool::new(50)) as _; diff --git a/datafusion/execution/src/memory_pool/pool.rs b/datafusion/execution/src/memory_pool/pool.rs index 261332180e571..e623246eb976b 100644 --- a/datafusion/execution/src/memory_pool/pool.rs +++ b/datafusion/execution/src/memory_pool/pool.rs @@ -15,14 +15,14 @@ // specific language governing permissions and limitations // under the License. -use crate::memory_pool::{MemoryConsumer, MemoryPool, MemoryReservation}; +use crate::memory_pool::{MemoryConsumer, MemoryLimit, MemoryPool, MemoryReservation}; use datafusion_common::HashMap; use datafusion_common::{resources_datafusion_err, DataFusionError, Result}; use log::debug; use parking_lot::Mutex; use std::{ num::NonZeroUsize, - sync::atomic::{AtomicU64, AtomicUsize, Ordering}, + sync::atomic::{AtomicUsize, Ordering}, }; /// A [`MemoryPool`] that enforces no limit @@ -48,6 +48,10 @@ impl MemoryPool for UnboundedMemoryPool { fn reserved(&self) -> usize { self.used.load(Ordering::Relaxed) } + + fn memory_limit(&self) -> MemoryLimit { + MemoryLimit::Infinite + } } /// A [`MemoryPool`] that implements a greedy first-come first-serve limit. @@ -100,6 +104,10 @@ impl MemoryPool for GreedyMemoryPool { fn reserved(&self) -> usize { self.used.load(Ordering::Relaxed) } + + fn memory_limit(&self) -> MemoryLimit { + MemoryLimit::Finite(self.pool_size) + } } /// A [`MemoryPool`] that prevents spillable reservations from using more than @@ -233,6 +241,10 @@ impl MemoryPool for FairSpillPool { let state = self.state.lock(); state.spillable + state.unspillable } + + fn memory_limit(&self) -> MemoryLimit { + MemoryLimit::Finite(self.pool_size) + } } /// Constructs a resources error based upon the individual [`MemoryReservation`]. @@ -249,6 +261,32 @@ fn insufficient_capacity_err( resources_datafusion_err!("Failed to allocate additional {} bytes for {} with {} bytes already allocated for this reservation - {} bytes remain available for the total pool", additional, reservation.registration.consumer.name, reservation.size, available) } +#[derive(Debug)] +struct TrackedConsumer { + name: String, + can_spill: bool, + reserved: AtomicUsize, +} + +impl TrackedConsumer { + /// Shorthand to return the currently reserved value + fn reserved(&self) -> usize { + self.reserved.load(Ordering::Relaxed) + } + + /// Grows the tracked consumer's reserved size, + /// should be called after the pool has successfully performed the grow(). + fn grow(&self, additional: usize) { + self.reserved.fetch_add(additional, Ordering::Relaxed); + } + + /// Reduce the tracked consumer's reserved size, + /// should be called after the pool has successfully performed the shrink(). + fn shrink(&self, shrink: usize) { + self.reserved.fetch_sub(shrink, Ordering::Relaxed); + } +} + /// A [`MemoryPool`] that tracks the consumers that have /// reserved memory within the inner memory pool. /// @@ -259,9 +297,12 @@ fn insufficient_capacity_err( /// The same consumer can have multiple reservations. #[derive(Debug)] pub struct TrackConsumersPool { + /// The wrapped memory pool that actually handles reservation logic inner: I, + /// The amount of consumers to report(ordered top to bottom by reservation size) top: NonZeroUsize, - tracked_consumers: Mutex>, + /// Maps consumer_id --> TrackedConsumer + tracked_consumers: Mutex>, } impl TrackConsumersPool { @@ -277,27 +318,20 @@ impl TrackConsumersPool { } } - /// Determine if there are multiple [`MemoryConsumer`]s registered - /// which have the same name. - /// - /// This is very tied to the implementation of the memory consumer. - fn has_multiple_consumers(&self, name: &String) -> bool { - let consumer = MemoryConsumer::new(name); - let consumer_with_spill = consumer.clone().with_can_spill(true); - let guard = self.tracked_consumers.lock(); - guard.contains_key(&consumer) && guard.contains_key(&consumer_with_spill) - } - /// The top consumers in a report string. pub fn report_top(&self, top: usize) -> String { let mut consumers = self .tracked_consumers .lock() .iter() - .map(|(consumer, reserved)| { + .map(|(consumer_id, tracked_consumer)| { ( - (consumer.name().to_owned(), consumer.can_spill()), - reserved.load(Ordering::Acquire), + ( + *consumer_id, + tracked_consumer.name.to_owned(), + tracked_consumer.can_spill, + ), + tracked_consumer.reserved(), ) }) .collect::>(); @@ -305,12 +339,8 @@ impl TrackConsumersPool { consumers[0..std::cmp::min(top, consumers.len())] .iter() - .map(|((name, can_spill), size)| { - if self.has_multiple_consumers(name) { - format!("{name}(can_spill={}) consumed {:?} bytes", can_spill, size) - } else { - format!("{name} consumed {:?} bytes", size) - } + .map(|((id, name, can_spill), size)| { + format!("{name}#{id}(can spill: {can_spill}) consumed {size} bytes") }) .collect::>() .join(", ") @@ -322,29 +352,33 @@ impl MemoryPool for TrackConsumersPool { self.inner.register(consumer); let mut guard = self.tracked_consumers.lock(); - if let Some(already_reserved) = guard.insert(consumer.clone(), Default::default()) - { - guard.entry_ref(consumer).and_modify(|bytes| { - bytes.fetch_add( - already_reserved.load(Ordering::Acquire), - Ordering::AcqRel, - ); - }); - } + let existing = guard.insert( + consumer.id(), + TrackedConsumer { + name: consumer.name().to_string(), + can_spill: consumer.can_spill(), + reserved: Default::default(), + }, + ); + + debug_assert!( + existing.is_none(), + "Registered was called twice on the same consumer" + ); } fn unregister(&self, consumer: &MemoryConsumer) { self.inner.unregister(consumer); - self.tracked_consumers.lock().remove(consumer); + self.tracked_consumers.lock().remove(&consumer.id()); } fn grow(&self, reservation: &MemoryReservation, additional: usize) { self.inner.grow(reservation, additional); self.tracked_consumers .lock() - .entry_ref(reservation.consumer()) - .and_modify(|bytes| { - bytes.fetch_add(additional as u64, Ordering::AcqRel); + .entry(reservation.consumer().id()) + .and_modify(|tracked_consumer| { + tracked_consumer.grow(additional); }); } @@ -352,9 +386,9 @@ impl MemoryPool for TrackConsumersPool { self.inner.shrink(reservation, shrink); self.tracked_consumers .lock() - .entry_ref(reservation.consumer()) - .and_modify(|bytes| { - bytes.fetch_sub(shrink as u64, Ordering::AcqRel); + .entry(reservation.consumer().id()) + .and_modify(|tracked_consumer| { + tracked_consumer.shrink(shrink); }); } @@ -376,9 +410,9 @@ impl MemoryPool for TrackConsumersPool { self.tracked_consumers .lock() - .entry_ref(reservation.consumer()) - .and_modify(|bytes| { - bytes.fetch_add(additional as u64, Ordering::AcqRel); + .entry(reservation.consumer().id()) + .and_modify(|tracked_consumer| { + tracked_consumer.grow(additional); }); Ok(()) } @@ -386,6 +420,10 @@ impl MemoryPool for TrackConsumersPool { fn reserved(&self) -> usize { self.inner.reserved() } + + fn memory_limit(&self) -> MemoryLimit { + self.inner.memory_limit() + } } fn provide_top_memory_consumers_to_error_msg( @@ -501,12 +539,12 @@ mod tests { // Test: reports if new reservation causes error // using the previously set sizes for other consumers let mut r5 = MemoryConsumer::new("r5").register(&pool); - let expected = "Additional allocation failed with top memory consumers (across reservations) as: r1 consumed 50 bytes, r3 consumed 20 bytes, r2 consumed 15 bytes. Error: Failed to allocate additional 150 bytes for r5 with 0 bytes already allocated for this reservation - 5 bytes remain available for the total pool"; + let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: r1#{}(can spill: false) consumed 50 bytes, r3#{}(can spill: false) consumed 20 bytes, r2#{}(can spill: false) consumed 15 bytes. Error: Failed to allocate additional 150 bytes for r5 with 0 bytes already allocated for this reservation - 5 bytes remain available for the total pool", r1.consumer().id(), r3.consumer().id(), r2.consumer().id()); let res = r5.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) ), "should provide list of top memory consumers, instead found {:?}", res @@ -524,45 +562,45 @@ mod tests { // Test: see error message when no consumers recorded yet let mut r0 = MemoryConsumer::new(same_name).register(&pool); - let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 100 bytes remain available for the total pool"; + let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 100 bytes remain available for the total pool", r0.consumer().id()); let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) ), "should provide proper error when no reservations have been made yet, instead found {:?}", res ); // API: multiple registrations using the same hashed consumer, - // will be recognized as the same in the TrackConsumersPool. + // will be recognized *differently* in the TrackConsumersPool. - // Test: will be the same per Top Consumers reported. r0.grow(10); // make r0=10, pool available=90 let new_consumer_same_name = MemoryConsumer::new(same_name); let mut r1 = new_consumer_same_name.register(&pool); // TODO: the insufficient_capacity_err() message is per reservation, not per consumer. // a followup PR will clarify this message "0 bytes already allocated for this reservation" - let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo consumed 10 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 90 bytes remain available for the total pool"; + let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 10 bytes, foo#{}(can spill: false) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 90 bytes remain available for the total pool", r0.consumer().id(), r1.consumer().id()); let res = r1.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) ), - "should provide proper error with same hashed consumer (a single foo=10 bytes, available=90), instead found {:?}", res + "should provide proper error for 2 consumers, instead found {:?}", + res ); // Test: will accumulate size changes per consumer, not per reservation r1.grow(20); - let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo consumed 30 bytes. Error: Failed to allocate additional 150 bytes for foo with 20 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; + let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 20 bytes, foo#{}(can spill: false) consumed 10 bytes. Error: Failed to allocate additional 150 bytes for foo with 20 bytes already allocated for this reservation - 70 bytes remain available for the total pool", r1.consumer().id(), r0.consumer().id()); let res = r1.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) ), - "should provide proper error with same hashed consumer (a single foo=30 bytes, available=70), instead found {:?}", res + "should provide proper error for 2 consumers(one foo=20 bytes, another foo=10 bytes, available=70), instead found {:?}", res ); // Test: different hashed consumer, (even with the same name), @@ -570,14 +608,14 @@ mod tests { let consumer_with_same_name_but_different_hash = MemoryConsumer::new(same_name).with_can_spill(true); let mut r2 = consumer_with_same_name_but_different_hash.register(&pool); - let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo(can_spill=false) consumed 30 bytes, foo(can_spill=true) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; + let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 20 bytes, foo#{}(can spill: false) consumed 10 bytes, foo#{}(can spill: true) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 70 bytes remain available for the total pool", r1.consumer().id(), r0.consumer().id(), r2.consumer().id()); let res = r2.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) ), - "should provide proper error with different hashed consumer (foo(can_spill=false)=30 bytes and foo(can_spill=true)=0 bytes, available=70), instead found {:?}", res + "should provide proper error with 3 separate consumers(1 = 20 bytes, 2 = 10 bytes, 3 = 0 bytes), instead found {:?}", res ); } @@ -588,14 +626,15 @@ mod tests { let mut r0 = MemoryConsumer::new("r0").register(&pool); r0.grow(10); let r1_consumer = MemoryConsumer::new("r1"); - let mut r1 = r1_consumer.clone().register(&pool); + let mut r1 = r1_consumer.register(&pool); r1.grow(20); - let expected = "Additional allocation failed with top memory consumers (across reservations) as: r1 consumed 20 bytes, r0 consumed 10 bytes. Error: Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; + + let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: r1#{}(can spill: false) consumed 20 bytes, r0#{}(can spill: false) consumed 10 bytes. Error: Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 70 bytes remain available for the total pool", r1.consumer().id(), r0.consumer().id()); let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) ), "should provide proper error with both consumers, instead found {:?}", res @@ -603,32 +642,31 @@ mod tests { // Test: unregister one // only the remaining one should be listed - pool.unregister(&r1_consumer); - let expected_consumers = "Additional allocation failed with top memory consumers (across reservations) as: r0 consumed 10 bytes"; + drop(r1); + let expected_consumers = format!("Additional allocation failed with top memory consumers (across reservations) as: r0#{}(can spill: false) consumed 10 bytes", r0.consumer().id()); let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected_consumers) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected_consumers) ), "should provide proper error with only 1 consumer left registered, instead found {:?}", res ); // Test: actual message we see is the `available is 70`. When it should be `available is 90`. // This is because the pool.shrink() does not automatically occur within the inner_pool.deregister(). - let expected_70_available = "Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; + let expected_90_available = "Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 90 bytes remain available for the total pool"; let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected_70_available) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected_90_available) ), "should find that the inner pool will still count all bytes for the deregistered consumer until the reservation is dropped, instead found {:?}", res ); // Test: the registration needs to free itself (or be dropped), // for the proper error message - r1.free(); let expected_90_available = "Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 90 bytes remain available for the total pool"; let res = r0.try_grow(150); assert!( @@ -678,7 +716,7 @@ mod tests { .unwrap(); // Test: can get runtime metrics, even without an error thrown - let expected = "r3 consumed 45 bytes, r1 consumed 20 bytes"; + let expected = format!("r3#{}(can spill: false) consumed 45 bytes, r1#{}(can spill: false) consumed 20 bytes", r3.consumer().id(), r1.consumer().id()); let res = downcasted.report_top(2); assert_eq!( res, expected, diff --git a/datafusion/execution/src/runtime_env.rs b/datafusion/execution/src/runtime_env.rs index 95f14f485792a..cb085108819eb 100644 --- a/datafusion/execution/src/runtime_env.rs +++ b/datafusion/execution/src/runtime_env.rs @@ -27,7 +27,7 @@ use crate::{ }; use crate::cache::cache_manager::{CacheManager, CacheManagerConfig}; -use datafusion_common::Result; +use datafusion_common::{config::ConfigEntry, Result}; use object_store::ObjectStore; use std::path::PathBuf; use std::sync::Arc; @@ -268,4 +268,56 @@ impl RuntimeEnvBuilder { pub fn build_arc(self) -> Result> { self.build().map(Arc::new) } + + /// Create a new RuntimeEnvBuilder from an existing RuntimeEnv + pub fn from_runtime_env(runtime_env: &RuntimeEnv) -> Self { + let cache_config = CacheManagerConfig { + table_files_statistics_cache: runtime_env + .cache_manager + .get_file_statistic_cache(), + list_files_cache: runtime_env.cache_manager.get_list_files_cache(), + }; + + Self { + disk_manager: DiskManagerConfig::Existing(Arc::clone( + &runtime_env.disk_manager, + )), + memory_pool: Some(Arc::clone(&runtime_env.memory_pool)), + cache_manager: cache_config, + object_store_registry: Arc::clone(&runtime_env.object_store_registry), + } + } + + /// Returns a list of all available runtime configurations with their current values and descriptions + pub fn entries(&self) -> Vec { + // Memory pool configuration + vec![ConfigEntry { + key: "datafusion.runtime.memory_limit".to_string(), + value: None, // Default is system-dependent + description: "Maximum memory limit for query execution. Supports suffixes K (kilobytes), M (megabytes), and G (gigabytes). Example: '2G' for 2 gigabytes.", + }] + } + + /// Generate documentation that can be included in the user guide + pub fn generate_config_markdown() -> String { + use std::fmt::Write as _; + + let s = Self::default(); + + let mut docs = "| key | default | description |\n".to_string(); + docs += "|-----|---------|-------------|\n"; + let mut entries = s.entries(); + entries.sort_unstable_by(|a, b| a.key.cmp(&b.key)); + + for entry in &entries { + let _ = writeln!( + &mut docs, + "| {} | {} | {} |", + entry.key, + entry.value.as_deref().unwrap_or("NULL"), + entry.description + ); + } + docs + } } diff --git a/datafusion/expr-common/src/interval_arithmetic.rs b/datafusion/expr-common/src/interval_arithmetic.rs index 9d00b45962bc2..6af4322df29ea 100644 --- a/datafusion/expr-common/src/interval_arithmetic.rs +++ b/datafusion/expr-common/src/interval_arithmetic.rs @@ -174,7 +174,7 @@ macro_rules! value_transition { /// - `INF` values are converted to `NULL`s while constructing an interval to /// ensure consistency, with other data types. /// - `NaN` (Not a Number) results are conservatively result in unbounded -/// endpoints. +/// endpoints. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Interval { lower: ScalarValue, diff --git a/datafusion/expr-common/src/signature.rs b/datafusion/expr-common/src/signature.rs index 063417a254be3..a7c9330201bc0 100644 --- a/datafusion/expr-common/src/signature.rs +++ b/datafusion/expr-common/src/signature.rs @@ -391,10 +391,11 @@ impl TypeSignature { vec![format!("{}, ..", Self::join_types(types, "/"))] } TypeSignature::Uniform(arg_count, valid_types) => { - vec![std::iter::repeat(Self::join_types(valid_types, "/")) - .take(*arg_count) - .collect::>() - .join(", ")] + vec![ + std::iter::repeat_n(Self::join_types(valid_types, "/"), *arg_count) + .collect::>() + .join(", "), + ] } TypeSignature::String(num) => { vec![format!("String({num})")] @@ -412,8 +413,7 @@ impl TypeSignature { vec![Self::join_types(types, ", ")] } TypeSignature::Any(arg_count) => { - vec![std::iter::repeat("Any") - .take(*arg_count) + vec![std::iter::repeat_n("Any", *arg_count) .collect::>() .join(", ")] } diff --git a/datafusion/expr-common/src/type_coercion/aggregates.rs b/datafusion/expr-common/src/type_coercion/aggregates.rs index 13d52959aba65..44839378d52c9 100644 --- a/datafusion/expr-common/src/type_coercion/aggregates.rs +++ b/datafusion/expr-common/src/type_coercion/aggregates.rs @@ -210,6 +210,7 @@ pub fn avg_return_type(func_name: &str, arg_type: &DataType) -> Result let new_scale = DECIMAL256_MAX_SCALE.min(*scale + 4); Ok(DataType::Decimal256(new_precision, new_scale)) } + DataType::Duration(time_unit) => Ok(DataType::Duration(*time_unit)), arg_type if NUMERICS.contains(arg_type) => Ok(DataType::Float64), DataType::Dictionary(_, dict_value_type) => { avg_return_type(func_name, dict_value_type.as_ref()) @@ -231,6 +232,7 @@ pub fn avg_sum_type(arg_type: &DataType) -> Result { let new_precision = DECIMAL256_MAX_PRECISION.min(*precision + 10); Ok(DataType::Decimal256(new_precision, *scale)) } + DataType::Duration(time_unit) => Ok(DataType::Duration(*time_unit)), arg_type if NUMERICS.contains(arg_type) => Ok(DataType::Float64), DataType::Dictionary(_, dict_value_type) => { avg_sum_type(dict_value_type.as_ref()) @@ -298,6 +300,7 @@ pub fn coerce_avg_type(func_name: &str, arg_types: &[DataType]) -> Result Ok(DataType::Decimal128(*p, *s)), DataType::Decimal256(p, s) => Ok(DataType::Decimal256(*p, *s)), d if d.is_numeric() => Ok(DataType::Float64), + DataType::Duration(time_unit) => Ok(DataType::Duration(*time_unit)), DataType::Dictionary(_, v) => coerced_type(func_name, v.as_ref()), _ => { plan_err!( diff --git a/datafusion/expr-common/src/type_coercion/binary.rs b/datafusion/expr-common/src/type_coercion/binary.rs index c49de3984097f..fdee00f81b1e6 100644 --- a/datafusion/expr-common/src/type_coercion/binary.rs +++ b/datafusion/expr-common/src/type_coercion/binary.rs @@ -733,6 +733,7 @@ pub fn comparison_coercion(lhs_type: &DataType, rhs_type: &DataType) -> Option Field Arc::new(Field::new(name, common_type, is_nullable)) } +/// coerce two types if they are Maps by coercing their inner 'entries' fields' types +/// using struct coercion +fn map_coercion(lhs_type: &DataType, rhs_type: &DataType) -> Option { + use arrow::datatypes::DataType::*; + match (lhs_type, rhs_type) { + (Map(lhs_field, lhs_ordered), Map(rhs_field, rhs_ordered)) => { + struct_coercion(lhs_field.data_type(), rhs_field.data_type()).map( + |key_value_type| { + Map( + Arc::new((**lhs_field).clone().with_data_type(key_value_type)), + *lhs_ordered && *rhs_ordered, + ) + }, + ) + } + _ => None, + } +} + /// Returns the output type of applying mathematics operations such as /// `+` to arguments of `lhs_type` and `rhs_type`. fn mathematics_numerical_coercion( @@ -1277,6 +1297,10 @@ fn binary_coercion(lhs_type: &DataType, rhs_type: &DataType) -> Option Some(LargeBinary) } (Binary, Utf8) | (Utf8, Binary) => Some(Binary), + + // Cast FixedSizeBinary to Binary + (FixedSizeBinary(_), Binary) | (Binary, FixedSizeBinary(_)) => Some(Binary), + _ => None, } } @@ -2483,4 +2507,49 @@ mod tests { ); Ok(()) } + + #[test] + fn test_map_coercion() -> Result<()> { + let lhs = Field::new_map( + "lhs", + "entries", + Arc::new(Field::new("keys", DataType::Utf8, false)), + Arc::new(Field::new("values", DataType::LargeUtf8, false)), + true, + false, + ); + let rhs = Field::new_map( + "rhs", + "kvp", + Arc::new(Field::new("k", DataType::Utf8, false)), + Arc::new(Field::new("v", DataType::Utf8, true)), + false, + true, + ); + + let expected = Field::new_map( + "expected", + "entries", // struct coercion takes lhs name + Arc::new(Field::new( + "keys", // struct coercion takes lhs name + DataType::Utf8, + false, + )), + Arc::new(Field::new( + "values", // struct coercion takes lhs name + DataType::LargeUtf8, // lhs is large string + true, // rhs is nullable + )), + false, // both sides must be sorted + true, // rhs is nullable + ); + + test_coercion_binary_rule!( + lhs.data_type(), + rhs.data_type(), + Operator::Eq, + expected.data_type().clone() + ); + Ok(()) + } } diff --git a/datafusion/expr/src/logical_plan/builder.rs b/datafusion/expr/src/logical_plan/builder.rs index 91a871d52e9ad..24a5c0fe9a211 100644 --- a/datafusion/expr/src/logical_plan/builder.rs +++ b/datafusion/expr/src/logical_plan/builder.rs @@ -501,6 +501,21 @@ impl LogicalPlanBuilder { if table_scan.filters.is_empty() { if let Some(p) = table_scan.source.get_logical_plan() { let sub_plan = p.into_owned(); + + if let Some(proj) = table_scan.projection { + let projection_exprs = proj + .into_iter() + .map(|i| { + Expr::Column(Column::from( + sub_plan.schema().qualified_field(i), + )) + }) + .collect::>(); + return Self::new(sub_plan) + .project(projection_exprs)? + .alias(table_scan.table_name); + } + // Ensures that the reference to the inlined table remains the // same, meaning we don't have to change any of the parent nodes // that reference this table. @@ -1117,8 +1132,6 @@ impl LogicalPlanBuilder { .collect::>()?; let on: Vec<(_, _)> = left_keys.into_iter().zip(right_keys).collect(); - let join_schema = - build_join_schema(self.plan.schema(), right.schema(), &join_type)?; let mut join_on: Vec<(Expr, Expr)> = vec![]; let mut filters: Option = None; for (l, r) in &on { @@ -1151,33 +1164,33 @@ impl LogicalPlanBuilder { DataFusionError::Internal("filters should not be None here".to_string()) })?) } else { - Ok(Self::new(LogicalPlan::Join(Join { - left: self.plan, - right: Arc::new(right), - on: join_on, - filter: filters, + let join = Join::try_new( + self.plan, + Arc::new(right), + join_on, + filters, join_type, - join_constraint: JoinConstraint::Using, - schema: DFSchemaRef::new(join_schema), - null_equals_null: false, - }))) + JoinConstraint::Using, + false, + )?; + + Ok(Self::new(LogicalPlan::Join(join))) } } /// Apply a cross join pub fn cross_join(self, right: LogicalPlan) -> Result { - let join_schema = - build_join_schema(self.plan.schema(), right.schema(), &JoinType::Inner)?; - Ok(Self::new(LogicalPlan::Join(Join { - left: self.plan, - right: Arc::new(right), - on: vec![], - filter: None, - join_type: JoinType::Inner, - join_constraint: JoinConstraint::On, - null_equals_null: false, - schema: DFSchemaRef::new(join_schema), - }))) + let join = Join::try_new( + self.plan, + Arc::new(right), + vec![], + None, + JoinType::Inner, + JoinConstraint::On, + false, + )?; + + Ok(Self::new(LogicalPlan::Join(join))) } /// Repartition @@ -1338,7 +1351,7 @@ impl LogicalPlanBuilder { /// to columns from the existing input. `r`, the second element of the tuple, /// must only refer to columns from the right input. /// - /// `filter` contains any other other filter expression to apply during the + /// `filter` contains any other filter expression to apply during the /// join. Note that `equi_exprs` predicates are evaluated more efficiently /// than the filter expressions, so they are preferred. pub fn join_with_expr_keys( @@ -1388,19 +1401,17 @@ impl LogicalPlanBuilder { }) .collect::>>()?; - let join_schema = - build_join_schema(self.plan.schema(), right.schema(), &join_type)?; - - Ok(Self::new(LogicalPlan::Join(Join { - left: self.plan, - right: Arc::new(right), - on: join_key_pairs, + let join = Join::try_new( + self.plan, + Arc::new(right), + join_key_pairs, filter, join_type, - join_constraint: JoinConstraint::On, - schema: DFSchemaRef::new(join_schema), - null_equals_null: false, - }))) + JoinConstraint::On, + false, + )?; + + Ok(Self::new(LogicalPlan::Join(join))) } /// Unnest the given column. @@ -1468,19 +1479,37 @@ impl ValuesFields { } } +// `name_map` tracks a mapping between a field name and the number of appearances of that field. +// +// Some field names might already come to this function with the count (number of times it appeared) +// as a sufix e.g. id:1, so there's still a chance of name collisions, for example, +// if these three fields passed to this function: "col:1", "col" and "col", the function +// would rename them to -> col:1, col, col:1 causing a posteriror error when building the DFSchema. +// that's why we need the `seen` set, so the fields are always unique. +// pub fn change_redundant_column(fields: &Fields) -> Vec { let mut name_map = HashMap::new(); + let mut seen: HashSet = HashSet::new(); + fields .into_iter() .map(|field| { - let counter = name_map.entry(field.name().to_string()).or_insert(0); - *counter += 1; - if *counter > 1 { - let new_name = format!("{}:{}", field.name(), *counter - 1); - Field::new(new_name, field.data_type().clone(), field.is_nullable()) - } else { - field.as_ref().clone() + let base_name = field.name(); + let count = name_map.entry(base_name.clone()).or_insert(0); + let mut new_name = base_name.clone(); + + // Loop until we find a name that hasn't been used + while seen.contains(&new_name) { + *count += 1; + new_name = format!("{}:{}", base_name, count); } + + seen.insert(new_name.clone()); + + let mut modified_field = + Field::new(&new_name, field.data_type().clone(), field.is_nullable()); + modified_field.set_metadata(field.metadata().clone()); + modified_field }) .collect() } @@ -2174,7 +2203,7 @@ pub fn unnest_with_options( // new columns dependent on the same original index dependency_indices - .extend(std::iter::repeat(index).take(transformed_columns.len())); + .extend(std::iter::repeat_n(index, transformed_columns.len())); Ok(transformed_columns .iter() .map(|(col, field)| (col.relation.to_owned(), field.to_owned())) @@ -2730,10 +2759,13 @@ mod tests { let t1_field_1 = Field::new("a", DataType::Int32, false); let t2_field_1 = Field::new("a", DataType::Int32, false); let t2_field_3 = Field::new("a", DataType::Int32, false); + let t2_field_4 = Field::new("a:1", DataType::Int32, false); let t1_field_2 = Field::new("b", DataType::Int32, false); let t2_field_2 = Field::new("b", DataType::Int32, false); - let field_vec = vec![t1_field_1, t2_field_1, t1_field_2, t2_field_2, t2_field_3]; + let field_vec = vec![ + t1_field_1, t2_field_1, t1_field_2, t2_field_2, t2_field_3, t2_field_4, + ]; let remove_redundant = change_redundant_column(&Fields::from(field_vec)); assert_eq!( @@ -2744,6 +2776,7 @@ mod tests { Field::new("b", DataType::Int32, false), Field::new("b:1", DataType::Int32, false), Field::new("a:2", DataType::Int32, false), + Field::new("a:1:1", DataType::Int32, false), ] ); Ok(()) diff --git a/datafusion/expr/src/logical_plan/invariants.rs b/datafusion/expr/src/logical_plan/invariants.rs index d83410bf99c98..0c30c9785766b 100644 --- a/datafusion/expr/src/logical_plan/invariants.rs +++ b/datafusion/expr/src/logical_plan/invariants.rs @@ -112,11 +112,11 @@ fn assert_valid_semantic_plan(plan: &LogicalPlan) -> Result<()> { /// Returns an error if the plan does not have the expected schema. /// Ignores metadata and nullability. pub fn assert_expected_schema(schema: &DFSchemaRef, plan: &LogicalPlan) -> Result<()> { - let compatible = plan.schema().has_equivalent_names_and_types(schema); + let compatible = plan.schema().logically_equivalent_names_and_types(schema); - if let Err(e) = compatible { + if !compatible { internal_err!( - "Failed due to a difference in schemas: {e}, original schema: {:?}, new schema: {:?}", + "Failed due to a difference in schemas: original schema: {:?}, new schema: {:?}", schema, plan.schema() ) diff --git a/datafusion/expr/src/logical_plan/plan.rs b/datafusion/expr/src/logical_plan/plan.rs index 76b45d5d723ae..edf5f1126be93 100644 --- a/datafusion/expr/src/logical_plan/plan.rs +++ b/datafusion/expr/src/logical_plan/plan.rs @@ -3709,6 +3709,47 @@ pub struct Join { } impl Join { + /// Creates a new Join operator with automatically computed schema. + /// + /// This constructor computes the schema based on the join type and inputs, + /// removing the need to manually specify the schema or call `recompute_schema`. + /// + /// # Arguments + /// + /// * `left` - Left input plan + /// * `right` - Right input plan + /// * `on` - Join condition as a vector of (left_expr, right_expr) pairs + /// * `filter` - Optional filter expression (for non-equijoin conditions) + /// * `join_type` - Type of join (Inner, Left, Right, etc.) + /// * `join_constraint` - Join constraint (On, Using) + /// * `null_equals_null` - Whether NULL = NULL in join comparisons + /// + /// # Returns + /// + /// A new Join operator with the computed schema + pub fn try_new( + left: Arc, + right: Arc, + on: Vec<(Expr, Expr)>, + filter: Option, + join_type: JoinType, + join_constraint: JoinConstraint, + null_equals_null: bool, + ) -> Result { + let join_schema = build_join_schema(left.schema(), right.schema(), &join_type)?; + + Ok(Join { + left, + right, + on, + filter, + join_type, + join_constraint, + schema: Arc::new(join_schema), + null_equals_null, + }) + } + /// Create Join with input which wrapped with projection, this method is used to help create physical join. pub fn try_new_with_project_input( original: &LogicalPlan, @@ -4916,4 +4957,379 @@ digraph { Ok(()) } + + #[test] + fn test_join_try_new() -> Result<()> { + let schema = Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ]); + + let left_scan = table_scan(Some("t1"), &schema, None)?.build()?; + + let right_scan = table_scan(Some("t2"), &schema, None)?.build()?; + + let join_types = vec![ + JoinType::Inner, + JoinType::Left, + JoinType::Right, + JoinType::Full, + JoinType::LeftSemi, + JoinType::LeftAnti, + JoinType::RightSemi, + JoinType::RightAnti, + JoinType::LeftMark, + ]; + + for join_type in join_types { + let join = Join::try_new( + Arc::new(left_scan.clone()), + Arc::new(right_scan.clone()), + vec![(col("t1.a"), col("t2.a"))], + Some(col("t1.b").gt(col("t2.b"))), + join_type, + JoinConstraint::On, + false, + )?; + + match join_type { + JoinType::LeftSemi | JoinType::LeftAnti => { + assert_eq!(join.schema.fields().len(), 2); + + let fields = join.schema.fields(); + assert_eq!( + fields[0].name(), + "a", + "First field should be 'a' from left table" + ); + assert_eq!( + fields[1].name(), + "b", + "Second field should be 'b' from left table" + ); + } + JoinType::RightSemi | JoinType::RightAnti => { + assert_eq!(join.schema.fields().len(), 2); + + let fields = join.schema.fields(); + assert_eq!( + fields[0].name(), + "a", + "First field should be 'a' from right table" + ); + assert_eq!( + fields[1].name(), + "b", + "Second field should be 'b' from right table" + ); + } + JoinType::LeftMark => { + assert_eq!(join.schema.fields().len(), 3); + + let fields = join.schema.fields(); + assert_eq!( + fields[0].name(), + "a", + "First field should be 'a' from left table" + ); + assert_eq!( + fields[1].name(), + "b", + "Second field should be 'b' from left table" + ); + assert_eq!( + fields[2].name(), + "mark", + "Third field should be the mark column" + ); + + assert!(!fields[0].is_nullable()); + assert!(!fields[1].is_nullable()); + assert!(!fields[2].is_nullable()); + } + _ => { + assert_eq!(join.schema.fields().len(), 4); + + let fields = join.schema.fields(); + assert_eq!( + fields[0].name(), + "a", + "First field should be 'a' from left table" + ); + assert_eq!( + fields[1].name(), + "b", + "Second field should be 'b' from left table" + ); + assert_eq!( + fields[2].name(), + "a", + "Third field should be 'a' from right table" + ); + assert_eq!( + fields[3].name(), + "b", + "Fourth field should be 'b' from right table" + ); + + if join_type == JoinType::Left { + // Left side fields (first two) shouldn't be nullable + assert!(!fields[0].is_nullable()); + assert!(!fields[1].is_nullable()); + // Right side fields (third and fourth) should be nullable + assert!(fields[2].is_nullable()); + assert!(fields[3].is_nullable()); + } else if join_type == JoinType::Right { + // Left side fields (first two) should be nullable + assert!(fields[0].is_nullable()); + assert!(fields[1].is_nullable()); + // Right side fields (third and fourth) shouldn't be nullable + assert!(!fields[2].is_nullable()); + assert!(!fields[3].is_nullable()); + } else if join_type == JoinType::Full { + assert!(fields[0].is_nullable()); + assert!(fields[1].is_nullable()); + assert!(fields[2].is_nullable()); + assert!(fields[3].is_nullable()); + } + } + } + + assert_eq!(join.on, vec![(col("t1.a"), col("t2.a"))]); + assert_eq!(join.filter, Some(col("t1.b").gt(col("t2.b")))); + assert_eq!(join.join_type, join_type); + assert_eq!(join.join_constraint, JoinConstraint::On); + assert!(!join.null_equals_null); + } + + Ok(()) + } + + #[test] + fn test_join_try_new_with_using_constraint_and_overlapping_columns() -> Result<()> { + let left_schema = Schema::new(vec![ + Field::new("id", DataType::Int32, false), // Common column in both tables + Field::new("name", DataType::Utf8, false), // Unique to left + Field::new("value", DataType::Int32, false), // Common column, different meaning + ]); + + let right_schema = Schema::new(vec![ + Field::new("id", DataType::Int32, false), // Common column in both tables + Field::new("category", DataType::Utf8, false), // Unique to right + Field::new("value", DataType::Float64, true), // Common column, different meaning + ]); + + let left_plan = table_scan(Some("t1"), &left_schema, None)?.build()?; + + let right_plan = table_scan(Some("t2"), &right_schema, None)?.build()?; + + // Test 1: USING constraint with a common column + { + // In the logical plan, both copies of the `id` column are preserved + // The USING constraint is handled later during physical execution, where the common column appears once + let join = Join::try_new( + Arc::new(left_plan.clone()), + Arc::new(right_plan.clone()), + vec![(col("t1.id"), col("t2.id"))], + None, + JoinType::Inner, + JoinConstraint::Using, + false, + )?; + + let fields = join.schema.fields(); + + assert_eq!(fields.len(), 6); + + assert_eq!( + fields[0].name(), + "id", + "First field should be 'id' from left table" + ); + assert_eq!( + fields[1].name(), + "name", + "Second field should be 'name' from left table" + ); + assert_eq!( + fields[2].name(), + "value", + "Third field should be 'value' from left table" + ); + assert_eq!( + fields[3].name(), + "id", + "Fourth field should be 'id' from right table" + ); + assert_eq!( + fields[4].name(), + "category", + "Fifth field should be 'category' from right table" + ); + assert_eq!( + fields[5].name(), + "value", + "Sixth field should be 'value' from right table" + ); + + assert_eq!(join.join_constraint, JoinConstraint::Using); + } + + // Test 2: Complex join condition with expressions + { + // Complex condition: join on id equality AND where left.value < right.value + let join = Join::try_new( + Arc::new(left_plan.clone()), + Arc::new(right_plan.clone()), + vec![(col("t1.id"), col("t2.id"))], // Equijoin condition + Some(col("t1.value").lt(col("t2.value"))), // Non-equi filter condition + JoinType::Inner, + JoinConstraint::On, + false, + )?; + + let fields = join.schema.fields(); + assert_eq!(fields.len(), 6); + + assert_eq!( + fields[0].name(), + "id", + "First field should be 'id' from left table" + ); + assert_eq!( + fields[1].name(), + "name", + "Second field should be 'name' from left table" + ); + assert_eq!( + fields[2].name(), + "value", + "Third field should be 'value' from left table" + ); + assert_eq!( + fields[3].name(), + "id", + "Fourth field should be 'id' from right table" + ); + assert_eq!( + fields[4].name(), + "category", + "Fifth field should be 'category' from right table" + ); + assert_eq!( + fields[5].name(), + "value", + "Sixth field should be 'value' from right table" + ); + + assert_eq!(join.filter, Some(col("t1.value").lt(col("t2.value")))); + } + + // Test 3: Join with null equality behavior set to true + { + let join = Join::try_new( + Arc::new(left_plan.clone()), + Arc::new(right_plan.clone()), + vec![(col("t1.id"), col("t2.id"))], + None, + JoinType::Inner, + JoinConstraint::On, + true, + )?; + + assert!(join.null_equals_null); + } + + Ok(()) + } + + #[test] + fn test_join_try_new_schema_validation() -> Result<()> { + let left_schema = Schema::new(vec![ + Field::new("id", DataType::Int32, false), + Field::new("name", DataType::Utf8, false), + Field::new("value", DataType::Float64, true), + ]); + + let right_schema = Schema::new(vec![ + Field::new("id", DataType::Int32, false), + Field::new("category", DataType::Utf8, true), + Field::new("code", DataType::Int16, false), + ]); + + let left_plan = table_scan(Some("t1"), &left_schema, None)?.build()?; + + let right_plan = table_scan(Some("t2"), &right_schema, None)?.build()?; + + let join_types = vec![ + JoinType::Inner, + JoinType::Left, + JoinType::Right, + JoinType::Full, + ]; + + for join_type in join_types { + let join = Join::try_new( + Arc::new(left_plan.clone()), + Arc::new(right_plan.clone()), + vec![(col("t1.id"), col("t2.id"))], + Some(col("t1.value").gt(lit(5.0))), + join_type, + JoinConstraint::On, + false, + )?; + + let fields = join.schema.fields(); + assert_eq!( + fields.len(), + 6, + "Expected 6 fields for {:?} join", + join_type + ); + + for (i, field) in fields.iter().enumerate() { + let expected_nullable = match (i, &join_type) { + // Left table fields (indices 0, 1, 2) + (0, JoinType::Right | JoinType::Full) => true, // id becomes nullable in RIGHT/FULL + (1, JoinType::Right | JoinType::Full) => true, // name becomes nullable in RIGHT/FULL + (2, _) => true, // value is already nullable + + // Right table fields (indices 3, 4, 5) + (3, JoinType::Left | JoinType::Full) => true, // id becomes nullable in LEFT/FULL + (4, _) => true, // category is already nullable + (5, JoinType::Left | JoinType::Full) => true, // code becomes nullable in LEFT/FULL + + _ => false, + }; + + assert_eq!( + field.is_nullable(), + expected_nullable, + "Field {} ({}) nullability incorrect for {:?} join", + i, + field.name(), + join_type + ); + } + } + + let using_join = Join::try_new( + Arc::new(left_plan.clone()), + Arc::new(right_plan.clone()), + vec![(col("t1.id"), col("t2.id"))], + None, + JoinType::Inner, + JoinConstraint::Using, + false, + )?; + + assert_eq!( + using_join.schema.fields().len(), + 6, + "USING join should have all fields" + ); + assert_eq!(using_join.join_constraint, JoinConstraint::Using); + + Ok(()) + } } diff --git a/datafusion/expr/src/type_coercion/functions.rs b/datafusion/expr/src/type_coercion/functions.rs index 0ec017bdc27f6..3b34718062eb4 100644 --- a/datafusion/expr/src/type_coercion/functions.rs +++ b/datafusion/expr/src/type_coercion/functions.rs @@ -49,7 +49,7 @@ pub fn data_types_with_scalar_udf( let signature = func.signature(); let type_signature = &signature.type_signature; - if current_types.is_empty() { + if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { @@ -87,7 +87,7 @@ pub fn data_types_with_aggregate_udf( let signature = func.signature(); let type_signature = &signature.type_signature; - if current_types.is_empty() { + if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { @@ -124,7 +124,7 @@ pub fn data_types_with_window_udf( let signature = func.signature(); let type_signature = &signature.type_signature; - if current_types.is_empty() { + if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { @@ -161,7 +161,7 @@ pub fn data_types( ) -> Result> { let type_signature = &signature.type_signature; - if current_types.is_empty() { + if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { diff --git a/datafusion/expr/src/udaf.rs b/datafusion/expr/src/udaf.rs index b75e8fd3cd3c4..97507433814b9 100644 --- a/datafusion/expr/src/udaf.rs +++ b/datafusion/expr/src/udaf.rs @@ -315,6 +315,16 @@ impl AggregateUDF { self.inner.default_value(data_type) } + /// See [`AggregateUDFImpl::supports_null_handling_clause`] for more details. + pub fn supports_null_handling_clause(&self) -> bool { + self.inner.supports_null_handling_clause() + } + + /// See [`AggregateUDFImpl::is_ordered_set_aggregate`] for more details. + pub fn is_ordered_set_aggregate(&self) -> bool { + self.inner.is_ordered_set_aggregate() + } + /// Returns the documentation for this Aggregate UDF. /// /// Documentation can be accessed programmatically as well as @@ -432,6 +442,14 @@ pub trait AggregateUDFImpl: Debug + Send + Sync { null_treatment, } = params; + // exclude the first function argument(= column) in ordered set aggregate function, + // because it is duplicated with the WITHIN GROUP clause in schema name. + let args = if self.is_ordered_set_aggregate() { + &args[1..] + } else { + &args[..] + }; + let mut schema_name = String::new(); schema_name.write_fmt(format_args!( @@ -450,8 +468,14 @@ pub trait AggregateUDFImpl: Debug + Send + Sync { }; if let Some(order_by) = order_by { + let clause = match self.is_ordered_set_aggregate() { + true => "WITHIN GROUP", + false => "ORDER BY", + }; + schema_name.write_fmt(format_args!( - " ORDER BY [{}]", + " {} [{}]", + clause, schema_name_from_sorts(order_by)? ))?; }; @@ -891,6 +915,18 @@ pub trait AggregateUDFImpl: Debug + Send + Sync { ScalarValue::try_from(data_type) } + /// If this function supports `[IGNORE NULLS | RESPECT NULLS]` clause, return true + /// If the function does not, return false + fn supports_null_handling_clause(&self) -> bool { + true + } + + /// If this function is ordered-set aggregate function, return true + /// If the function is not, return false + fn is_ordered_set_aggregate(&self) -> bool { + false + } + /// Returns the documentation for this Aggregate UDF. /// /// Documentation can be accessed programmatically as well as diff --git a/datafusion/ffi/Cargo.toml b/datafusion/ffi/Cargo.toml index 5c80c1b042256..29f40df51444c 100644 --- a/datafusion/ffi/Cargo.toml +++ b/datafusion/ffi/Cargo.toml @@ -40,6 +40,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] abi_stable = "0.11.3" arrow = { workspace = true, features = ["ffi"] } +arrow-schema = { workspace = true } async-ffi = { version = "0.5.0", features = ["abi_stable"] } async-trait = { workspace = true } datafusion = { workspace = true, default-features = false } diff --git a/datafusion/ffi/src/lib.rs b/datafusion/ffi/src/lib.rs index 877129fc5bb12..d877e182a1d89 100644 --- a/datafusion/ffi/src/lib.rs +++ b/datafusion/ffi/src/lib.rs @@ -35,6 +35,7 @@ pub mod session_config; pub mod table_provider; pub mod table_source; pub mod udf; +pub mod udtf; pub mod util; pub mod volatility; diff --git a/datafusion/ffi/src/table_provider.rs b/datafusion/ffi/src/table_provider.rs index a7391a85031e0..890511997a706 100644 --- a/datafusion/ffi/src/table_provider.rs +++ b/datafusion/ffi/src/table_provider.rs @@ -110,8 +110,8 @@ pub struct FFI_TableProvider { /// * `session_config` - session configuration /// * `projections` - if specified, only a subset of the columns are returned /// * `filters_serialized` - filters to apply to the scan, which are a - /// [`LogicalExprList`] protobuf message serialized into bytes to pass - /// across the FFI boundary. + /// [`LogicalExprList`] protobuf message serialized into bytes to pass + /// across the FFI boundary. /// * `limit` - if specified, limit the number of rows returned pub scan: unsafe extern "C" fn( provider: &Self, @@ -259,14 +259,10 @@ unsafe extern "C" fn scan_fn_wrapper( }; let projections: Vec<_> = projections.into_iter().collect(); - let maybe_projections = match projections.is_empty() { - true => None, - false => Some(&projections), - }; let plan = rresult_return!( internal_provider - .scan(&ctx.state(), maybe_projections, &filters, limit.into()) + .scan(&ctx.state(), Some(&projections), &filters, limit.into()) .await ); @@ -600,4 +596,49 @@ mod tests { Ok(()) } + + #[tokio::test] + async fn test_aggregation() -> Result<()> { + use arrow::datatypes::Field; + use datafusion::arrow::{ + array::Float32Array, datatypes::DataType, record_batch::RecordBatch, + }; + use datafusion::common::assert_batches_eq; + use datafusion::datasource::MemTable; + + let schema = + Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)])); + + // define data in two partitions + let batch1 = RecordBatch::try_new( + Arc::clone(&schema), + vec![Arc::new(Float32Array::from(vec![2.0, 4.0, 8.0]))], + )?; + + let ctx = SessionContext::new(); + + let provider = Arc::new(MemTable::try_new(schema, vec![vec![batch1]])?); + + let ffi_provider = FFI_TableProvider::new(provider, true, None); + + let foreign_table_provider: ForeignTableProvider = (&ffi_provider).into(); + + ctx.register_table("t", Arc::new(foreign_table_provider))?; + + let result = ctx + .sql("SELECT COUNT(*) as cnt FROM t") + .await? + .collect() + .await?; + #[rustfmt::skip] + let expected = [ + "+-----+", + "| cnt |", + "+-----+", + "| 3 |", + "+-----+" + ]; + assert_batches_eq!(expected, &result); + Ok(()) + } } diff --git a/datafusion/ffi/src/tests/mod.rs b/datafusion/ffi/src/tests/mod.rs index 4b4a29276d9a8..7a36ee52bdb4b 100644 --- a/datafusion/ffi/src/tests/mod.rs +++ b/datafusion/ffi/src/tests/mod.rs @@ -27,7 +27,7 @@ use abi_stable::{ }; use catalog::create_catalog_provider; -use crate::catalog_provider::FFI_CatalogProvider; +use crate::{catalog_provider::FFI_CatalogProvider, udtf::FFI_TableFunction}; use super::{table_provider::FFI_TableProvider, udf::FFI_ScalarUDF}; use arrow::array::RecordBatch; @@ -37,12 +37,13 @@ use datafusion::{ common::record_batch, }; use sync_provider::create_sync_table_provider; -use udf_udaf_udwf::create_ffi_abs_func; +use udf_udaf_udwf::{create_ffi_abs_func, create_ffi_random_func, create_ffi_table_func}; mod async_provider; pub mod catalog; mod sync_provider; mod udf_udaf_udwf; +pub mod utils; #[repr(C)] #[derive(StableAbi)] @@ -60,6 +61,10 @@ pub struct ForeignLibraryModule { /// Create a scalar UDF pub create_scalar_udf: extern "C" fn() -> FFI_ScalarUDF, + pub create_nullary_udf: extern "C" fn() -> FFI_ScalarUDF, + + pub create_table_function: extern "C" fn() -> FFI_TableFunction, + pub version: extern "C" fn() -> u64, } @@ -105,6 +110,8 @@ pub fn get_foreign_library_module() -> ForeignLibraryModuleRef { create_catalog: create_catalog_provider, create_table: construct_table_provider, create_scalar_udf: create_ffi_abs_func, + create_nullary_udf: create_ffi_random_func, + create_table_function: create_ffi_table_func, version: super::version, } .leak_into_prefix() diff --git a/datafusion/ffi/src/tests/udf_udaf_udwf.rs b/datafusion/ffi/src/tests/udf_udaf_udwf.rs index e8a13aac13081..c3cb1bcc35338 100644 --- a/datafusion/ffi/src/tests/udf_udaf_udwf.rs +++ b/datafusion/ffi/src/tests/udf_udaf_udwf.rs @@ -15,8 +15,13 @@ // specific language governing permissions and limitations // under the License. -use crate::udf::FFI_ScalarUDF; -use datafusion::{functions::math::abs::AbsFunc, logical_expr::ScalarUDF}; +use crate::{udf::FFI_ScalarUDF, udtf::FFI_TableFunction}; +use datafusion::{ + catalog::TableFunctionImpl, + functions::math::{abs::AbsFunc, random::RandomFunc}, + functions_table::generate_series::RangeFunc, + logical_expr::ScalarUDF, +}; use std::sync::Arc; @@ -25,3 +30,15 @@ pub(crate) extern "C" fn create_ffi_abs_func() -> FFI_ScalarUDF { udf.into() } + +pub(crate) extern "C" fn create_ffi_random_func() -> FFI_ScalarUDF { + let udf: Arc = Arc::new(RandomFunc::new().into()); + + udf.into() +} + +pub(crate) extern "C" fn create_ffi_table_func() -> FFI_TableFunction { + let udtf: Arc = Arc::new(RangeFunc {}); + + FFI_TableFunction::new(udtf, None) +} diff --git a/datafusion/ffi/src/tests/utils.rs b/datafusion/ffi/src/tests/utils.rs new file mode 100644 index 0000000000000..6465b17d9b60c --- /dev/null +++ b/datafusion/ffi/src/tests/utils.rs @@ -0,0 +1,87 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use crate::tests::ForeignLibraryModuleRef; +use abi_stable::library::RootModule; +use datafusion::error::{DataFusionError, Result}; +use std::path::Path; + +/// Compute the path to the library. It would be preferable to simply use +/// abi_stable::library::development_utils::compute_library_path however +/// our current CI pipeline has a `ci` profile that we need to use to +/// find the library. +pub fn compute_library_path( + target_path: &Path, +) -> std::io::Result { + let debug_dir = target_path.join("debug"); + let release_dir = target_path.join("release"); + let ci_dir = target_path.join("ci"); + + let debug_path = M::get_library_path(&debug_dir.join("deps")); + let release_path = M::get_library_path(&release_dir.join("deps")); + let ci_path = M::get_library_path(&ci_dir.join("deps")); + + let all_paths = vec![ + (debug_dir.clone(), debug_path), + (release_dir, release_path), + (ci_dir, ci_path), + ]; + + let best_path = all_paths + .into_iter() + .filter(|(_, path)| path.exists()) + .filter_map(|(dir, path)| path.metadata().map(|m| (dir, m)).ok()) + .filter_map(|(dir, meta)| meta.modified().map(|m| (dir, m)).ok()) + .max_by_key(|(_, date)| *date) + .map(|(dir, _)| dir) + .unwrap_or(debug_dir); + + Ok(best_path) +} + +pub fn get_module() -> Result { + let expected_version = crate::version(); + + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let target_dir = crate_root + .parent() + .expect("Failed to find crate parent") + .parent() + .expect("Failed to find workspace root") + .join("target"); + + // Find the location of the library. This is specific to the build environment, + // so you will need to change the approach here based on your use case. + // let target: &std::path::Path = "../../../../target/".as_ref(); + let library_path = + compute_library_path::(target_dir.as_path()) + .map_err(|e| DataFusionError::External(Box::new(e)))? + .join("deps"); + + // Load the module + let module = ForeignLibraryModuleRef::load_from_directory(&library_path) + .map_err(|e| DataFusionError::External(Box::new(e)))?; + + assert_eq!( + module + .version() + .expect("Unable to call version on FFI module")(), + expected_version + ); + + Ok(module) +} diff --git a/datafusion/ffi/src/udf.rs b/datafusion/ffi/src/udf/mod.rs similarity index 87% rename from datafusion/ffi/src/udf.rs rename to datafusion/ffi/src/udf/mod.rs index bbc9cf936ceec..706b9fabedcb4 100644 --- a/datafusion/ffi/src/udf.rs +++ b/datafusion/ffi/src/udf/mod.rs @@ -29,7 +29,9 @@ use arrow::{ }; use datafusion::{ error::DataFusionError, - logical_expr::type_coercion::functions::data_types_with_scalar_udf, + logical_expr::{ + type_coercion::functions::data_types_with_scalar_udf, ReturnInfo, ReturnTypeArgs, + }, }; use datafusion::{ error::Result, @@ -37,6 +39,10 @@ use datafusion::{ ColumnarValue, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, }, }; +use return_info::FFI_ReturnInfo; +use return_type_args::{ + FFI_ReturnTypeArgs, ForeignReturnTypeArgs, ForeignReturnTypeArgsOwned, +}; use crate::{ arrow_wrappers::{WrappedArray, WrappedSchema}, @@ -45,6 +51,9 @@ use crate::{ volatility::FFI_Volatility, }; +pub mod return_info; +pub mod return_type_args; + /// A stable struct for sharing a [`ScalarUDF`] across FFI boundaries. #[repr(C)] #[derive(Debug, StableAbi)] @@ -66,6 +75,14 @@ pub struct FFI_ScalarUDF { arg_types: RVec, ) -> RResult, + /// Determines the return info of the underlying [`ScalarUDF`]. Either this + /// or return_type may be implemented on a UDF. + pub return_type_from_args: unsafe extern "C" fn( + udf: &Self, + args: FFI_ReturnTypeArgs, + ) + -> RResult, + /// Execute the underlying [`ScalarUDF`] and return the result as a `FFI_ArrowArray` /// within an AbiStable wrapper. pub invoke_with_args: unsafe extern "C" fn( @@ -123,6 +140,23 @@ unsafe extern "C" fn return_type_fn_wrapper( rresult!(return_type) } +unsafe extern "C" fn return_type_from_args_fn_wrapper( + udf: &FFI_ScalarUDF, + args: FFI_ReturnTypeArgs, +) -> RResult { + let private_data = udf.private_data as *const ScalarUDFPrivateData; + let udf = &(*private_data).udf; + + let args: ForeignReturnTypeArgsOwned = rresult_return!((&args).try_into()); + let args_ref: ForeignReturnTypeArgs = (&args).into(); + + let return_type = udf + .return_type_from_args((&args_ref).into()) + .and_then(FFI_ReturnInfo::try_from); + + rresult!(return_type) +} + unsafe extern "C" fn coerce_types_fn_wrapper( udf: &FFI_ScalarUDF, arg_types: RVec, @@ -209,6 +243,7 @@ impl From> for FFI_ScalarUDF { short_circuits, invoke_with_args: invoke_with_args_fn_wrapper, return_type: return_type_fn_wrapper, + return_type_from_args: return_type_from_args_fn_wrapper, coerce_types: coerce_types_fn_wrapper, clone: clone_fn_wrapper, release: release_fn_wrapper, @@ -281,6 +316,16 @@ impl ScalarUDFImpl for ForeignScalarUDF { result.and_then(|r| (&r.0).try_into().map_err(DataFusionError::from)) } + fn return_type_from_args(&self, args: ReturnTypeArgs) -> Result { + let args: FFI_ReturnTypeArgs = args.try_into()?; + + let result = unsafe { (self.udf.return_type_from_args)(&self.udf, args) }; + + let result = df_result!(result); + + result.and_then(|r| r.try_into()) + } + fn invoke_with_args(&self, invoke_args: ScalarFunctionArgs) -> Result { let ScalarFunctionArgs { args, diff --git a/datafusion/ffi/src/udf/return_info.rs b/datafusion/ffi/src/udf/return_info.rs new file mode 100644 index 0000000000000..cf76ddd1db762 --- /dev/null +++ b/datafusion/ffi/src/udf/return_info.rs @@ -0,0 +1,53 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use abi_stable::StableAbi; +use arrow::{datatypes::DataType, ffi::FFI_ArrowSchema}; +use datafusion::{error::DataFusionError, logical_expr::ReturnInfo}; + +use crate::arrow_wrappers::WrappedSchema; + +/// A stable struct for sharing a [`ReturnInfo`] across FFI boundaries. +#[repr(C)] +#[derive(Debug, StableAbi)] +#[allow(non_camel_case_types)] +pub struct FFI_ReturnInfo { + return_type: WrappedSchema, + nullable: bool, +} + +impl TryFrom for FFI_ReturnInfo { + type Error = DataFusionError; + + fn try_from(value: ReturnInfo) -> Result { + let return_type = WrappedSchema(FFI_ArrowSchema::try_from(value.return_type())?); + Ok(Self { + return_type, + nullable: value.nullable(), + }) + } +} + +impl TryFrom for ReturnInfo { + type Error = DataFusionError; + + fn try_from(value: FFI_ReturnInfo) -> Result { + let return_type = DataType::try_from(&value.return_type.0)?; + + Ok(ReturnInfo::new(return_type, value.nullable)) + } +} diff --git a/datafusion/ffi/src/udf/return_type_args.rs b/datafusion/ffi/src/udf/return_type_args.rs new file mode 100644 index 0000000000000..a0897630e2ea9 --- /dev/null +++ b/datafusion/ffi/src/udf/return_type_args.rs @@ -0,0 +1,142 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use abi_stable::{ + std_types::{ROption, RVec}, + StableAbi, +}; +use arrow::datatypes::DataType; +use datafusion::{ + common::exec_datafusion_err, error::DataFusionError, logical_expr::ReturnTypeArgs, + scalar::ScalarValue, +}; + +use crate::{ + arrow_wrappers::WrappedSchema, + util::{rvec_wrapped_to_vec_datatype, vec_datatype_to_rvec_wrapped}, +}; +use prost::Message; + +/// A stable struct for sharing a [`ReturnTypeArgs`] across FFI boundaries. +#[repr(C)] +#[derive(Debug, StableAbi)] +#[allow(non_camel_case_types)] +pub struct FFI_ReturnTypeArgs { + arg_types: RVec, + scalar_arguments: RVec>>, + nullables: RVec, +} + +impl TryFrom> for FFI_ReturnTypeArgs { + type Error = DataFusionError; + + fn try_from(value: ReturnTypeArgs) -> Result { + let arg_types = vec_datatype_to_rvec_wrapped(value.arg_types)?; + let scalar_arguments: Result, Self::Error> = value + .scalar_arguments + .iter() + .map(|maybe_arg| { + maybe_arg + .map(|arg| { + let proto_value: datafusion_proto::protobuf::ScalarValue = + arg.try_into()?; + let proto_bytes: RVec = proto_value.encode_to_vec().into(); + Ok(proto_bytes) + }) + .transpose() + }) + .collect(); + let scalar_arguments = scalar_arguments?.into_iter().map(ROption::from).collect(); + + let nullables = value.nullables.into(); + Ok(Self { + arg_types, + scalar_arguments, + nullables, + }) + } +} + +// TODO(tsaucer) It would be good to find a better way around this, but it +// appears a restriction based on the need to have a borrowed ScalarValue +// in the arguments when converted to ReturnTypeArgs +pub struct ForeignReturnTypeArgsOwned { + arg_types: Vec, + scalar_arguments: Vec>, + nullables: Vec, +} + +pub struct ForeignReturnTypeArgs<'a> { + arg_types: &'a [DataType], + scalar_arguments: Vec>, + nullables: &'a [bool], +} + +impl TryFrom<&FFI_ReturnTypeArgs> for ForeignReturnTypeArgsOwned { + type Error = DataFusionError; + + fn try_from(value: &FFI_ReturnTypeArgs) -> Result { + let arg_types = rvec_wrapped_to_vec_datatype(&value.arg_types)?; + let scalar_arguments: Result, Self::Error> = value + .scalar_arguments + .iter() + .map(|maybe_arg| { + let maybe_arg = maybe_arg.as_ref().map(|arg| { + let proto_value = + datafusion_proto::protobuf::ScalarValue::decode(arg.as_ref()) + .map_err(|err| exec_datafusion_err!("{}", err))?; + let scalar_value: ScalarValue = (&proto_value).try_into()?; + Ok(scalar_value) + }); + Option::from(maybe_arg).transpose() + }) + .collect(); + let scalar_arguments = scalar_arguments?.into_iter().collect(); + + let nullables = value.nullables.iter().cloned().collect(); + + Ok(Self { + arg_types, + scalar_arguments, + nullables, + }) + } +} + +impl<'a> From<&'a ForeignReturnTypeArgsOwned> for ForeignReturnTypeArgs<'a> { + fn from(value: &'a ForeignReturnTypeArgsOwned) -> Self { + Self { + arg_types: &value.arg_types, + scalar_arguments: value + .scalar_arguments + .iter() + .map(|opt| opt.as_ref()) + .collect(), + nullables: &value.nullables, + } + } +} + +impl<'a> From<&'a ForeignReturnTypeArgs<'a>> for ReturnTypeArgs<'a> { + fn from(value: &'a ForeignReturnTypeArgs) -> Self { + ReturnTypeArgs { + arg_types: value.arg_types, + scalar_arguments: &value.scalar_arguments, + nullables: value.nullables, + } + } +} diff --git a/datafusion/ffi/src/udtf.rs b/datafusion/ffi/src/udtf.rs new file mode 100644 index 0000000000000..1e06247546be7 --- /dev/null +++ b/datafusion/ffi/src/udtf.rs @@ -0,0 +1,321 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::{ffi::c_void, sync::Arc}; + +use abi_stable::{ + std_types::{RResult, RString, RVec}, + StableAbi, +}; + +use datafusion::error::Result; +use datafusion::{ + catalog::{TableFunctionImpl, TableProvider}, + prelude::{Expr, SessionContext}, +}; +use datafusion_proto::{ + logical_plan::{ + from_proto::parse_exprs, to_proto::serialize_exprs, DefaultLogicalExtensionCodec, + }, + protobuf::LogicalExprList, +}; +use prost::Message; +use tokio::runtime::Handle; + +use crate::{ + df_result, rresult_return, + table_provider::{FFI_TableProvider, ForeignTableProvider}, +}; + +/// A stable struct for sharing a [`TableFunctionImpl`] across FFI boundaries. +#[repr(C)] +#[derive(Debug, StableAbi)] +#[allow(non_camel_case_types)] +pub struct FFI_TableFunction { + /// Equivalent to the `call` function of the TableFunctionImpl. + /// The arguments are Expr passed as protobuf encoded bytes. + pub call: unsafe extern "C" fn( + udtf: &Self, + args: RVec, + ) -> RResult, + + /// Used to create a clone on the provider of the udtf. This should + /// only need to be called by the receiver of the udtf. + pub clone: unsafe extern "C" fn(udtf: &Self) -> Self, + + /// Release the memory of the private data when it is no longer being used. + pub release: unsafe extern "C" fn(udtf: &mut Self), + + /// Internal data. This is only to be accessed by the provider of the udtf. + /// A [`ForeignTableFunction`] should never attempt to access this data. + pub private_data: *mut c_void, +} + +unsafe impl Send for FFI_TableFunction {} +unsafe impl Sync for FFI_TableFunction {} + +pub struct TableFunctionPrivateData { + udtf: Arc, + runtime: Option, +} + +impl FFI_TableFunction { + fn inner(&self) -> &Arc { + let private_data = self.private_data as *const TableFunctionPrivateData; + unsafe { &(*private_data).udtf } + } + + fn runtime(&self) -> Option { + let private_data = self.private_data as *const TableFunctionPrivateData; + unsafe { (*private_data).runtime.clone() } + } +} + +unsafe extern "C" fn call_fn_wrapper( + udtf: &FFI_TableFunction, + args: RVec, +) -> RResult { + let runtime = udtf.runtime(); + let udtf = udtf.inner(); + + let default_ctx = SessionContext::new(); + let codec = DefaultLogicalExtensionCodec {}; + + let proto_filters = rresult_return!(LogicalExprList::decode(args.as_ref())); + + let args = + rresult_return!(parse_exprs(proto_filters.expr.iter(), &default_ctx, &codec)); + + let table_provider = rresult_return!(udtf.call(&args)); + RResult::ROk(FFI_TableProvider::new(table_provider, false, runtime)) +} + +unsafe extern "C" fn release_fn_wrapper(udtf: &mut FFI_TableFunction) { + let private_data = Box::from_raw(udtf.private_data as *mut TableFunctionPrivateData); + drop(private_data); +} + +unsafe extern "C" fn clone_fn_wrapper(udtf: &FFI_TableFunction) -> FFI_TableFunction { + let runtime = udtf.runtime(); + let udtf = udtf.inner(); + + FFI_TableFunction::new(Arc::clone(udtf), runtime) +} + +impl Clone for FFI_TableFunction { + fn clone(&self) -> Self { + unsafe { (self.clone)(self) } + } +} + +impl FFI_TableFunction { + pub fn new(udtf: Arc, runtime: Option) -> Self { + let private_data = Box::new(TableFunctionPrivateData { udtf, runtime }); + + Self { + call: call_fn_wrapper, + clone: clone_fn_wrapper, + release: release_fn_wrapper, + private_data: Box::into_raw(private_data) as *mut c_void, + } + } +} + +impl From> for FFI_TableFunction { + fn from(udtf: Arc) -> Self { + let private_data = Box::new(TableFunctionPrivateData { + udtf, + runtime: None, + }); + + Self { + call: call_fn_wrapper, + clone: clone_fn_wrapper, + release: release_fn_wrapper, + private_data: Box::into_raw(private_data) as *mut c_void, + } + } +} + +impl Drop for FFI_TableFunction { + fn drop(&mut self) { + unsafe { (self.release)(self) } + } +} + +/// This struct is used to access an UDTF provided by a foreign +/// library across a FFI boundary. +/// +/// The ForeignTableFunction is to be used by the caller of the UDTF, so it has +/// no knowledge or access to the private data. All interaction with the UDTF +/// must occur through the functions defined in FFI_TableFunction. +#[derive(Debug)] +pub struct ForeignTableFunction(FFI_TableFunction); + +unsafe impl Send for ForeignTableFunction {} +unsafe impl Sync for ForeignTableFunction {} + +impl From for ForeignTableFunction { + fn from(value: FFI_TableFunction) -> Self { + Self(value) + } +} + +impl TableFunctionImpl for ForeignTableFunction { + fn call(&self, args: &[Expr]) -> Result> { + let codec = DefaultLogicalExtensionCodec {}; + let expr_list = LogicalExprList { + expr: serialize_exprs(args, &codec)?, + }; + let filters_serialized = expr_list.encode_to_vec().into(); + + let table_provider = unsafe { (self.0.call)(&self.0, filters_serialized) }; + + let table_provider = df_result!(table_provider)?; + let table_provider: ForeignTableProvider = (&table_provider).into(); + + Ok(Arc::new(table_provider)) + } +} + +#[cfg(test)] +mod tests { + use arrow::{ + array::{ + record_batch, ArrayRef, Float64Array, RecordBatch, StringArray, UInt64Array, + }, + datatypes::{DataType, Field, Schema}, + }; + use datafusion::{ + catalog::MemTable, common::exec_err, prelude::lit, scalar::ScalarValue, + }; + + use super::*; + + #[derive(Debug)] + struct TestUDTF {} + + impl TableFunctionImpl for TestUDTF { + fn call(&self, args: &[Expr]) -> Result> { + let args = args + .iter() + .map(|arg| { + if let Expr::Literal(scalar) = arg { + Ok(scalar) + } else { + exec_err!("Expected only literal arguments to table udf") + } + }) + .collect::>>()?; + + if args.len() < 2 { + exec_err!("Expected at least two arguments to table udf")? + } + + let ScalarValue::UInt64(Some(num_rows)) = args[0].to_owned() else { + exec_err!( + "First argument must be the number of elements to create as u64" + )? + }; + let num_rows = num_rows as usize; + + let mut fields = Vec::default(); + let mut arrays1 = Vec::default(); + let mut arrays2 = Vec::default(); + + let split = num_rows / 3; + for (idx, arg) in args[1..].iter().enumerate() { + let (field, array) = match arg { + ScalarValue::Utf8(s) => { + let s_vec = vec![s.to_owned(); num_rows]; + ( + Field::new(format!("field-{}", idx), DataType::Utf8, true), + Arc::new(StringArray::from(s_vec)) as ArrayRef, + ) + } + ScalarValue::UInt64(v) => { + let v_vec = vec![v.to_owned(); num_rows]; + ( + Field::new(format!("field-{}", idx), DataType::UInt64, true), + Arc::new(UInt64Array::from(v_vec)) as ArrayRef, + ) + } + ScalarValue::Float64(v) => { + let v_vec = vec![v.to_owned(); num_rows]; + ( + Field::new(format!("field-{}", idx), DataType::Float64, true), + Arc::new(Float64Array::from(v_vec)) as ArrayRef, + ) + } + _ => exec_err!( + "Test case only supports utf8, u64, and f64. Found {}", + arg.data_type() + )?, + }; + + fields.push(field); + arrays1.push(array.slice(0, split)); + arrays2.push(array.slice(split, num_rows - split)); + } + + let schema = Arc::new(Schema::new(fields)); + let batches = vec![ + RecordBatch::try_new(Arc::clone(&schema), arrays1)?, + RecordBatch::try_new(Arc::clone(&schema), arrays2)?, + ]; + + let table_provider = MemTable::try_new(schema, vec![batches])?; + + Ok(Arc::new(table_provider)) + } + } + + #[tokio::test] + async fn test_round_trip_udtf() -> Result<()> { + let original_udtf = Arc::new(TestUDTF {}) as Arc; + + let local_udtf: FFI_TableFunction = + FFI_TableFunction::new(Arc::clone(&original_udtf), None); + + let foreign_udf: ForeignTableFunction = local_udtf.into(); + + let table = + foreign_udf.call(&vec![lit(6_u64), lit("one"), lit(2.0), lit(3_u64)])?; + + let ctx = SessionContext::default(); + let _ = ctx.register_table("test-table", table)?; + + let returned_batches = ctx.table("test-table").await?.collect().await?; + + assert_eq!(returned_batches.len(), 2); + let expected_batch_0 = record_batch!( + ("field-0", Utf8, ["one", "one"]), + ("field-1", Float64, [2.0, 2.0]), + ("field-2", UInt64, [3, 3]) + )?; + assert_eq!(returned_batches[0], expected_batch_0); + + let expected_batch_1 = record_batch!( + ("field-0", Utf8, ["one", "one", "one", "one"]), + ("field-1", Float64, [2.0, 2.0, 2.0, 2.0]), + ("field-2", UInt64, [3, 3, 3, 3]) + )?; + assert_eq!(returned_batches[1], expected_batch_1); + + Ok(()) + } +} diff --git a/datafusion/ffi/tests/ffi_integration.rs b/datafusion/ffi/tests/ffi_integration.rs index f610f12c8244e..c6df324e9a17c 100644 --- a/datafusion/ffi/tests/ffi_integration.rs +++ b/datafusion/ffi/tests/ffi_integration.rs @@ -20,84 +20,14 @@ #[cfg(feature = "integration-tests")] mod tests { - use abi_stable::library::RootModule; - use datafusion::common::record_batch; use datafusion::error::{DataFusionError, Result}; - use datafusion::logical_expr::ScalarUDF; - use datafusion::prelude::{col, SessionContext}; + use datafusion::prelude::SessionContext; use datafusion_ffi::catalog_provider::ForeignCatalogProvider; use datafusion_ffi::table_provider::ForeignTableProvider; - use datafusion_ffi::tests::{create_record_batch, ForeignLibraryModuleRef}; - use datafusion_ffi::udf::ForeignScalarUDF; - use std::path::Path; + use datafusion_ffi::tests::create_record_batch; + use datafusion_ffi::tests::utils::get_module; use std::sync::Arc; - /// Compute the path to the library. It would be preferable to simply use - /// abi_stable::library::development_utils::compute_library_path however - /// our current CI pipeline has a `ci` profile that we need to use to - /// find the library. - pub fn compute_library_path( - target_path: &Path, - ) -> std::io::Result { - let debug_dir = target_path.join("debug"); - let release_dir = target_path.join("release"); - let ci_dir = target_path.join("ci"); - - let debug_path = M::get_library_path(&debug_dir.join("deps")); - let release_path = M::get_library_path(&release_dir.join("deps")); - let ci_path = M::get_library_path(&ci_dir.join("deps")); - - let all_paths = vec![ - (debug_dir.clone(), debug_path), - (release_dir, release_path), - (ci_dir, ci_path), - ]; - - let best_path = all_paths - .into_iter() - .filter(|(_, path)| path.exists()) - .filter_map(|(dir, path)| path.metadata().map(|m| (dir, m)).ok()) - .filter_map(|(dir, meta)| meta.modified().map(|m| (dir, m)).ok()) - .max_by_key(|(_, date)| *date) - .map(|(dir, _)| dir) - .unwrap_or(debug_dir); - - Ok(best_path) - } - - fn get_module() -> Result { - let expected_version = datafusion_ffi::version(); - - let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let target_dir = crate_root - .parent() - .expect("Failed to find crate parent") - .parent() - .expect("Failed to find workspace root") - .join("target"); - - // Find the location of the library. This is specific to the build environment, - // so you will need to change the approach here based on your use case. - // let target: &std::path::Path = "../../../../target/".as_ref(); - let library_path = - compute_library_path::(target_dir.as_path()) - .map_err(|e| DataFusionError::External(Box::new(e)))? - .join("deps"); - - // Load the module - let module = ForeignLibraryModuleRef::load_from_directory(&library_path) - .map_err(|e| DataFusionError::External(Box::new(e)))?; - - assert_eq!( - module - .version() - .expect("Unable to call version on FFI module")(), - expected_version - ); - - Ok(module) - } - /// It is important that this test is in the `tests` directory and not in the /// library directory so we can verify we are building a dynamic library and /// testing it via a different executable. @@ -141,46 +71,6 @@ mod tests { test_table_provider(true).await } - /// This test validates that we can load an external module and use a scalar - /// udf defined in it via the foreign function interface. In this case we are - /// using the abs() function as our scalar UDF. - #[tokio::test] - async fn test_scalar_udf() -> Result<()> { - let module = get_module()?; - - let ffi_abs_func = - module - .create_scalar_udf() - .ok_or(DataFusionError::NotImplemented( - "External table provider failed to implement create_scalar_udf" - .to_string(), - ))?(); - let foreign_abs_func: ForeignScalarUDF = (&ffi_abs_func).try_into()?; - - let udf: ScalarUDF = foreign_abs_func.into(); - - let ctx = SessionContext::default(); - let df = ctx.read_batch(create_record_batch(-5, 5))?; - - let df = df - .with_column("abs_a", udf.call(vec![col("a")]))? - .with_column("abs_b", udf.call(vec![col("b")]))?; - - let result = df.collect().await?; - - let expected = record_batch!( - ("a", Int32, vec![-5, -4, -3, -2, -1]), - ("b", Float64, vec![-5., -4., -3., -2., -1.]), - ("abs_a", Int32, vec![5, 4, 3, 2, 1]), - ("abs_b", Float64, vec![5., 4., 3., 2., 1.]) - )?; - - assert!(result.len() == 1); - assert!(result[0] == expected); - - Ok(()) - } - #[tokio::test] async fn test_catalog() -> Result<()> { let module = get_module()?; diff --git a/datafusion/ffi/tests/ffi_udf.rs b/datafusion/ffi/tests/ffi_udf.rs new file mode 100644 index 0000000000000..bbc23552def43 --- /dev/null +++ b/datafusion/ffi/tests/ffi_udf.rs @@ -0,0 +1,104 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/// Add an additional module here for convenience to scope this to only +/// when the feature integtation-tests is built +#[cfg(feature = "integration-tests")] +mod tests { + + use arrow::datatypes::DataType; + use datafusion::common::record_batch; + use datafusion::error::{DataFusionError, Result}; + use datafusion::logical_expr::ScalarUDF; + use datafusion::prelude::{col, SessionContext}; + + use datafusion_ffi::tests::create_record_batch; + use datafusion_ffi::tests::utils::get_module; + use datafusion_ffi::udf::ForeignScalarUDF; + + /// This test validates that we can load an external module and use a scalar + /// udf defined in it via the foreign function interface. In this case we are + /// using the abs() function as our scalar UDF. + #[tokio::test] + async fn test_scalar_udf() -> Result<()> { + let module = get_module()?; + + let ffi_abs_func = + module + .create_scalar_udf() + .ok_or(DataFusionError::NotImplemented( + "External table provider failed to implement create_scalar_udf" + .to_string(), + ))?(); + let foreign_abs_func: ForeignScalarUDF = (&ffi_abs_func).try_into()?; + + let udf: ScalarUDF = foreign_abs_func.into(); + + let ctx = SessionContext::default(); + let df = ctx.read_batch(create_record_batch(-5, 5))?; + + let df = df + .with_column("abs_a", udf.call(vec![col("a")]))? + .with_column("abs_b", udf.call(vec![col("b")]))?; + + let result = df.collect().await?; + + let expected = record_batch!( + ("a", Int32, vec![-5, -4, -3, -2, -1]), + ("b", Float64, vec![-5., -4., -3., -2., -1.]), + ("abs_a", Int32, vec![5, 4, 3, 2, 1]), + ("abs_b", Float64, vec![5., 4., 3., 2., 1.]) + )?; + + assert!(result.len() == 1); + assert!(result[0] == expected); + + Ok(()) + } + + /// This test validates nullary input UDFs + #[tokio::test] + async fn test_nullary_scalar_udf() -> Result<()> { + let module = get_module()?; + + let ffi_abs_func = + module + .create_nullary_udf() + .ok_or(DataFusionError::NotImplemented( + "External table provider failed to implement create_scalar_udf" + .to_string(), + ))?(); + let foreign_abs_func: ForeignScalarUDF = (&ffi_abs_func).try_into()?; + + let udf: ScalarUDF = foreign_abs_func.into(); + + let ctx = SessionContext::default(); + let df = ctx.read_batch(create_record_batch(-5, 5))?; + + let df = df.with_column("time_now", udf.call(vec![]))?; + + let result = df.collect().await?; + + assert!(result.len() == 1); + assert_eq!( + result[0].column_by_name("time_now").unwrap().data_type(), + &DataType::Float64 + ); + + Ok(()) + } +} diff --git a/datafusion/ffi/tests/ffi_udtf.rs b/datafusion/ffi/tests/ffi_udtf.rs new file mode 100644 index 0000000000000..5a46211d3b9c6 --- /dev/null +++ b/datafusion/ffi/tests/ffi_udtf.rs @@ -0,0 +1,64 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +/// Add an additional module here for convenience to scope this to only +/// when the feature integtation-tests is built +#[cfg(feature = "integration-tests")] +mod tests { + + use std::sync::Arc; + + use arrow::array::{create_array, ArrayRef}; + use datafusion::error::{DataFusionError, Result}; + use datafusion::prelude::SessionContext; + + use datafusion_ffi::tests::utils::get_module; + use datafusion_ffi::udtf::ForeignTableFunction; + + /// This test validates that we can load an external module and use a scalar + /// udf defined in it via the foreign function interface. In this case we are + /// using the abs() function as our scalar UDF. + #[tokio::test] + async fn test_user_defined_table_function() -> Result<()> { + let module = get_module()?; + + let ffi_table_func = module + .create_table_function() + .ok_or(DataFusionError::NotImplemented( + "External table function provider failed to implement create_table_function" + .to_string(), + ))?(); + let foreign_table_func: ForeignTableFunction = ffi_table_func.into(); + + let udtf = Arc::new(foreign_table_func); + + let ctx = SessionContext::default(); + ctx.register_udtf("my_range", udtf); + + let result = ctx + .sql("SELECT * FROM my_range(5)") + .await? + .collect() + .await?; + let expected = create_array!(Int64, [0, 1, 2, 3, 4]) as ArrayRef; + + assert!(result.len() == 1); + assert!(result[0].column(0) == &expected); + + Ok(()) + } +} diff --git a/datafusion/functions-aggregate/benches/array_agg.rs b/datafusion/functions-aggregate/benches/array_agg.rs index fb605e87ed0cc..e22be611d8d76 100644 --- a/datafusion/functions-aggregate/benches/array_agg.rs +++ b/datafusion/functions-aggregate/benches/array_agg.rs @@ -19,17 +19,23 @@ use std::sync::Arc; use arrow::array::{ Array, ArrayRef, ArrowPrimitiveType, AsArray, ListArray, NullBufferBuilder, + PrimitiveArray, }; use arrow::datatypes::{Field, Int64Type}; -use arrow::util::bench_util::create_primitive_array; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use datafusion_expr::Accumulator; use datafusion_functions_aggregate::array_agg::ArrayAggAccumulator; use arrow::buffer::OffsetBuffer; -use arrow::util::test_util::seedable_rng; use rand::distributions::{Distribution, Standard}; +use rand::prelude::StdRng; use rand::Rng; +use rand::SeedableRng; + +/// Returns fixed seedable RNG +pub fn seedable_rng() -> StdRng { + StdRng::seed_from_u64(42) +} fn merge_batch_bench(c: &mut Criterion, name: &str, values: ArrayRef) { let list_item_data_type = values.as_list::().values().data_type().clone(); @@ -46,6 +52,24 @@ fn merge_batch_bench(c: &mut Criterion, name: &str, values: ArrayRef) { }); } +pub fn create_primitive_array(size: usize, null_density: f32) -> PrimitiveArray +where + T: ArrowPrimitiveType, + Standard: Distribution, +{ + let mut rng = seedable_rng(); + + (0..size) + .map(|_| { + if rng.gen::() < null_density { + None + } else { + Some(rng.gen()) + } + }) + .collect() +} + /// Create List array with the given item data type, null density, null locations and zero length lists density /// Creates an random (but fixed-seeded) array of a given size and null density pub fn create_list_array( diff --git a/datafusion/functions-aggregate/src/approx_median.rs b/datafusion/functions-aggregate/src/approx_median.rs index 787e08bae2867..9a202879d94ab 100644 --- a/datafusion/functions-aggregate/src/approx_median.rs +++ b/datafusion/functions-aggregate/src/approx_median.rs @@ -45,7 +45,7 @@ make_udaf_expr_and_func!( /// APPROX_MEDIAN aggregate expression #[user_doc( doc_section(label = "Approximate Functions"), - description = "Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(x, 0.5)`.", + description = "Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY x)`.", syntax_example = "approx_median(expression)", sql_example = r#"```sql > SELECT approx_median(column_name) FROM table_name; diff --git a/datafusion/functions-aggregate/src/approx_percentile_cont.rs b/datafusion/functions-aggregate/src/approx_percentile_cont.rs index 1fad5f73703c7..41281733f5deb 100644 --- a/datafusion/functions-aggregate/src/approx_percentile_cont.rs +++ b/datafusion/functions-aggregate/src/approx_percentile_cont.rs @@ -34,6 +34,7 @@ use datafusion_common::{ downcast_value, internal_err, not_impl_datafusion_err, not_impl_err, plan_err, Result, ScalarValue, }; +use datafusion_expr::expr::{AggregateFunction, Sort}; use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs}; use datafusion_expr::type_coercion::aggregates::{INTEGERS, NUMERICS}; use datafusion_expr::utils::format_state_name; @@ -51,29 +52,39 @@ create_func!(ApproxPercentileCont, approx_percentile_cont_udaf); /// Computes the approximate percentile continuous of a set of numbers pub fn approx_percentile_cont( - expression: Expr, + order_by: Sort, percentile: Expr, centroids: Option, ) -> Expr { + let expr = order_by.expr.clone(); + let args = if let Some(centroids) = centroids { - vec![expression, percentile, centroids] + vec![expr, percentile, centroids] } else { - vec![expression, percentile] + vec![expr, percentile] }; - approx_percentile_cont_udaf().call(args) + + Expr::AggregateFunction(AggregateFunction::new_udf( + approx_percentile_cont_udaf(), + args, + false, + None, + Some(vec![order_by]), + None, + )) } #[user_doc( doc_section(label = "Approximate Functions"), description = "Returns the approximate percentile of input values using the t-digest algorithm.", - syntax_example = "approx_percentile_cont(expression, percentile, centroids)", + syntax_example = "approx_percentile_cont(percentile, centroids) WITHIN GROUP (ORDER BY expression)", sql_example = r#"```sql -> SELECT approx_percentile_cont(column_name, 0.75, 100) FROM table_name; -+-------------------------------------------------+ -| approx_percentile_cont(column_name, 0.75, 100) | -+-------------------------------------------------+ -| 65.0 | -+-------------------------------------------------+ +> SELECT approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) FROM table_name; ++-----------------------------------------------------------------------+ +| approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) | ++-----------------------------------------------------------------------+ +| 65.0 | ++-----------------------------------------------------------------------+ ```"#, standard_argument(name = "expression",), argument( @@ -130,6 +141,19 @@ impl ApproxPercentileCont { args: AccumulatorArgs, ) -> Result { let percentile = validate_input_percentile_expr(&args.exprs[1])?; + + let is_descending = args + .ordering_req + .first() + .map(|sort_expr| sort_expr.options.descending) + .unwrap_or(false); + + let percentile = if is_descending { + 1.0 - percentile + } else { + percentile + }; + let tdigest_max_size = if args.exprs.len() == 3 { Some(validate_input_max_size_expr(&args.exprs[2])?) } else { @@ -292,6 +316,14 @@ impl AggregateUDFImpl for ApproxPercentileCont { Ok(arg_types[0].clone()) } + fn supports_null_handling_clause(&self) -> bool { + false + } + + fn is_ordered_set_aggregate(&self) -> bool { + true + } + fn documentation(&self) -> Option<&Documentation> { self.doc() } diff --git a/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs b/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs index 16dac2c1b8f04..0316757f26d08 100644 --- a/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs +++ b/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs @@ -52,14 +52,14 @@ make_udaf_expr_and_func!( #[user_doc( doc_section(label = "Approximate Functions"), description = "Returns the weighted approximate percentile of input values using the t-digest algorithm.", - syntax_example = "approx_percentile_cont_with_weight(expression, weight, percentile)", + syntax_example = "approx_percentile_cont_with_weight(weight, percentile) WITHIN GROUP (ORDER BY expression)", sql_example = r#"```sql -> SELECT approx_percentile_cont_with_weight(column_name, weight_column, 0.90) FROM table_name; -+----------------------------------------------------------------------+ -| approx_percentile_cont_with_weight(column_name, weight_column, 0.90) | -+----------------------------------------------------------------------+ -| 78.5 | -+----------------------------------------------------------------------+ +> SELECT approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) FROM table_name; ++---------------------------------------------------------------------------------------------+ +| approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) | ++---------------------------------------------------------------------------------------------+ +| 78.5 | ++---------------------------------------------------------------------------------------------+ ```"#, standard_argument(name = "expression", prefix = "The"), argument( @@ -178,6 +178,14 @@ impl AggregateUDFImpl for ApproxPercentileContWithWeight { self.approx_percentile_cont.state_fields(args) } + fn supports_null_handling_clause(&self) -> bool { + false + } + + fn is_ordered_set_aggregate(&self) -> bool { + true + } + fn documentation(&self) -> Option<&Documentation> { self.doc() } diff --git a/datafusion/functions-aggregate/src/array_agg.rs b/datafusion/functions-aggregate/src/array_agg.rs index 573624ce4d491..d658744c1ba5d 100644 --- a/datafusion/functions-aggregate/src/array_agg.rs +++ b/datafusion/functions-aggregate/src/array_agg.rs @@ -289,7 +289,7 @@ impl Accumulator for ArrayAggAccumulator { } let val = Arc::clone(&values[0]); - if val.len() > 0 { + if !val.is_empty() { self.values.push(val); } Ok(()) @@ -310,7 +310,7 @@ impl Accumulator for ArrayAggAccumulator { match Self::get_optional_values_to_merge_as_is(list_arr) { Some(values) => { // Make sure we don't insert empty lists - if values.len() > 0 { + if !values.is_empty() { self.values.push(values); } } diff --git a/datafusion/functions-aggregate/src/average.rs b/datafusion/functions-aggregate/src/average.rs index 758a775e06d7c..30d1e09fe3cc0 100644 --- a/datafusion/functions-aggregate/src/average.rs +++ b/datafusion/functions-aggregate/src/average.rs @@ -24,8 +24,9 @@ use arrow::array::{ use arrow::compute::sum; use arrow::datatypes::{ - i256, ArrowNativeType, DataType, Decimal128Type, Decimal256Type, DecimalType, Field, - Float64Type, UInt64Type, + i256, ArrowNativeType, DataType, Decimal128Type, Decimal256Type, DecimalType, + DurationMicrosecondType, DurationMillisecondType, DurationNanosecondType, + DurationSecondType, Field, Float64Type, TimeUnit, UInt64Type, }; use datafusion_common::{ exec_err, not_impl_err, utils::take_function_args, Result, ScalarValue, @@ -151,6 +152,16 @@ impl AggregateUDFImpl for Avg { target_precision: *target_precision, target_scale: *target_scale, })), + + (Duration(time_unit), Duration(result_unit)) => { + Ok(Box::new(DurationAvgAccumulator { + sum: None, + count: 0, + time_unit: *time_unit, + result_unit: *result_unit, + })) + } + _ => exec_err!( "AvgAccumulator for ({} --> {})", &data_type, @@ -406,6 +417,105 @@ impl Accumulator for DecimalAvgAccumu } } +/// An accumulator to compute the average for duration values +#[derive(Debug)] +struct DurationAvgAccumulator { + sum: Option, + count: u64, + time_unit: TimeUnit, + result_unit: TimeUnit, +} + +impl Accumulator for DurationAvgAccumulator { + fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> { + let array = &values[0]; + self.count += (array.len() - array.null_count()) as u64; + + let sum_value = match self.time_unit { + TimeUnit::Second => sum(array.as_primitive::()), + TimeUnit::Millisecond => sum(array.as_primitive::()), + TimeUnit::Microsecond => sum(array.as_primitive::()), + TimeUnit::Nanosecond => sum(array.as_primitive::()), + }; + + if let Some(x) = sum_value { + let v = self.sum.get_or_insert(0); + *v += x; + } + Ok(()) + } + + fn evaluate(&mut self) -> Result { + let avg = self.sum.map(|sum| sum / self.count as i64); + + match self.result_unit { + TimeUnit::Second => Ok(ScalarValue::DurationSecond(avg)), + TimeUnit::Millisecond => Ok(ScalarValue::DurationMillisecond(avg)), + TimeUnit::Microsecond => Ok(ScalarValue::DurationMicrosecond(avg)), + TimeUnit::Nanosecond => Ok(ScalarValue::DurationNanosecond(avg)), + } + } + + fn size(&self) -> usize { + size_of_val(self) + } + + fn state(&mut self) -> Result> { + let duration_value = match self.time_unit { + TimeUnit::Second => ScalarValue::DurationSecond(self.sum), + TimeUnit::Millisecond => ScalarValue::DurationMillisecond(self.sum), + TimeUnit::Microsecond => ScalarValue::DurationMicrosecond(self.sum), + TimeUnit::Nanosecond => ScalarValue::DurationNanosecond(self.sum), + }; + + Ok(vec![ScalarValue::from(self.count), duration_value]) + } + + fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> { + self.count += sum(states[0].as_primitive::()).unwrap_or_default(); + + let sum_value = match self.time_unit { + TimeUnit::Second => sum(states[1].as_primitive::()), + TimeUnit::Millisecond => { + sum(states[1].as_primitive::()) + } + TimeUnit::Microsecond => { + sum(states[1].as_primitive::()) + } + TimeUnit::Nanosecond => { + sum(states[1].as_primitive::()) + } + }; + + if let Some(x) = sum_value { + let v = self.sum.get_or_insert(0); + *v += x; + } + Ok(()) + } + + fn retract_batch(&mut self, values: &[ArrayRef]) -> Result<()> { + let array = &values[0]; + self.count -= (array.len() - array.null_count()) as u64; + + let sum_value = match self.time_unit { + TimeUnit::Second => sum(array.as_primitive::()), + TimeUnit::Millisecond => sum(array.as_primitive::()), + TimeUnit::Microsecond => sum(array.as_primitive::()), + TimeUnit::Nanosecond => sum(array.as_primitive::()), + }; + + if let Some(x) = sum_value { + self.sum = Some(self.sum.unwrap() - x); + } + Ok(()) + } + + fn supports_retract_batch(&self) -> bool { + true + } +} + /// An accumulator to compute the average of `[PrimitiveArray]`. /// Stores values as native types, and does overflow checking /// diff --git a/datafusion/functions-aggregate/src/first_last.rs b/datafusion/functions-aggregate/src/first_last.rs index 28e6a8723dfd4..ec8c440b77e5f 100644 --- a/datafusion/functions-aggregate/src/first_last.rs +++ b/datafusion/functions-aggregate/src/first_last.rs @@ -52,6 +52,7 @@ use datafusion_macros::user_doc; use datafusion_physical_expr_common::sort_expr::LexOrdering; create_func!(FirstValue, first_value_udaf); +create_func!(LastValue, last_value_udaf); /// Returns the first value in a group of values. pub fn first_value(expression: Expr, order_by: Option>) -> Expr { @@ -67,6 +68,20 @@ pub fn first_value(expression: Expr, order_by: Option>) -> Expr { } } +/// Returns the last value in a group of values. +pub fn last_value(expression: Expr, order_by: Option>) -> Expr { + if let Some(order_by) = order_by { + last_value_udaf() + .call(vec![expression]) + .order_by(order_by) + .build() + // guaranteed to be `Expr::AggregateFunction` + .unwrap() + } else { + last_value_udaf().call(vec![expression]) + } +} + #[user_doc( doc_section(label = "General Functions"), description = "Returns the first element in an aggregation group according to the requested ordering. If no ordering is given, returns an arbitrary element from the group.", @@ -166,6 +181,7 @@ impl AggregateUDFImpl for FirstValue { } fn groups_accumulator_supported(&self, args: AccumulatorArgs) -> bool { + // TODO: extract to function use DataType::*; matches!( args.return_type, @@ -193,6 +209,7 @@ impl AggregateUDFImpl for FirstValue { &self, args: AccumulatorArgs, ) -> Result> { + // TODO: extract to function fn create_accumulator( args: AccumulatorArgs, ) -> Result> @@ -210,6 +227,7 @@ impl AggregateUDFImpl for FirstValue { args.ignore_nulls, args.return_type, &ordering_dtypes, + true, )?)) } @@ -258,10 +276,12 @@ impl AggregateUDFImpl for FirstValue { create_accumulator::(args) } - _ => internal_err!( - "GroupsAccumulator not supported for first({})", - args.return_type - ), + _ => { + internal_err!( + "GroupsAccumulator not supported for first_value({})", + args.return_type + ) + } } } @@ -291,6 +311,7 @@ impl AggregateUDFImpl for FirstValue { } } +// TODO: rename to PrimitiveGroupsAccumulator struct FirstPrimitiveGroupsAccumulator where T: ArrowPrimitiveType + Send, @@ -316,12 +337,16 @@ where // buffer for `get_filtered_min_of_each_group` // filter_min_of_each_group_buf.0[group_idx] -> idx_in_val // only valid if filter_min_of_each_group_buf.1[group_idx] == true + // TODO: rename to extreme_of_each_group_buf min_of_each_group_buf: (Vec, BooleanBufferBuilder), // =========== option ============ // Stores the applicable ordering requirement. ordering_req: LexOrdering, + // true: take first element in an aggregation group according to the requested ordering. + // false: take last element in an aggregation group according to the requested ordering. + pick_first_in_group: bool, // derived from `ordering_req`. sort_options: Vec, // Stores whether incoming data already satisfies the ordering requirement. @@ -342,6 +367,7 @@ where ignore_nulls: bool, data_type: &DataType, ordering_dtypes: &[DataType], + pick_first_in_group: bool, ) -> Result { let requirement_satisfied = ordering_req.is_empty(); @@ -365,6 +391,7 @@ where is_sets: BooleanBufferBuilder::new(0), size_of_orderings: 0, min_of_each_group_buf: (Vec::new(), BooleanBufferBuilder::new(0)), + pick_first_in_group, }) } @@ -391,8 +418,13 @@ where assert!(new_ordering_values.len() == self.ordering_req.len()); let current_ordering = &self.orderings[group_idx]; - compare_rows(current_ordering, new_ordering_values, &self.sort_options) - .map(|x| x.is_gt()) + compare_rows(current_ordering, new_ordering_values, &self.sort_options).map(|x| { + if self.pick_first_in_group { + x.is_gt() + } else { + x.is_lt() + } + }) } fn take_orderings(&mut self, emit_to: EmitTo) -> Vec> { @@ -501,10 +533,10 @@ where .map(ScalarValue::size_of_vec) .sum::() } - /// Returns a vector of tuples `(group_idx, idx_in_val)` representing the index of the /// minimum value in `orderings` for each group, using lexicographical comparison. /// Values are filtered using `opt_filter` and `is_set_arr` if provided. + /// TODO: rename to get_filtered_extreme_of_each_group fn get_filtered_min_of_each_group( &mut self, orderings: &[ArrayRef], @@ -556,15 +588,19 @@ where } let is_valid = self.min_of_each_group_buf.1.get_bit(group_idx); - if is_valid - && comparator - .compare(self.min_of_each_group_buf.0[group_idx], idx_in_val) - .is_gt() - { - self.min_of_each_group_buf.0[group_idx] = idx_in_val; - } else if !is_valid { + + if !is_valid { self.min_of_each_group_buf.1.set_bit(group_idx, true); self.min_of_each_group_buf.0[group_idx] = idx_in_val; + } else { + let ordering = comparator + .compare(self.min_of_each_group_buf.0[group_idx], idx_in_val); + + if (ordering.is_gt() && self.pick_first_in_group) + || (ordering.is_lt() && !self.pick_first_in_group) + { + self.min_of_each_group_buf.0[group_idx] = idx_in_val; + } } } @@ -918,13 +954,6 @@ impl Accumulator for FirstValueAccumulator { } } -make_udaf_expr_and_func!( - LastValue, - last_value, - "Returns the last value in a group of values.", - last_value_udaf -); - #[user_doc( doc_section(label = "General Functions"), description = "Returns the last element in an aggregation group according to the requested ordering. If no ordering is given, returns an arbitrary element from the group.", @@ -1052,6 +1081,109 @@ impl AggregateUDFImpl for LastValue { fn documentation(&self) -> Option<&Documentation> { self.doc() } + + fn groups_accumulator_supported(&self, args: AccumulatorArgs) -> bool { + use DataType::*; + matches!( + args.return_type, + Int8 | Int16 + | Int32 + | Int64 + | UInt8 + | UInt16 + | UInt32 + | UInt64 + | Float16 + | Float32 + | Float64 + | Decimal128(_, _) + | Decimal256(_, _) + | Date32 + | Date64 + | Time32(_) + | Time64(_) + | Timestamp(_, _) + ) + } + + fn create_groups_accumulator( + &self, + args: AccumulatorArgs, + ) -> Result> { + fn create_accumulator( + args: AccumulatorArgs, + ) -> Result> + where + T: ArrowPrimitiveType + Send, + { + let ordering_dtypes = args + .ordering_req + .iter() + .map(|e| e.expr.data_type(args.schema)) + .collect::>>()?; + + Ok(Box::new(FirstPrimitiveGroupsAccumulator::::try_new( + args.ordering_req.clone(), + args.ignore_nulls, + args.return_type, + &ordering_dtypes, + false, + )?)) + } + + match args.return_type { + DataType::Int8 => create_accumulator::(args), + DataType::Int16 => create_accumulator::(args), + DataType::Int32 => create_accumulator::(args), + DataType::Int64 => create_accumulator::(args), + DataType::UInt8 => create_accumulator::(args), + DataType::UInt16 => create_accumulator::(args), + DataType::UInt32 => create_accumulator::(args), + DataType::UInt64 => create_accumulator::(args), + DataType::Float16 => create_accumulator::(args), + DataType::Float32 => create_accumulator::(args), + DataType::Float64 => create_accumulator::(args), + + DataType::Decimal128(_, _) => create_accumulator::(args), + DataType::Decimal256(_, _) => create_accumulator::(args), + + DataType::Timestamp(TimeUnit::Second, _) => { + create_accumulator::(args) + } + DataType::Timestamp(TimeUnit::Millisecond, _) => { + create_accumulator::(args) + } + DataType::Timestamp(TimeUnit::Microsecond, _) => { + create_accumulator::(args) + } + DataType::Timestamp(TimeUnit::Nanosecond, _) => { + create_accumulator::(args) + } + + DataType::Date32 => create_accumulator::(args), + DataType::Date64 => create_accumulator::(args), + DataType::Time32(TimeUnit::Second) => { + create_accumulator::(args) + } + DataType::Time32(TimeUnit::Millisecond) => { + create_accumulator::(args) + } + + DataType::Time64(TimeUnit::Microsecond) => { + create_accumulator::(args) + } + DataType::Time64(TimeUnit::Nanosecond) => { + create_accumulator::(args) + } + + _ => { + internal_err!( + "GroupsAccumulator not supported for last_value({})", + args.return_type + ) + } + } + } } #[derive(Debug)] @@ -1411,6 +1543,7 @@ mod tests { true, &DataType::Int64, &[DataType::Int64], + true, )?; let mut val_with_orderings = { @@ -1485,7 +1618,7 @@ mod tests { } #[test] - fn test_frist_group_acc_size_of_ordering() -> Result<()> { + fn test_group_acc_size_of_ordering() -> Result<()> { let schema = Arc::new(Schema::new(vec![ Field::new("a", DataType::Int64, true), Field::new("b", DataType::Int64, true), @@ -1504,6 +1637,7 @@ mod tests { true, &DataType::Int64, &[DataType::Int64], + true, )?; let val_with_orderings = { @@ -1563,4 +1697,79 @@ mod tests { Ok(()) } + + #[test] + fn test_last_group_acc() -> Result<()> { + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int64, true), + Field::new("b", DataType::Int64, true), + Field::new("c", DataType::Int64, true), + Field::new("d", DataType::Int32, true), + Field::new("e", DataType::Boolean, true), + ])); + + let sort_key = LexOrdering::new(vec![PhysicalSortExpr { + expr: col("c", &schema).unwrap(), + options: SortOptions::default(), + }]); + + let mut group_acc = FirstPrimitiveGroupsAccumulator::::try_new( + sort_key, + true, + &DataType::Int64, + &[DataType::Int64], + false, + )?; + + let mut val_with_orderings = { + let mut val_with_orderings = Vec::::new(); + + let vals = Arc::new(Int64Array::from(vec![Some(1), None, Some(3), Some(-6)])); + let orderings = Arc::new(Int64Array::from(vec![1, -9, 3, -6])); + + val_with_orderings.push(vals); + val_with_orderings.push(orderings); + + val_with_orderings + }; + + group_acc.update_batch( + &val_with_orderings, + &[0, 1, 2, 1], + Some(&BooleanArray::from(vec![true, true, false, true])), + 3, + )?; + + let state = group_acc.state(EmitTo::All)?; + + let expected_state: Vec> = vec![ + Arc::new(Int64Array::from(vec![Some(1), Some(-6), None])), + Arc::new(Int64Array::from(vec![Some(1), Some(-6), None])), + Arc::new(BooleanArray::from(vec![true, true, false])), + ]; + assert_eq!(state, expected_state); + + group_acc.merge_batch( + &state, + &[0, 1, 2], + Some(&BooleanArray::from(vec![true, false, false])), + 3, + )?; + + val_with_orderings.clear(); + val_with_orderings.push(Arc::new(Int64Array::from(vec![66, 6]))); + val_with_orderings.push(Arc::new(Int64Array::from(vec![66, 6]))); + + group_acc.update_batch(&val_with_orderings, &[1, 2], None, 4)?; + + let binding = group_acc.evaluate(EmitTo::All)?; + let eval_result = binding.as_any().downcast_ref::().unwrap(); + + let expect: PrimitiveArray = + Int64Array::from(vec![Some(1), Some(66), Some(6), None]); + + assert_eq!(eval_result, &expect); + + Ok(()) + } } diff --git a/datafusion/functions-aggregate/src/string_agg.rs b/datafusion/functions-aggregate/src/string_agg.rs index 64314ef6df687..a7594b9ccb01f 100644 --- a/datafusion/functions-aggregate/src/string_agg.rs +++ b/datafusion/functions-aggregate/src/string_agg.rs @@ -17,15 +17,17 @@ //! [`StringAgg`] accumulator for the `string_agg` function +use crate::array_agg::ArrayAgg; use arrow::array::ArrayRef; -use arrow::datatypes::DataType; +use arrow::datatypes::{DataType, Field}; use datafusion_common::cast::as_generic_string_array; use datafusion_common::Result; -use datafusion_common::{not_impl_err, ScalarValue}; +use datafusion_common::{internal_err, not_impl_err, ScalarValue}; use datafusion_expr::function::AccumulatorArgs; use datafusion_expr::{ Accumulator, AggregateUDFImpl, Documentation, Signature, TypeSignature, Volatility, }; +use datafusion_functions_aggregate_common::accumulator::StateFieldsArgs; use datafusion_macros::user_doc; use datafusion_physical_expr::expressions::Literal; use std::any::Any; @@ -41,15 +43,31 @@ make_udaf_expr_and_func!( #[user_doc( doc_section(label = "General Functions"), - description = "Concatenates the values of string expressions and places separator values between them.", - syntax_example = "string_agg(expression, delimiter)", + description = "Concatenates the values of string expressions and places separator values between them. \ +If ordering is required, strings are concatenated in the specified order. \ +This aggregation function can only mix DISTINCT and ORDER BY if the ordering expression is exactly the same as the first argument expression.", + syntax_example = "string_agg([DISTINCT] expression, delimiter [ORDER BY expression])", sql_example = r#"```sql > SELECT string_agg(name, ', ') AS names_list FROM employee; +--------------------------+ | names_list | +--------------------------+ -| Alice, Bob, Charlie | +| Alice, Bob, Bob, Charlie | ++--------------------------+ +> SELECT string_agg(name, ', ' ORDER BY name DESC) AS names_list + FROM employee; ++--------------------------+ +| names_list | ++--------------------------+ +| Charlie, Bob, Bob, Alice | ++--------------------------+ +> SELECT string_agg(DISTINCT name, ', ' ORDER BY name DESC) AS names_list + FROM employee; ++--------------------------+ +| names_list | ++--------------------------+ +| Charlie, Bob, Alice | +--------------------------+ ```"#, argument( @@ -65,6 +83,7 @@ make_udaf_expr_and_func!( #[derive(Debug)] pub struct StringAgg { signature: Signature, + array_agg: ArrayAgg, } impl StringAgg { @@ -76,9 +95,13 @@ impl StringAgg { TypeSignature::Exact(vec![DataType::LargeUtf8, DataType::Utf8]), TypeSignature::Exact(vec![DataType::LargeUtf8, DataType::LargeUtf8]), TypeSignature::Exact(vec![DataType::LargeUtf8, DataType::Null]), + TypeSignature::Exact(vec![DataType::Utf8, DataType::Utf8]), + TypeSignature::Exact(vec![DataType::Utf8, DataType::LargeUtf8]), + TypeSignature::Exact(vec![DataType::Utf8, DataType::Null]), ], Volatility::Immutable, ), + array_agg: Default::default(), } } } @@ -106,20 +129,40 @@ impl AggregateUDFImpl for StringAgg { Ok(DataType::LargeUtf8) } + fn state_fields(&self, args: StateFieldsArgs) -> Result> { + self.array_agg.state_fields(args) + } + fn accumulator(&self, acc_args: AccumulatorArgs) -> Result> { - if let Some(lit) = acc_args.exprs[1].as_any().downcast_ref::() { - return match lit.value().try_as_str() { - Some(Some(delimiter)) => { - Ok(Box::new(StringAggAccumulator::new(delimiter))) - } - Some(None) => Ok(Box::new(StringAggAccumulator::new(""))), - None => { - not_impl_err!("StringAgg not supported for delimiter {}", lit.value()) - } - }; - } + let Some(lit) = acc_args.exprs[1].as_any().downcast_ref::() else { + return not_impl_err!( + "The second argument of the string_agg function must be a string literal" + ); + }; + + let delimiter = if lit.value().is_null() { + // If the second argument (the delimiter that joins strings) is NULL, join + // on an empty string. (e.g. [a, b, c] => "abc"). + "" + } else if let Some(lit_string) = lit.value().try_as_str() { + lit_string.unwrap_or("") + } else { + return not_impl_err!( + "StringAgg not supported for delimiter \"{}\"", + lit.value() + ); + }; + + let array_agg_acc = self.array_agg.accumulator(AccumulatorArgs { + return_type: &DataType::new_list(acc_args.return_type.clone(), true), + exprs: &filter_index(acc_args.exprs, 1), + ..acc_args + })?; - not_impl_err!("expect literal") + Ok(Box::new(StringAggAccumulator::new( + array_agg_acc, + delimiter, + ))) } fn documentation(&self) -> Option<&Documentation> { @@ -129,14 +172,14 @@ impl AggregateUDFImpl for StringAgg { #[derive(Debug)] pub(crate) struct StringAggAccumulator { - values: Option, + array_agg_acc: Box, delimiter: String, } impl StringAggAccumulator { - pub fn new(delimiter: &str) -> Self { + pub fn new(array_agg_acc: Box, delimiter: &str) -> Self { Self { - values: None, + array_agg_acc, delimiter: delimiter.to_string(), } } @@ -144,37 +187,311 @@ impl StringAggAccumulator { impl Accumulator for StringAggAccumulator { fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> { - let string_array: Vec<_> = as_generic_string_array::(&values[0])? - .iter() - .filter_map(|v| v.as_ref().map(ToString::to_string)) - .collect(); - if !string_array.is_empty() { - let s = string_array.join(self.delimiter.as_str()); - let v = self.values.get_or_insert("".to_string()); - if !v.is_empty() { - v.push_str(self.delimiter.as_str()); + self.array_agg_acc.update_batch(&filter_index(values, 1)) + } + + fn evaluate(&mut self) -> Result { + let scalar = self.array_agg_acc.evaluate()?; + + let ScalarValue::List(list) = scalar else { + return internal_err!("Expected a DataType::List while evaluating underlying ArrayAggAccumulator, but got {}", scalar.data_type()); + }; + + let string_arr: Vec<_> = match list.value_type() { + DataType::LargeUtf8 => as_generic_string_array::(list.values())? + .iter() + .flatten() + .collect(), + DataType::Utf8 => as_generic_string_array::(list.values())? + .iter() + .flatten() + .collect(), + _ => { + return internal_err!( + "Expected elements to of type Utf8 or LargeUtf8, but got {}", + list.value_type() + ) } - v.push_str(s.as_str()); + }; + + if string_arr.is_empty() { + return Ok(ScalarValue::LargeUtf8(None)); } - Ok(()) + + Ok(ScalarValue::LargeUtf8(Some( + string_arr.join(&self.delimiter), + ))) + } + + fn size(&self) -> usize { + size_of_val(self) - size_of_val(&self.array_agg_acc) + + self.array_agg_acc.size() + + self.delimiter.capacity() + } + + fn state(&mut self) -> Result> { + self.array_agg_acc.state() } fn merge_batch(&mut self, values: &[ArrayRef]) -> Result<()> { - self.update_batch(values)?; + self.array_agg_acc.merge_batch(values) + } +} + +fn filter_index(values: &[T], index: usize) -> Vec { + values + .iter() + .enumerate() + .filter(|(i, _)| *i != index) + .map(|(_, v)| v) + .cloned() + .collect::>() +} + +#[cfg(test)] +mod tests { + use super::*; + use arrow::array::LargeStringArray; + use arrow::compute::SortOptions; + use arrow::datatypes::{Fields, Schema}; + use datafusion_common::internal_err; + use datafusion_physical_expr::expressions::Column; + use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr}; + use std::sync::Arc; + + #[test] + fn no_duplicates_no_distinct() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",").build_two()?; + + acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; + acc2.update_batch(&[data(["d", "e", "f"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str(acc1.evaluate()?); + + assert_eq!(result, "a,b,c,d,e,f"); + Ok(()) } - fn state(&mut self) -> Result> { - Ok(vec![self.evaluate()?]) + #[test] + fn no_duplicates_distinct() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") + .distinct() + .build_two()?; + + acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; + acc2.update_batch(&[data(["d", "e", "f"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str_sorted(acc1.evaluate()?, ","); + + assert_eq!(result, "a,b,c,d,e,f"); + + Ok(()) } - fn evaluate(&mut self) -> Result { - Ok(ScalarValue::LargeUtf8(self.values.clone())) + #[test] + fn duplicates_no_distinct() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",").build_two()?; + + acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; + acc2.update_batch(&[data(["a", "b", "c"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str(acc1.evaluate()?); + + assert_eq!(result, "a,b,c,a,b,c"); + + Ok(()) } - fn size(&self) -> usize { - size_of_val(self) - + self.values.as_ref().map(|v| v.capacity()).unwrap_or(0) - + self.delimiter.capacity() + #[test] + fn duplicates_distinct() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") + .distinct() + .build_two()?; + + acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; + acc2.update_batch(&[data(["a", "b", "c"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str_sorted(acc1.evaluate()?, ","); + + assert_eq!(result, "a,b,c"); + + Ok(()) + } + + #[test] + fn no_duplicates_distinct_sort_asc() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") + .distinct() + .order_by_col("col", SortOptions::new(false, false)) + .build_two()?; + + acc1.update_batch(&[data(["e", "b", "d"]), data([","])])?; + acc2.update_batch(&[data(["f", "a", "c"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str(acc1.evaluate()?); + + assert_eq!(result, "a,b,c,d,e,f"); + + Ok(()) + } + + #[test] + fn no_duplicates_distinct_sort_desc() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") + .distinct() + .order_by_col("col", SortOptions::new(true, false)) + .build_two()?; + + acc1.update_batch(&[data(["e", "b", "d"]), data([","])])?; + acc2.update_batch(&[data(["f", "a", "c"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str(acc1.evaluate()?); + + assert_eq!(result, "f,e,d,c,b,a"); + + Ok(()) + } + + #[test] + fn duplicates_distinct_sort_asc() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") + .distinct() + .order_by_col("col", SortOptions::new(false, false)) + .build_two()?; + + acc1.update_batch(&[data(["a", "c", "b"]), data([","])])?; + acc2.update_batch(&[data(["b", "c", "a"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str(acc1.evaluate()?); + + assert_eq!(result, "a,b,c"); + + Ok(()) + } + + #[test] + fn duplicates_distinct_sort_desc() -> Result<()> { + let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") + .distinct() + .order_by_col("col", SortOptions::new(true, false)) + .build_two()?; + + acc1.update_batch(&[data(["a", "c", "b"]), data([","])])?; + acc2.update_batch(&[data(["b", "c", "a"]), data([","])])?; + acc1 = merge(acc1, acc2)?; + + let result = some_str(acc1.evaluate()?); + + assert_eq!(result, "c,b,a"); + + Ok(()) + } + + struct StringAggAccumulatorBuilder { + sep: String, + distinct: bool, + ordering: LexOrdering, + schema: Schema, + } + + impl StringAggAccumulatorBuilder { + fn new(sep: &str) -> Self { + Self { + sep: sep.to_string(), + distinct: Default::default(), + ordering: Default::default(), + schema: Schema { + fields: Fields::from(vec![Field::new( + "col", + DataType::LargeUtf8, + true, + )]), + metadata: Default::default(), + }, + } + } + fn distinct(mut self) -> Self { + self.distinct = true; + self + } + + fn order_by_col(mut self, col: &str, sort_options: SortOptions) -> Self { + self.ordering.extend([PhysicalSortExpr::new( + Arc::new( + Column::new_with_schema(col, &self.schema) + .expect("column not available in schema"), + ), + sort_options, + )]); + self + } + + fn build(&self) -> Result> { + StringAgg::new().accumulator(AccumulatorArgs { + return_type: &DataType::LargeUtf8, + schema: &self.schema, + ignore_nulls: false, + ordering_req: &self.ordering, + is_reversed: false, + name: "", + is_distinct: self.distinct, + exprs: &[ + Arc::new(Column::new("col", 0)), + Arc::new(Literal::new(ScalarValue::Utf8(Some(self.sep.to_string())))), + ], + }) + } + + fn build_two(&self) -> Result<(Box, Box)> { + Ok((self.build()?, self.build()?)) + } + } + + fn some_str(value: ScalarValue) -> String { + str(value) + .expect("ScalarValue was not a String") + .expect("ScalarValue was None") + } + + fn some_str_sorted(value: ScalarValue, sep: &str) -> String { + let value = some_str(value); + let mut parts: Vec<&str> = value.split(sep).collect(); + parts.sort(); + parts.join(sep) + } + + fn str(value: ScalarValue) -> Result> { + match value { + ScalarValue::LargeUtf8(v) => Ok(v), + _ => internal_err!( + "Expected ScalarValue::LargeUtf8, got {}", + value.data_type() + ), + } + } + + fn data(list: [&str; N]) -> ArrayRef { + Arc::new(LargeStringArray::from(list.to_vec())) + } + + fn merge( + mut acc1: Box, + mut acc2: Box, + ) -> Result> { + let intermediate_state = acc2.state().and_then(|e| { + e.iter() + .map(|v| v.to_array()) + .collect::>>() + })?; + acc1.merge_batch(&intermediate_state)?; + Ok(acc1) } } diff --git a/datafusion/functions-nested/src/array_has.rs b/datafusion/functions-nested/src/array_has.rs index 48ee341566b90..5ef1491313b13 100644 --- a/datafusion/functions-nested/src/array_has.rs +++ b/datafusion/functions-nested/src/array_has.rs @@ -271,7 +271,7 @@ fn array_has_dispatch_for_scalar( let offsets = haystack.value_offsets(); // If first argument is empty list (second argument is non-null), return false // i.e. array_has([], non-null element) -> false - if values.len() == 0 { + if values.is_empty() { return Ok(Arc::new(BooleanArray::new( BooleanBuffer::new_unset(haystack.len()), None, @@ -488,7 +488,7 @@ fn array_has_all_and_any_dispatch( ) -> Result { let haystack = as_generic_list_array::(haystack)?; let needle = as_generic_list_array::(needle)?; - if needle.values().len() == 0 { + if needle.values().is_empty() { let buffer = match comparison_type { ComparisonType::All => BooleanBuffer::new_set(haystack.len()), ComparisonType::Any => BooleanBuffer::new_unset(haystack.len()), diff --git a/datafusion/functions-nested/src/flatten.rs b/datafusion/functions-nested/src/flatten.rs index f288035948dcb..4279f04e3dc44 100644 --- a/datafusion/functions-nested/src/flatten.rs +++ b/datafusion/functions-nested/src/flatten.rs @@ -18,19 +18,18 @@ //! [`ScalarUDFImpl`] definitions for flatten function. use crate::utils::make_scalar_function; -use arrow::array::{ArrayRef, GenericListArray, OffsetSizeTrait}; +use arrow::array::{Array, ArrayRef, GenericListArray, OffsetSizeTrait}; use arrow::buffer::OffsetBuffer; use arrow::datatypes::{ DataType, DataType::{FixedSizeList, LargeList, List, Null}, }; -use datafusion_common::cast::{ - as_generic_list_array, as_large_list_array, as_list_array, -}; +use datafusion_common::cast::{as_large_list_array, as_list_array}; +use datafusion_common::utils::ListCoercion; use datafusion_common::{exec_err, utils::take_function_args, Result}; use datafusion_expr::{ - ArrayFunctionSignature, ColumnarValue, Documentation, ScalarUDFImpl, Signature, - TypeSignature, Volatility, + ArrayFunctionArgument, ArrayFunctionSignature, ColumnarValue, Documentation, + ScalarUDFImpl, Signature, TypeSignature, Volatility, }; use datafusion_macros::user_doc; use std::any::Any; @@ -77,9 +76,11 @@ impl Flatten { pub fn new() -> Self { Self { signature: Signature { - // TODO (https://github.com/apache/datafusion/issues/13757) flatten should be single-step, not recursive type_signature: TypeSignature::ArraySignature( - ArrayFunctionSignature::RecursiveArray, + ArrayFunctionSignature::Array { + arguments: vec![ArrayFunctionArgument::Array], + array_coercion: Some(ListCoercion::FixedSizedListToList), + }, ), volatility: Volatility::Immutable, }, @@ -102,25 +103,23 @@ impl ScalarUDFImpl for Flatten { } fn return_type(&self, arg_types: &[DataType]) -> Result { - fn get_base_type(data_type: &DataType) -> Result { - match data_type { - List(field) | FixedSizeList(field, _) - if matches!(field.data_type(), List(_) | FixedSizeList(_, _)) => - { - get_base_type(field.data_type()) - } - LargeList(field) if matches!(field.data_type(), LargeList(_)) => { - get_base_type(field.data_type()) + let data_type = match &arg_types[0] { + List(field) | FixedSizeList(field, _) => match field.data_type() { + List(field) | FixedSizeList(field, _) => List(Arc::clone(field)), + _ => arg_types[0].clone(), + }, + LargeList(field) => match field.data_type() { + List(field) | LargeList(field) | FixedSizeList(field, _) => { + LargeList(Arc::clone(field)) } - Null | List(_) | LargeList(_) => Ok(data_type.to_owned()), - FixedSizeList(field, _) => Ok(List(Arc::clone(field))), - _ => exec_err!( - "Not reachable, data_type should be List, LargeList or FixedSizeList" - ), - } - } + _ => arg_types[0].clone(), + }, + Null => Null, + _ => exec_err!( + "Not reachable, data_type should be List, LargeList or FixedSizeList" + )?, + }; - let data_type = get_base_type(&arg_types[0])?; Ok(data_type) } @@ -146,14 +145,62 @@ pub fn flatten_inner(args: &[ArrayRef]) -> Result { match array.data_type() { List(_) => { - let list_arr = as_list_array(&array)?; - let flattened_array = flatten_internal::(list_arr.clone(), None)?; - Ok(Arc::new(flattened_array) as ArrayRef) + let (field, offsets, values, nulls) = + as_list_array(&array)?.clone().into_parts(); + + match field.data_type() { + List(_) => { + let (inner_field, inner_offsets, inner_values, _) = + as_list_array(&values)?.clone().into_parts(); + let offsets = get_offsets_for_flatten::(inner_offsets, offsets); + let flattened_array = GenericListArray::::new( + inner_field, + offsets, + inner_values, + nulls, + ); + + Ok(Arc::new(flattened_array) as ArrayRef) + } + LargeList(_) => { + exec_err!("flatten does not support type '{:?}'", array.data_type())? + } + _ => Ok(Arc::clone(array) as ArrayRef), + } } LargeList(_) => { - let list_arr = as_large_list_array(&array)?; - let flattened_array = flatten_internal::(list_arr.clone(), None)?; - Ok(Arc::new(flattened_array) as ArrayRef) + let (field, offsets, values, nulls) = + as_large_list_array(&array)?.clone().into_parts(); + + match field.data_type() { + List(_) => { + let (inner_field, inner_offsets, inner_values, _) = + as_list_array(&values)?.clone().into_parts(); + let offsets = get_large_offsets_for_flatten(inner_offsets, offsets); + let flattened_array = GenericListArray::::new( + inner_field, + offsets, + inner_values, + nulls, + ); + + Ok(Arc::new(flattened_array) as ArrayRef) + } + LargeList(_) => { + let (inner_field, inner_offsets, inner_values, nulls) = + as_large_list_array(&values)?.clone().into_parts(); + let offsets = get_offsets_for_flatten::(inner_offsets, offsets); + let flattened_array = GenericListArray::::new( + inner_field, + offsets, + inner_values, + nulls, + ); + + Ok(Arc::new(flattened_array) as ArrayRef) + } + _ => Ok(Arc::clone(array) as ArrayRef), + } } Null => Ok(Arc::clone(array)), _ => { @@ -162,37 +209,6 @@ pub fn flatten_inner(args: &[ArrayRef]) -> Result { } } -fn flatten_internal( - list_arr: GenericListArray, - indexes: Option>, -) -> Result> { - let (field, offsets, values, _) = list_arr.clone().into_parts(); - let data_type = field.data_type(); - - match data_type { - // Recursively get the base offsets for flattened array - List(_) | LargeList(_) => { - let sub_list = as_generic_list_array::(&values)?; - if let Some(indexes) = indexes { - let offsets = get_offsets_for_flatten(offsets, indexes); - flatten_internal::(sub_list.clone(), Some(offsets)) - } else { - flatten_internal::(sub_list.clone(), Some(offsets)) - } - } - // Reach the base level, create a new list array - _ => { - if let Some(indexes) = indexes { - let offsets = get_offsets_for_flatten(offsets, indexes); - let list_arr = GenericListArray::::new(field, offsets, values, None); - Ok(list_arr) - } else { - Ok(list_arr) - } - } - } -} - // Create new offsets that are equivalent to `flatten` the array. fn get_offsets_for_flatten( offsets: OffsetBuffer, @@ -205,3 +221,16 @@ fn get_offsets_for_flatten( .collect(); OffsetBuffer::new(offsets.into()) } + +// Create new large offsets that are equivalent to `flatten` the array. +fn get_large_offsets_for_flatten( + offsets: OffsetBuffer, + indexes: OffsetBuffer

, +) -> OffsetBuffer { + let buffer = offsets.into_inner(); + let offsets: Vec = indexes + .iter() + .map(|i| buffer[i.to_usize().unwrap()].to_i64().unwrap()) + .collect(); + OffsetBuffer::new(offsets.into()) +} diff --git a/datafusion/functions-nested/src/sort.rs b/datafusion/functions-nested/src/sort.rs index 1db245fe52fed..85737ef135bce 100644 --- a/datafusion/functions-nested/src/sort.rs +++ b/datafusion/functions-nested/src/sort.rs @@ -20,6 +20,7 @@ use crate::utils::make_scalar_function; use arrow::array::{new_null_array, Array, ArrayRef, ListArray, NullBufferBuilder}; use arrow::buffer::OffsetBuffer; +use arrow::compute::SortColumn; use arrow::datatypes::DataType::{FixedSizeList, LargeList, List}; use arrow::datatypes::{DataType, Field}; use arrow::{compute, compute::SortOptions}; @@ -207,9 +208,24 @@ pub fn array_sort_inner(args: &[ArrayRef]) -> Result { valid.append_null(); } else { let arr_ref = list_array.value(i); - let arr_ref = arr_ref.as_ref(); - let sorted_array = compute::sort(arr_ref, sort_option)?; + // arrow sort kernel does not support Structs, so use + // lexsort_to_indices instead: + // https://github.com/apache/arrow-rs/issues/6911#issuecomment-2562928843 + let sorted_array = match arr_ref.data_type() { + DataType::Struct(_) => { + let sort_columns: Vec = vec![SortColumn { + values: Arc::clone(&arr_ref), + options: sort_option, + }]; + let indices = compute::lexsort_to_indices(&sort_columns, None)?; + compute::take(arr_ref.as_ref(), &indices, None)? + } + _ => { + let arr_ref = arr_ref.as_ref(); + compute::sort(arr_ref, sort_option)? + } + }; array_lengths.push(sorted_array.len()); arrays.push(sorted_array); valid.append_non_null(); diff --git a/datafusion/functions-table/src/generate_series.rs b/datafusion/functions-table/src/generate_series.rs index 5bb56f28bc8d3..ee95567ab73dc 100644 --- a/datafusion/functions-table/src/generate_series.rs +++ b/datafusion/functions-table/src/generate_series.rs @@ -138,12 +138,15 @@ impl TableProvider for GenerateSeriesTable { async fn scan( &self, state: &dyn Session, - _projection: Option<&Vec>, + projection: Option<&Vec>, _filters: &[Expr], _limit: Option, ) -> Result> { let batch_size = state.config_options().execution.batch_size; - + let schema = match projection { + Some(projection) => Arc::new(self.schema.project(projection)?), + None => self.schema(), + }; let state = match self.args { // if args have null, then return 0 row GenSeriesArgs::ContainsNull { include_end, name } => GenerateSeriesState { @@ -175,7 +178,7 @@ impl TableProvider for GenerateSeriesTable { }; Ok(Arc::new(LazyMemoryExec::try_new( - self.schema(), + schema, vec![Arc::new(RwLock::new(state))], )?)) } diff --git a/datafusion/functions-window-common/src/expr.rs b/datafusion/functions-window-common/src/expr.rs index 1d99fe7acf152..76e27b045b0a3 100644 --- a/datafusion/functions-window-common/src/expr.rs +++ b/datafusion/functions-window-common/src/expr.rs @@ -36,9 +36,9 @@ impl<'a> ExpressionArgs<'a> { /// # Arguments /// /// * `input_exprs` - The expressions passed as arguments - /// to the user-defined window function. + /// to the user-defined window function. /// * `input_types` - The data types corresponding to the - /// arguments to the user-defined window function. + /// arguments to the user-defined window function. /// pub fn new( input_exprs: &'a [Arc], diff --git a/datafusion/functions-window-common/src/field.rs b/datafusion/functions-window-common/src/field.rs index 8011b7b0f05f0..03f88b0b95cc8 100644 --- a/datafusion/functions-window-common/src/field.rs +++ b/datafusion/functions-window-common/src/field.rs @@ -33,9 +33,9 @@ impl<'a> WindowUDFFieldArgs<'a> { /// # Arguments /// /// * `input_types` - The data types corresponding to the - /// arguments to the user-defined window function. + /// arguments to the user-defined window function. /// * `function_name` - The qualified schema name of the - /// user-defined window function expression. + /// user-defined window function expression. /// pub fn new(input_types: &'a [DataType], display_name: &'a str) -> Self { WindowUDFFieldArgs { diff --git a/datafusion/functions-window-common/src/partition.rs b/datafusion/functions-window-common/src/partition.rs index 64786d2fe7c70..e853aa8fb05d5 100644 --- a/datafusion/functions-window-common/src/partition.rs +++ b/datafusion/functions-window-common/src/partition.rs @@ -41,13 +41,13 @@ impl<'a> PartitionEvaluatorArgs<'a> { /// # Arguments /// /// * `input_exprs` - The expressions passed as arguments - /// to the user-defined window function. + /// to the user-defined window function. /// * `input_types` - The data types corresponding to the - /// arguments to the user-defined window function. + /// arguments to the user-defined window function. /// * `is_reversed` - Set to `true` if and only if the user-defined - /// window function is reversible and is reversed. + /// window function is reversible and is reversed. /// * `ignore_nulls` - Set to `true` when `IGNORE NULLS` is - /// specified. + /// specified. /// pub fn new( input_exprs: &'a [Arc], diff --git a/datafusion/functions-window/src/cume_dist.rs b/datafusion/functions-window/src/cume_dist.rs index d777f7932b0e6..d156416a82a4b 100644 --- a/datafusion/functions-window/src/cume_dist.rs +++ b/datafusion/functions-window/src/cume_dist.rs @@ -43,8 +43,23 @@ define_udwf_and_expr!( /// CumeDist calculates the cume_dist in the window function with order by #[user_doc( doc_section(label = "Ranking Functions"), - description = "Relative rank of the current row: (number of rows preceding or peer with current row) / (total rows).", - syntax_example = "cume_dist()" + description = "Relative rank of the current row: (number of rows preceding or peer with the current row) / (total rows).", + syntax_example = "cume_dist()", + sql_example = r#"```sql + --Example usage of the cume_dist window function: + SELECT salary, + cume_dist() OVER (ORDER BY salary) AS cume_dist + FROM employees; +``` +```sql ++--------+-----------+ +| salary | cume_dist | ++--------+-----------+ +| 30000 | 0.33 | +| 50000 | 0.67 | +| 70000 | 1.00 | ++--------+-----------+ +```"# )] #[derive(Debug)] pub struct CumeDist { @@ -113,7 +128,7 @@ impl PartitionEvaluator for CumeDistEvaluator { let len = range.end - range.start; *acc += len as u64; let value: f64 = (*acc as f64) / scalar; - let result = iter::repeat(value).take(len); + let result = iter::repeat_n(value, len); Some(result) }) .flatten(), diff --git a/datafusion/functions-window/src/macros.rs b/datafusion/functions-window/src/macros.rs index 0a86ba6255330..2ef1eacba953d 100644 --- a/datafusion/functions-window/src/macros.rs +++ b/datafusion/functions-window/src/macros.rs @@ -29,12 +29,12 @@ /// # Parameters /// /// * `$UDWF`: The struct which defines the [`Signature`](datafusion_expr::Signature) -/// of the user-defined window function. +/// of the user-defined window function. /// * `$OUT_FN_NAME`: The basename to generate a unique function name like -/// `$OUT_FN_NAME_udwf`. +/// `$OUT_FN_NAME_udwf`. /// * `$DOC`: Doc comments for UDWF. /// * (optional) `$CTOR`: Pass a custom constructor. When omitted it -/// automatically resolves to `$UDWF::default()`. +/// automatically resolves to `$UDWF::default()`. /// /// # Example /// @@ -122,13 +122,13 @@ macro_rules! get_or_init_udwf { /// # Parameters /// /// * `$UDWF`: The struct which defines the [`Signature`] of the -/// user-defined window function. +/// user-defined window function. /// * `$OUT_FN_NAME`: The basename to generate a unique function name like -/// `$OUT_FN_NAME_udwf`. +/// `$OUT_FN_NAME_udwf`. /// * `$DOC`: Doc comments for UDWF. /// * (optional) `[$($PARAM:ident),+]`: An array of 1 or more parameters -/// for the generated function. The type of parameters is [`Expr`]. -/// When omitted this creates a function with zero parameters. +/// for the generated function. The type of parameters is [`Expr`]. +/// When omitted this creates a function with zero parameters. /// /// [`Signature`]: datafusion_expr::Signature /// [`Expr`]: datafusion_expr::Expr @@ -332,15 +332,15 @@ macro_rules! create_udwf_expr { /// # Arguments /// /// * `$UDWF`: The struct which defines the [`Signature`] of the -/// user-defined window function. +/// user-defined window function. /// * `$OUT_FN_NAME`: The basename to generate a unique function name like -/// `$OUT_FN_NAME_udwf`. +/// `$OUT_FN_NAME_udwf`. /// * (optional) `[$($PARAM:ident),+]`: An array of 1 or more parameters -/// for the generated function. The type of parameters is [`Expr`]. -/// When omitted this creates a function with zero parameters. +/// for the generated function. The type of parameters is [`Expr`]. +/// When omitted this creates a function with zero parameters. /// * `$DOC`: Doc comments for UDWF. /// * (optional) `$CTOR`: Pass a custom constructor. When omitted it -/// automatically resolves to `$UDWF::default()`. +/// automatically resolves to `$UDWF::default()`. /// /// [`Signature`]: datafusion_expr::Signature /// [`Expr`]: datafusion_expr::Expr diff --git a/datafusion/functions-window/src/nth_value.rs b/datafusion/functions-window/src/nth_value.rs index 1c781bd8e5f3f..36e6b83d61ce4 100644 --- a/datafusion/functions-window/src/nth_value.rs +++ b/datafusion/functions-window/src/nth_value.rs @@ -160,16 +160,49 @@ fn get_last_value_doc() -> &'static Documentation { static NTH_VALUE_DOCUMENTATION: LazyLock = LazyLock::new(|| { Documentation::builder( DOC_SECTION_ANALYTICAL, - "Returns value evaluated at the row that is the nth row of the window \ - frame (counting from 1); null if no such row.", + "Returns the value evaluated at the nth row of the window frame \ + (counting from 1). Returns NULL if no such row exists.", "nth_value(expression, n)", ) .with_argument( "expression", - "The name the column of which nth \ - value to retrieve", + "The column from which to retrieve the nth value.", + ) + .with_argument( + "n", + "Integer. Specifies the row number (starting from 1) in the window frame.", + ) + .with_sql_example( + r#"```sql +-- Sample employees table: +CREATE TABLE employees (id INT, salary INT); +INSERT INTO employees (id, salary) VALUES +(1, 30000), +(2, 40000), +(3, 50000), +(4, 60000), +(5, 70000); + +-- Example usage of nth_value: +SELECT nth_value(salary, 2) OVER ( + ORDER BY salary + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW +) AS nth_value +FROM employees; +``` + +```text ++-----------+ +| nth_value | ++-----------+ +| 40000 | +| 40000 | +| 40000 | +| 40000 | +| 40000 | ++-----------+ +```"#, ) - .with_argument("n", "Integer. Specifies the n in nth") .build() }); diff --git a/datafusion/functions-window/src/rank.rs b/datafusion/functions-window/src/rank.rs index bd2edc5722eb6..2ff2c31d8c2aa 100644 --- a/datafusion/functions-window/src/rank.rs +++ b/datafusion/functions-window/src/rank.rs @@ -261,7 +261,7 @@ impl PartitionEvaluator for RankEvaluator { .iter() .scan(1_u64, |acc, range| { let len = range.end - range.start; - let result = iter::repeat(*acc).take(len); + let result = iter::repeat_n(*acc, len); *acc += len as u64; Some(result) }) @@ -274,7 +274,7 @@ impl PartitionEvaluator for RankEvaluator { .zip(1u64..) .flat_map(|(range, rank)| { let len = range.end - range.start; - iter::repeat(rank).take(len) + iter::repeat_n(rank, len) }), )), @@ -287,7 +287,7 @@ impl PartitionEvaluator for RankEvaluator { .scan(0_u64, |acc, range| { let len = range.end - range.start; let value = (*acc as f64) / (denominator - 1.0).max(1.0); - let result = iter::repeat(value).take(len); + let result = iter::repeat_n(value, len); *acc += len as u64; Some(result) }) diff --git a/datafusion/functions/Cargo.toml b/datafusion/functions/Cargo.toml index 31ff55121b771..729770b8a65c6 100644 --- a/datafusion/functions/Cargo.toml +++ b/datafusion/functions/Cargo.toml @@ -66,7 +66,7 @@ arrow = { workspace = true } arrow-buffer = { workspace = true } base64 = { version = "0.22", optional = true } blake2 = { version = "^0.10.2", optional = true } -blake3 = { version = "1.7", optional = true } +blake3 = { version = "1.8", optional = true } chrono = { workspace = true } datafusion-common = { workspace = true } datafusion-doc = { workspace = true } diff --git a/datafusion/functions/benches/chr.rs b/datafusion/functions/benches/chr.rs index 4750fb4666532..8575809c21c8b 100644 --- a/datafusion/functions/benches/chr.rs +++ b/datafusion/functions/benches/chr.rs @@ -17,15 +17,21 @@ extern crate criterion; -use arrow::{array::PrimitiveArray, datatypes::Int64Type, util::test_util::seedable_rng}; +use arrow::{array::PrimitiveArray, datatypes::Int64Type}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use datafusion_expr::{ColumnarValue, ScalarFunctionArgs}; use datafusion_functions::string::chr; -use rand::Rng; +use rand::{Rng, SeedableRng}; use arrow::datatypes::DataType; +use rand::rngs::StdRng; use std::sync::Arc; +/// Returns fixed seedable RNG +pub fn seedable_rng() -> StdRng { + StdRng::seed_from_u64(42) +} + fn criterion_benchmark(c: &mut Criterion) { let cot_fn = chr(); let size = 1024; diff --git a/datafusion/functions/benches/regx.rs b/datafusion/functions/benches/regx.rs index 1f99cc3a5f0bc..3a1a6a71173e8 100644 --- a/datafusion/functions/benches/regx.rs +++ b/datafusion/functions/benches/regx.rs @@ -197,7 +197,7 @@ fn criterion_benchmark(c: &mut Criterion) { let regex = Arc::new(regex(&mut rng)) as ArrayRef; let flags = Arc::new(flags(&mut rng)) as ArrayRef; let replacement = - Arc::new(StringArray::from_iter_values(iter::repeat("XX").take(1000))) + Arc::new(StringArray::from_iter_values(iter::repeat_n("XX", 1000))) as ArrayRef; b.iter(|| { @@ -219,9 +219,9 @@ fn criterion_benchmark(c: &mut Criterion) { let regex = cast(®ex(&mut rng), &DataType::Utf8View).unwrap(); // flags are not allowed to be utf8view according to the function let flags = Arc::new(flags(&mut rng)) as ArrayRef; - let replacement = Arc::new(StringViewArray::from_iter_values( - iter::repeat("XX").take(1000), - )); + let replacement = Arc::new(StringViewArray::from_iter_values(iter::repeat_n( + "XX", 1000, + ))); b.iter(|| { black_box( diff --git a/datafusion/functions/src/datetime/to_timestamp.rs b/datafusion/functions/src/datetime/to_timestamp.rs index f1c61fe2b964d..52c86733f3327 100644 --- a/datafusion/functions/src/datetime/to_timestamp.rs +++ b/datafusion/functions/src/datetime/to_timestamp.rs @@ -18,15 +18,14 @@ use std::any::Any; use std::sync::Arc; +use crate::datetime::common::*; use arrow::datatypes::DataType::*; use arrow::datatypes::TimeUnit::{Microsecond, Millisecond, Nanosecond, Second}; use arrow::datatypes::{ ArrowTimestampType, DataType, TimeUnit, TimestampMicrosecondType, TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, }; - -use crate::datetime::common::*; -use datafusion_common::{exec_err, Result, ScalarType}; +use datafusion_common::{exec_err, Result, ScalarType, ScalarValue}; use datafusion_expr::{ ColumnarValue, Documentation, ScalarUDFImpl, Signature, Volatility, }; @@ -329,6 +328,30 @@ impl ScalarUDFImpl for ToTimestampFunc { Utf8View | LargeUtf8 | Utf8 => { to_timestamp_impl::(&args, "to_timestamp") } + Decimal128(_, _) => { + match &args[0] { + ColumnarValue::Scalar(ScalarValue::Decimal128( + Some(value), + _, + scale, + )) => { + // Convert decimal to seconds and nanoseconds + let scale_factor = 10_i128.pow(*scale as u32); + let seconds = value / scale_factor; + let fraction = value % scale_factor; + + let nanos = (fraction * 1_000_000_000) / scale_factor; + + let timestamp_nanos = seconds * 1_000_000_000 + nanos; + + Ok(ColumnarValue::Scalar(ScalarValue::TimestampNanosecond( + Some(timestamp_nanos as i64), + None, + ))) + } + _ => exec_err!("Invalid decimal value"), + } + } other => { exec_err!( "Unsupported data type {:?} for function to_timestamp", @@ -377,7 +400,7 @@ impl ScalarUDFImpl for ToTimestampSecondsFunc { } match args[0].data_type() { - Null | Int32 | Int64 | Timestamp(_, None) => { + Null | Int32 | Int64 | Timestamp(_, None) | Decimal128(_, _) => { args[0].cast_to(&Timestamp(Second, None), None) } Timestamp(_, Some(tz)) => args[0].cast_to(&Timestamp(Second, Some(tz)), None), diff --git a/datafusion/optimizer/Cargo.toml b/datafusion/optimizer/Cargo.toml index 3413b365f67de..60358d20e2a1a 100644 --- a/datafusion/optimizer/Cargo.toml +++ b/datafusion/optimizer/Cargo.toml @@ -55,9 +55,15 @@ regex-syntax = "0.8.0" [dev-dependencies] async-trait = { workspace = true } +criterion = { workspace = true } ctor = { workspace = true } datafusion-functions-aggregate = { workspace = true } datafusion-functions-window = { workspace = true } datafusion-functions-window-common = { workspace = true } datafusion-sql = { workspace = true } env_logger = { workspace = true } +insta = { workspace = true } + +[[bench]] +name = "projection_unnecessary" +harness = false diff --git a/datafusion/optimizer/benches/projection_unnecessary.rs b/datafusion/optimizer/benches/projection_unnecessary.rs new file mode 100644 index 0000000000000..100ee97542ebb --- /dev/null +++ b/datafusion/optimizer/benches/projection_unnecessary.rs @@ -0,0 +1,79 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use arrow::datatypes::{DataType, Field, Schema}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use datafusion_common::ToDFSchema; +use datafusion_common::{Column, TableReference}; +use datafusion_expr::{logical_plan::LogicalPlan, projection_schema, Expr}; +use datafusion_optimizer::optimize_projections::is_projection_unnecessary; +use std::sync::Arc; + +fn is_projection_unnecessary_old( + input: &LogicalPlan, + proj_exprs: &[Expr], +) -> datafusion_common::Result { + // First check if all expressions are trivial (cheaper operation than `projection_schema`) + if !proj_exprs + .iter() + .all(|expr| matches!(expr, Expr::Column(_) | Expr::Literal(_))) + { + return Ok(false); + } + let proj_schema = projection_schema(input, proj_exprs)?; + Ok(&proj_schema == input.schema()) +} + +fn create_plan_with_many_exprs(num_exprs: usize) -> (LogicalPlan, Vec) { + // Create schema with many fields + let fields = (0..num_exprs) + .map(|i| Field::new(format!("col{}", i), DataType::Int32, false)) + .collect::>(); + let schema = Schema::new(fields); + + // Create table scan + let table_scan = LogicalPlan::EmptyRelation(datafusion_expr::EmptyRelation { + produce_one_row: true, + schema: Arc::new(schema.clone().to_dfschema().unwrap()), + }); + + // Create projection expressions (just column references) + let exprs = (0..num_exprs) + .map(|i| Expr::Column(Column::new(None::, format!("col{}", i)))) + .collect(); + + (table_scan, exprs) +} + +fn benchmark_is_projection_unnecessary(c: &mut Criterion) { + let (plan, exprs) = create_plan_with_many_exprs(1000); + + let mut group = c.benchmark_group("projection_unnecessary_comparison"); + + group.bench_function("is_projection_unnecessary_new", |b| { + b.iter(|| black_box(is_projection_unnecessary(&plan, &exprs).unwrap())) + }); + + group.bench_function("is_projection_unnecessary_old", |b| { + b.iter(|| black_box(is_projection_unnecessary_old(&plan, &exprs).unwrap())) + }); + + group.finish(); +} + +criterion_group!(benches, benchmark_is_projection_unnecessary); +criterion_main!(benches); diff --git a/datafusion/optimizer/src/decorrelate.rs b/datafusion/optimizer/src/decorrelate.rs index 71ff863b51a18..418619c8399e3 100644 --- a/datafusion/optimizer/src/decorrelate.rs +++ b/datafusion/optimizer/src/decorrelate.rs @@ -501,10 +501,7 @@ fn agg_exprs_evaluation_result_on_empty_batch( let info = SimplifyContext::new(&props).with_schema(Arc::clone(schema)); let simplifier = ExprSimplifier::new(info); let result_expr = simplifier.simplify(result_expr)?; - if matches!(result_expr, Expr::Literal(ScalarValue::Int64(_))) { - expr_result_map_for_count_bug - .insert(e.schema_name().to_string(), result_expr); - } + expr_result_map_for_count_bug.insert(e.schema_name().to_string(), result_expr); } Ok(()) } diff --git a/datafusion/optimizer/src/optimize_projections/mod.rs b/datafusion/optimizer/src/optimize_projections/mod.rs index b3a09e2dcbcc7..4452b2d4ce034 100644 --- a/datafusion/optimizer/src/optimize_projections/mod.rs +++ b/datafusion/optimizer/src/optimize_projections/mod.rs @@ -31,8 +31,7 @@ use datafusion_common::{ use datafusion_expr::expr::Alias; use datafusion_expr::Unnest; use datafusion_expr::{ - logical_plan::LogicalPlan, projection_schema, Aggregate, Distinct, Expr, Projection, - TableScan, Window, + logical_plan::LogicalPlan, Aggregate, Distinct, Expr, Projection, TableScan, Window, }; use crate::optimize_projections::required_indices::RequiredIndices; @@ -455,6 +454,17 @@ fn merge_consecutive_projections(proj: Projection) -> Result::new(); expr.iter() @@ -774,9 +784,24 @@ fn rewrite_projection_given_requirements( /// Projection is unnecessary, when /// - input schema of the projection, output schema of the projection are same, and /// - all projection expressions are either Column or Literal -fn is_projection_unnecessary(input: &LogicalPlan, proj_exprs: &[Expr]) -> Result { - let proj_schema = projection_schema(input, proj_exprs)?; - Ok(&proj_schema == input.schema() && proj_exprs.iter().all(is_expr_trivial)) +pub fn is_projection_unnecessary( + input: &LogicalPlan, + proj_exprs: &[Expr], +) -> Result { + // First check if the number of expressions is equal to the number of fields in the input schema. + if proj_exprs.len() != input.schema().fields().len() { + return Ok(false); + } + Ok(input.schema().iter().zip(proj_exprs.iter()).all( + |((field_relation, field_name), expr)| { + // Check if the expression is a column and if it matches the field name + if let Expr::Column(col) = expr { + col.relation.as_ref() == field_relation && col.name.eq(field_name.name()) + } else { + false + } + }, + )) } #[cfg(test)] diff --git a/datafusion/optimizer/src/optimizer.rs b/datafusion/optimizer/src/optimizer.rs index ffbb95cb7f74e..b40121dbfeb7e 100644 --- a/datafusion/optimizer/src/optimizer.rs +++ b/datafusion/optimizer/src/optimizer.rs @@ -506,8 +506,11 @@ mod tests { }); let err = opt.optimize(plan, &config, &observe).unwrap_err(); - // Simplify assert to check the error message contains the expected message, which is only the schema length mismatch - assert_contains!(err.strip_backtrace(), "Schema mismatch: the schema length are not same Expected schema length: 3, got: 0"); + // Simplify assert to check the error message contains the expected message + assert_contains!( + err.strip_backtrace(), + "Failed due to a difference in schemas: original schema: DFSchema" + ); } #[test] diff --git a/datafusion/optimizer/src/scalar_subquery_to_join.rs b/datafusion/optimizer/src/scalar_subquery_to_join.rs index 499447861a58b..5c89bc29a596a 100644 --- a/datafusion/optimizer/src/scalar_subquery_to_join.rs +++ b/datafusion/optimizer/src/scalar_subquery_to_join.rs @@ -22,9 +22,10 @@ use std::sync::Arc; use crate::decorrelate::{PullUpCorrelatedExpr, UN_MATCHED_ROW_INDICATOR}; use crate::optimizer::ApplyOrder; -use crate::utils::replace_qualified_name; +use crate::utils::{evaluates_to_null, replace_qualified_name}; use crate::{OptimizerConfig, OptimizerRule}; +use crate::analyzer::type_coercion::TypeCoercionRewriter; use datafusion_common::alias::AliasGenerator; use datafusion_common::tree_node::{ Transformed, TransformedResult, TreeNode, TreeNodeRecursion, TreeNodeRewriter, @@ -348,6 +349,10 @@ fn build_join( let mut computation_project_expr = HashMap::new(); if let Some(expr_map) = collected_count_expr_map { for (name, result) in expr_map { + if evaluates_to_null(result.clone(), result.column_refs())? { + // If expr always returns null when column is null, skip processing + continue; + } let computer_expr = if let Some(filter) = &pull_up.pull_up_having_expr { Expr::Case(expr::Case { expr: None, @@ -381,7 +386,11 @@ fn build_join( )))), }) }; - computation_project_expr.insert(name, computer_expr); + let mut expr_rewrite = TypeCoercionRewriter { + schema: new_plan.schema(), + }; + computation_project_expr + .insert(name, computer_expr.rewrite(&mut expr_rewrite).data()?); } } @@ -425,18 +434,18 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: Int32(1) < __scalar_sq_1.max(orders.o_custkey) AND Int32(1) < __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: __scalar_sq_2.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: Int32(1) < __scalar_sq_1.max(orders.o_custkey) AND Int32(1) < __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: __scalar_sq_2.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], plan, @@ -480,19 +489,19 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_acctbal < __scalar_sq_1.sum(orders.o_totalprice) [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [sum(orders.o_totalprice):Float64;N, o_custkey:Int64]\ - \n Projection: sum(orders.o_totalprice), orders.o_custkey [sum(orders.o_totalprice):Float64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[sum(orders.o_totalprice)]] [o_custkey:Int64, sum(orders.o_totalprice):Float64;N]\ - \n Filter: orders.o_totalprice < __scalar_sq_2.sum(lineitem.l_extendedprice) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N]\ - \n Left Join: Filter: __scalar_sq_2.l_orderkey = orders.o_orderkey [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n SubqueryAlias: __scalar_sq_2 [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64]\ - \n Projection: sum(lineitem.l_extendedprice), lineitem.l_orderkey [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64]\ - \n Aggregate: groupBy=[[lineitem.l_orderkey]], aggr=[[sum(lineitem.l_extendedprice)]] [l_orderkey:Int64, sum(lineitem.l_extendedprice):Float64;N]\ - \n TableScan: lineitem [l_orderkey:Int64, l_partkey:Int64, l_suppkey:Int64, l_linenumber:Int32, l_quantity:Float64, l_extendedprice:Float64]"; + \n Filter: customer.c_acctbal < __scalar_sq_1.sum(orders.o_totalprice) [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [sum(orders.o_totalprice):Float64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: sum(orders.o_totalprice), orders.o_custkey, __always_true [sum(orders.o_totalprice):Float64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[sum(orders.o_totalprice)]] [o_custkey:Int64, __always_true:Boolean, sum(orders.o_totalprice):Float64;N]\ + \n Filter: orders.o_totalprice < __scalar_sq_2.sum(lineitem.l_extendedprice) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: __scalar_sq_2.l_orderkey = orders.o_orderkey [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n SubqueryAlias: __scalar_sq_2 [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64, __always_true:Boolean]\ + \n Projection: sum(lineitem.l_extendedprice), lineitem.l_orderkey, __always_true [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[lineitem.l_orderkey, Boolean(true) AS __always_true]], aggr=[[sum(lineitem.l_extendedprice)]] [l_orderkey:Int64, __always_true:Boolean, sum(lineitem.l_extendedprice):Float64;N]\ + \n TableScan: lineitem [l_orderkey:Int64, l_partkey:Int64, l_suppkey:Int64, l_linenumber:Int32, l_quantity:Float64, l_extendedprice:Float64]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], plan, @@ -522,14 +531,14 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n Filter: orders.o_orderkey = Int32(1) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n Filter: orders.o_orderkey = Int32(1) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -760,13 +769,56 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) + Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey) + Int32(1), orders.o_custkey [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) + Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey) + Int32(1), orders.o_custkey, __always_true [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + + assert_multi_rules_optimized_plan_eq_display_indent( + vec![Arc::new(ScalarSubqueryToJoin::new())], + plan, + expected, + ); + Ok(()) + } + + /// Test for correlated scalar subquery with non-strong project + #[test] + fn scalar_subquery_with_non_strong_project() -> Result<()> { + let case = Expr::Case(expr::Case { + expr: None, + when_then_expr: vec![( + Box::new(col("max(orders.o_totalprice)")), + Box::new(lit("a")), + )], + else_expr: Some(Box::new(lit("b"))), + }); + + let sq = Arc::new( + LogicalPlanBuilder::from(scan_tpch_table("orders")) + .filter( + out_ref_col(DataType::Int64, "customer.c_custkey") + .eq(col("orders.o_custkey")), + )? + .aggregate(Vec::::new(), vec![max(col("orders.o_totalprice"))])? + .project(vec![case])? + .build()?, + ); + + let plan = LogicalPlanBuilder::from(scan_tpch_table("customer")) + .project(vec![col("customer.c_custkey"), scalar_subquery(sq)])? + .build()?; + + let expected = "Projection: customer.c_custkey, CASE WHEN __scalar_sq_1.__always_true IS NULL THEN CASE WHEN CAST(NULL AS Boolean) THEN Utf8(\"a\") ELSE Utf8(\"b\") END ELSE __scalar_sq_1.CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END END AS CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END [c_custkey:Int64, CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END, orders.o_custkey, __always_true [CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_totalprice)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_totalprice):Float64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -824,13 +876,13 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey >= __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey >= __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -863,13 +915,13 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -903,13 +955,13 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) OR customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) OR customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -936,13 +988,13 @@ mod tests { .build()?; let expected = "Projection: test.c [c:UInt32]\ - \n Filter: test.c < __scalar_sq_1.min(sq.c) [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N]\ - \n Left Join: Filter: test.a = __scalar_sq_1.a [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N]\ - \n TableScan: test [a:UInt32, b:UInt32, c:UInt32]\ - \n SubqueryAlias: __scalar_sq_1 [min(sq.c):UInt32;N, a:UInt32]\ - \n Projection: min(sq.c), sq.a [min(sq.c):UInt32;N, a:UInt32]\ - \n Aggregate: groupBy=[[sq.a]], aggr=[[min(sq.c)]] [a:UInt32, min(sq.c):UInt32;N]\ - \n TableScan: sq [a:UInt32, b:UInt32, c:UInt32]"; + \n Filter: test.c < __scalar_sq_1.min(sq.c) [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N, __always_true:Boolean;N]\ + \n Left Join: Filter: test.a = __scalar_sq_1.a [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N, __always_true:Boolean;N]\ + \n TableScan: test [a:UInt32, b:UInt32, c:UInt32]\ + \n SubqueryAlias: __scalar_sq_1 [min(sq.c):UInt32;N, a:UInt32, __always_true:Boolean]\ + \n Projection: min(sq.c), sq.a, __always_true [min(sq.c):UInt32;N, a:UInt32, __always_true:Boolean]\ + \n Aggregate: groupBy=[[sq.a, Boolean(true) AS __always_true]], aggr=[[min(sq.c)]] [a:UInt32, __always_true:Boolean, min(sq.c):UInt32;N]\ + \n TableScan: sq [a:UInt32, b:UInt32, c:UInt32]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -1051,18 +1103,18 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey BETWEEN __scalar_sq_1.min(orders.o_custkey) AND __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_2.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [min(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: min(orders.o_custkey), orders.o_custkey [min(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[min(orders.o_custkey)]] [o_custkey:Int64, min(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ - \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey BETWEEN __scalar_sq_1.min(orders.o_custkey) AND __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_2.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [min(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: min(orders.o_custkey), orders.o_custkey, __always_true [min(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[min(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, min(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ + \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], diff --git a/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs b/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs index 9003467703df2..b1c03dcd00aaa 100644 --- a/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs +++ b/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs @@ -33,8 +33,8 @@ use datafusion_common::{ }; use datafusion_common::{internal_err, DFSchema, DataFusionError, Result, ScalarValue}; use datafusion_expr::{ - and, lit, or, BinaryExpr, Case, ColumnarValue, Expr, Like, Operator, Volatility, - WindowFunctionDefinition, + and, binary::BinaryTypeCoercer, lit, or, BinaryExpr, Case, ColumnarValue, Expr, Like, + Operator, Volatility, WindowFunctionDefinition, }; use datafusion_expr::{expr::ScalarFunction, interval_arithmetic::NullableInterval}; use datafusion_expr::{ @@ -188,7 +188,7 @@ impl ExprSimplifier { /// assert_eq!(expr, b_lt_2); /// ``` pub fn simplify(&self, expr: Expr) -> Result { - Ok(self.simplify_with_cycle_count(expr)?.0) + Ok(self.simplify_with_cycle_count_transformed(expr)?.0.data) } /// Like [Self::simplify], simplifies this [`Expr`] as much as possible, evaluating @@ -198,7 +198,34 @@ impl ExprSimplifier { /// /// See [Self::simplify] for details and usage examples. /// + #[deprecated( + since = "48.0.0", + note = "Use `simplify_with_cycle_count_transformed` instead" + )] + #[allow(unused_mut)] pub fn simplify_with_cycle_count(&self, mut expr: Expr) -> Result<(Expr, u32)> { + let (transformed, cycle_count) = + self.simplify_with_cycle_count_transformed(expr)?; + Ok((transformed.data, cycle_count)) + } + + /// Like [Self::simplify], simplifies this [`Expr`] as much as possible, evaluating + /// constants and applying algebraic simplifications. Additionally returns a `u32` + /// representing the number of simplification cycles performed, which can be useful for testing + /// optimizations. + /// + /// # Returns + /// + /// A tuple containing: + /// - The simplified expression wrapped in a `Transformed` indicating if changes were made + /// - The number of simplification cycles that were performed + /// + /// See [Self::simplify] for details and usage examples. + /// + pub fn simplify_with_cycle_count_transformed( + &self, + mut expr: Expr, + ) -> Result<(Transformed, u32)> { let mut simplifier = Simplifier::new(&self.info); let mut const_evaluator = ConstEvaluator::try_new(self.info.execution_props())?; let mut shorten_in_list_simplifier = ShortenInListSimplifier::new(); @@ -212,6 +239,7 @@ impl ExprSimplifier { // simplifications can enable new constant evaluation // see `Self::with_max_cycles` let mut num_cycles = 0; + let mut has_transformed = false; loop { let Transformed { data, transformed, .. @@ -221,13 +249,18 @@ impl ExprSimplifier { .transform_data(|expr| expr.rewrite(&mut guarantee_rewriter))?; expr = data; num_cycles += 1; + // Track if any transformation occurred + has_transformed = has_transformed || transformed; if !transformed || num_cycles >= self.max_simplifier_cycles { break; } } // shorten inlist should be started after other inlist rules are applied expr = expr.rewrite(&mut shorten_in_list_simplifier).data()?; - Ok((expr, num_cycles)) + Ok(( + Transformed::new_transformed(expr, has_transformed), + num_cycles, + )) } /// Apply type coercion to an [`Expr`] so that it can be @@ -392,15 +425,15 @@ impl ExprSimplifier { /// let expr = col("a").is_not_null(); /// /// // When using default maximum cycles, 2 cycles will be performed. - /// let (simplified_expr, count) = simplifier.simplify_with_cycle_count(expr.clone()).unwrap(); - /// assert_eq!(simplified_expr, lit(true)); + /// let (simplified_expr, count) = simplifier.simplify_with_cycle_count_transformed(expr.clone()).unwrap(); + /// assert_eq!(simplified_expr.data, lit(true)); /// // 2 cycles were executed, but only 1 was needed /// assert_eq!(count, 2); /// /// // Only 1 simplification pass is necessary here, so we can set the maximum cycles to 1. - /// let (simplified_expr, count) = simplifier.with_max_cycles(1).simplify_with_cycle_count(expr.clone()).unwrap(); + /// let (simplified_expr, count) = simplifier.with_max_cycles(1).simplify_with_cycle_count_transformed(expr.clone()).unwrap(); /// // Expression has been rewritten to: (c = a AND b = 1) - /// assert_eq!(simplified_expr, lit(true)); + /// assert_eq!(simplified_expr.data, lit(true)); /// // Only 1 cycle was executed /// assert_eq!(count, 1); /// @@ -760,6 +793,25 @@ impl TreeNodeRewriter for Simplifier<'_, S> { None => lit_bool_null(), }) } + // According to SQL's null semantics, NULL = NULL evaluates to NULL + // Both sides are the same expression (A = A) and A is non-volatile expression + // A = A --> A IS NOT NULL OR NULL + // A = A --> true (if A not nullable) + Expr::BinaryExpr(BinaryExpr { + left, + op: Eq, + right, + }) if (left == right) & !left.is_volatile() => { + Transformed::yes(match !info.nullable(&left)? { + true => lit(true), + false => Expr::BinaryExpr(BinaryExpr { + left: Box::new(Expr::IsNotNull(left)), + op: Or, + right: Box::new(lit_bool_null()), + }), + }) + } + // Rules for NotEq // @@ -976,30 +1028,39 @@ impl TreeNodeRewriter for Simplifier<'_, S> { // Rules for Multiply // - // A * 1 --> A + // A * 1 --> A (with type coercion if needed) Expr::BinaryExpr(BinaryExpr { left, op: Multiply, right, - }) if is_one(&right) => Transformed::yes(*left), - // 1 * A --> A + }) if is_one(&right) => { + simplify_right_is_one_case(info, left, &Multiply, &right)? + } + // A * null --> null Expr::BinaryExpr(BinaryExpr { left, op: Multiply, right, - }) if is_one(&left) => Transformed::yes(*right), - // A * null --> null + }) if is_null(&right) => { + simplify_right_is_null_case(info, &left, &Multiply, right)? + } + // 1 * A --> A Expr::BinaryExpr(BinaryExpr { - left: _, + left, op: Multiply, right, - }) if is_null(&right) => Transformed::yes(*right), + }) if is_one(&left) => { + // 1 * A is equivalent to A * 1 + simplify_right_is_one_case(info, right, &Multiply, &left)? + } // null * A --> null Expr::BinaryExpr(BinaryExpr { left, op: Multiply, - right: _, - }) if is_null(&left) => Transformed::yes(*left), + right, + }) if is_null(&left) => { + simplify_right_is_null_case(info, &right, &Multiply, left)? + } // A * 0 --> 0 (if A is not null and not floating, since NAN * 0 -> NAN) Expr::BinaryExpr(BinaryExpr { @@ -1033,19 +1094,23 @@ impl TreeNodeRewriter for Simplifier<'_, S> { left, op: Divide, right, - }) if is_one(&right) => Transformed::yes(*left), - // null / A --> null + }) if is_one(&right) => { + simplify_right_is_one_case(info, left, &Divide, &right)? + } + // A / null --> null Expr::BinaryExpr(BinaryExpr { left, op: Divide, - right: _, - }) if is_null(&left) => Transformed::yes(*left), - // A / null --> null + right, + }) if is_null(&right) => { + simplify_right_is_null_case(info, &left, &Divide, right)? + } + // null / A --> null Expr::BinaryExpr(BinaryExpr { - left: _, + left, op: Divide, right, - }) if is_null(&right) => Transformed::yes(*right), + }) if is_null(&left) => simplify_null_div_other_case(info, left, &right)?, // // Rules for Modulo @@ -1997,6 +2062,84 @@ fn is_exactly_true(expr: Expr, info: &impl SimplifyInfo) -> Result { } } +// A * 1 -> A +// A / 1 -> A +// +// Move this function body out of the large match branch avoid stack overflow +fn simplify_right_is_one_case( + info: &S, + left: Box, + op: &Operator, + right: &Expr, +) -> Result> { + // Check if resulting type would be different due to coercion + let left_type = info.get_data_type(&left)?; + let right_type = info.get_data_type(right)?; + match BinaryTypeCoercer::new(&left_type, op, &right_type).get_result_type() { + Ok(result_type) => { + // Only cast if the types differ + if left_type != result_type { + Ok(Transformed::yes(Expr::Cast(Cast::new(left, result_type)))) + } else { + Ok(Transformed::yes(*left)) + } + } + Err(_) => Ok(Transformed::yes(*left)), + } +} + +// A * null -> null +// A / null -> null +// +// Move this function body out of the large match branch avoid stack overflow +fn simplify_right_is_null_case( + info: &S, + left: &Expr, + op: &Operator, + right: Box, +) -> Result> { + // Check if resulting type would be different due to coercion + let left_type = info.get_data_type(left)?; + let right_type = info.get_data_type(&right)?; + match BinaryTypeCoercer::new(&left_type, op, &right_type).get_result_type() { + Ok(result_type) => { + // Only cast if the types differ + if right_type != result_type { + Ok(Transformed::yes(Expr::Cast(Cast::new(right, result_type)))) + } else { + Ok(Transformed::yes(*right)) + } + } + Err(_) => Ok(Transformed::yes(*right)), + } +} + +// null / A --> null +// +// Move this function body out of the large match branch avoid stack overflow +fn simplify_null_div_other_case( + info: &S, + left: Box, + right: &Expr, +) -> Result> { + // Check if resulting type would be different due to coercion + let left_type = info.get_data_type(&left)?; + let right_type = info.get_data_type(right)?; + match BinaryTypeCoercer::new(&left_type, &Operator::Divide, &right_type) + .get_result_type() + { + Ok(result_type) => { + // Only cast if the types differ + if left_type != result_type { + Ok(Transformed::yes(Expr::Cast(Cast::new(left, result_type)))) + } else { + Ok(Transformed::yes(*left)) + } + } + Err(_) => Ok(Transformed::yes(*left)), + } +} + #[cfg(test)] mod tests { use crate::simplify_expressions::SimplifyContext; @@ -2152,6 +2295,21 @@ mod tests { } } + #[test] + fn test_simplify_eq_not_self() { + // `expr_a`: column `c2` is nullable, so `c2 = c2` simplifies to `c2 IS NOT NULL OR NULL` + // This ensures the expression is only true when `c2` is not NULL, accounting for SQL's NULL semantics. + let expr_a = col("c2").eq(col("c2")); + let expected_a = col("c2").is_not_null().or(lit_bool_null()); + + // `expr_b`: column `c2_non_null` is explicitly non-nullable, so `c2_non_null = c2_non_null` is always true + let expr_b = col("c2_non_null").eq(col("c2_non_null")); + let expected_b = lit(true); + + assert_eq!(simplify(expr_a), expected_a); + assert_eq!(simplify(expr_b), expected_b); + } + #[test] fn test_simplify_or_true() { let expr_a = col("c2").or(lit(true)); @@ -2316,12 +2474,12 @@ mod tests { // A / null --> null let null = lit(ScalarValue::Null); { - let expr = col("c") / null.clone(); + let expr = col("c1") / null.clone(); assert_eq!(simplify(expr), null); } // null / A --> null { - let expr = null.clone() / col("c"); + let expr = null.clone() / col("c1"); assert_eq!(simplify(expr), null); } } @@ -3185,6 +3343,15 @@ mod tests { simplifier.simplify(expr) } + fn coerce(expr: Expr) -> Expr { + let schema = expr_test_schema(); + let execution_props = ExecutionProps::new(); + let simplifier = ExprSimplifier::new( + SimplifyContext::new(&execution_props).with_schema(Arc::clone(&schema)), + ); + simplifier.coerce(expr, schema.as_ref()).unwrap() + } + fn simplify(expr: Expr) -> Expr { try_simplify(expr).unwrap() } @@ -3195,7 +3362,8 @@ mod tests { let simplifier = ExprSimplifier::new( SimplifyContext::new(&execution_props).with_schema(schema), ); - simplifier.simplify_with_cycle_count(expr) + let (expr, count) = simplifier.simplify_with_cycle_count_transformed(expr)?; + Ok((expr.data, count)) } fn simplify_with_cycle_count(expr: Expr) -> (Expr, u32) { @@ -3227,6 +3395,7 @@ mod tests { Field::new("c2_non_null", DataType::Boolean, false), Field::new("c3_non_null", DataType::Int64, false), Field::new("c4_non_null", DataType::UInt32, false), + Field::new("c5", DataType::FixedSizeBinary(3), true), ] .into(), HashMap::new(), @@ -4350,6 +4519,34 @@ mod tests { } } + #[test] + fn simplify_fixed_size_binary_eq_lit() { + let bytes = [1u8, 2, 3].as_slice(); + + // The expression starts simple. + let expr = col("c5").eq(lit(bytes)); + + // The type coercer introduces a cast. + let coerced = coerce(expr.clone()); + let schema = expr_test_schema(); + assert_eq!( + coerced, + col("c5") + .cast_to(&DataType::Binary, schema.as_ref()) + .unwrap() + .eq(lit(bytes)) + ); + + // The simplifier removes the cast. + assert_eq!( + simplify(coerced), + col("c5").eq(Expr::Literal(ScalarValue::FixedSizeBinary( + 3, + Some(bytes.to_vec()), + ))) + ); + } + fn if_not_null(expr: Expr, then: bool) -> Expr { Expr::Case(Case { expr: Some(expr.is_not_null().into()), diff --git a/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs b/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs index e33869ca2b636..6314209dc7670 100644 --- a/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs +++ b/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs @@ -123,10 +123,11 @@ impl SimplifyExpressions { let name_preserver = NamePreserver::new(&plan); let mut rewrite_expr = |expr: Expr| { let name = name_preserver.save(&expr); - let expr = simplifier.simplify(expr)?; - // TODO it would be nice to have a way to know if the expression was simplified - // or not. For now conservatively return Transformed::yes - Ok(Transformed::yes(name.restore(expr))) + let expr = simplifier.simplify_with_cycle_count_transformed(expr)?.0; + Ok(Transformed::new_transformed( + name.restore(expr.data), + expr.transformed, + )) }; plan.map_expressions(|expr| { diff --git a/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs b/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs index be71a8cd19b00..37116018cdca5 100644 --- a/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs +++ b/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs @@ -197,6 +197,7 @@ fn is_supported_type(data_type: &DataType) -> bool { is_supported_numeric_type(data_type) || is_supported_string_type(data_type) || is_supported_dictionary_type(data_type) + || is_supported_binary_type(data_type) } /// Returns true if unwrap_cast_in_comparison support this numeric type @@ -230,6 +231,10 @@ fn is_supported_dictionary_type(data_type: &DataType) -> bool { DataType::Dictionary(_, inner) if is_supported_type(inner)) } +fn is_supported_binary_type(data_type: &DataType) -> bool { + matches!(data_type, DataType::Binary | DataType::FixedSizeBinary(_)) +} + ///// Tries to move a cast from an expression (such as column) to the literal other side of a comparison operator./ /// /// Specifically, rewrites @@ -292,6 +297,7 @@ pub(super) fn try_cast_literal_to_type( try_cast_numeric_literal(lit_value, target_type) .or_else(|| try_cast_string_literal(lit_value, target_type)) .or_else(|| try_cast_dictionary(lit_value, target_type)) + .or_else(|| try_cast_binary(lit_value, target_type)) } /// Convert a numeric value from one numeric data type to another @@ -501,6 +507,20 @@ fn cast_between_timestamp(from: &DataType, to: &DataType, value: i128) -> Option } } +fn try_cast_binary( + lit_value: &ScalarValue, + target_type: &DataType, +) -> Option { + match (lit_value, target_type) { + (ScalarValue::Binary(Some(v)), DataType::FixedSizeBinary(n)) + if v.len() == *n as usize => + { + Some(ScalarValue::FixedSizeBinary(*n, Some(v.clone()))) + } + _ => None, + } +} + #[cfg(test)] mod tests { use super::*; @@ -1450,4 +1470,13 @@ mod tests { ) } } + + #[test] + fn try_cast_to_fixed_size_binary() { + expect_cast( + ScalarValue::Binary(Some(vec![1, 2, 3])), + DataType::FixedSizeBinary(3), + ExpectedCast::Value(ScalarValue::FixedSizeBinary(3, Some(vec![1, 2, 3]))), + ) + } } diff --git a/datafusion/optimizer/src/utils.rs b/datafusion/optimizer/src/utils.rs index c734d908f6d6c..41c40ec06d652 100644 --- a/datafusion/optimizer/src/utils.rs +++ b/datafusion/optimizer/src/utils.rs @@ -79,6 +79,50 @@ pub fn is_restrict_null_predicate<'a>( return Ok(true); } + // If result is single `true`, return false; + // If result is single `NULL` or `false`, return true; + Ok( + match evaluate_expr_with_null_column(predicate, join_cols_of_predicate)? { + ColumnarValue::Array(array) => { + if array.len() == 1 { + let boolean_array = as_boolean_array(&array)?; + boolean_array.is_null(0) || !boolean_array.value(0) + } else { + false + } + } + ColumnarValue::Scalar(scalar) => matches!( + scalar, + ScalarValue::Boolean(None) | ScalarValue::Boolean(Some(false)) + ), + }, + ) +} + +/// Determines if an expression will always evaluate to null. +/// `c0 + 8` return true +/// `c0 IS NULL` return false +/// `CASE WHEN c0 > 1 then 0 else 1` return false +pub fn evaluates_to_null<'a>( + predicate: Expr, + null_columns: impl IntoIterator, +) -> Result { + if matches!(predicate, Expr::Column(_)) { + return Ok(true); + } + + Ok( + match evaluate_expr_with_null_column(predicate, null_columns)? { + ColumnarValue::Array(_) => false, + ColumnarValue::Scalar(scalar) => scalar.is_null(), + }, + ) +} + +fn evaluate_expr_with_null_column<'a>( + predicate: Expr, + null_columns: impl IntoIterator, +) -> Result { static DUMMY_COL_NAME: &str = "?"; let schema = Schema::new(vec![Field::new(DUMMY_COL_NAME, DataType::Null, true)]); let input_schema = DFSchema::try_from(schema.clone())?; @@ -87,37 +131,15 @@ pub fn is_restrict_null_predicate<'a>( let execution_props = ExecutionProps::default(); let null_column = Column::from_name(DUMMY_COL_NAME); - let join_cols_to_replace = join_cols_of_predicate + let join_cols_to_replace = null_columns .into_iter() .map(|column| (column, &null_column)) .collect::>(); let replaced_predicate = replace_col(predicate, &join_cols_to_replace)?; let coerced_predicate = coerce(replaced_predicate, &input_schema)?; - let phys_expr = - create_physical_expr(&coerced_predicate, &input_schema, &execution_props)?; - - let result_type = phys_expr.data_type(&schema)?; - if !matches!(&result_type, DataType::Boolean) { - return Ok(false); - } - - // If result is single `true`, return false; - // If result is single `NULL` or `false`, return true; - Ok(match phys_expr.evaluate(&input_batch)? { - ColumnarValue::Array(array) => { - if array.len() == 1 { - let boolean_array = as_boolean_array(&array)?; - boolean_array.is_null(0) || !boolean_array.value(0) - } else { - false - } - } - ColumnarValue::Scalar(scalar) => matches!( - scalar, - ScalarValue::Boolean(None) | ScalarValue::Boolean(Some(false)) - ), - }) + create_physical_expr(&coerced_predicate, &input_schema, &execution_props)? + .evaluate(&input_batch) } fn coerce(expr: Expr, schema: &DFSchema) -> Result { diff --git a/datafusion/optimizer/tests/optimizer_integration.rs b/datafusion/optimizer/tests/optimizer_integration.rs index 098027dd06420..941e5bd7b4d77 100644 --- a/datafusion/optimizer/tests/optimizer_integration.rs +++ b/datafusion/optimizer/tests/optimizer_integration.rs @@ -37,6 +37,7 @@ use datafusion_sql::planner::{ContextProvider, SqlToRel}; use datafusion_sql::sqlparser::ast::Statement; use datafusion_sql::sqlparser::dialect::GenericDialect; use datafusion_sql::sqlparser::parser::Parser; +use insta::assert_snapshot; #[cfg(test)] #[ctor::ctor] @@ -49,16 +50,25 @@ fn init() { fn case_when() -> Result<()> { let sql = "SELECT CASE WHEN col_int32 > 0 THEN 1 ELSE 0 END FROM test"; let plan = test_sql(sql)?; - let expected = - "Projection: CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" +Projection: CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END + TableScan: test projection=[col_int32] +"# + ); let sql = "SELECT CASE WHEN col_uint32 > 0 THEN 1 ELSE 0 END FROM test"; let plan = test_sql(sql)?; - let expected = "Projection: CASE WHEN test.col_uint32 > UInt32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_uint32 > Int64(0) THEN Int64(1) ELSE Int64(0) END\ - \n TableScan: test projection=[col_uint32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: CASE WHEN test.col_uint32 > UInt32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_uint32 > Int64(0) THEN Int64(1) ELSE Int64(0) END + TableScan: test projection=[col_uint32] + "# + ); Ok(()) } @@ -72,16 +82,21 @@ fn subquery_filter_with_cast() -> Result<()> { AND (cast('2002-05-08' as date) + interval '5 days')\ )"; let plan = test_sql(sql)?; - let expected = "Projection: test.col_int32\ - \n Inner Join: Filter: CAST(test.col_int32 AS Float64) > __scalar_sq_1.avg(test.col_int32)\ - \n TableScan: test projection=[col_int32]\ - \n SubqueryAlias: __scalar_sq_1\ - \n Aggregate: groupBy=[[]], aggr=[[avg(CAST(test.col_int32 AS Float64))]]\ - \n Projection: test.col_int32\ - \n Filter: __common_expr_4 >= Date32(\"2002-05-08\") AND __common_expr_4 <= Date32(\"2002-05-13\")\ - \n Projection: CAST(test.col_utf8 AS Date32) AS __common_expr_4, test.col_int32\ - \n TableScan: test projection=[col_int32, col_utf8]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: test.col_int32 + Inner Join: Filter: CAST(test.col_int32 AS Float64) > __scalar_sq_1.avg(test.col_int32) + TableScan: test projection=[col_int32] + SubqueryAlias: __scalar_sq_1 + Aggregate: groupBy=[[]], aggr=[[avg(CAST(test.col_int32 AS Float64))]] + Projection: test.col_int32 + Filter: __common_expr_4 >= Date32("2002-05-08") AND __common_expr_4 <= Date32("2002-05-13") + Projection: CAST(test.col_utf8 AS Date32) AS __common_expr_4, test.col_int32 + TableScan: test projection=[col_int32, col_utf8] + "# + ); Ok(()) } @@ -89,10 +104,15 @@ fn subquery_filter_with_cast() -> Result<()> { fn case_when_aggregate() -> Result<()> { let sql = "SELECT col_utf8, sum(CASE WHEN col_int32 > 0 THEN 1 ELSE 0 END) AS n FROM test GROUP BY col_utf8"; let plan = test_sql(sql)?; - let expected = "Projection: test.col_utf8, sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END) AS n\ - \n Aggregate: groupBy=[[test.col_utf8]], aggr=[[sum(CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END) AS sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END)]]\ - \n TableScan: test projection=[col_int32, col_utf8]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: test.col_utf8, sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END) AS n + Aggregate: groupBy=[[test.col_utf8]], aggr=[[sum(CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END) AS sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END)]] + TableScan: test projection=[col_int32, col_utf8] + "# + ); Ok(()) } @@ -100,10 +120,15 @@ fn case_when_aggregate() -> Result<()> { fn unsigned_target_type() -> Result<()> { let sql = "SELECT col_utf8 FROM test WHERE col_uint32 > 0"; let plan = test_sql(sql)?; - let expected = "Projection: test.col_utf8\ - \n Filter: test.col_uint32 > UInt32(0)\ - \n TableScan: test projection=[col_uint32, col_utf8]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: test.col_utf8 + Filter: test.col_uint32 > UInt32(0) + TableScan: test projection=[col_uint32, col_utf8] + "# + ); Ok(()) } @@ -112,9 +137,14 @@ fn distribute_by() -> Result<()> { // regression test for https://github.com/apache/datafusion/issues/3234 let sql = "SELECT col_int32, col_utf8 FROM test DISTRIBUTE BY (col_utf8)"; let plan = test_sql(sql)?; - let expected = "Repartition: DistributeBy(test.col_utf8)\ - \n TableScan: test projection=[col_int32, col_utf8]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Repartition: DistributeBy(test.col_utf8) + TableScan: test projection=[col_int32, col_utf8] + "# + ); Ok(()) } @@ -125,15 +155,20 @@ fn semi_join_with_join_filter() -> Result<()> { SELECT col_utf8 FROM test t2 WHERE test.col_int32 = t2.col_int32 \ AND test.col_uint32 != t2.col_uint32)"; let plan = test_sql(sql)?; - let expected = "Projection: test.col_utf8\ - \n LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32, col_uint32, col_utf8]\ - \n SubqueryAlias: __correlated_sq_1\ - \n SubqueryAlias: t2\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32, col_uint32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: test.col_utf8 + LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32, col_uint32, col_utf8] + SubqueryAlias: __correlated_sq_1 + SubqueryAlias: t2 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32, col_uint32] + "# + ); Ok(()) } @@ -144,14 +179,19 @@ fn anti_join_with_join_filter() -> Result<()> { SELECT col_utf8 FROM test t2 WHERE test.col_int32 = t2.col_int32 \ AND test.col_uint32 != t2.col_uint32)"; let plan = test_sql(sql)?; - let expected = "Projection: test.col_utf8\ - \n LeftAnti Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32\ - \n TableScan: test projection=[col_int32, col_uint32, col_utf8]\ - \n SubqueryAlias: __correlated_sq_1\ - \n SubqueryAlias: t2\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32, col_uint32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" +Projection: test.col_utf8 + LeftAnti Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32 + TableScan: test projection=[col_int32, col_uint32, col_utf8] + SubqueryAlias: __correlated_sq_1 + SubqueryAlias: t2 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32, col_uint32] +"# + ); Ok(()) } @@ -160,15 +200,21 @@ fn where_exists_distinct() -> Result<()> { let sql = "SELECT col_int32 FROM test WHERE EXISTS (\ SELECT DISTINCT col_int32 FROM test t2 WHERE test.col_int32 = t2.col_int32)"; let plan = test_sql(sql)?; - let expected = "LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32]\ - \n SubqueryAlias: __correlated_sq_1\ - \n Aggregate: groupBy=[[t2.col_int32]], aggr=[[]]\ - \n SubqueryAlias: t2\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" +LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32] + SubqueryAlias: __correlated_sq_1 + Aggregate: groupBy=[[t2.col_int32]], aggr=[[]] + SubqueryAlias: t2 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32] +"# + + ); Ok(()) } @@ -178,15 +224,19 @@ fn intersect() -> Result<()> { INTERSECT SELECT col_int32, col_utf8 FROM test \ INTERSECT SELECT col_int32, col_utf8 FROM test"; let plan = test_sql(sql)?; - let expected = - "LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8\ - \n Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]]\ - \n LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8\ - \n Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]]\ - \n TableScan: test projection=[col_int32, col_utf8]\ - \n TableScan: test projection=[col_int32, col_utf8]\ - \n TableScan: test projection=[col_int32, col_utf8]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" +LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8 + Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]] + LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8 + Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]] + TableScan: test projection=[col_int32, col_utf8] + TableScan: test projection=[col_int32, col_utf8] + TableScan: test projection=[col_int32, col_utf8] +"# + ); Ok(()) } @@ -195,12 +245,16 @@ fn between_date32_plus_interval() -> Result<()> { let sql = "SELECT count(1) FROM test \ WHERE col_date32 between '1998-03-18' AND cast('1998-03-18' as date) + INTERVAL '90 days'"; let plan = test_sql(sql)?; - let expected = - "Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]]\ - \n Projection: \ - \n Filter: test.col_date32 >= Date32(\"1998-03-18\") AND test.col_date32 <= Date32(\"1998-06-16\")\ - \n TableScan: test projection=[col_date32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" +Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] + Projection: + Filter: test.col_date32 >= Date32("1998-03-18") AND test.col_date32 <= Date32("1998-06-16") + TableScan: test projection=[col_date32] +"# + ); Ok(()) } @@ -209,12 +263,16 @@ fn between_date64_plus_interval() -> Result<()> { let sql = "SELECT count(1) FROM test \ WHERE col_date64 between '1998-03-18T00:00:00' AND cast('1998-03-18' as date) + INTERVAL '90 days'"; let plan = test_sql(sql)?; - let expected = - "Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]]\ - \n Projection: \ - \n Filter: test.col_date64 >= Date64(\"1998-03-18\") AND test.col_date64 <= Date64(\"1998-06-16\")\ - \n TableScan: test projection=[col_date64]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] + Projection: + Filter: test.col_date64 >= Date64("1998-03-18") AND test.col_date64 <= Date64("1998-06-16") + TableScan: test projection=[col_date64] + "# + ); Ok(()) } @@ -223,54 +281,73 @@ fn propagate_empty_relation() { let sql = "SELECT test.col_int32 FROM test JOIN ( SELECT col_int32 FROM test WHERE false ) AS ta1 ON test.col_int32 = ta1.col_int32;"; let plan = test_sql(sql).unwrap(); // when children exist EmptyRelation, it will bottom-up propagate. - let expected = "EmptyRelation"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + EmptyRelation + "# + ); } #[test] fn join_keys_in_subquery_alias() { let sql = "SELECT * FROM test AS A, ( SELECT col_int32 as key FROM test ) AS B where A.col_int32 = B.key;"; let plan = test_sql(sql).unwrap(); - let expected = "Inner Join: a.col_int32 = b.key\ - \n SubqueryAlias: a\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]\ - \n SubqueryAlias: b\ - \n Projection: test.col_int32 AS key\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32]"; - - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Inner Join: a.col_int32 = b.key + SubqueryAlias: a + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] + SubqueryAlias: b + Projection: test.col_int32 AS key + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32] + "# + ); } #[test] fn join_keys_in_subquery_alias_1() { let sql = "SELECT * FROM test AS A, ( SELECT test.col_int32 AS key FROM test JOIN test AS C on test.col_int32 = C.col_int32 ) AS B where A.col_int32 = B.key;"; let plan = test_sql(sql).unwrap(); - let expected = "Inner Join: a.col_int32 = b.key\ - \n SubqueryAlias: a\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]\ - \n SubqueryAlias: b\ - \n Projection: test.col_int32 AS key\ - \n Inner Join: test.col_int32 = c.col_int32\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32]\ - \n SubqueryAlias: c\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Inner Join: a.col_int32 = b.key + SubqueryAlias: a + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] + SubqueryAlias: b + Projection: test.col_int32 AS key + Inner Join: test.col_int32 = c.col_int32 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32] + SubqueryAlias: c + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32] + "# + ); } #[test] fn push_down_filter_groupby_expr_contains_alias() { let sql = "SELECT * FROM (SELECT (col_int32 + col_uint32) AS c, count(*) FROM test GROUP BY 1) where c > 3"; let plan = test_sql(sql).unwrap(); - let expected = "Projection: test.col_int32 + test.col_uint32 AS c, count(Int64(1)) AS count(*)\ - \n Aggregate: groupBy=[[CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64)]], aggr=[[count(Int64(1))]]\ - \n Filter: CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64) > Int64(3)\ - \n TableScan: test projection=[col_int32, col_uint32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: test.col_int32 + test.col_uint32 AS c, count(Int64(1)) AS count(*) + Aggregate: groupBy=[[CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64)]], aggr=[[count(Int64(1))]] + Filter: CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64) > Int64(3) + TableScan: test projection=[col_int32, col_uint32] + "# + ); } #[test] @@ -278,13 +355,18 @@ fn push_down_filter_groupby_expr_contains_alias() { fn test_same_name_but_not_ambiguous() { let sql = "SELECT t1.col_int32 AS col_int32 FROM test t1 intersect SELECT col_int32 FROM test t2"; let plan = test_sql(sql).unwrap(); - let expected = "LeftSemi Join: t1.col_int32 = t2.col_int32\ - \n Aggregate: groupBy=[[t1.col_int32]], aggr=[[]]\ - \n SubqueryAlias: t1\ - \n TableScan: test projection=[col_int32]\ - \n SubqueryAlias: t2\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + LeftSemi Join: t1.col_int32 = t2.col_int32 + Aggregate: groupBy=[[t1.col_int32]], aggr=[[]] + SubqueryAlias: t1 + TableScan: test projection=[col_int32] + SubqueryAlias: t2 + TableScan: test projection=[col_int32] + "# + ); } #[test] @@ -295,11 +377,14 @@ fn eliminate_nested_filters() { AND (1=1) AND (1=0 OR 1=1)"; let plan = test_sql(sql).unwrap(); - let expected = "\ - Filter: test.col_int32 > Int32(0)\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + assert_snapshot!( + format!("{plan}"), + @r#" +Filter: test.col_int32 > Int32(0) + TableScan: test projection=[col_int32] + "# + ); } #[test] @@ -310,10 +395,15 @@ fn eliminate_redundant_null_check_on_count() { GROUP BY col_int32 HAVING c IS NOT NULL"; let plan = test_sql(sql).unwrap(); - let expected = "Projection: test.col_int32, count(Int64(1)) AS count(*) AS c\ - \n Aggregate: groupBy=[[test.col_int32]], aggr=[[count(Int64(1))]]\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: test.col_int32, count(Int64(1)) AS count(*) AS c + Aggregate: groupBy=[[test.col_int32]], aggr=[[count(Int64(1))]] + TableScan: test projection=[col_int32] + "# + ); } #[test] @@ -333,13 +423,16 @@ fn test_propagate_empty_relation_inner_join_and_unions() { SELECT test.col_int32 FROM test WHERE 1 = 0"; let plan = test_sql(sql).unwrap(); - let expected = "\ - Union\ - \n TableScan: test projection=[col_int32]\ - \n TableScan: test projection=[col_int32]\ - \n Filter: test.col_int32 < Int32(0)\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" +Union + TableScan: test projection=[col_int32] + TableScan: test projection=[col_int32] + Filter: test.col_int32 < Int32(0) + TableScan: test projection=[col_int32] + "#); } #[test] @@ -347,10 +440,14 @@ fn select_wildcard_with_repeated_column_but_is_aliased() { let sql = "SELECT *, col_int32 as col_32 FROM test"; let plan = test_sql(sql).unwrap(); - let expected = "Projection: test.col_int32, test.col_uint32, test.col_utf8, test.col_date32, test.col_date64, test.col_ts_nano_none, test.col_ts_nano_utc, test.col_int32 AS col_32\ - \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]"; - assert_eq!(expected, format!("{plan}")); + assert_snapshot!( + format!("{plan}"), + @r#" + Projection: test.col_int32, test.col_uint32, test.col_utf8, test.col_date32, test.col_date64, test.col_ts_nano_none, test.col_ts_nano_utc, test.col_int32 AS col_32 + TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] + "# + ); } #[test] @@ -367,15 +464,20 @@ fn select_correlated_predicate_subquery_with_uppercase_ident() { ) "#; let plan = test_sql(sql).unwrap(); - let expected = "LeftSemi Join: test.col_int32 = __correlated_sq_1.COL_INT32\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]\ - \n SubqueryAlias: __correlated_sq_1\ - \n SubqueryAlias: T1\ - \n Projection: test.col_int32 AS COL_INT32\ - \n Filter: test.col_int32 IS NOT NULL\ - \n TableScan: test projection=[col_int32]"; - assert_eq!(expected, format!("{plan}")); + + assert_snapshot!( + format!("{plan}"), + @r#" + LeftSemi Join: test.col_int32 = __correlated_sq_1.COL_INT32 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] + SubqueryAlias: __correlated_sq_1 + SubqueryAlias: T1 + Projection: test.col_int32 AS COL_INT32 + Filter: test.col_int32 IS NOT NULL + TableScan: test projection=[col_int32] + "# + ); } fn test_sql(sql: &str) -> Result { diff --git a/datafusion/physical-expr-common/src/physical_expr.rs b/datafusion/physical-expr-common/src/physical_expr.rs index 43f214607f9fc..3bc41d2652d9a 100644 --- a/datafusion/physical-expr-common/src/physical_expr.rs +++ b/datafusion/physical-expr-common/src/physical_expr.rs @@ -27,6 +27,7 @@ use arrow::array::BooleanArray; use arrow::compute::filter_record_batch; use arrow::datatypes::{DataType, Schema}; use arrow::record_batch::RecordBatch; +use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode}; use datafusion_common::{internal_err, not_impl_err, Result, ScalarValue}; use datafusion_expr_common::columnar_value::ColumnarValue; use datafusion_expr_common::interval_arithmetic::Interval; @@ -283,6 +284,55 @@ pub trait PhysicalExpr: Send + Sync + Display + Debug + DynEq + DynHash { /// See the [`fmt_sql`] function for an example of printing `PhysicalExpr`s as SQL. /// fn fmt_sql(&self, f: &mut Formatter<'_>) -> fmt::Result; + + /// Take a snapshot of this `PhysicalExpr`, if it is dynamic. + /// + /// "Dynamic" in this case means containing references to structures that may change + /// during plan execution, such as hash tables. + /// + /// This method is used to capture the current state of `PhysicalExpr`s that may contain + /// dynamic references to other operators in order to serialize it over the wire + /// or treat it via downcast matching. + /// + /// You should not call this method directly as it does not handle recursion. + /// Instead use [`snapshot_physical_expr`] to handle recursion and capture the + /// full state of the `PhysicalExpr`. + /// + /// This is expected to return "simple" expressions that do not have mutable state + /// and are composed of DataFusion's built-in `PhysicalExpr` implementations. + /// Callers however should *not* assume anything about the returned expressions + /// since callers and implementers may not agree on what "simple" or "built-in" + /// means. + /// In other words, if you need to serialize a `PhysicalExpr` across the wire + /// you should call this method and then try to serialize the result, + /// but you should handle unknown or unexpected `PhysicalExpr` implementations gracefully + /// just as if you had not called this method at all. + /// + /// In particular, consider: + /// * A `PhysicalExpr` that references the current state of a `datafusion::physical_plan::TopK` + /// that is involved in a query with `SELECT * FROM t1 ORDER BY a LIMIT 10`. + /// This function may return something like `a >= 12`. + /// * A `PhysicalExpr` that references the current state of a `datafusion::physical_plan::joins::HashJoinExec` + /// from a query such as `SELECT * FROM t1 JOIN t2 ON t1.a = t2.b`. + /// This function may return something like `t2.b IN (1, 5, 7)`. + /// + /// A system or function that can only deal with a hardcoded set of `PhysicalExpr` implementations + /// or needs to serialize this state to bytes may not be able to handle these dynamic references. + /// In such cases, we should return a simplified version of the `PhysicalExpr` that does not + /// contain these dynamic references. + /// + /// Systems that implement remote execution of plans, e.g. serialize a portion of the query plan + /// and send it across the wire to a remote executor may want to call this method after + /// every batch on the source side and brodcast / update the current snaphot to the remote executor. + /// + /// Note for implementers: this method should *not* handle recursion. + /// Recursion is handled in [`snapshot_physical_expr`]. + fn snapshot(&self) -> Result>> { + // By default, we return None to indicate that this PhysicalExpr does not + // have any dynamic references or state. + // This is a safe default behavior. + Ok(None) + } } /// [`PhysicalExpr`] can't be constrained by [`Eq`] directly because it must remain object @@ -446,3 +496,30 @@ pub fn fmt_sql(expr: &dyn PhysicalExpr) -> impl Display + '_ { Wrapper { expr } } + +/// Take a snapshot of the given `PhysicalExpr` if it is dynamic. +/// +/// Take a snapshot of this `PhysicalExpr` if it is dynamic. +/// This is used to capture the current state of `PhysicalExpr`s that may contain +/// dynamic references to other operators in order to serialize it over the wire +/// or treat it via downcast matching. +/// +/// See the documentation of [`PhysicalExpr::snapshot`] for more details. +/// +/// # Returns +/// +/// Returns an `Option>` which is the snapshot of the +/// `PhysicalExpr` if it is dynamic. If the `PhysicalExpr` does not have +/// any dynamic references or state, it returns `None`. +pub fn snapshot_physical_expr( + expr: Arc, +) -> Result> { + expr.transform_up(|e| { + if let Some(snapshot) = e.snapshot()? { + Ok(Transformed::yes(snapshot)) + } else { + Ok(Transformed::no(Arc::clone(&e))) + } + }) + .data() +} diff --git a/datafusion/physical-expr/Cargo.toml b/datafusion/physical-expr/Cargo.toml index 72baa0db00a21..47e3291e5cb4d 100644 --- a/datafusion/physical-expr/Cargo.toml +++ b/datafusion/physical-expr/Cargo.toml @@ -57,6 +57,7 @@ petgraph = "0.7.1" arrow = { workspace = true, features = ["test_utils"] } criterion = { workspace = true } datafusion-functions = { workspace = true } +insta = { workspace = true } rand = { workspace = true } rstest = { workspace = true } @@ -71,3 +72,7 @@ name = "case_when" [[bench]] harness = false name = "is_null" + +[[bench]] +harness = false +name = "binary_op" diff --git a/datafusion/physical-expr/benches/binary_op.rs b/datafusion/physical-expr/benches/binary_op.rs new file mode 100644 index 0000000000000..216d8a520e489 --- /dev/null +++ b/datafusion/physical-expr/benches/binary_op.rs @@ -0,0 +1,312 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use arrow::{ + array::BooleanArray, + datatypes::{DataType, Field, Schema}, +}; +use arrow::{array::StringArray, record_batch::RecordBatch}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use datafusion_expr::{and, binary_expr, col, lit, or, Operator}; +use datafusion_physical_expr::{ + expressions::{BinaryExpr, Column}, + planner::logical2physical, + PhysicalExpr, +}; +use std::sync::Arc; + +/// Generates BooleanArrays with different true/false distributions for benchmarking. +/// +/// Returns a vector of tuples containing scenario name and corresponding BooleanArray. +/// +/// # Arguments +/// - `TEST_ALL_FALSE` - Used to generate what kind of test data +/// - `len` - Length of the BooleanArray to generate +fn generate_boolean_cases( + len: usize, +) -> Vec<(String, BooleanArray)> { + let mut cases = Vec::with_capacity(6); + + // Scenario 1: All elements false or all elements true + if TEST_ALL_FALSE { + let all_false = BooleanArray::from(vec![false; len]); + cases.push(("all_false".to_string(), all_false)); + } else { + let all_true = BooleanArray::from(vec![true; len]); + cases.push(("all_true".to_string(), all_true)); + } + + // Scenario 2: Single true at first position or single false at first position + if TEST_ALL_FALSE { + let mut first_true = vec![false; len]; + first_true[0] = true; + cases.push(("one_true_first".to_string(), BooleanArray::from(first_true))); + } else { + let mut first_false = vec![true; len]; + first_false[0] = false; + cases.push(( + "one_false_first".to_string(), + BooleanArray::from(first_false), + )); + } + + // Scenario 3: Single true at last position or single false at last position + if TEST_ALL_FALSE { + let mut last_true = vec![false; len]; + last_true[len - 1] = true; + cases.push(("one_true_last".to_string(), BooleanArray::from(last_true))); + } else { + let mut last_false = vec![true; len]; + last_false[len - 1] = false; + cases.push(("one_false_last".to_string(), BooleanArray::from(last_false))); + } + + // Scenario 4: Single true at exact middle or single false at exact middle + let mid = len / 2; + if TEST_ALL_FALSE { + let mut mid_true = vec![false; len]; + mid_true[mid] = true; + cases.push(("one_true_middle".to_string(), BooleanArray::from(mid_true))); + } else { + let mut mid_false = vec![true; len]; + mid_false[mid] = false; + cases.push(( + "one_false_middle".to_string(), + BooleanArray::from(mid_false), + )); + } + + // Scenario 5: Single true at 25% position or single false at 25% position + let mid_left = len / 4; + if TEST_ALL_FALSE { + let mut mid_left_true = vec![false; len]; + mid_left_true[mid_left] = true; + cases.push(( + "one_true_middle_left".to_string(), + BooleanArray::from(mid_left_true), + )); + } else { + let mut mid_left_false = vec![true; len]; + mid_left_false[mid_left] = false; + cases.push(( + "one_false_middle_left".to_string(), + BooleanArray::from(mid_left_false), + )); + } + + // Scenario 6: Single true at 75% position or single false at 75% position + let mid_right = (3 * len) / 4; + if TEST_ALL_FALSE { + let mut mid_right_true = vec![false; len]; + mid_right_true[mid_right] = true; + cases.push(( + "one_true_middle_right".to_string(), + BooleanArray::from(mid_right_true), + )); + } else { + let mut mid_right_false = vec![true; len]; + mid_right_false[mid_right] = false; + cases.push(( + "one_false_middle_right".to_string(), + BooleanArray::from(mid_right_false), + )); + } + + // Scenario 7: Test all true or all false in AND/OR + // This situation won't cause a short circuit, but it can skip the bool calculation + if TEST_ALL_FALSE { + let all_true = vec![true; len]; + cases.push(("all_true_in_and".to_string(), BooleanArray::from(all_true))); + } else { + let all_false = vec![false; len]; + cases.push(("all_false_in_or".to_string(), BooleanArray::from(all_false))); + } + + cases +} + +/// Benchmarks AND/OR operator short-circuiting by evaluating complex regex conditions. +/// +/// Creates 7 test scenarios per operator: +/// 1. All values enable short-circuit (all_true/all_false) +/// 2. 2-6 Single true/false value at different positions to measure early exit +/// 3. Test all true or all false in AND/OR +/// +/// You can run this benchmark with: +/// ```sh +/// cargo bench --bench binary_op -- short_circuit +/// ``` +fn benchmark_binary_op_in_short_circuit(c: &mut Criterion) { + // Create schema with three columns + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Boolean, false), + Field::new("b", DataType::Utf8, false), + Field::new("c", DataType::Utf8, false), + ])); + + // Generate test data with extended content + let (b_values, c_values) = generate_test_strings(8192); + + let batches_and = + create_record_batch::(schema.clone(), &b_values, &c_values).unwrap(); + let batches_or = + create_record_batch::(schema.clone(), &b_values, &c_values).unwrap(); + + // Build complex string matching conditions + let right_condition_and = and( + // Check for API endpoint pattern in URLs + binary_expr( + col("b"), + Operator::RegexMatch, + lit(r#"^https://(\w+\.)?example\.(com|org)/"#), + ), + // Check for markdown code blocks and summary section + binary_expr( + col("c"), + Operator::RegexMatch, + lit("```(rust|python|go)\nfn? main$$"), + ), + ); + + let right_condition_or = or( + // Check for secure HTTPS protocol + binary_expr( + col("b"), + Operator::RegexMatch, + lit(r#"^https://(\w+\.)?example\.(com|org)/"#), + ), + // Check for Rust code examples + binary_expr( + col("c"), + Operator::RegexMatch, + lit("```(rust|python|go)\nfn? main$$"), + ), + ); + + // Create physical binary expressions + // a AND ((b ~ regex) AND (c ~ regex)) + let expr_and = BinaryExpr::new( + Arc::new(Column::new("a", 0)), + Operator::And, + logical2physical(&right_condition_and, &schema), + ); + + // a OR ((b ~ regex) OR (c ~ regex)) + let expr_or = BinaryExpr::new( + Arc::new(Column::new("a", 0)), + Operator::Or, + logical2physical(&right_condition_or, &schema), + ); + + // Each scenario when the test operator is `and` + { + for (name, batch) in batches_and.into_iter() { + c.bench_function(&format!("short_circuit/and/{}", name), |b| { + b.iter(|| expr_and.evaluate(black_box(&batch)).unwrap()) + }); + } + } + // Each scenario when the test operator is `or` + { + for (name, batch) in batches_or.into_iter() { + c.bench_function(&format!("short_circuit/or/{}", name), |b| { + b.iter(|| expr_or.evaluate(black_box(&batch)).unwrap()) + }); + } + } +} + +/// Generate test data with computationally expensive patterns +fn generate_test_strings(num_rows: usize) -> (Vec, Vec) { + // Extended URL patterns with query parameters and paths + let base_urls = [ + "https://api.example.com/v2/users/12345/posts?category=tech&sort=date&lang=en-US", + "https://cdn.example.net/assets/images/2023/08/15/sample-image-highres.jpg?width=1920&quality=85", + "http://service.demo.org:8080/api/data/transactions/20230815123456.csv", + "ftp://legacy.archive.example/backups/2023/Q3/database-dump.sql.gz", + "https://docs.example.co.uk/reference/advanced-topics/concurrency/parallel-processing.md#implementation-details", + ]; + + // Extended markdown content with code blocks and structure + let base_markdowns = [ + concat!( + "# Advanced Topics in Computer Science\n\n", + "## Summary\nThis article explores complex system design patterns and...\n\n", + "```rust\nfn process_data(data: &mut [i32]) {\n // Parallel processing example\n data.par_iter_mut().for_each(|x| *x *= 2);\n}\n```\n\n", + "## Performance Considerations\nWhen implementing concurrent systems...\n" + ), + concat!( + "## API Documentation\n\n", + "```json\n{\n \"endpoint\": \"/api/v2/users\",\n \"methods\": [\"GET\", \"POST\"],\n \"parameters\": {\n \"page\": \"number\"\n }\n}\n```\n\n", + "# Authentication Guide\nSecure your API access using OAuth 2.0...\n" + ), + concat!( + "# Data Processing Pipeline\n\n", + "```python\nfrom multiprocessing import Pool\n\ndef main():\n with Pool(8) as p:\n results = p.map(process_item, data)\n```\n\n", + "## Summary of Optimizations\n1. Batch processing\n2. Memory pooling\n3. Concurrent I/O operations\n" + ), + concat!( + "# System Architecture Overview\n\n", + "## Components\n- Load Balancer\n- Database Cluster\n- Cache Service\n\n", + "```go\nfunc main() {\n router := gin.Default()\n router.GET(\"/api/health\", healthCheck)\n router.Run(\":8080\")\n}\n```\n" + ), + concat!( + "## Configuration Reference\n\n", + "```yaml\nserver:\n port: 8080\n max_threads: 32\n\ndatabase:\n url: postgres://user@prod-db:5432/main\n```\n\n", + "# Deployment Strategies\nBlue-green deployment patterns with...\n" + ), + ]; + + let mut urls = Vec::with_capacity(num_rows); + let mut markdowns = Vec::with_capacity(num_rows); + + for i in 0..num_rows { + urls.push(base_urls[i % 5].to_string()); + markdowns.push(base_markdowns[i % 5].to_string()); + } + + (urls, markdowns) +} + +/// Creates record batches with boolean arrays that test different short-circuit scenarios. +/// When TEST_ALL_FALSE = true: creates data for AND operator benchmarks (needs early false exit) +/// When TEST_ALL_FALSE = false: creates data for OR operator benchmarks (needs early true exit) +fn create_record_batch( + schema: Arc, + b_values: &[String], + c_values: &[String], +) -> arrow::error::Result> { + // Generate data for six scenarios, but only the data for the "all_false" and "all_true" cases can be optimized through short-circuiting + let boolean_array = generate_boolean_cases::(b_values.len()); + let mut rbs = Vec::with_capacity(boolean_array.len()); + for (name, a_array) in boolean_array { + let b_array = StringArray::from(b_values.to_vec()); + let c_array = StringArray::from(c_values.to_vec()); + rbs.push(( + name, + RecordBatch::try_new( + schema.clone(), + vec![Arc::new(a_array), Arc::new(b_array), Arc::new(c_array)], + )?, + )); + } + Ok(rbs) +} + +criterion_group!(benches, benchmark_binary_op_in_short_circuit); + +criterion_main!(benches); diff --git a/datafusion/physical-expr/src/aggregate.rs b/datafusion/physical-expr/src/aggregate.rs index ae3d9050fa628..49912954ac81c 100644 --- a/datafusion/physical-expr/src/aggregate.rs +++ b/datafusion/physical-expr/src/aggregate.rs @@ -97,6 +97,94 @@ impl AggregateExprBuilder { /// Constructs an `AggregateFunctionExpr` from the builder /// /// Note that an [`Self::alias`] must be provided before calling this method. + /// + /// # Example: Create an [`AggregateUDF`] + /// + /// In the following example, [`AggregateFunctionExpr`] will be built using [`AggregateExprBuilder`] + /// which provides a build function. Full example could be accessed from the source file. + /// + /// ``` + /// # use std::any::Any; + /// # use std::sync::Arc; + /// # use arrow::datatypes::DataType; + /// # use datafusion_common::{Result, ScalarValue}; + /// # use datafusion_expr::{col, ColumnarValue, Documentation, Signature, Volatility, Expr}; + /// # use datafusion_expr::{AggregateUDFImpl, AggregateUDF, Accumulator, function::{AccumulatorArgs, StateFieldsArgs}}; + /// # use arrow::datatypes::Field; + /// # + /// # #[derive(Debug, Clone)] + /// # struct FirstValueUdf { + /// # signature: Signature, + /// # } + /// # + /// # impl FirstValueUdf { + /// # fn new() -> Self { + /// # Self { + /// # signature: Signature::any(1, Volatility::Immutable), + /// # } + /// # } + /// # } + /// # + /// # impl AggregateUDFImpl for FirstValueUdf { + /// # fn as_any(&self) -> &dyn Any { + /// # unimplemented!() + /// # } + /// # fn name(&self) -> &str { + /// # unimplemented!() + /// } + /// # fn signature(&self) -> &Signature { + /// # unimplemented!() + /// # } + /// # fn return_type(&self, args: &[DataType]) -> Result { + /// # unimplemented!() + /// # } + /// # + /// # fn accumulator(&self, acc_args: AccumulatorArgs) -> Result> { + /// # unimplemented!() + /// # } + /// # + /// # fn state_fields(&self, args: StateFieldsArgs) -> Result> { + /// # unimplemented!() + /// # } + /// # + /// # fn documentation(&self) -> Option<&Documentation> { + /// # unimplemented!() + /// # } + /// # } + /// # + /// # let first_value = AggregateUDF::from(FirstValueUdf::new()); + /// # let expr = first_value.call(vec![col("a")]); + /// # + /// # use datafusion_physical_expr::expressions::Column; + /// # use datafusion_physical_expr_common::physical_expr::PhysicalExpr; + /// # use datafusion_physical_expr::aggregate::AggregateExprBuilder; + /// # use datafusion_physical_expr::expressions::PhysicalSortExpr; + /// # use datafusion_physical_expr::PhysicalSortRequirement; + /// # + /// fn build_aggregate_expr() -> Result<()> { + /// let args = vec![Arc::new(Column::new("a", 0)) as Arc]; + /// let order_by = vec![PhysicalSortExpr { + /// expr: Arc::new(Column::new("x", 1)) as Arc, + /// options: Default::default(), + /// }]; + /// + /// let first_value = AggregateUDF::from(FirstValueUdf::new()); + /// + /// let aggregate_expr = AggregateExprBuilder::new( + /// Arc::new(first_value), + /// args + /// ) + /// .order_by(order_by.into()) + /// .alias("first_a_by_x") + /// .ignore_nulls() + /// .build()?; + /// + /// Ok(()) + /// } + /// ``` + /// + /// This creates a physical expression equivalent to SQL: + /// `first_value(a ORDER BY x) IGNORE NULLS AS first_a_by_x` pub fn build(self) -> Result { let Self { fun, diff --git a/datafusion/physical-expr/src/equivalence/projection.rs b/datafusion/physical-expr/src/equivalence/projection.rs index 035678fbf1f39..a33339091c85d 100644 --- a/datafusion/physical-expr/src/equivalence/projection.rs +++ b/datafusion/physical-expr/src/equivalence/projection.rs @@ -67,8 +67,8 @@ impl ProjectionMapping { let matching_input_field = input_schema.field(idx); if col.name() != matching_input_field.name() { return internal_err!("Input field name {} does not match with the projection expression {}", - matching_input_field.name(),col.name()) - } + matching_input_field.name(),col.name()) + } let matching_input_column = Column::new(matching_input_field.name(), idx); Ok(Transformed::yes(Arc::new(matching_input_column))) diff --git a/datafusion/physical-expr/src/equivalence/properties/mod.rs b/datafusion/physical-expr/src/equivalence/properties/mod.rs index c7c33ba5b2ba5..5b34a02a91424 100644 --- a/datafusion/physical-expr/src/equivalence/properties/mod.rs +++ b/datafusion/physical-expr/src/equivalence/properties/mod.rs @@ -546,22 +546,26 @@ impl EquivalenceProperties { self.ordering_satisfy_requirement(&sort_requirements) } - /// Checks whether the given sort requirements are satisfied by any of the - /// existing orderings. - pub fn ordering_satisfy_requirement(&self, reqs: &LexRequirement) -> bool { - let mut eq_properties = self.clone(); - // First, standardize the given requirement: - let normalized_reqs = eq_properties.normalize_sort_requirements(reqs); - + /// Returns the number of consecutive requirements (starting from the left) + /// that are satisfied by the plan ordering. + fn compute_common_sort_prefix_length( + &self, + normalized_reqs: &LexRequirement, + ) -> usize { // Check whether given ordering is satisfied by constraints first - if self.satisfied_by_constraints(&normalized_reqs) { - return true; + if self.satisfied_by_constraints(normalized_reqs) { + // If the constraints satisfy all requirements, return the full normalized requirements length + return normalized_reqs.len(); } - for normalized_req in normalized_reqs { + let mut eq_properties = self.clone(); + + for (i, normalized_req) in normalized_reqs.iter().enumerate() { // Check whether given ordering is satisfied - if !eq_properties.ordering_satisfy_single(&normalized_req) { - return false; + if !eq_properties.ordering_satisfy_single(normalized_req) { + // As soon as one requirement is not satisfied, return + // how many we've satisfied so far + return i; } // Treat satisfied keys as constants in subsequent iterations. We // can do this because the "next" key only matters in a lexicographical @@ -575,10 +579,35 @@ impl EquivalenceProperties { // From the analysis above, we know that `[a ASC]` is satisfied. Then, // we add column `a` as constant to the algorithm state. This enables us // to deduce that `(b + c) ASC` is satisfied, given `a` is constant. - eq_properties = eq_properties - .with_constants(std::iter::once(ConstExpr::from(normalized_req.expr))); + eq_properties = eq_properties.with_constants(std::iter::once( + ConstExpr::from(Arc::clone(&normalized_req.expr)), + )); } - true + + // All requirements are satisfied. + normalized_reqs.len() + } + + /// Determines the longest prefix of `reqs` that is satisfied by the existing ordering. + /// Returns that prefix as a new `LexRequirement`, and a boolean indicating if all the requirements are satisfied. + pub fn extract_common_sort_prefix( + &self, + reqs: &LexRequirement, + ) -> (LexRequirement, bool) { + // First, standardize the given requirement: + let normalized_reqs = self.normalize_sort_requirements(reqs); + + let prefix_len = self.compute_common_sort_prefix_length(&normalized_reqs); + ( + LexRequirement::new(normalized_reqs[..prefix_len].to_vec()), + prefix_len == normalized_reqs.len(), + ) + } + + /// Checks whether the given sort requirements are satisfied by any of the + /// existing orderings. + pub fn ordering_satisfy_requirement(&self, reqs: &LexRequirement) -> bool { + self.extract_common_sort_prefix(reqs).1 } /// Checks if the sort requirements are satisfied by any of the table constraints (primary key or unique). @@ -1083,7 +1112,7 @@ impl EquivalenceProperties { /// # Arguments /// /// * `mapping` - A reference to `ProjectionMapping` that defines how expressions are mapped - /// in the projection operation + /// in the projection operation /// /// # Returns /// diff --git a/datafusion/physical-expr/src/expressions/binary.rs b/datafusion/physical-expr/src/expressions/binary.rs index f21d3e7652cdc..6c68d11e2c94c 100644 --- a/datafusion/physical-expr/src/expressions/binary.rs +++ b/datafusion/physical-expr/src/expressions/binary.rs @@ -29,7 +29,9 @@ use arrow::compute::kernels::boolean::{and_kleene, not, or_kleene}; use arrow::compute::kernels::cmp::*; use arrow::compute::kernels::comparison::{regexp_is_match, regexp_is_match_scalar}; use arrow::compute::kernels::concat_elements::concat_elements_utf8; -use arrow::compute::{cast, ilike, like, nilike, nlike}; +use arrow::compute::{ + cast, filter_record_batch, ilike, like, nilike, nlike, SlicesIterator, +}; use arrow::datatypes::*; use arrow::error::ArrowError; use datafusion_common::cast::as_boolean_array; @@ -358,7 +360,26 @@ impl PhysicalExpr for BinaryExpr { fn evaluate(&self, batch: &RecordBatch) -> Result { use arrow::compute::kernels::numeric::*; + // Evaluate left-hand side expression. let lhs = self.left.evaluate(batch)?; + + // Check if we can apply short-circuit evaluation. + match check_short_circuit(&lhs, &self.op) { + ShortCircuitStrategy::None => {} + ShortCircuitStrategy::ReturnLeft => return Ok(lhs), + ShortCircuitStrategy::ReturnRight => { + let rhs = self.right.evaluate(batch)?; + return Ok(rhs); + } + ShortCircuitStrategy::PreSelection(selection) => { + // The function `evaluate_selection` was not called for filtering and calculation, + // as it takes into account cases where the selection contains null values. + let batch = filter_record_batch(batch, selection)?; + let right_ret = self.right.evaluate(&batch)?; + return pre_selection_scatter(selection, right_ret); + } + } + let rhs = self.right.evaluate(batch)?; let left_data_type = lhs.data_type(); let right_data_type = rhs.data_type(); @@ -399,23 +420,19 @@ impl PhysicalExpr for BinaryExpr { let result_type = self.data_type(input_schema)?; - // Attempt to use special kernels if one input is scalar and the other is an array - let scalar_result = match (&lhs, &rhs) { - (ColumnarValue::Array(array), ColumnarValue::Scalar(scalar)) => { - // if left is array and right is literal(not NULL) - use scalar operations - if scalar.is_null() { - None - } else { - self.evaluate_array_scalar(array, scalar.clone())?.map(|r| { - r.and_then(|a| to_result_type_array(&self.op, a, &result_type)) - }) + // If the left-hand side is an array and the right-hand side is a non-null scalar, try the optimized kernel. + if let (ColumnarValue::Array(array), ColumnarValue::Scalar(ref scalar)) = + (&lhs, &rhs) + { + if !scalar.is_null() { + if let Some(result_array) = + self.evaluate_array_scalar(array, scalar.clone())? + { + let final_array = result_array + .and_then(|a| to_result_type_array(&self.op, a, &result_type)); + return final_array.map(ColumnarValue::Array); } } - (_, _) => None, // default to array implementation - }; - - if let Some(result) = scalar_result { - return result.map(ColumnarValue::Array); } // if both arrays or both literals - extract arrays and continue execution @@ -805,6 +822,201 @@ impl BinaryExpr { } } +enum ShortCircuitStrategy<'a> { + None, + ReturnLeft, + ReturnRight, + PreSelection(&'a BooleanArray), +} + +/// Based on the results calculated from the left side of the short-circuit operation, +/// if the proportion of `true` is less than 0.2 and the current operation is an `and`, +/// the `RecordBatch` will be filtered in advance. +const PRE_SELECTION_THRESHOLD: f32 = 0.2; + +/// Checks if a logical operator (`AND`/`OR`) can short-circuit evaluation based on the left-hand side (lhs) result. +/// +/// Short-circuiting occurs under these circumstances: +/// - For `AND`: +/// - if LHS is all false => short-circuit → return LHS +/// - if LHS is all true => short-circuit → return RHS +/// - if LHS is mixed and true_count/sum_count <= [`PRE_SELECTION_THRESHOLD`] -> pre-selection +/// - For `OR`: +/// - if LHS is all true => short-circuit → return LHS +/// - if LHS is all false => short-circuit → return RHS +/// # Arguments +/// * `lhs` - The left-hand side (lhs) columnar value (array or scalar) +/// * `lhs` - The left-hand side (lhs) columnar value (array or scalar) +/// * `op` - The logical operator (`AND` or `OR`) +/// +/// # Implementation Notes +/// 1. Only works with Boolean-typed arguments (other types automatically return `false`) +/// 2. Handles both scalar values and array values +/// 3. For arrays, uses optimized bit counting techniques for boolean arrays +fn check_short_circuit<'a>( + lhs: &'a ColumnarValue, + op: &Operator, +) -> ShortCircuitStrategy<'a> { + // Quick reject for non-logical operators,and quick judgment when op is and + let is_and = match op { + Operator::And => true, + Operator::Or => false, + _ => return ShortCircuitStrategy::None, + }; + + // Non-boolean types can't be short-circuited + if lhs.data_type() != DataType::Boolean { + return ShortCircuitStrategy::None; + } + + match lhs { + ColumnarValue::Array(array) => { + // Fast path for arrays - try to downcast to boolean array + if let Ok(bool_array) = as_boolean_array(array) { + // Arrays with nulls can't be short-circuited + if bool_array.null_count() > 0 { + return ShortCircuitStrategy::None; + } + + let len = bool_array.len(); + if len == 0 { + return ShortCircuitStrategy::None; + } + + let true_count = bool_array.values().count_set_bits(); + if is_and { + // For AND, prioritize checking for all-false (short circuit case) + // Uses optimized false_count() method provided by Arrow + + // Short circuit if all values are false + if true_count == 0 { + return ShortCircuitStrategy::ReturnLeft; + } + + // If no false values, then all must be true + if true_count == len { + return ShortCircuitStrategy::ReturnRight; + } + + // determine if we can pre-selection + if true_count as f32 / len as f32 <= PRE_SELECTION_THRESHOLD { + return ShortCircuitStrategy::PreSelection(bool_array); + } + } else { + // For OR, prioritize checking for all-true (short circuit case) + // Uses optimized true_count() method provided by Arrow + + // Short circuit if all values are true + if true_count == len { + return ShortCircuitStrategy::ReturnLeft; + } + + // If no true values, then all must be false + if true_count == 0 { + return ShortCircuitStrategy::ReturnRight; + } + } + } + } + ColumnarValue::Scalar(scalar) => { + // Fast path for scalar values + if let ScalarValue::Boolean(Some(is_true)) = scalar { + // Return Left for: + // - AND with false value + // - OR with true value + if (is_and && !is_true) || (!is_and && *is_true) { + return ShortCircuitStrategy::ReturnLeft; + } else { + return ShortCircuitStrategy::ReturnRight; + } + } + } + } + + // If we can't short-circuit, indicate that normal evaluation should continue + ShortCircuitStrategy::None +} + +/// Creates a new boolean array based on the evaluation of the right expression, +/// but only for positions where the left_result is true. +/// +/// This function is used for short-circuit evaluation optimization of logical AND operations: +/// - When left_result has few true values, we only evaluate the right expression for those positions +/// - Values are copied from right_array where left_result is true +/// - All other positions are filled with false values +/// +/// # Parameters +/// - `left_result` Boolean array with selection mask (typically from left side of AND) +/// - `right_result` Result of evaluating right side of expression (only for selected positions) +/// +/// # Returns +/// A combined ColumnarValue with values from right_result where left_result is true +/// +/// # Example +/// Initial Data: { 1, 2, 3, 4, 5 } +/// Left Evaluation +/// (Condition: Equal to 2 or 3) +/// ↓ +/// Filtered Data: {2, 3} +/// Left Bitmap: { 0, 1, 1, 0, 0 } +/// ↓ +/// Right Evaluation +/// (Condition: Even numbers) +/// ↓ +/// Right Data: { 2 } +/// Right Bitmap: { 1, 0 } +/// ↓ +/// Combine Results +/// Final Bitmap: { 0, 1, 0, 0, 0 } +/// +/// # Note +/// Perhaps it would be better to modify `left_result` directly without creating a copy? +/// In practice, `left_result` should have only one owner, so making changes should be safe. +/// However, this is difficult to achieve under the immutable constraints of [`Arc`] and [`BooleanArray`]. +fn pre_selection_scatter( + left_result: &BooleanArray, + right_result: ColumnarValue, +) -> Result { + let right_boolean_array = match &right_result { + ColumnarValue::Array(array) => array.as_boolean(), + ColumnarValue::Scalar(_) => return Ok(right_result), + }; + + let result_len = left_result.len(); + + let mut result_array_builder = BooleanArray::builder(result_len); + + // keep track of current position we have in right boolean array + let mut right_array_pos = 0; + + // keep track of how much is filled + let mut last_end = 0; + SlicesIterator::new(left_result).for_each(|(start, end)| { + // the gap needs to be filled with false + if start > last_end { + result_array_builder.append_n(start - last_end, false); + } + + // copy values from right array for this slice + let len = end - start; + right_boolean_array + .slice(right_array_pos, len) + .iter() + .for_each(|v| result_array_builder.append_option(v)); + + right_array_pos += len; + last_end = end; + }); + + // Fill any remaining positions with false + if last_end < result_len { + result_array_builder.append_n(result_len - last_end, false); + } + let boolean_result = result_array_builder.finish(); + + Ok(ColumnarValue::Array(Arc::new(boolean_result))) +} + fn concat_elements(left: Arc, right: Arc) -> Result { Ok(match left.data_type() { DataType::Utf8 => Arc::new(concat_elements_utf8( @@ -859,10 +1071,14 @@ pub fn similar_to( mod tests { use super::*; use crate::expressions::{col, lit, try_cast, Column, Literal}; + use datafusion_expr::lit as expr_lit; use datafusion_common::plan_datafusion_err; use datafusion_physical_expr_common::physical_expr::fmt_sql; + use crate::planner::logical2physical; + use arrow::array::BooleanArray; + use datafusion_expr::col as logical_col; /// Performs a binary operation, applying any type coercion necessary fn binary_op( left: Arc, @@ -4832,4 +5048,262 @@ mod tests { Ok(()) } + + #[test] + fn test_check_short_circuit() { + // Test with non-nullable arrays + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ])); + let a_array = Int32Array::from(vec![1, 3, 4, 5, 6]); + let b_array = Int32Array::from(vec![1, 2, 3, 4, 5]); + let batch = RecordBatch::try_new( + Arc::clone(&schema), + vec![Arc::new(a_array), Arc::new(b_array)], + ) + .unwrap(); + + // op: AND left: all false + let left_expr = logical2physical(&logical_col("a").eq(expr_lit(2)), &schema); + let left_value = left_expr.evaluate(&batch).unwrap(); + assert!(matches!( + check_short_circuit(&left_value, &Operator::And), + ShortCircuitStrategy::ReturnLeft + )); + + // op: AND left: not all false + let left_expr = logical2physical(&logical_col("a").eq(expr_lit(3)), &schema); + let left_value = left_expr.evaluate(&batch).unwrap(); + let ColumnarValue::Array(array) = &left_value else { + panic!("Expected ColumnarValue::Array"); + }; + let ShortCircuitStrategy::PreSelection(value) = + check_short_circuit(&left_value, &Operator::And) + else { + panic!("Expected ShortCircuitStrategy::PreSelection"); + }; + let expected_boolean_arr: Vec<_> = + as_boolean_array(array).unwrap().iter().collect(); + let boolean_arr: Vec<_> = value.iter().collect(); + assert_eq!(expected_boolean_arr, boolean_arr); + + // op: OR left: all true + let left_expr = logical2physical(&logical_col("a").gt(expr_lit(0)), &schema); + let left_value = left_expr.evaluate(&batch).unwrap(); + assert!(matches!( + check_short_circuit(&left_value, &Operator::Or), + ShortCircuitStrategy::ReturnLeft + )); + + // op: OR left: not all true + let left_expr: Arc = + logical2physical(&logical_col("a").gt(expr_lit(2)), &schema); + let left_value = left_expr.evaluate(&batch).unwrap(); + assert!(matches!( + check_short_circuit(&left_value, &Operator::Or), + ShortCircuitStrategy::None + )); + + // Test with nullable arrays and null values + let schema_nullable = Arc::new(Schema::new(vec![ + Field::new("c", DataType::Boolean, true), + Field::new("d", DataType::Boolean, true), + ])); + + // Create arrays with null values + let c_array = Arc::new(BooleanArray::from(vec![ + Some(true), + Some(false), + None, + Some(true), + None, + ])) as ArrayRef; + let d_array = Arc::new(BooleanArray::from(vec![ + Some(false), + Some(true), + Some(false), + None, + Some(true), + ])) as ArrayRef; + + let batch_nullable = RecordBatch::try_new( + Arc::clone(&schema_nullable), + vec![Arc::clone(&c_array), Arc::clone(&d_array)], + ) + .unwrap(); + + // Case: Mixed values with nulls - shouldn't short-circuit for AND + let mixed_nulls = logical2physical(&logical_col("c"), &schema_nullable); + let mixed_nulls_value = mixed_nulls.evaluate(&batch_nullable).unwrap(); + assert!(matches!( + check_short_circuit(&mixed_nulls_value, &Operator::And), + ShortCircuitStrategy::None + )); + + // Case: Mixed values with nulls - shouldn't short-circuit for OR + assert!(matches!( + check_short_circuit(&mixed_nulls_value, &Operator::Or), + ShortCircuitStrategy::None + )); + + // Test with all nulls + let all_nulls = Arc::new(BooleanArray::from(vec![None, None, None])) as ArrayRef; + let null_batch = RecordBatch::try_new( + Arc::new(Schema::new(vec![Field::new("e", DataType::Boolean, true)])), + vec![all_nulls], + ) + .unwrap(); + + let null_expr = logical2physical(&logical_col("e"), &null_batch.schema()); + let null_value = null_expr.evaluate(&null_batch).unwrap(); + + // All nulls shouldn't short-circuit for AND or OR + assert!(matches!( + check_short_circuit(&null_value, &Operator::And), + ShortCircuitStrategy::None + )); + assert!(matches!( + check_short_circuit(&null_value, &Operator::Or), + ShortCircuitStrategy::None + )); + + // Test with scalar values + // Scalar true + let scalar_true = ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))); + assert!(matches!( + check_short_circuit(&scalar_true, &Operator::Or), + ShortCircuitStrategy::ReturnLeft + )); // Should short-circuit OR + assert!(matches!( + check_short_circuit(&scalar_true, &Operator::And), + ShortCircuitStrategy::ReturnRight + )); // Should return the RHS for AND + + // Scalar false + let scalar_false = ColumnarValue::Scalar(ScalarValue::Boolean(Some(false))); + assert!(matches!( + check_short_circuit(&scalar_false, &Operator::And), + ShortCircuitStrategy::ReturnLeft + )); // Should short-circuit AND + assert!(matches!( + check_short_circuit(&scalar_false, &Operator::Or), + ShortCircuitStrategy::ReturnRight + )); // Should return the RHS for OR + + // Scalar null + let scalar_null = ColumnarValue::Scalar(ScalarValue::Boolean(None)); + assert!(matches!( + check_short_circuit(&scalar_null, &Operator::And), + ShortCircuitStrategy::None + )); + assert!(matches!( + check_short_circuit(&scalar_null, &Operator::Or), + ShortCircuitStrategy::None + )); + } + + /// Test for [pre_selection_scatter] + /// Since [check_short_circuit] ensures that the left side does not contain null and is neither all_true nor all_false, as well as not being empty, + /// the following tests have been designed: + /// 1. Test sparse left with interleaved true/false + /// 2. Test multiple consecutive true blocks + /// 3. Test multiple consecutive true blocks + /// 4. Test single true at first position + /// 5. Test single true at last position + /// 6. Test nulls in right array + /// 7. Test scalar right handling + #[test] + fn test_pre_selection_scatter() { + fn create_bool_array(bools: Vec) -> BooleanArray { + BooleanArray::from(bools.into_iter().map(Some).collect::>()) + } + // Test sparse left with interleaved true/false + { + // Left: [T, F, T, F, T] + // Right: [F, T, F] (values for 3 true positions) + let left = create_bool_array(vec![true, false, true, false, true]); + let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![ + false, true, false, + ]))); + + let result = pre_selection_scatter(&left, right).unwrap(); + let result_arr = result.into_array(left.len()).unwrap(); + + let expected = create_bool_array(vec![false, false, true, false, false]); + assert_eq!(&expected, result_arr.as_boolean()); + } + // Test multiple consecutive true blocks + { + // Left: [F, T, T, F, T, T, T] + // Right: [T, F, F, T, F] + let left = + create_bool_array(vec![false, true, true, false, true, true, true]); + let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![ + true, false, false, true, false, + ]))); + + let result = pre_selection_scatter(&left, right).unwrap(); + let result_arr = result.into_array(left.len()).unwrap(); + + let expected = + create_bool_array(vec![false, true, false, false, false, true, false]); + assert_eq!(&expected, result_arr.as_boolean()); + } + // Test single true at first position + { + // Left: [T, F, F] + // Right: [F] + let left = create_bool_array(vec![true, false, false]); + let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![false]))); + + let result = pre_selection_scatter(&left, right).unwrap(); + let result_arr = result.into_array(left.len()).unwrap(); + + let expected = create_bool_array(vec![false, false, false]); + assert_eq!(&expected, result_arr.as_boolean()); + } + // Test single true at last position + { + // Left: [F, F, T] + // Right: [F] + let left = create_bool_array(vec![false, false, true]); + let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![false]))); + + let result = pre_selection_scatter(&left, right).unwrap(); + let result_arr = result.into_array(left.len()).unwrap(); + + let expected = create_bool_array(vec![false, false, false]); + assert_eq!(&expected, result_arr.as_boolean()); + } + // Test nulls in right array + { + // Left: [F, T, F, T] + // Right: [None, Some(false)] (with null at first position) + let left = create_bool_array(vec![false, true, false, true]); + let right_arr = BooleanArray::from(vec![None, Some(false)]); + let right = ColumnarValue::Array(Arc::new(right_arr)); + + let result = pre_selection_scatter(&left, right).unwrap(); + let result_arr = result.into_array(left.len()).unwrap(); + + let expected = BooleanArray::from(vec![ + Some(false), + None, // null from right + Some(false), + Some(false), + ]); + assert_eq!(&expected, result_arr.as_boolean()); + } + // Test scalar right handling + { + // Left: [T, F, T] + // Right: Scalar true + let left = create_bool_array(vec![true, false, true]); + let right = ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))); + + let result = pre_selection_scatter(&left, right).unwrap(); + assert!(matches!(result, ColumnarValue::Scalar(_))); + } + } } diff --git a/datafusion/physical-expr/src/expressions/dynamic_filters.rs b/datafusion/physical-expr/src/expressions/dynamic_filters.rs new file mode 100644 index 0000000000000..c0a3285f0e781 --- /dev/null +++ b/datafusion/physical-expr/src/expressions/dynamic_filters.rs @@ -0,0 +1,474 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::{ + any::Any, + fmt::Display, + hash::Hash, + sync::{Arc, RwLock}, +}; + +use crate::PhysicalExpr; +use arrow::datatypes::{DataType, Schema}; +use datafusion_common::{ + tree_node::{Transformed, TransformedResult, TreeNode}, + Result, +}; +use datafusion_expr::ColumnarValue; +use datafusion_physical_expr_common::physical_expr::{DynEq, DynHash}; + +/// A dynamic [`PhysicalExpr`] that can be updated by anyone with a reference to it. +#[derive(Debug)] +pub struct DynamicFilterPhysicalExpr { + /// The original children of this PhysicalExpr, if any. + /// This is necessary because the dynamic filter may be initialized with a placeholder (e.g. `lit(true)`) + /// and later remapped to the actual expressions that are being filtered. + /// But we need to know the children (e.g. columns referenced in the expression) ahead of time to evaluate the expression correctly. + children: Vec>, + /// If any of the children were remapped / modified (e.g. to adjust for projections) we need to keep track of the new children + /// so that when we update `current()` in subsequent iterations we can re-apply the replacements. + remapped_children: Option>>, + /// The source of dynamic filters. + inner: Arc>>, + /// For testing purposes track the data type and nullability to make sure they don't change. + /// If they do, there's a bug in the implementation. + /// But this can have overhead in production, so it's only included in our tests. + data_type: Arc>>, + nullable: Arc>>, +} + +impl Hash for DynamicFilterPhysicalExpr { + fn hash(&self, state: &mut H) { + let inner = self.current().expect("Failed to get current expression"); + inner.dyn_hash(state); + self.children.dyn_hash(state); + self.remapped_children.dyn_hash(state); + } +} + +impl PartialEq for DynamicFilterPhysicalExpr { + fn eq(&self, other: &Self) -> bool { + let inner = self.current().expect("Failed to get current expression"); + let our_children = self.remapped_children.as_ref().unwrap_or(&self.children); + let other_children = other.remapped_children.as_ref().unwrap_or(&other.children); + let other = other.current().expect("Failed to get current expression"); + inner.dyn_eq(other.as_any()) && our_children == other_children + } +} + +impl Eq for DynamicFilterPhysicalExpr {} + +impl Display for DynamicFilterPhysicalExpr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = self.current().expect("Failed to get current expression"); + write!(f, "DynamicFilterPhysicalExpr [ {} ]", inner) + } +} + +impl DynamicFilterPhysicalExpr { + /// Create a new [`DynamicFilterPhysicalExpr`] + /// from an initial expression and a list of children. + /// The list of children is provided separately because + /// the initial expression may not have the same children. + /// For example, if the initial expression is just `true` + /// it will not reference any columns, but we may know that + /// we are going to replace this expression with a real one + /// that does reference certain columns. + /// In this case you **must** pass in the columns that will be + /// used in the final expression as children to this function + /// since DataFusion is generally not compatible with dynamic + /// *children* in expressions. + /// + /// To determine the children you can: + /// + /// - Use [`collect_columns`] to collect the columns from the expression. + /// - Use existing information, such as the sort columns in a `SortExec`. + /// + /// Generally the important bit is that the *leaf children that reference columns + /// do not change* since those will be used to determine what columns need to read or projected + /// when evaluating the expression. + /// + /// [`collect_columns`]: crate::utils::collect_columns + #[allow(dead_code)] // Only used in tests for now + pub fn new( + children: Vec>, + inner: Arc, + ) -> Self { + Self { + children, + remapped_children: None, // Initially no remapped children + inner: Arc::new(RwLock::new(inner)), + data_type: Arc::new(RwLock::new(None)), + nullable: Arc::new(RwLock::new(None)), + } + } + + fn remap_children( + children: &[Arc], + remapped_children: Option<&Vec>>, + expr: Arc, + ) -> Result> { + if let Some(remapped_children) = remapped_children { + // Remap the children to the new children + // of the expression. + expr.transform_up(|child| { + // Check if this is any of our original children + if let Some(pos) = + children.iter().position(|c| c.as_ref() == child.as_ref()) + { + // If so, remap it to the current children + // of the expression. + let new_child = Arc::clone(&remapped_children[pos]); + Ok(Transformed::yes(new_child)) + } else { + // Otherwise, just return the expression + Ok(Transformed::no(child)) + } + }) + .data() + } else { + // If we don't have any remapped children, just return the expression + Ok(Arc::clone(&expr)) + } + } + + /// Get the current expression. + /// This will return the current expression with any children + /// remapped to match calls to [`PhysicalExpr::with_new_children`]. + pub fn current(&self) -> Result> { + let inner = self + .inner + .read() + .map_err(|_| { + datafusion_common::DataFusionError::Execution( + "Failed to acquire read lock for inner".to_string(), + ) + })? + .clone(); + let inner = + Self::remap_children(&self.children, self.remapped_children.as_ref(), inner)?; + Ok(inner) + } + + /// Update the current expression. + /// Any children of this expression must be a subset of the original children + /// passed to the constructor. + /// This should be called e.g.: + /// - When we've computed the probe side's hash table in a HashJoinExec + /// - After every batch is processed if we update the TopK heap in a SortExec using a TopK approach. + #[allow(dead_code)] // Only used in tests for now + pub fn update(&self, new_expr: Arc) -> Result<()> { + let mut current = self.inner.write().map_err(|_| { + datafusion_common::DataFusionError::Execution( + "Failed to acquire write lock for inner".to_string(), + ) + })?; + // Remap the children of the new expression to match the original children + // We still do this again in `current()` but doing it preventively here + // reduces the work needed in some cases if `current()` is called multiple times + // and the same externally facing `PhysicalExpr` is used for both `with_new_children` and `update()`.` + let new_expr = Self::remap_children( + &self.children, + self.remapped_children.as_ref(), + new_expr, + )?; + *current = new_expr; + Ok(()) + } +} + +impl PhysicalExpr for DynamicFilterPhysicalExpr { + fn as_any(&self) -> &dyn Any { + self + } + + fn children(&self) -> Vec<&Arc> { + self.remapped_children + .as_ref() + .unwrap_or(&self.children) + .iter() + .collect() + } + + fn with_new_children( + self: Arc, + children: Vec>, + ) -> Result> { + Ok(Arc::new(Self { + children: self.children.clone(), + remapped_children: Some(children), + inner: Arc::clone(&self.inner), + data_type: Arc::clone(&self.data_type), + nullable: Arc::clone(&self.nullable), + })) + } + + fn data_type(&self, input_schema: &Schema) -> Result { + let res = self.current()?.data_type(input_schema)?; + #[cfg(test)] + { + use datafusion_common::internal_err; + // Check if the data type has changed. + let mut data_type_lock = self + .data_type + .write() + .expect("Failed to acquire write lock for data_type"); + if let Some(existing) = &*data_type_lock { + if existing != &res { + // If the data type has changed, we have a bug. + return internal_err!( + "DynamicFilterPhysicalExpr data type has changed unexpectedly. \ + Expected: {existing:?}, Actual: {res:?}" + ); + } + } else { + *data_type_lock = Some(res.clone()); + } + } + Ok(res) + } + + fn nullable(&self, input_schema: &Schema) -> Result { + let res = self.current()?.nullable(input_schema)?; + #[cfg(test)] + { + use datafusion_common::internal_err; + // Check if the nullability has changed. + let mut nullable_lock = self + .nullable + .write() + .expect("Failed to acquire write lock for nullable"); + if let Some(existing) = *nullable_lock { + if existing != res { + // If the nullability has changed, we have a bug. + return internal_err!( + "DynamicFilterPhysicalExpr nullability has changed unexpectedly. \ + Expected: {existing}, Actual: {res}" + ); + } + } else { + *nullable_lock = Some(res); + } + } + Ok(res) + } + + fn evaluate( + &self, + batch: &arrow::record_batch::RecordBatch, + ) -> Result { + let current = self.current()?; + #[cfg(test)] + { + // Ensure that we are not evaluating after the expression has changed. + let schema = batch.schema(); + self.nullable(&schema)?; + self.data_type(&schema)?; + }; + current.evaluate(batch) + } + + fn fmt_sql(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let inner = self.current().map_err(|_| std::fmt::Error)?; + inner.fmt_sql(f) + } + + fn snapshot(&self) -> Result>> { + // Return the current expression as a snapshot. + Ok(Some(self.current()?)) + } +} + +#[cfg(test)] +mod test { + use crate::{ + expressions::{col, lit, BinaryExpr}, + utils::reassign_predicate_columns, + }; + use arrow::{ + array::RecordBatch, + datatypes::{DataType, Field, Schema}, + }; + use datafusion_common::ScalarValue; + + use super::*; + + #[test] + fn test_remap_children() { + let table_schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ])); + let expr = Arc::new(BinaryExpr::new( + col("a", &table_schema).unwrap(), + datafusion_expr::Operator::Eq, + lit(42) as Arc, + )); + let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new( + vec![col("a", &table_schema).unwrap()], + expr as Arc, + )); + // Simulate two `ParquetSource` files with different filter schemas + // Both of these should hit the same inner `PhysicalExpr` even after `update()` is called + // and be able to remap children independently. + let filter_schema_1 = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + ])); + let filter_schema_2 = Arc::new(Schema::new(vec![ + Field::new("b", DataType::Int32, false), + Field::new("a", DataType::Int32, false), + ])); + // Each ParquetExec calls `with_new_children` on the DynamicFilterPhysicalExpr + // and remaps the children to the file schema. + let dynamic_filter_1 = reassign_predicate_columns( + Arc::clone(&dynamic_filter) as Arc, + &filter_schema_1, + false, + ) + .unwrap(); + let snap = dynamic_filter_1.snapshot().unwrap().unwrap(); + insta::assert_snapshot!(format!("{snap:?}"), @r#"BinaryExpr { left: Column { name: "a", index: 0 }, op: Eq, right: Literal { value: Int32(42) }, fail_on_overflow: false }"#); + let dynamic_filter_2 = reassign_predicate_columns( + Arc::clone(&dynamic_filter) as Arc, + &filter_schema_2, + false, + ) + .unwrap(); + let snap = dynamic_filter_2.snapshot().unwrap().unwrap(); + insta::assert_snapshot!(format!("{snap:?}"), @r#"BinaryExpr { left: Column { name: "a", index: 1 }, op: Eq, right: Literal { value: Int32(42) }, fail_on_overflow: false }"#); + // Both filters allow evaluating the same expression + let batch_1 = RecordBatch::try_new( + Arc::clone(&filter_schema_1), + vec![ + // a + ScalarValue::Int32(Some(42)).to_array_of_size(1).unwrap(), + // b + ScalarValue::Int32(Some(43)).to_array_of_size(1).unwrap(), + ], + ) + .unwrap(); + let batch_2 = RecordBatch::try_new( + Arc::clone(&filter_schema_2), + vec![ + // b + ScalarValue::Int32(Some(43)).to_array_of_size(1).unwrap(), + // a + ScalarValue::Int32(Some(42)).to_array_of_size(1).unwrap(), + ], + ) + .unwrap(); + // Evaluate the expression on both batches + let result_1 = dynamic_filter_1.evaluate(&batch_1).unwrap(); + let result_2 = dynamic_filter_2.evaluate(&batch_2).unwrap(); + // Check that the results are the same + let ColumnarValue::Array(arr_1) = result_1 else { + panic!("Expected ColumnarValue::Array"); + }; + let ColumnarValue::Array(arr_2) = result_2 else { + panic!("Expected ColumnarValue::Array"); + }; + assert!(arr_1.eq(&arr_2)); + let expected = ScalarValue::Boolean(Some(true)) + .to_array_of_size(1) + .unwrap(); + assert!(arr_1.eq(&expected)); + // Now lets update the expression + // Note that we update the *original* expression and that should be reflected in both the derived expressions + let new_expr = Arc::new(BinaryExpr::new( + col("a", &table_schema).unwrap(), + datafusion_expr::Operator::Gt, + lit(43) as Arc, + )); + dynamic_filter + .update(Arc::clone(&new_expr) as Arc) + .expect("Failed to update expression"); + // Now we should be able to evaluate the new expression on both batches + let result_1 = dynamic_filter_1.evaluate(&batch_1).unwrap(); + let result_2 = dynamic_filter_2.evaluate(&batch_2).unwrap(); + // Check that the results are the same + let ColumnarValue::Array(arr_1) = result_1 else { + panic!("Expected ColumnarValue::Array"); + }; + let ColumnarValue::Array(arr_2) = result_2 else { + panic!("Expected ColumnarValue::Array"); + }; + assert!(arr_1.eq(&arr_2)); + let expected = ScalarValue::Boolean(Some(false)) + .to_array_of_size(1) + .unwrap(); + assert!(arr_1.eq(&expected)); + } + + #[test] + fn test_snapshot() { + let expr = lit(42) as Arc; + let dynamic_filter = DynamicFilterPhysicalExpr::new(vec![], Arc::clone(&expr)); + + // Take a snapshot of the current expression + let snapshot = dynamic_filter.snapshot().unwrap(); + assert_eq!(snapshot, Some(expr)); + + // Update the current expression + let new_expr = lit(100) as Arc; + dynamic_filter.update(Arc::clone(&new_expr)).unwrap(); + // Take another snapshot + let snapshot = dynamic_filter.snapshot().unwrap(); + assert_eq!(snapshot, Some(new_expr)); + } + + #[test] + fn test_dynamic_filter_physical_expr_misbehaves_data_type_nullable() { + let dynamic_filter = + DynamicFilterPhysicalExpr::new(vec![], lit(42) as Arc); + + // First call to data_type and nullable should set the initial values. + let initial_data_type = dynamic_filter.data_type(&Schema::empty()).unwrap(); + let initial_nullable = dynamic_filter.nullable(&Schema::empty()).unwrap(); + + // Call again and expect no change. + let second_data_type = dynamic_filter.data_type(&Schema::empty()).unwrap(); + let second_nullable = dynamic_filter.nullable(&Schema::empty()).unwrap(); + assert_eq!( + initial_data_type, second_data_type, + "Data type should not change on second call." + ); + assert_eq!( + initial_nullable, second_nullable, + "Nullability should not change on second call." + ); + + // Now change the current expression to something else. + dynamic_filter + .update(lit(ScalarValue::Utf8(None)) as Arc) + .expect("Failed to update expression"); + // Check that we error if we call data_type, nullable or evaluate after changing the expression. + assert!( + dynamic_filter.data_type(&Schema::empty()).is_err(), + "Expected err when data_type is called after changing the expression." + ); + assert!( + dynamic_filter.nullable(&Schema::empty()).is_err(), + "Expected err when nullable is called after changing the expression." + ); + let batch = RecordBatch::new_empty(Arc::new(Schema::empty())); + assert!( + dynamic_filter.evaluate(&batch).is_err(), + "Expected err when evaluate is called after changing the expression." + ); + } +} diff --git a/datafusion/physical-expr/src/expressions/mod.rs b/datafusion/physical-expr/src/expressions/mod.rs index f00b49f503141..d77207fbbcd76 100644 --- a/datafusion/physical-expr/src/expressions/mod.rs +++ b/datafusion/physical-expr/src/expressions/mod.rs @@ -22,6 +22,7 @@ mod binary; mod case; mod cast; mod column; +mod dynamic_filters; mod in_list; mod is_not_null; mod is_null; diff --git a/datafusion/physical-expr/src/lib.rs b/datafusion/physical-expr/src/lib.rs index 93ced2eb628d8..9f795c81fa48e 100644 --- a/datafusion/physical-expr/src/lib.rs +++ b/datafusion/physical-expr/src/lib.rs @@ -68,7 +68,7 @@ pub use planner::{create_physical_expr, create_physical_exprs}; pub use scalar_function::ScalarFunctionExpr; pub use datafusion_physical_expr_common::utils::reverse_order_bys; -pub use utils::split_conjunction; +pub use utils::{conjunction, conjunction_opt, split_conjunction}; // For backwards compatibility pub mod tree_node { diff --git a/datafusion/physical-expr/src/planner.rs b/datafusion/physical-expr/src/planner.rs index fac83dfc45247..8660bff796d5a 100644 --- a/datafusion/physical-expr/src/planner.rs +++ b/datafusion/physical-expr/src/planner.rs @@ -102,7 +102,7 @@ use datafusion_expr::{ /// /// * `e` - The logical expression /// * `input_dfschema` - The DataFusion schema for the input, used to resolve `Column` references -/// to qualified or unqualified fields by name. +/// to qualified or unqualified fields by name. pub fn create_physical_expr( e: &Expr, input_dfschema: &DFSchema, diff --git a/datafusion/physical-expr/src/utils/mod.rs b/datafusion/physical-expr/src/utils/mod.rs index 7e4c7f0e10ba8..b4d0758fd2e81 100644 --- a/datafusion/physical-expr/src/utils/mod.rs +++ b/datafusion/physical-expr/src/utils/mod.rs @@ -47,6 +47,31 @@ pub fn split_conjunction( split_impl(Operator::And, predicate, vec![]) } +/// Create a conjunction of the given predicates. +/// If the input is empty, return a literal true. +/// If the input contains a single predicate, return the predicate. +/// Otherwise, return a conjunction of the predicates (e.g. `a AND b AND c`). +pub fn conjunction( + predicates: impl IntoIterator>, +) -> Arc { + conjunction_opt(predicates).unwrap_or_else(|| crate::expressions::lit(true)) +} + +/// Create a conjunction of the given predicates. +/// If the input is empty or the return None. +/// If the input contains a single predicate, return Some(predicate). +/// Otherwise, return a Some(..) of a conjunction of the predicates (e.g. `Some(a AND b AND c)`). +pub fn conjunction_opt( + predicates: impl IntoIterator>, +) -> Option> { + predicates + .into_iter() + .fold(None, |acc, predicate| match acc { + None => Some(predicate), + Some(acc) => Some(Arc::new(BinaryExpr::new(acc, Operator::And, predicate))), + }) +} + /// Assume the predicate is in the form of DNF, split the predicate to a Vec of PhysicalExprs. /// /// For example, split "a1 = a2 OR b1 <= b2 OR c1 != c2" into ["a1 = a2", "b1 <= b2", "c1 != c2"] diff --git a/datafusion/physical-optimizer/src/aggregate_statistics.rs b/datafusion/physical-optimizer/src/aggregate_statistics.rs index 0d3d83c58373f..28ee10eb650a0 100644 --- a/datafusion/physical-optimizer/src/aggregate_statistics.rs +++ b/datafusion/physical-optimizer/src/aggregate_statistics.rs @@ -42,6 +42,7 @@ impl AggregateStatistics { impl PhysicalOptimizerRule for AggregateStatistics { #[cfg_attr(feature = "recursive_protection", recursive::recursive)] + #[allow(clippy::only_used_in_recursion)] // See https://github.com/rust-lang/rust-clippy/issues/14566 fn optimize( &self, plan: Arc, diff --git a/datafusion/physical-optimizer/src/enforce_distribution.rs b/datafusion/physical-optimizer/src/enforce_distribution.rs index 5e76edad1f569..523762401dfad 100644 --- a/datafusion/physical-optimizer/src/enforce_distribution.rs +++ b/datafusion/physical-optimizer/src/enforce_distribution.rs @@ -837,7 +837,7 @@ fn new_join_conditions( /// /// * `input`: Current node. /// * `n_target`: desired target partition number, if partition number of the -/// current executor is less than this value. Partition number will be increased. +/// current executor is less than this value. Partition number will be increased. /// /// # Returns /// @@ -880,7 +880,7 @@ fn add_roundrobin_on_top( /// * `input`: Current node. /// * `hash_exprs`: Stores Physical Exprs that are used during hashing. /// * `n_target`: desired target partition number, if partition number of the -/// current executor is less than this value. Partition number will be increased. +/// current executor is less than this value. Partition number will be increased. /// /// # Returns /// @@ -1018,7 +1018,7 @@ fn remove_dist_changing_operators( /// " RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=2", /// " DataSourceExec: file_groups={2 groups: \[\[x], \[y]]}, projection=\[a, b, c, d, e], output_ordering=\[a@0 ASC], file_type=parquet", /// ``` -fn replace_order_preserving_variants( +pub fn replace_order_preserving_variants( mut context: DistributionContext, ) -> Result { context.children = context @@ -1035,7 +1035,10 @@ fn replace_order_preserving_variants( if is_sort_preserving_merge(&context.plan) { let child_plan = Arc::clone(&context.children[0].plan); - context.plan = Arc::new(CoalescePartitionsExec::new(child_plan)); + // It's safe to unwrap because `CoalescePartitionsExec` supports `fetch`. + context.plan = CoalescePartitionsExec::new(child_plan) + .with_fetch(context.plan.fetch()) + .unwrap(); return Ok(context); } else if let Some(repartition) = context.plan.as_any().downcast_ref::() diff --git a/datafusion/physical-optimizer/src/enforce_sorting/mod.rs b/datafusion/physical-optimizer/src/enforce_sorting/mod.rs index 20733b65692fc..b606aa85c1e16 100644 --- a/datafusion/physical-optimizer/src/enforce_sorting/mod.rs +++ b/datafusion/physical-optimizer/src/enforce_sorting/mod.rs @@ -400,6 +400,7 @@ pub fn parallelize_sorts( ), )) } else if is_coalesce_partitions(&requirements.plan) { + let fetch = requirements.plan.fetch(); // There is an unnecessary `CoalescePartitionsExec` in the plan. // This will handle the recursive `CoalescePartitionsExec` plans. requirements = remove_bottleneck_in_subplan(requirements)?; @@ -408,7 +409,10 @@ pub fn parallelize_sorts( Ok(Transformed::yes( PlanWithCorrespondingCoalescePartitions::new( - Arc::new(CoalescePartitionsExec::new(Arc::clone(&requirements.plan))), + // Safe to unwrap, because `CoalescePartitionsExec` has a fetch + CoalescePartitionsExec::new(Arc::clone(&requirements.plan)) + .with_fetch(fetch) + .unwrap(), false, vec![requirements], ), diff --git a/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs b/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs index 2c5c0d4d510ec..7fe62a146afb9 100644 --- a/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs +++ b/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs @@ -27,7 +27,7 @@ use crate::utils::{ use datafusion_common::config::ConfigOptions; use datafusion_common::tree_node::Transformed; -use datafusion_common::Result; +use datafusion_common::{internal_err, Result}; use datafusion_physical_expr_common::sort_expr::LexOrdering; use datafusion_physical_plan::coalesce_partitions::CoalescePartitionsExec; use datafusion_physical_plan::execution_plan::EmissionType; @@ -93,7 +93,7 @@ pub fn update_order_preservation_ctx_children_data(opc: &mut OrderPreservationCo /// inside `sort_input` with their order-preserving variants. This will /// generate an alternative plan, which will be accepted or rejected later on /// depending on whether it helps us remove a `SortExec`. -fn plan_with_order_preserving_variants( +pub fn plan_with_order_preserving_variants( mut sort_input: OrderPreservationContext, // Flag indicating that it is desirable to replace `RepartitionExec`s with // `SortPreservingRepartitionExec`s: @@ -138,6 +138,19 @@ fn plan_with_order_preserving_variants( } else if is_coalesce_partitions(&sort_input.plan) && is_spm_better { let child = &sort_input.children[0].plan; if let Some(ordering) = child.output_ordering() { + let mut fetch = fetch; + if let Some(coalesce_fetch) = sort_input.plan.fetch() { + if let Some(sort_fetch) = fetch { + if coalesce_fetch < sort_fetch { + return internal_err!( + "CoalescePartitionsExec fetch [{:?}] should be greater than or equal to SortExec fetch [{:?}]", coalesce_fetch, sort_fetch + ); + } + } else { + // If the sort node does not have a fetch, we need to keep the coalesce node's fetch. + fetch = Some(coalesce_fetch); + } + }; // When the input of a `CoalescePartitionsExec` has an ordering, // replace it with a `SortPreservingMergeExec` if appropriate: let spm = SortPreservingMergeExec::new(ordering.clone(), Arc::clone(child)) diff --git a/datafusion/physical-optimizer/src/lib.rs b/datafusion/physical-optimizer/src/lib.rs index 35503f3b0b5f9..57dac21b6eeed 100644 --- a/datafusion/physical-optimizer/src/lib.rs +++ b/datafusion/physical-optimizer/src/lib.rs @@ -36,6 +36,7 @@ pub mod optimizer; pub mod output_requirements; pub mod projection_pushdown; pub mod pruning; +pub mod push_down_filter; pub mod sanity_checker; pub mod topk_aggregation; pub mod update_aggr_exprs; diff --git a/datafusion/physical-optimizer/src/limit_pushdown.rs b/datafusion/physical-optimizer/src/limit_pushdown.rs index 5887cb51a727b..7469c3af9344c 100644 --- a/datafusion/physical-optimizer/src/limit_pushdown.rs +++ b/datafusion/physical-optimizer/src/limit_pushdown.rs @@ -246,16 +246,7 @@ pub fn pushdown_limit_helper( Ok((Transformed::no(pushdown_plan), global_state)) } } else { - // Add fetch or a `LimitExec`: - // If the plan's children have limit and the child's limit < parent's limit, we shouldn't change the global state to true, - // because the children limit will be overridden if the global state is changed. - if !pushdown_plan - .children() - .iter() - .any(|&child| extract_limit(child).is_some()) - { - global_state.satisfied = true; - } + global_state.satisfied = true; pushdown_plan = if let Some(plan_with_fetch) = maybe_fetchable { if global_skip > 0 { add_global_limit(plan_with_fetch, global_skip, Some(global_fetch)) diff --git a/datafusion/physical-optimizer/src/optimizer.rs b/datafusion/physical-optimizer/src/optimizer.rs index bab31150e2508..d4ff7d6b9e153 100644 --- a/datafusion/physical-optimizer/src/optimizer.rs +++ b/datafusion/physical-optimizer/src/optimizer.rs @@ -30,6 +30,7 @@ use crate::limit_pushdown::LimitPushdown; use crate::limited_distinct_aggregation::LimitedDistinctAggregation; use crate::output_requirements::OutputRequirements; use crate::projection_pushdown::ProjectionPushdown; +use crate::push_down_filter::PushdownFilter; use crate::sanity_checker::SanityCheckPlan; use crate::topk_aggregation::TopKAggregation; use crate::update_aggr_exprs::OptimizeAggregateOrder; @@ -121,6 +122,10 @@ impl PhysicalOptimizer { // into an `order by max(x) limit y`. In this case it will copy the limit value down // to the aggregation, allowing it to use only y number of accumulators. Arc::new(TopKAggregation::new()), + // The FilterPushdown rule tries to push down filters as far as it can. + // For example, it will push down filtering from a `FilterExec` to + // a `DataSourceExec`, or from a `TopK`'s current state to a `DataSourceExec`. + Arc::new(PushdownFilter::new()), // The LimitPushdown rule tries to push limits down as far as possible, // replacing operators with fetching variants, or adding limits // past operators that support limit pushdown. diff --git a/datafusion/physical-optimizer/src/pruning.rs b/datafusion/physical-optimizer/src/pruning.rs index b5287f3d33f3c..1dd168f181676 100644 --- a/datafusion/physical-optimizer/src/pruning.rs +++ b/datafusion/physical-optimizer/src/pruning.rs @@ -41,6 +41,7 @@ use datafusion_common::{Column, DFSchema}; use datafusion_expr_common::operator::Operator; use datafusion_physical_expr::utils::{collect_columns, Guarantee, LiteralGuarantee}; use datafusion_physical_expr::{expressions as phys_expr, PhysicalExprRef}; +use datafusion_physical_expr_common::physical_expr::snapshot_physical_expr; use datafusion_physical_plan::{ColumnarValue, PhysicalExpr}; /// A source of runtime statistical information to [`PruningPredicate`]s. @@ -312,13 +313,13 @@ pub trait PruningStatistics { /// * `true`: there MAY be rows that pass the predicate, **KEEPS** the container /// /// * `NULL`: there MAY be rows that pass the predicate, **KEEPS** the container -/// Note that rewritten predicate can evaluate to NULL when some of -/// the min/max values are not known. *Note that this is different than -/// the SQL filter semantics where `NULL` means the row is filtered -/// out.* +/// Note that rewritten predicate can evaluate to NULL when some of +/// the min/max values are not known. *Note that this is different than +/// the SQL filter semantics where `NULL` means the row is filtered +/// out.* /// /// * `false`: there are no rows that could possibly match the predicate, -/// **PRUNES** the container +/// **PRUNES** the container /// /// For example, given a column `x`, the `x_min`, `x_max`, `x_null_count`, and /// `x_row_count` represent the minimum and maximum values, the null count of @@ -527,6 +528,9 @@ impl PruningPredicate { /// See the struct level documentation on [`PruningPredicate`] for more /// details. pub fn try_new(expr: Arc, schema: SchemaRef) -> Result { + // Get a (simpler) snapshot of the physical expr here to use with `PruningPredicate` + // which does not handle dynamic exprs in general + let expr = snapshot_physical_expr(expr)?; let unhandled_hook = Arc::new(ConstantUnhandledPredicateHook::default()) as _; // build predicate expression once diff --git a/datafusion/physical-optimizer/src/push_down_filter.rs b/datafusion/physical-optimizer/src/push_down_filter.rs new file mode 100644 index 0000000000000..80201454d06d4 --- /dev/null +++ b/datafusion/physical-optimizer/src/push_down_filter.rs @@ -0,0 +1,535 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::sync::Arc; + +use crate::PhysicalOptimizerRule; + +use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRecursion}; +use datafusion_common::{config::ConfigOptions, Result}; +use datafusion_physical_expr::conjunction; +use datafusion_physical_plan::filter::FilterExec; +use datafusion_physical_plan::filter_pushdown::{ + FilterDescription, FilterPushdownResult, FilterPushdownSupport, +}; +use datafusion_physical_plan::tree_node::PlanContext; +use datafusion_physical_plan::ExecutionPlan; + +/// Attempts to recursively push given filters from the top of the tree into leafs. +/// +/// # Default Implementation +/// +/// The default implementation in [`ExecutionPlan::try_pushdown_filters`] is a no-op +/// that assumes that: +/// +/// * Parent filters can't be passed onto children. +/// * This node has no filters to contribute. +/// +/// # Example: Push filter into a `DataSourceExec` +/// +/// For example, consider the following plan: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ CoalesceBatchesExec │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ FilterExec │ +/// │ filters = [ id=1] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// └──────────────────────┘ +/// ``` +/// +/// Our goal is to move the `id = 1` filter from the [`FilterExec`] node to the `DataSourceExec` node. +/// +/// If this filter is selective pushing it into the scan can avoid massive +/// amounts of data being read from the source (the projection is `*` so all +/// matching columns are read). +/// +/// The new plan looks like: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ CoalesceBatchesExec │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// │ filters = [ id=1] │ +/// └──────────────────────┘ +/// ``` +/// +/// # Example: Push filters with `ProjectionExec` +/// +/// Let's consider a more complex example involving a [`ProjectionExec`] +/// node in between the [`FilterExec`] and `DataSourceExec` nodes that +/// creates a new column that the filter depends on. +/// +/// ```text +/// ┌──────────────────────┐ +/// │ CoalesceBatchesExec │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ FilterExec │ +/// │ filters = │ +/// │ [cost>50,id=1] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ ProjectionExec │ +/// │ cost = price * 1.2 │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// └──────────────────────┘ +/// ``` +/// +/// We want to push down the filters `[id=1]` to the `DataSourceExec` node, +/// but can't push down `cost>50` because it requires the [`ProjectionExec`] +/// node to be executed first. A simple thing to do would be to split up the +/// filter into two separate filters and push down the first one: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ CoalesceBatchesExec │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ FilterExec │ +/// │ filters = │ +/// │ [cost>50] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ ProjectionExec │ +/// │ cost = price * 1.2 │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// │ filters = [ id=1] │ +/// └──────────────────────┘ +/// ``` +/// +/// We can actually however do better by pushing down `price * 1.2 > 50` +/// instead of `cost > 50`: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ CoalesceBatchesExec │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ ProjectionExec │ +/// │ cost = price * 1.2 │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// │ filters = [id=1, │ +/// │ price * 1.2 > 50] │ +/// └──────────────────────┘ +/// ``` +/// +/// # Example: Push filters within a subtree +/// +/// There are also cases where we may be able to push down filters within a +/// subtree but not the entire tree. A good example of this is aggregation +/// nodes: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ ProjectionExec │ +/// │ projection = * │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ FilterExec │ +/// │ filters = [sum > 10] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌───────────────────────┐ +/// │ AggregateExec │ +/// │ group by = [id] │ +/// │ aggregate = │ +/// │ [sum(price)] │ +/// └───────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ FilterExec │ +/// │ filters = [id=1] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// └──────────────────────┘ +/// ``` +/// +/// The transformation here is to push down the `id=1` filter to the +/// `DataSourceExec` node: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ ProjectionExec │ +/// │ projection = * │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ FilterExec │ +/// │ filters = [sum > 10] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌───────────────────────┐ +/// │ AggregateExec │ +/// │ group by = [id] │ +/// │ aggregate = │ +/// │ [sum(price)] │ +/// └───────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// │ filters = [id=1] │ +/// └──────────────────────┘ +/// ``` +/// +/// The point here is that: +/// 1. We cannot push down `sum > 10` through the [`AggregateExec`] node into the `DataSourceExec` node. +/// Any filters above the [`AggregateExec`] node are not pushed down. +/// This is determined by calling [`ExecutionPlan::try_pushdown_filters`] on the [`AggregateExec`] node. +/// 2. We need to keep recursing into the tree so that we can discover the other [`FilterExec`] node and push +/// down the `id=1` filter. +/// +/// # Example: Push filters through Joins +/// +/// It is also possible to push down filters through joins and filters that +/// originate from joins. For example, a hash join where we build a hash +/// table of the left side and probe the right side (ignoring why we would +/// choose this order, typically it depends on the size of each table, +/// etc.). +/// +/// ```text +/// ┌─────────────────────┐ +/// │ FilterExec │ +/// │ filters = │ +/// │ [d.size > 100] │ +/// └─────────────────────┘ +/// │ +/// │ +/// ┌──────────▼──────────┐ +/// │ │ +/// │ HashJoinExec │ +/// │ [u.dept@hash(d.id)] │ +/// │ │ +/// └─────────────────────┘ +/// │ +/// ┌────────────┴────────────┐ +/// ┌──────────▼──────────┐ ┌──────────▼──────────┐ +/// │ DataSourceExec │ │ DataSourceExec │ +/// │ alias [users as u] │ │ alias [dept as d] │ +/// │ │ │ │ +/// └─────────────────────┘ └─────────────────────┘ +/// ``` +/// +/// There are two pushdowns we can do here: +/// 1. Push down the `d.size > 100` filter through the `HashJoinExec` node to the `DataSourceExec` +/// node for the `departments` table. +/// 2. Push down the hash table state from the `HashJoinExec` node to the `DataSourceExec` node to avoid reading +/// rows from the `users` table that will be eliminated by the join. +/// This can be done via a bloom filter or similar and is not (yet) supported +/// in DataFusion. See . +/// +/// ```text +/// ┌─────────────────────┐ +/// │ │ +/// │ HashJoinExec │ +/// │ [u.dept@hash(d.id)] │ +/// │ │ +/// └─────────────────────┘ +/// │ +/// ┌────────────┴────────────┐ +/// ┌──────────▼──────────┐ ┌──────────▼──────────┐ +/// │ DataSourceExec │ │ DataSourceExec │ +/// │ alias [users as u] │ │ alias [dept as d] │ +/// │ filters = │ │ filters = │ +/// │ [depg@hash(d.id)] │ │ [ d.size > 100] │ +/// └─────────────────────┘ └─────────────────────┘ +/// ``` +/// +/// You may notice in this case that the filter is *dynamic*: the hash table +/// is built _after_ the `departments` table is read and at runtime. We +/// don't have a concrete `InList` filter or similar to push down at +/// optimization time. These sorts of dynamic filters are handled by +/// building a specialized [`PhysicalExpr`] that can be evaluated at runtime +/// and internally maintains a reference to the hash table or other state. +/// +/// To make working with these sorts of dynamic filters more tractable we have the method [`PhysicalExpr::snapshot`] +/// which attempts to simplify a dynamic filter into a "basic" non-dynamic filter. +/// For a join this could mean converting it to an `InList` filter or a min/max filter for example. +/// See `datafusion/physical-plan/src/dynamic_filters.rs` for more details. +/// +/// # Example: Push TopK filters into Scans +/// +/// Another form of dynamic filter is pushing down the state of a `TopK` +/// operator for queries like `SELECT * FROM t ORDER BY id LIMIT 10`: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ TopK │ +/// │ limit = 10 │ +/// │ order by = [id] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// └──────────────────────┘ +/// ``` +/// +/// We can avoid large amounts of data processing by transforming this into: +/// +/// ```text +/// ┌──────────────────────┐ +/// │ TopK │ +/// │ limit = 10 │ +/// │ order by = [id] │ +/// └──────────────────────┘ +/// │ +/// ▼ +/// ┌──────────────────────┐ +/// │ DataSourceExec │ +/// │ projection = * │ +/// │ filters = │ +/// │ [id < @ TopKHeap] │ +/// └──────────────────────┘ +/// ``` +/// +/// Now as we fill our `TopK` heap we can push down the state of the heap to +/// the `DataSourceExec` node to avoid reading files / row groups / pages / +/// rows that could not possibly be in the top 10. +/// +/// This is not yet implemented in DataFusion. See +/// +/// +/// [`PhysicalExpr`]: datafusion_physical_plan::PhysicalExpr +/// [`PhysicalExpr::snapshot`]: datafusion_physical_plan::PhysicalExpr::snapshot +/// [`FilterExec`]: datafusion_physical_plan::filter::FilterExec +/// [`ProjectionExec`]: datafusion_physical_plan::projection::ProjectionExec +/// [`AggregateExec`]: datafusion_physical_plan::aggregates::AggregateExec +#[derive(Debug)] +pub struct PushdownFilter {} + +impl Default for PushdownFilter { + fn default() -> Self { + Self::new() + } +} + +pub type FilterDescriptionContext = PlanContext; + +impl PhysicalOptimizerRule for PushdownFilter { + fn optimize( + &self, + plan: Arc, + config: &ConfigOptions, + ) -> Result> { + let context = FilterDescriptionContext::new_default(plan); + + context + .transform_up(|node| { + if node.plan.as_any().downcast_ref::().is_some() { + let initial_plan = Arc::clone(&node.plan); + let mut accept_updated = false; + let updated_node = node.transform_down(|filter_node| { + Self::try_pushdown(filter_node, config, &mut accept_updated) + }); + + if accept_updated { + updated_node + } else { + Ok(Transformed::no(FilterDescriptionContext::new_default( + initial_plan, + ))) + } + } + // Other filter introducing operators extends here + else { + Ok(Transformed::no(node)) + } + }) + .map(|updated| updated.data.plan) + } + + fn name(&self) -> &str { + "PushdownFilter" + } + + fn schema_check(&self) -> bool { + true // Filter pushdown does not change the schema of the plan + } +} + +impl PushdownFilter { + pub fn new() -> Self { + Self {} + } + + fn try_pushdown( + mut node: FilterDescriptionContext, + config: &ConfigOptions, + accept_updated: &mut bool, + ) -> Result> { + let initial_description = FilterDescription { + filters: node.data.take_description(), + }; + + let FilterPushdownResult { + support, + remaining_description, + } = node + .plan + .try_pushdown_filters(initial_description, config)?; + + match support { + FilterPushdownSupport::Supported { + mut child_descriptions, + op, + revisit, + } => { + if revisit { + // This check handles cases where the current operator is entirely removed + // from the plan and replaced with its child. In such cases, to not skip + // over the new node, we need to explicitly re-apply this pushdown logic + // to the new node. + // + // TODO: If TreeNodeRecursion supports a Revisit mechanism in the future, + // this manual recursion could be removed. + + // If the operator is removed, it should not leave any filters as remaining + debug_assert!(remaining_description.filters.is_empty()); + // Operators having 2 children cannot be removed + debug_assert_eq!(child_descriptions.len(), 1); + debug_assert_eq!(node.children.len(), 1); + + node.plan = op; + node.data = child_descriptions.swap_remove(0); + node.children = node.children.swap_remove(0).children; + Self::try_pushdown(node, config, accept_updated) + } else { + if remaining_description.filters.is_empty() { + // Filter can be pushed down safely + node.plan = op; + if node.children.is_empty() { + *accept_updated = true; + } else { + for (child, descr) in + node.children.iter_mut().zip(child_descriptions) + { + child.data = descr; + } + } + } else { + // Filter cannot be pushed down + node = insert_filter_exec( + node, + child_descriptions, + remaining_description, + )?; + } + Ok(Transformed::yes(node)) + } + } + FilterPushdownSupport::NotSupported => { + if remaining_description.filters.is_empty() { + Ok(Transformed { + data: node, + transformed: false, + tnr: TreeNodeRecursion::Stop, + }) + } else { + node = insert_filter_exec( + node, + vec![FilterDescription::empty(); 1], + remaining_description, + )?; + Ok(Transformed { + data: node, + transformed: true, + tnr: TreeNodeRecursion::Stop, + }) + } + } + } + } +} + +fn insert_filter_exec( + node: FilterDescriptionContext, + mut child_descriptions: Vec, + remaining_description: FilterDescription, +) -> Result { + let mut new_child_node = node; + + // Filter has one child + if !child_descriptions.is_empty() { + debug_assert_eq!(child_descriptions.len(), 1); + new_child_node.data = child_descriptions.swap_remove(0); + } + let new_plan = Arc::new(FilterExec::try_new( + conjunction(remaining_description.filters), + Arc::clone(&new_child_node.plan), + )?); + let new_children = vec![new_child_node]; + let new_data = FilterDescription::empty(); + + Ok(FilterDescriptionContext::new( + new_plan, + new_data, + new_children, + )) +} diff --git a/datafusion/physical-plan/Cargo.toml b/datafusion/physical-plan/Cargo.toml index 1f38e2ed31263..5210ee26755c9 100644 --- a/datafusion/physical-plan/Cargo.toml +++ b/datafusion/physical-plan/Cargo.toml @@ -72,6 +72,7 @@ insta = { workspace = true } rand = { workspace = true } rstest = { workspace = true } rstest_reuse = "0.7.0" +tempfile = "3.19.1" tokio = { workspace = true, features = [ "rt-multi-thread", "fs", @@ -81,3 +82,7 @@ tokio = { workspace = true, features = [ [[bench]] harness = false name = "partial_ordering" + +[[bench]] +harness = false +name = "spill_io" diff --git a/datafusion/physical-plan/benches/spill_io.rs b/datafusion/physical-plan/benches/spill_io.rs new file mode 100644 index 0000000000000..3b877671ad583 --- /dev/null +++ b/datafusion/physical-plan/benches/spill_io.rs @@ -0,0 +1,123 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use arrow::array::{ + Date32Builder, Decimal128Builder, Int32Builder, RecordBatch, StringBuilder, +}; +use arrow::datatypes::{DataType, Field, Schema}; +use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; +use datafusion_execution::runtime_env::RuntimeEnv; +use datafusion_physical_plan::common::collect; +use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, SpillMetrics}; +use datafusion_physical_plan::SpillManager; +use std::sync::Arc; +use tokio::runtime::Runtime; + +pub fn create_batch(num_rows: usize, allow_nulls: bool) -> RecordBatch { + let schema = Arc::new(Schema::new(vec![ + Field::new("c0", DataType::Int32, true), + Field::new("c1", DataType::Utf8, true), + Field::new("c2", DataType::Date32, true), + Field::new("c3", DataType::Decimal128(11, 2), true), + ])); + + let mut a = Int32Builder::new(); + let mut b = StringBuilder::new(); + let mut c = Date32Builder::new(); + let mut d = Decimal128Builder::new() + .with_precision_and_scale(11, 2) + .unwrap(); + + for i in 0..num_rows { + a.append_value(i as i32); + c.append_value(i as i32); + d.append_value((i * 1000000) as i128); + if allow_nulls && i % 10 == 0 { + b.append_null(); + } else { + b.append_value(format!("this is string number {i}")); + } + } + + let a = a.finish(); + let b = b.finish(); + let c = c.finish(); + let d = d.finish(); + + RecordBatch::try_new( + schema.clone(), + vec![Arc::new(a), Arc::new(b), Arc::new(c), Arc::new(d)], + ) + .unwrap() +} + +// BENCHMARK: REVALIDATION OVERHEAD COMPARISON +// --------------------------------------------------------- +// To compare performance with/without Arrow IPC validation: +// +// 1. Locate the function `read_spill` +// 2. Modify the `skip_validation` flag: +// - Set to `false` to enable validation +// 3. Rerun `cargo bench --bench spill_io` +fn bench_spill_io(c: &mut Criterion) { + let env = Arc::new(RuntimeEnv::default()); + let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0); + let schema = Arc::new(Schema::new(vec![ + Field::new("c0", DataType::Int32, true), + Field::new("c1", DataType::Utf8, true), + Field::new("c2", DataType::Date32, true), + Field::new("c3", DataType::Decimal128(11, 2), true), + ])); + let spill_manager = SpillManager::new(env, metrics, schema); + + let mut group = c.benchmark_group("spill_io"); + let rt = Runtime::new().unwrap(); + + group.bench_with_input( + BenchmarkId::new("StreamReader/read_100", ""), + &spill_manager, + |b, spill_manager| { + b.iter_batched( + // Setup phase: Create fresh state for each benchmark iteration. + // - generate an ipc file. + // This ensures each iteration starts with clean resources. + || { + let batch = create_batch(8192, true); + spill_manager + .spill_record_batch_and_finish(&vec![batch; 100], "Test") + .unwrap() + .unwrap() + }, + // Benchmark phase: + // - Execute the read operation via SpillManager + // - Wait for the consumer to finish processing + |spill_file| { + rt.block_on(async { + let stream = + spill_manager.read_spill_as_stream(spill_file).unwrap(); + let _ = collect(stream).await.unwrap(); + }) + }, + BatchSize::LargeInput, + ) + }, + ); + group.finish(); +} + +criterion_group!(benches, bench_spill_io); +criterion_main!(benches); diff --git a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs index 005dcc8da3863..e9c3c42e632b5 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs @@ -158,7 +158,7 @@ impl GroupColumn (true, Some(false)) => { self.nulls.append_n(rows.len(), true); self.group_values - .extend(iter::repeat(T::default_value()).take(rows.len())); + .extend(iter::repeat_n(T::default_value(), rows.len())); } (false, _) => { diff --git a/datafusion/physical-plan/src/aggregates/group_values/row.rs b/datafusion/physical-plan/src/aggregates/group_values/row.rs index 63751d4703135..75c0e32491abc 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/row.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/row.rs @@ -202,6 +202,7 @@ impl GroupValues for GroupValuesRows { EmitTo::All => { let output = self.row_converter.convert_rows(&group_values)?; group_values.clear(); + self.map.clear(); output } EmitTo::First(n) => { diff --git a/datafusion/physical-plan/src/aggregates/order/full.rs b/datafusion/physical-plan/src/aggregates/order/full.rs index 218855459b1e2..eb98611f79dfb 100644 --- a/datafusion/physical-plan/src/aggregates/order/full.rs +++ b/datafusion/physical-plan/src/aggregates/order/full.rs @@ -92,7 +92,7 @@ impl GroupOrderingFull { Some(EmitTo::First(*current)) } } - State::Complete { .. } => Some(EmitTo::All), + State::Complete => Some(EmitTo::All), } } @@ -106,7 +106,7 @@ impl GroupOrderingFull { assert!(*current >= n); *current -= n; } - State::Complete { .. } => panic!("invalid state: complete"), + State::Complete => panic!("invalid state: complete"), } } @@ -133,7 +133,7 @@ impl GroupOrderingFull { current: max_group_index, } } - State::Complete { .. } => { + State::Complete => { panic!("Saw new group after input was complete"); } }; diff --git a/datafusion/physical-plan/src/aggregates/order/partial.rs b/datafusion/physical-plan/src/aggregates/order/partial.rs index aff69277a4cef..c7a75e5f26404 100644 --- a/datafusion/physical-plan/src/aggregates/order/partial.rs +++ b/datafusion/physical-plan/src/aggregates/order/partial.rs @@ -181,7 +181,7 @@ impl GroupOrderingPartial { assert!(*current_sort >= n); *current_sort -= n; } - State::Complete { .. } => panic!("invalid state: complete"), + State::Complete => panic!("invalid state: complete"), } } diff --git a/datafusion/physical-plan/src/aggregates/row_hash.rs b/datafusion/physical-plan/src/aggregates/row_hash.rs index 077f18d510339..232565a04466f 100644 --- a/datafusion/physical-plan/src/aggregates/row_hash.rs +++ b/datafusion/physical-plan/src/aggregates/row_hash.rs @@ -507,6 +507,16 @@ impl GroupedHashAggregateStream { AggregateMode::Partial, )?; + // Need to update the GROUP BY expressions to point to the correct column after schema change + let merging_group_by_expr = agg_group_by + .expr + .iter() + .enumerate() + .map(|(idx, (_, name))| { + (Arc::new(Column::new(name.as_str(), idx)) as _, name.clone()) + }) + .collect(); + let partial_agg_schema = Arc::new(partial_agg_schema); let spill_expr = group_schema @@ -550,7 +560,7 @@ impl GroupedHashAggregateStream { spill_schema: partial_agg_schema, is_stream_merging: false, merging_aggregate_arguments, - merging_group_by: PhysicalGroupBy::new_single(agg_group_by.expr.clone()), + merging_group_by: PhysicalGroupBy::new_single(merging_group_by_expr), peak_mem_used: MetricBuilder::new(&agg.metrics) .gauge("peak_mem_used", partition), spill_manager, @@ -965,7 +975,7 @@ impl GroupedHashAggregateStream { /// memory. Currently only [`GroupOrdering::None`] is supported for spilling. fn spill_previous_if_necessary(&mut self, batch: &RecordBatch) -> Result<()> { // TODO: support group_ordering for spilling - if self.group_values.len() > 0 + if !self.group_values.is_empty() && batch.num_rows() > 0 && matches!(self.group_ordering, GroupOrdering::None) && !self.spill_state.is_stream_merging diff --git a/datafusion/physical-plan/src/coalesce/mod.rs b/datafusion/physical-plan/src/coalesce/mod.rs index eb4a7d875c95a..0eca27f8e40e0 100644 --- a/datafusion/physical-plan/src/coalesce/mod.rs +++ b/datafusion/physical-plan/src/coalesce/mod.rs @@ -90,7 +90,7 @@ impl BatchCoalescer { /// # Arguments /// - `schema` - the schema of the output batches /// - `target_batch_size` - the minimum number of rows for each - /// output batch (until limit reached) + /// output batch (until limit reached) /// - `fetch` - the maximum number of rows to fetch, `None` means fetch all rows pub fn new( schema: SchemaRef, @@ -285,7 +285,7 @@ mod tests { fn test_coalesce() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat(batch).take(10)) + .with_batches(std::iter::repeat_n(batch, 10)) // expected output is batches of at least 20 rows (except for the final batch) .with_target_batch_size(21) .with_expected_output_sizes(vec![24, 24, 24, 8]) @@ -296,7 +296,7 @@ mod tests { fn test_coalesce_with_fetch_larger_than_input_size() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat(batch).take(10)) + .with_batches(std::iter::repeat_n(batch, 10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 100 // expected to behave the same as `test_concat_batches` .with_target_batch_size(21) @@ -309,7 +309,7 @@ mod tests { fn test_coalesce_with_fetch_less_than_input_size() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat(batch).take(10)) + .with_batches(std::iter::repeat_n(batch, 10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 50 .with_target_batch_size(21) .with_fetch(Some(50)) @@ -321,7 +321,7 @@ mod tests { fn test_coalesce_with_fetch_less_than_target_and_no_remaining_rows() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat(batch).take(10)) + .with_batches(std::iter::repeat_n(batch, 10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 48 .with_target_batch_size(21) .with_fetch(Some(48)) @@ -333,7 +333,7 @@ mod tests { fn test_coalesce_with_fetch_less_target_batch_size() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat(batch).take(10)) + .with_batches(std::iter::repeat_n(batch, 10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 10 .with_target_batch_size(21) .with_fetch(Some(10)) diff --git a/datafusion/physical-plan/src/coalesce_batches.rs b/datafusion/physical-plan/src/coalesce_batches.rs index 5244038b9ae27..faab5fdc5eb6c 100644 --- a/datafusion/physical-plan/src/coalesce_batches.rs +++ b/datafusion/physical-plan/src/coalesce_batches.rs @@ -35,6 +35,10 @@ use datafusion_execution::TaskContext; use crate::coalesce::{BatchCoalescer, CoalescerState}; use crate::execution_plan::CardinalityEffect; +use crate::filter_pushdown::{ + filter_pushdown_transparent, FilterDescription, FilterPushdownResult, +}; +use datafusion_common::config::ConfigOptions; use futures::ready; use futures::stream::{Stream, StreamExt}; @@ -212,6 +216,17 @@ impl ExecutionPlan for CoalesceBatchesExec { fn cardinality_effect(&self) -> CardinalityEffect { CardinalityEffect::Equal } + + fn try_pushdown_filters( + &self, + fd: FilterDescription, + _config: &ConfigOptions, + ) -> Result>> { + Ok(filter_pushdown_transparent::>( + Arc::new(self.clone()), + fd, + )) + } } /// Stream for [`CoalesceBatchesExec`]. See [`CoalesceBatchesExec`] for more details. diff --git a/datafusion/physical-plan/src/display.rs b/datafusion/physical-plan/src/display.rs index f437295a35551..e247f5ad9d194 100644 --- a/datafusion/physical-plan/src/display.rs +++ b/datafusion/physical-plan/src/display.rs @@ -657,7 +657,7 @@ impl TreeRenderVisitor<'_, '_> { } } - let halfway_point = (extra_height + 1) / 2; + let halfway_point = extra_height.div_ceil(2); // Render the actual node. for render_y in 0..=extra_height { diff --git a/datafusion/physical-plan/src/execution_plan.rs b/datafusion/physical-plan/src/execution_plan.rs index 2bc5706ee0e18..2b6eac7be0675 100644 --- a/datafusion/physical-plan/src/execution_plan.rs +++ b/datafusion/physical-plan/src/execution_plan.rs @@ -16,6 +16,9 @@ // under the License. pub use crate::display::{DefaultDisplay, DisplayAs, DisplayFormatType, VerboseDisplay}; +use crate::filter_pushdown::{ + filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, +}; pub use crate::metrics::Metric; pub use crate::ordering::InputOrderMode; pub use crate::stream::EmptyRecordBatchStream; @@ -467,6 +470,41 @@ pub trait ExecutionPlan: Debug + DisplayAs + Send + Sync { ) -> Result>> { Ok(None) } + + /// Attempts to recursively push given filters from the top of the tree into leafs. + /// + /// This is used for various optimizations, such as: + /// + /// * Pushing down filters into scans in general to minimize the amount of data that needs to be materialzied. + /// * Pushing down dynamic filters from operators like TopK and Joins into scans. + /// + /// Generally the further down (closer to leaf nodes) that filters can be pushed, the better. + /// + /// Consider the case of a query such as `SELECT * FROM t WHERE a = 1 AND b = 2`. + /// With no filter pushdown the scan needs to read and materialize all the data from `t` and then filter based on `a` and `b`. + /// With filter pushdown into the scan it can first read only `a`, then `b` and keep track of + /// which rows match the filter. + /// Then only for rows that match the filter does it have to materialize the rest of the columns. + /// + /// # Default Implementation + /// + /// The default implementation assumes: + /// * Parent filters can't be passed onto children. + /// * This node has no filters to contribute. + /// + /// # Implementation Notes + /// + /// Most of the actual logic is implemented as a Physical Optimizer rule. + /// See [`PushdownFilter`] for more details. + /// + /// [`PushdownFilter`]: https://docs.rs/datafusion/latest/datafusion/physical_optimizer/filter_pushdown/struct.PushdownFilter.html + fn try_pushdown_filters( + &self, + fd: FilterDescription, + _config: &ConfigOptions, + ) -> Result>> { + Ok(filter_pushdown_not_supported(fd)) + } } /// [`ExecutionPlan`] Invariant Level @@ -519,13 +557,15 @@ pub trait ExecutionPlanProperties { /// If this ExecutionPlan makes no changes to the schema of the rows flowing /// through it or how columns within each row relate to each other, it /// should return the equivalence properties of its input. For - /// example, since `FilterExec` may remove rows from its input, but does not + /// example, since [`FilterExec`] may remove rows from its input, but does not /// otherwise modify them, it preserves its input equivalence properties. /// However, since `ProjectionExec` may calculate derived expressions, it /// needs special handling. /// /// See also [`ExecutionPlan::maintains_input_order`] and [`Self::output_ordering`] /// for related concepts. + /// + /// [`FilterExec`]: crate::filter::FilterExec fn equivalence_properties(&self) -> &EquivalenceProperties; } diff --git a/datafusion/physical-plan/src/filter.rs b/datafusion/physical-plan/src/filter.rs index a8a9973ea0434..95fa67025e90d 100644 --- a/datafusion/physical-plan/src/filter.rs +++ b/datafusion/physical-plan/src/filter.rs @@ -26,6 +26,9 @@ use super::{ }; use crate::common::can_project; use crate::execution_plan::CardinalityEffect; +use crate::filter_pushdown::{ + FilterDescription, FilterPushdownResult, FilterPushdownSupport, +}; use crate::projection::{ make_with_child, try_embed_projection, update_expr, EmbeddedProjection, ProjectionExec, @@ -39,6 +42,7 @@ use arrow::compute::filter_record_batch; use arrow::datatypes::{DataType, SchemaRef}; use arrow::record_batch::RecordBatch; use datafusion_common::cast::as_boolean_array; +use datafusion_common::config::ConfigOptions; use datafusion_common::stats::Precision; use datafusion_common::{ internal_err, plan_err, project_schema, DataFusionError, Result, ScalarValue, @@ -46,7 +50,7 @@ use datafusion_common::{ use datafusion_execution::TaskContext; use datafusion_expr::Operator; use datafusion_physical_expr::equivalence::ProjectionMapping; -use datafusion_physical_expr::expressions::BinaryExpr; +use datafusion_physical_expr::expressions::{BinaryExpr, Column}; use datafusion_physical_expr::intervals::utils::check_support; use datafusion_physical_expr::utils::collect_columns; use datafusion_physical_expr::{ @@ -433,6 +437,56 @@ impl ExecutionPlan for FilterExec { } try_embed_projection(projection, self) } + + fn try_pushdown_filters( + &self, + mut fd: FilterDescription, + _config: &ConfigOptions, + ) -> Result>> { + // Extend the filter descriptions + fd.filters.push(Arc::clone(&self.predicate)); + + // Extract the information + let child_descriptions = vec![fd]; + let remaining_description = FilterDescription { filters: vec![] }; + let filter_input = Arc::clone(self.input()); + + if let Some(projection_indices) = self.projection.as_ref() { + // Push the filters down, but leave a ProjectionExec behind, instead of the FilterExec + let filter_child_schema = filter_input.schema(); + let proj_exprs = projection_indices + .iter() + .map(|p| { + let field = filter_child_schema.field(*p).clone(); + ( + Arc::new(Column::new(field.name(), *p)) as Arc, + field.name().to_string(), + ) + }) + .collect::>(); + let projection_exec = + Arc::new(ProjectionExec::try_new(proj_exprs, filter_input)?) as _; + + Ok(FilterPushdownResult { + support: FilterPushdownSupport::Supported { + child_descriptions, + op: projection_exec, + revisit: false, + }, + remaining_description, + }) + } else { + // Pull out the FilterExec, and inform the rule as it should be re-run + Ok(FilterPushdownResult { + support: FilterPushdownSupport::Supported { + child_descriptions, + op: filter_input, + revisit: true, + }, + remaining_description, + }) + } + } } impl EmbeddedProjection for FilterExec { diff --git a/datafusion/physical-plan/src/filter_pushdown.rs b/datafusion/physical-plan/src/filter_pushdown.rs new file mode 100644 index 0000000000000..38f5aef5923e1 --- /dev/null +++ b/datafusion/physical-plan/src/filter_pushdown.rs @@ -0,0 +1,95 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::sync::Arc; + +use crate::ExecutionPlan; +use datafusion_physical_expr_common::physical_expr::PhysicalExpr; + +#[derive(Clone, Debug)] +pub struct FilterDescription { + /// Expressions coming from the parent nodes + pub filters: Vec>, +} + +impl Default for FilterDescription { + fn default() -> Self { + Self::empty() + } +} + +impl FilterDescription { + /// Takes the filters out of the struct, leaving an empty vector in its place. + pub fn take_description(&mut self) -> Vec> { + std::mem::take(&mut self.filters) + } + + pub fn empty() -> FilterDescription { + Self { filters: vec![] } + } +} + +#[derive(Debug)] +pub enum FilterPushdownSupport { + Supported { + // Filter predicates which can be pushed down through the operator. + // NOTE that these are not placed into any operator. + child_descriptions: Vec, + // Possibly updated new operator + op: T, + // Whether the node is removed from the plan and the rule should be re-run manually + // on the new node. + // TODO: If TreeNodeRecursion supports Revisit mechanism, this flag can be removed + revisit: bool, + }, + NotSupported, +} + +#[derive(Debug)] +pub struct FilterPushdownResult { + pub support: FilterPushdownSupport, + // Filters which cannot be pushed down through the operator. + // NOTE that caller of try_pushdown_filters() should handle these remanining predicates, + // possibly introducing a FilterExec on top of this operator. + pub remaining_description: FilterDescription, +} + +pub fn filter_pushdown_not_supported( + remaining_description: FilterDescription, +) -> FilterPushdownResult { + FilterPushdownResult { + support: FilterPushdownSupport::NotSupported, + remaining_description, + } +} + +pub fn filter_pushdown_transparent( + plan: Arc, + fd: FilterDescription, +) -> FilterPushdownResult> { + let child_descriptions = vec![fd]; + let remaining_description = FilterDescription::empty(); + + FilterPushdownResult { + support: FilterPushdownSupport::Supported { + child_descriptions, + op: plan, + revisit: false, + }, + remaining_description, + } +} diff --git a/datafusion/physical-plan/src/joins/cross_join.rs b/datafusion/physical-plan/src/joins/cross_join.rs index 639fae7615af0..8dd1addff15ce 100644 --- a/datafusion/physical-plan/src/joins/cross_join.rs +++ b/datafusion/physical-plan/src/joins/cross_join.rs @@ -25,7 +25,6 @@ use super::utils::{ BatchTransformer, BuildProbeJoinMetrics, NoopBatchTransformer, OnceAsync, OnceFut, StatefulStreamResult, }; -use crate::coalesce_partitions::CoalescePartitionsExec; use crate::execution_plan::{boundedness_from_children, EmissionType}; use crate::metrics::{ExecutionPlanMetricsSet, MetricsSet}; use crate::projection::{ @@ -189,19 +188,11 @@ impl CrossJoinExec { /// Asynchronously collect the result of the left child async fn load_left_input( - left: Arc, - context: Arc, + stream: SendableRecordBatchStream, metrics: BuildProbeJoinMetrics, reservation: MemoryReservation, ) -> Result { - // merge all left parts into a single stream - let left_schema = left.schema(); - let merge = if left.output_partitioning().partition_count() != 1 { - Arc::new(CoalescePartitionsExec::new(left)) - } else { - left - }; - let stream = merge.execute(0, context)?; + let left_schema = stream.schema(); // Load all batches and count the rows let (batches, _metrics, reservation) = stream @@ -291,6 +282,13 @@ impl ExecutionPlan for CrossJoinExec { partition: usize, context: Arc, ) -> Result { + if self.left.output_partitioning().partition_count() != 1 { + return internal_err!( + "Invalid CrossJoinExec, the output partition count of the left child must be 1,\ + consider using CoalescePartitionsExec or the EnforceDistribution rule" + ); + } + let stream = self.right.execute(partition, Arc::clone(&context))?; let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics); @@ -303,14 +301,15 @@ impl ExecutionPlan for CrossJoinExec { let enforce_batch_size_in_joins = context.session_config().enforce_batch_size_in_joins(); - let left_fut = self.left_fut.once(|| { - load_left_input( - Arc::clone(&self.left), - context, + let left_fut = self.left_fut.try_once(|| { + let left_stream = self.left.execute(0, context)?; + + Ok(load_left_input( + left_stream, join_metrics.clone(), reservation, - ) - }); + )) + })?; if enforce_batch_size_in_joins { Ok(Box::pin(CrossJoinStream { diff --git a/datafusion/physical-plan/src/joins/hash_join.rs b/datafusion/physical-plan/src/joins/hash_join.rs index c2a313edd1564..e8904db0f3eaf 100644 --- a/datafusion/physical-plan/src/joins/hash_join.rs +++ b/datafusion/physical-plan/src/joins/hash_join.rs @@ -32,6 +32,7 @@ use super::{ utils::{OnceAsync, OnceFut}, PartitionMode, SharedBitmapBuilder, }; +use super::{JoinOn, JoinOnRef}; use crate::execution_plan::{boundedness_from_children, EmissionType}; use crate::projection::{ try_embed_projection, try_pushdown_through_join, EmbeddedProjection, JoinData, @@ -40,7 +41,6 @@ use crate::projection::{ use crate::spill::get_record_batch_memory_size; use crate::ExecutionPlanProperties; use crate::{ - coalesce_partitions::CoalescePartitionsExec, common::can_project, handle_state, hash_utils::create_hashes, @@ -50,8 +50,7 @@ use crate::{ build_batch_from_indices, build_join_schema, check_join_is_valid, estimate_join_statistics, need_produce_result_in_final, symmetric_join_output_partitioning, BuildProbeJoinMetrics, ColumnIndex, - JoinFilter, JoinHashMap, JoinHashMapType, JoinOn, JoinOnRef, - StatefulStreamResult, + JoinFilter, JoinHashMap, JoinHashMapType, StatefulStreamResult, }, metrics::{ExecutionPlanMetricsSet, MetricsSet}, DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, Partitioning, @@ -792,34 +791,42 @@ impl ExecutionPlan for HashJoinExec { ); } + if self.mode == PartitionMode::CollectLeft && left_partitions != 1 { + return internal_err!( + "Invalid HashJoinExec, the output partition count of the left child must be 1 in CollectLeft mode,\ + consider using CoalescePartitionsExec or the EnforceDistribution rule" + ); + } + let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics); let left_fut = match self.mode { - PartitionMode::CollectLeft => self.left_fut.once(|| { + PartitionMode::CollectLeft => self.left_fut.try_once(|| { + let left_stream = self.left.execute(0, Arc::clone(&context))?; + let reservation = MemoryConsumer::new("HashJoinInput").register(context.memory_pool()); - collect_left_input( - None, + + Ok(collect_left_input( self.random_state.clone(), - Arc::clone(&self.left), + left_stream, on_left.clone(), - Arc::clone(&context), join_metrics.clone(), reservation, need_produce_result_in_final(self.join_type), self.right().output_partitioning().partition_count(), - ) - }), + )) + })?, PartitionMode::Partitioned => { + let left_stream = self.left.execute(partition, Arc::clone(&context))?; + let reservation = MemoryConsumer::new(format!("HashJoinInput[{partition}]")) .register(context.memory_pool()); OnceFut::new(collect_left_input( - Some(partition), self.random_state.clone(), - Arc::clone(&self.left), + left_stream, on_left.clone(), - Arc::clone(&context), join_metrics.clone(), reservation, need_produce_result_in_final(self.join_type), @@ -930,36 +937,22 @@ impl ExecutionPlan for HashJoinExec { /// Reads the left (build) side of the input, buffering it in memory, to build a /// hash table (`LeftJoinData`) -#[allow(clippy::too_many_arguments)] async fn collect_left_input( - partition: Option, random_state: RandomState, - left: Arc, + left_stream: SendableRecordBatchStream, on_left: Vec, - context: Arc, metrics: BuildProbeJoinMetrics, reservation: MemoryReservation, with_visited_indices_bitmap: bool, probe_threads_count: usize, ) -> Result { - let schema = left.schema(); - - let (left_input, left_input_partition) = if let Some(partition) = partition { - (left, partition) - } else if left.output_partitioning().partition_count() != 1 { - (Arc::new(CoalescePartitionsExec::new(left)) as _, 0) - } else { - (left, 0) - }; - - // Depending on partition argument load single partition or whole left side in memory - let stream = left_input.execute(left_input_partition, Arc::clone(&context))?; + let schema = left_stream.schema(); // This operation performs 2 steps at once: // 1. creates a [JoinHashMap] of all batches from the stream // 2. stores the batches in a vector. let initial = (Vec::new(), 0, metrics, reservation); - let (batches, num_rows, metrics, mut reservation) = stream + let (batches, num_rows, metrics, mut reservation) = left_stream .try_fold(initial, |mut acc, batch| async { let batch_size = get_record_batch_memory_size(&batch); // Reserve memory for incoming batch @@ -1655,6 +1648,7 @@ impl EmbeddedProjection for HashJoinExec { #[cfg(test)] mod tests { use super::*; + use crate::coalesce_partitions::CoalescePartitionsExec; use crate::test::TestMemoryExec; use crate::{ common, expressions::Column, repartition::RepartitionExec, test::build_table_i32, @@ -2105,6 +2099,7 @@ mod tests { let left = TestMemoryExec::try_new_exec(&[vec![batch1], vec![batch2]], schema, None) .unwrap(); + let left = Arc::new(CoalescePartitionsExec::new(left)); let right = build_table( ("a1", &vec![1, 2, 3]), @@ -2177,6 +2172,7 @@ mod tests { let left = TestMemoryExec::try_new_exec(&[vec![batch1], vec![batch2]], schema, None) .unwrap(); + let left = Arc::new(CoalescePartitionsExec::new(left)); let right = build_table( ("a2", &vec![20, 30, 10]), ("b2", &vec![5, 6, 4]), diff --git a/datafusion/physical-plan/src/joins/mod.rs b/datafusion/physical-plan/src/joins/mod.rs index 22a8c0bc798c8..1d36db996434e 100644 --- a/datafusion/physical-plan/src/joins/mod.rs +++ b/datafusion/physical-plan/src/joins/mod.rs @@ -19,6 +19,7 @@ use arrow::array::BooleanBufferBuilder; pub use cross_join::CrossJoinExec; +use datafusion_physical_expr::PhysicalExprRef; pub use hash_join::HashJoinExec; pub use nested_loop_join::NestedLoopJoinExec; use parking_lot::Mutex; @@ -39,6 +40,11 @@ mod join_hash_map; #[cfg(test)] pub mod test_utils; +/// The on clause of the join, as vector of (left, right) columns. +pub type JoinOn = Vec<(PhysicalExprRef, PhysicalExprRef)>; +/// Reference for JoinOn. +pub type JoinOnRef<'a> = &'a [(PhysicalExprRef, PhysicalExprRef)]; + #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Hash join Partitioning mode pub enum PartitionMode { diff --git a/datafusion/physical-plan/src/joins/nested_loop_join.rs b/datafusion/physical-plan/src/joins/nested_loop_join.rs index cdd2eaeca8997..b902795950966 100644 --- a/datafusion/physical-plan/src/joins/nested_loop_join.rs +++ b/datafusion/physical-plan/src/joins/nested_loop_join.rs @@ -28,7 +28,6 @@ use super::utils::{ need_produce_result_in_final, reorder_output_after_swap, swap_join_projection, BatchSplitter, BatchTransformer, NoopBatchTransformer, StatefulStreamResult, }; -use crate::coalesce_partitions::CoalescePartitionsExec; use crate::common::can_project; use crate::execution_plan::{boundedness_from_children, EmissionType}; use crate::joins::utils::{ @@ -483,6 +482,13 @@ impl ExecutionPlan for NestedLoopJoinExec { partition: usize, context: Arc, ) -> Result { + if self.left.output_partitioning().partition_count() != 1 { + return internal_err!( + "Invalid NestedLoopJoinExec, the output partition count of the left child must be 1,\ + consider using CoalescePartitionsExec or the EnforceDistribution rule" + ); + } + let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics); // Initialization reservation for load of inner table @@ -490,16 +496,17 @@ impl ExecutionPlan for NestedLoopJoinExec { MemoryConsumer::new(format!("NestedLoopJoinLoad[{partition}]")) .register(context.memory_pool()); - let inner_table = self.inner_table.once(|| { - collect_left_input( - Arc::clone(&self.left), - Arc::clone(&context), + let inner_table = self.inner_table.try_once(|| { + let stream = self.left.execute(0, Arc::clone(&context))?; + + Ok(collect_left_input( + stream, join_metrics.clone(), load_reservation, need_produce_result_in_final(self.join_type), self.right().output_partitioning().partition_count(), - ) - }); + )) + })?; let batch_size = context.session_config().batch_size(); let enforce_batch_size_in_joins = @@ -610,20 +617,13 @@ impl ExecutionPlan for NestedLoopJoinExec { /// Asynchronously collect input into a single batch, and creates `JoinLeftData` from it async fn collect_left_input( - input: Arc, - context: Arc, + stream: SendableRecordBatchStream, join_metrics: BuildProbeJoinMetrics, reservation: MemoryReservation, with_visited_left_side: bool, probe_threads_count: usize, ) -> Result { - let schema = input.schema(); - let merge = if input.output_partitioning().partition_count() != 1 { - Arc::new(CoalescePartitionsExec::new(input)) - } else { - input - }; - let stream = merge.execute(0, context)?; + let schema = stream.schema(); // Load all batches and count the rows let (batches, metrics, mut reservation) = stream diff --git a/datafusion/physical-plan/src/joins/sort_merge_join.rs b/datafusion/physical-plan/src/joins/sort_merge_join.rs index 716cff939f663..89f2e3c911f89 100644 --- a/datafusion/physical-plan/src/joins/sort_merge_join.rs +++ b/datafusion/physical-plan/src/joins/sort_merge_join.rs @@ -823,42 +823,65 @@ impl BufferedBatch { /// Sort-Merge join stream that consumes streamed and buffered data streams /// and produces joined output stream. struct SortMergeJoinStream { - /// Current state of the stream - pub state: SortMergeJoinState, + // ======================================================================== + // PROPERTIES: + // These fields are initialized at the start and remain constant throughout + // the execution. + // ======================================================================== /// Output schema pub schema: SchemaRef, - /// Sort options of join columns used to sort streamed and buffered data stream - pub sort_options: Vec, /// null == null? pub null_equals_null: bool, + /// Sort options of join columns used to sort streamed and buffered data stream + pub sort_options: Vec, + /// optional join filter + pub filter: Option, + /// How the join is performed + pub join_type: JoinType, + /// Target output batch size + pub batch_size: usize, + + // ======================================================================== + // STREAMED FIELDS: + // These fields manage the properties and state of the streamed input. + // ======================================================================== /// Input schema of streamed pub streamed_schema: SchemaRef, - /// Input schema of buffered - pub buffered_schema: SchemaRef, /// Streamed data stream pub streamed: SendableRecordBatchStream, - /// Buffered data stream - pub buffered: SendableRecordBatchStream, /// Current processing record batch of streamed pub streamed_batch: StreamedBatch, - /// Current buffered data - pub buffered_data: BufferedData, /// (used in outer join) Is current streamed row joined at least once? pub streamed_joined: bool, - /// (used in outer join) Is current buffered batches joined at least once? - pub buffered_joined: bool, /// State of streamed pub streamed_state: StreamedState, - /// State of buffered - pub buffered_state: BufferedState, - /// The comparison result of current streamed row and buffered batches - pub current_ordering: Ordering, /// Join key columns of streamed pub on_streamed: Vec, + + // ======================================================================== + // BUFFERED FIELDS: + // These fields manage the properties and state of the buffered input. + // ======================================================================== + /// Input schema of buffered + pub buffered_schema: SchemaRef, + /// Buffered data stream + pub buffered: SendableRecordBatchStream, + /// Current buffered data + pub buffered_data: BufferedData, + /// (used in outer join) Is current buffered batches joined at least once? + pub buffered_joined: bool, + /// State of buffered + pub buffered_state: BufferedState, /// Join key columns of buffered pub on_buffered: Vec, - /// optional join filter - pub filter: Option, + + // ======================================================================== + // MERGE JOIN STATES: + // These fields track the execution state of merge join and are updated + // during the execution. + // ======================================================================== + /// Current state of the stream + pub state: SortMergeJoinState, /// Staging output array builders pub staging_output_record_batches: JoinedRecordBatches, /// Output buffer. Currently used by filtering as it requires double buffering @@ -868,18 +891,21 @@ struct SortMergeJoinStream { /// Increased when we put rows into buffer and decreased after we actually output batches. /// Used to trigger output when sufficient rows are ready pub output_size: usize, - /// Target output batch size - pub batch_size: usize, - /// How the join is performed - pub join_type: JoinType, + /// The comparison result of current streamed row and buffered batches + pub current_ordering: Ordering, + /// Manages the process of spilling and reading back intermediate data + pub spill_manager: SpillManager, + + // ======================================================================== + // EXECUTION RESOURCES: + // Fields related to managing execution resources and monitoring performance. + // ======================================================================== /// Metrics pub join_metrics: SortMergeJoinMetrics, /// Memory reservation pub reservation: MemoryReservation, /// Runtime env pub runtime_env: Arc, - /// Manages the process of spilling and reading back intermediate data - pub spill_manager: SpillManager, /// A unique number for each batch pub streamed_batch_counter: AtomicUsize, } diff --git a/datafusion/physical-plan/src/joins/test_utils.rs b/datafusion/physical-plan/src/joins/test_utils.rs index e70007aa651f7..d38637dae0282 100644 --- a/datafusion/physical-plan/src/joins/test_utils.rs +++ b/datafusion/physical-plan/src/joins/test_utils.rs @@ -444,8 +444,7 @@ pub fn build_sides_record_batches( .collect::>(), )); let ordered_asc_null_first = Arc::new(Int32Array::from_iter({ - std::iter::repeat(None) - .take(index as usize) + std::iter::repeat_n(None, index as usize) .chain(rest_of.clone().map(Some)) .collect::>>() })); @@ -453,13 +452,12 @@ pub fn build_sides_record_batches( rest_of .clone() .map(Some) - .chain(std::iter::repeat(None).take(index as usize)) + .chain(std::iter::repeat_n(None, index as usize)) .collect::>>() })); let ordered_desc_null_first = Arc::new(Int32Array::from_iter({ - std::iter::repeat(None) - .take(index as usize) + std::iter::repeat_n(None, index as usize) .chain(rest_of.rev().map(Some)) .collect::>>() })); diff --git a/datafusion/physical-plan/src/joins/utils.rs b/datafusion/physical-plan/src/joins/utils.rs index f6c720dbb707a..5516f172d5101 100644 --- a/datafusion/physical-plan/src/joins/utils.rs +++ b/datafusion/physical-plan/src/joins/utils.rs @@ -32,6 +32,7 @@ use crate::{ // compatibility pub use super::join_filter::JoinFilter; pub use super::join_hash_map::{JoinHashMap, JoinHashMapType}; +pub use crate::joins::{JoinOn, JoinOnRef}; use arrow::array::{ builder::UInt64Builder, downcast_array, new_null_array, Array, ArrowPrimitiveType, @@ -62,11 +63,6 @@ use futures::future::{BoxFuture, Shared}; use futures::{ready, FutureExt}; use parking_lot::Mutex; -/// The on clause of the join, as vector of (left, right) columns. -pub type JoinOn = Vec<(PhysicalExprRef, PhysicalExprRef)>; -/// Reference for JoinOn. -pub type JoinOnRef<'a> = &'a [(PhysicalExprRef, PhysicalExprRef)]; - /// Checks whether the schemas "left" and "right" and columns "on" represent a valid join. /// They are valid whenever their columns' intersection equals the set `on` pub fn check_join_is_valid(left: &Schema, right: &Schema, on: JoinOnRef) -> Result<()> { @@ -328,7 +324,7 @@ pub fn build_join_schema( } /// A [`OnceAsync`] runs an `async` closure once, where multiple calls to -/// [`OnceAsync::once`] return a [`OnceFut`] that resolves to the result of the +/// [`OnceAsync::try_once`] return a [`OnceFut`] that resolves to the result of the /// same computation. /// /// This is useful for joins where the results of one child are needed to proceed @@ -341,7 +337,7 @@ pub fn build_join_schema( /// /// Each output partition waits on the same `OnceAsync` before proceeding. pub(crate) struct OnceAsync { - fut: Mutex>>, + fut: Mutex>>>, } impl Default for OnceAsync { @@ -360,19 +356,22 @@ impl Debug for OnceAsync { impl OnceAsync { /// If this is the first call to this function on this object, will invoke - /// `f` to obtain a future and return a [`OnceFut`] referring to this + /// `f` to obtain a future and return a [`OnceFut`] referring to this. `f` + /// may fail, in which case its error is returned. /// /// If this is not the first call, will return a [`OnceFut`] referring - /// to the same future as was returned by the first call - pub(crate) fn once(&self, f: F) -> OnceFut + /// to the same future as was returned by the first call - or the same + /// error if the initial call to `f` failed. + pub(crate) fn try_once(&self, f: F) -> Result> where - F: FnOnce() -> Fut, + F: FnOnce() -> Result, Fut: Future> + Send + 'static, { self.fut .lock() - .get_or_insert_with(|| OnceFut::new(f())) + .get_or_insert_with(|| f().map(OnceFut::new).map_err(Arc::new)) .clone() + .map_err(DataFusionError::Shared) } } diff --git a/datafusion/physical-plan/src/lib.rs b/datafusion/physical-plan/src/lib.rs index 04fbd06fabcde..a1862554b303e 100644 --- a/datafusion/physical-plan/src/lib.rs +++ b/datafusion/physical-plan/src/lib.rs @@ -50,6 +50,7 @@ pub use crate::ordering::InputOrderMode; pub use crate::stream::EmptyRecordBatchStream; pub use crate::topk::TopK; pub use crate::visitor::{accept, visit_execution_plan, ExecutionPlanVisitor}; +pub use spill::spill_manager::SpillManager; mod ordering; mod render_tree; @@ -66,6 +67,7 @@ pub mod empty; pub mod execution_plan; pub mod explain; pub mod filter; +pub mod filter_pushdown; pub mod joins; pub mod limit; pub mod memory; diff --git a/datafusion/physical-plan/src/projection.rs b/datafusion/physical-plan/src/projection.rs index 1d3e23ea90974..72934c74446eb 100644 --- a/datafusion/physical-plan/src/projection.rs +++ b/datafusion/physical-plan/src/projection.rs @@ -33,7 +33,7 @@ use super::{ SendableRecordBatchStream, Statistics, }; use crate::execution_plan::CardinalityEffect; -use crate::joins::utils::{ColumnIndex, JoinFilter}; +use crate::joins::utils::{ColumnIndex, JoinFilter, JoinOn, JoinOnRef}; use crate::{ColumnStatistics, DisplayFormatType, ExecutionPlan, PhysicalExpr}; use arrow::datatypes::{Field, Schema, SchemaRef}; @@ -446,11 +446,6 @@ pub fn try_embed_projection( } } -/// The on clause of the join, as vector of (left, right) columns. -pub type JoinOn = Vec<(PhysicalExprRef, PhysicalExprRef)>; -/// Reference for JoinOn. -pub type JoinOnRef<'a> = &'a [(PhysicalExprRef, PhysicalExprRef)]; - pub struct JoinData { pub projected_left_child: ProjectionExec, pub projected_right_child: ProjectionExec, diff --git a/datafusion/physical-plan/src/repartition/mod.rs b/datafusion/physical-plan/src/repartition/mod.rs index ebc751201378b..c480fc2abaa1a 100644 --- a/datafusion/physical-plan/src/repartition/mod.rs +++ b/datafusion/physical-plan/src/repartition/mod.rs @@ -43,6 +43,7 @@ use crate::{DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties, Stat use arrow::array::{PrimitiveArray, RecordBatch, RecordBatchOptions}; use arrow::compute::take_arrays; use arrow::datatypes::{SchemaRef, UInt32Type}; +use datafusion_common::config::ConfigOptions; use datafusion_common::utils::transpose; use datafusion_common::HashMap; use datafusion_common::{not_impl_err, DataFusionError, Result}; @@ -52,6 +53,9 @@ use datafusion_execution::TaskContext; use datafusion_physical_expr::{EquivalenceProperties, PhysicalExpr}; use datafusion_physical_expr_common::sort_expr::LexOrdering; +use crate::filter_pushdown::{ + filter_pushdown_transparent, FilterDescription, FilterPushdownResult, +}; use futures::stream::Stream; use futures::{FutureExt, StreamExt, TryStreamExt}; use log::trace; @@ -508,11 +512,18 @@ impl DisplayAs for RepartitionExec { } DisplayFormatType::TreeRender => { writeln!(f, "partitioning_scheme={}", self.partitioning(),)?; + + let input_partition_count = + self.input.output_partitioning().partition_count(); + let output_partition_count = self.partitioning().partition_count(); + let input_to_output_partition_str = + format!("{} -> {}", input_partition_count, output_partition_count); writeln!( f, - "output_partition_count={}", - self.input.output_partitioning().partition_count() + "partition_count(in->out)={}", + input_to_output_partition_str )?; + if self.preserve_order { writeln!(f, "preserve_order={}", self.preserve_order)?; } @@ -723,6 +734,17 @@ impl ExecutionPlan for RepartitionExec { new_partitioning, )?))) } + + fn try_pushdown_filters( + &self, + fd: FilterDescription, + _config: &ConfigOptions, + ) -> Result>> { + Ok(filter_pushdown_transparent::>( + Arc::new(self.clone()), + fd, + )) + } } impl RepartitionExec { diff --git a/datafusion/physical-plan/src/sorts/cursor.rs b/datafusion/physical-plan/src/sorts/cursor.rs index 3d3bd81948e03..efb9c0a47bf58 100644 --- a/datafusion/physical-plan/src/sorts/cursor.rs +++ b/datafusion/physical-plan/src/sorts/cursor.rs @@ -284,7 +284,7 @@ impl CursorArray for GenericByteArray { impl CursorArray for StringViewArray { type Values = StringViewArray; fn values(&self) -> Self { - self.clone() + self.gc() } } diff --git a/datafusion/physical-plan/src/sorts/merge.rs b/datafusion/physical-plan/src/sorts/merge.rs index 1c2b8cd0c91b7..2b42457635f7b 100644 --- a/datafusion/physical-plan/src/sorts/merge.rs +++ b/datafusion/physical-plan/src/sorts/merge.rs @@ -217,9 +217,8 @@ impl SortPreservingMergeStream { // we skip the following block. Until then, this function may be called multiple // times and can return Poll::Pending if any partition returns Poll::Pending. if self.loser_tree.is_empty() { - let remaining_partitions = self.uninitiated_partitions.clone(); - for i in remaining_partitions { - match self.maybe_poll_stream(cx, i) { + while let Some(&partition_idx) = self.uninitiated_partitions.front() { + match self.maybe_poll_stream(cx, partition_idx) { Poll::Ready(Err(e)) => { self.aborted = true; return Poll::Ready(Some(Err(e))); @@ -228,10 +227,8 @@ impl SortPreservingMergeStream { // If a partition returns Poll::Pending, to avoid continuously polling it // and potentially increasing upstream buffer sizes, we move it to the // back of the polling queue. - if let Some(front) = self.uninitiated_partitions.pop_front() { - // This pop_front can never return `None`. - self.uninitiated_partitions.push_back(front); - } + self.uninitiated_partitions.rotate_left(1); + // This function could remain in a pending state, so we manually wake it here. // However, this approach can be investigated further to find a more natural way // to avoid disrupting the runtime scheduler. @@ -241,10 +238,13 @@ impl SortPreservingMergeStream { _ => { // If the polling result is Poll::Ready(Some(batch)) or Poll::Ready(None), // we remove this partition from the queue so it is not polled again. - self.uninitiated_partitions.retain(|idx| *idx != i); + self.uninitiated_partitions.pop_front(); } } } + + // Claim the memory for the uninitiated partitions + self.uninitiated_partitions.shrink_to_fit(); self.init_loser_tree(); } diff --git a/datafusion/physical-plan/src/sorts/sort.rs b/datafusion/physical-plan/src/sorts/sort.rs index ed35492041be0..9d0f34cc7f0fd 100644 --- a/datafusion/physical-plan/src/sorts/sort.rs +++ b/datafusion/physical-plan/src/sorts/sort.rs @@ -49,8 +49,10 @@ use arrow::array::{ }; use arrow::compute::{concat_batches, lexsort_to_indices, take_arrays, SortColumn}; use arrow::datatypes::{DataType, SchemaRef}; -use arrow::row::{RowConverter, SortField}; -use datafusion_common::{internal_datafusion_err, internal_err, Result}; +use arrow::row::{RowConverter, Rows, SortField}; +use datafusion_common::{ + exec_datafusion_err, internal_datafusion_err, internal_err, DataFusionError, Result, +}; use datafusion_execution::disk_manager::RefCountedTempFile; use datafusion_execution::memory_pool::{MemoryConsumer, MemoryReservation}; use datafusion_execution::runtime_env::RuntimeEnv; @@ -87,8 +89,9 @@ impl ExternalSorterMetrics { /// 1. get a non-empty new batch from input /// /// 2. check with the memory manager there is sufficient space to -/// buffer the batch in memory 2.1 if memory sufficient, buffer -/// batch in memory, go to 1. +/// buffer the batch in memory. +/// +/// 2.1 if memory is sufficient, buffer batch in memory, go to 1. /// /// 2.2 if no more memory is available, sort all buffered batches and /// spill to file. buffer the next batch in memory, go to 1. @@ -203,8 +206,8 @@ struct ExternalSorter { schema: SchemaRef, /// Sort expressions expr: Arc<[PhysicalSortExpr]>, - /// If Some, the maximum number of output rows that will be produced - fetch: Option, + /// RowConverter corresponding to the sort expressions + sort_keys_row_converter: Arc, /// The target number of rows for output batches batch_size: usize, /// If the in size of buffered memory batches is below this size, @@ -216,10 +219,8 @@ struct ExternalSorter { // STATE BUFFERS: // Fields that hold intermediate data during sorting // ======================================================================== - /// Potentially unsorted in memory buffer + /// Unsorted input batches stored in the memory buffer in_mem_batches: Vec, - /// if `Self::in_mem_batches` are sorted - in_mem_batches_sorted: bool, /// During external sorting, in-memory intermediate data will be appended to /// this file incrementally. Once finished, this file will be moved to [`Self::finished_spill_files`]. @@ -260,12 +261,11 @@ impl ExternalSorter { schema: SchemaRef, expr: LexOrdering, batch_size: usize, - fetch: Option, sort_spill_reservation_bytes: usize, sort_in_place_threshold_bytes: usize, metrics: &ExecutionPlanMetricsSet, runtime: Arc, - ) -> Self { + ) -> Result { let metrics = ExternalSorterMetrics::new(metrics, partition_id); let reservation = MemoryConsumer::new(format!("ExternalSorter[{partition_id}]")) .with_can_spill(true) @@ -275,21 +275,36 @@ impl ExternalSorter { MemoryConsumer::new(format!("ExternalSorterMerge[{partition_id}]")) .register(&runtime.memory_pool); + // Construct RowConverter for sort keys + let sort_fields = expr + .iter() + .map(|e| { + let data_type = e + .expr + .data_type(&schema) + .map_err(|e| e.context("Resolving sort expression data type"))?; + Ok(SortField::new_with_options(data_type, e.options)) + }) + .collect::>>()?; + + let converter = RowConverter::new(sort_fields).map_err(|e| { + exec_datafusion_err!("Failed to create RowConverter: {:?}", e) + })?; + let spill_manager = SpillManager::new( Arc::clone(&runtime), metrics.spill_metrics.clone(), Arc::clone(&schema), ); - Self { + Ok(Self { schema, in_mem_batches: vec![], - in_mem_batches_sorted: false, in_progress_spill_file: None, finished_spill_files: vec![], expr: expr.into(), + sort_keys_row_converter: Arc::new(converter), metrics, - fetch, reservation, spill_manager, merge_reservation, @@ -297,7 +312,7 @@ impl ExternalSorter { batch_size, sort_spill_reservation_bytes, sort_in_place_threshold_bytes, - } + }) } /// Appends an unsorted [`RecordBatch`] to `in_mem_batches` @@ -309,18 +324,10 @@ impl ExternalSorter { } self.reserve_memory_for_merge()?; - - let size = get_reserved_byte_for_record_batch(&input); - if self.reservation.try_grow(size).is_err() { - self.sort_or_spill_in_mem_batches(false).await?; - // We've already freed more than half of reserved memory, - // so we can grow the reservation again. There's nothing we can do - // if this try_grow fails. - self.reservation.try_grow(size)?; - } + self.reserve_memory_for_batch_and_maybe_spill(&input) + .await?; self.in_mem_batches.push(input); - self.in_mem_batches_sorted = false; Ok(()) } @@ -350,7 +357,7 @@ impl ExternalSorter { // `in_mem_batches` and the memory limit is almost reached, merging // them with the spilled files at the same time might cause OOM. if !self.in_mem_batches.is_empty() { - self.sort_or_spill_in_mem_batches(true).await?; + self.sort_and_spill_in_mem_batches().await?; } for spill in self.finished_spill_files.drain(..) { @@ -369,7 +376,7 @@ impl ExternalSorter { .with_expressions(expressions.as_ref()) .with_metrics(self.metrics.baseline.clone()) .with_batch_size(self.batch_size) - .with_fetch(self.fetch) + .with_fetch(None) .with_reservation(self.merge_reservation.new_empty()) .build() } else { @@ -397,16 +404,13 @@ impl ExternalSorter { self.metrics.spill_metrics.spill_file_count.value() } - /// When calling, all `in_mem_batches` must be sorted (*), and then all of them will - /// be appended to the in-progress spill file. - /// - /// (*) 'Sorted' here means globally sorted for all buffered batches when the - /// memory limit is reached, instead of partially sorted within the batch. - async fn spill_append(&mut self) -> Result<()> { - assert!(self.in_mem_batches_sorted); - - // we could always get a chance to free some memory as long as we are holding some - if self.in_mem_batches.is_empty() { + /// Appending globally sorted batches to the in-progress spill file, and clears + /// the `globally_sorted_batches` (also its memory reservation) afterwards. + async fn consume_and_spill_append( + &mut self, + globally_sorted_batches: &mut Vec, + ) -> Result<()> { + if globally_sorted_batches.is_empty() { return Ok(()); } @@ -416,21 +420,25 @@ impl ExternalSorter { Some(self.spill_manager.create_in_progress_file("Sorting")?); } - self.organize_stringview_arrays()?; + Self::organize_stringview_arrays(globally_sorted_batches)?; debug!("Spilling sort data of ExternalSorter to disk whilst inserting"); - let batches = std::mem::take(&mut self.in_mem_batches); + let batches_to_spill = std::mem::take(globally_sorted_batches); self.reservation.free(); let in_progress_file = self.in_progress_spill_file.as_mut().ok_or_else(|| { internal_datafusion_err!("In-progress spill file should be initialized") })?; - for batch in batches { + for batch in batches_to_spill { in_progress_file.append_batch(&batch)?; } + if !globally_sorted_batches.is_empty() { + return internal_err!("This function consumes globally_sorted_batches, so it should be empty after taking."); + } + Ok(()) } @@ -449,7 +457,7 @@ impl ExternalSorter { Ok(()) } - /// Reconstruct `self.in_mem_batches` to organize the payload buffers of each + /// Reconstruct `globally_sorted_batches` to organize the payload buffers of each /// `StringViewArray` in sequential order by calling `gc()` on them. /// /// Note this is a workaround until is @@ -478,10 +486,12 @@ impl ExternalSorter { /// /// Then when spilling each batch, the writer has to write all referenced buffers /// repeatedly. - fn organize_stringview_arrays(&mut self) -> Result<()> { - let mut organized_batches = Vec::with_capacity(self.in_mem_batches.len()); + fn organize_stringview_arrays( + globally_sorted_batches: &mut Vec, + ) -> Result<()> { + let mut organized_batches = Vec::with_capacity(globally_sorted_batches.len()); - for batch in self.in_mem_batches.drain(..) { + for batch in globally_sorted_batches.drain(..) { let mut new_columns: Vec> = Vec::with_capacity(batch.num_columns()); @@ -507,43 +517,40 @@ impl ExternalSorter { organized_batches.push(organized_batch); } - self.in_mem_batches = organized_batches; + *globally_sorted_batches = organized_batches; Ok(()) } - /// Sorts the in_mem_batches in place - /// - /// Sorting may have freed memory, especially if fetch is `Some`. If - /// the memory usage has dropped by a factor of 2, then we don't have - /// to spill. Otherwise, we spill to free up memory for inserting - /// more batches. - /// The factor of 2 aims to avoid a degenerate case where the - /// memory required for `fetch` is just under the memory available, - /// causing repeated re-sorting of data - /// - /// # Arguments - /// - /// * `force_spill` - If true, the method will spill the in-memory batches - /// even if the memory usage has not dropped by a factor of 2. Otherwise it will - /// only spill when the memory usage has dropped by the pre-defined factor. - /// - async fn sort_or_spill_in_mem_batches(&mut self, force_spill: bool) -> Result<()> { + /// Sorts the in-memory batches and merges them into a single sorted run, then writes + /// the result to spill files. + async fn sort_and_spill_in_mem_batches(&mut self) -> Result<()> { + if self.in_mem_batches.is_empty() { + return internal_err!( + "in_mem_batches must not be empty when attempting to sort and spill" + ); + } + // Release the memory reserved for merge back to the pool so // there is some left when `in_mem_sort_stream` requests an // allocation. At the end of this function, memory will be // reserved again for the next spill. self.merge_reservation.free(); - let before = self.reservation.size(); - let mut sorted_stream = self.in_mem_sort_stream(self.metrics.baseline.intermediate())?; + // After `in_mem_sort_stream()` is constructed, all `in_mem_batches` is taken + // to construct a globally sorted stream. + if !self.in_mem_batches.is_empty() { + return internal_err!( + "in_mem_batches should be empty after constructing sorted stream" + ); + } + // 'global' here refers to all buffered batches when the memory limit is + // reached. This variable will buffer the sorted batches after + // sort-preserving merge and incrementally append to spill files. + let mut globally_sorted_batches: Vec = vec![]; - // `self.in_mem_batches` is already taken away by the sort_stream, now it is empty. - // We'll gradually collect the sorted stream into self.in_mem_batches, or directly - // write sorted batches to disk when the memory is insufficient. - let mut spilled = false; while let Some(batch) = sorted_stream.next().await { let batch = batch?; let sorted_size = get_reserved_byte_for_record_batch(&batch); @@ -551,12 +558,11 @@ impl ExternalSorter { // Although the reservation is not enough, the batch is // already in memory, so it's okay to combine it with previously // sorted batches, and spill together. - self.in_mem_batches.push(batch); - self.spill_append().await?; // reservation is freed in spill() - spilled = true; + globally_sorted_batches.push(batch); + self.consume_and_spill_append(&mut globally_sorted_batches) + .await?; // reservation is freed in spill() } else { - self.in_mem_batches.push(batch); - self.in_mem_batches_sorted = true; + globally_sorted_batches.push(batch); } } @@ -564,18 +570,17 @@ impl ExternalSorter { // upcoming `self.reserve_memory_for_merge()` may fail due to insufficient memory. drop(sorted_stream); - // Sorting may free up some memory especially when fetch is `Some`. If we have - // not freed more than 50% of the memory, then we have to spill to free up more - // memory for inserting more batches. - if (self.reservation.size() > before / 2) || force_spill { - // We have not freed more than 50% of the memory, so we have to spill to - // free up more memory - self.spill_append().await?; - spilled = true; - } - - if spilled { - self.spill_finish().await?; + self.consume_and_spill_append(&mut globally_sorted_batches) + .await?; + self.spill_finish().await?; + + // Sanity check after spilling + let buffers_cleared_property = + self.in_mem_batches.is_empty() && globally_sorted_batches.is_empty(); + if !buffers_cleared_property { + return internal_err!( + "in_mem_batches and globally_sorted_batches should be cleared before" + ); } // Reserve headroom for next sort/merge @@ -675,7 +680,8 @@ impl ExternalSorter { let batch = concat_batches(&self.schema, &self.in_mem_batches)?; self.in_mem_batches.clear(); self.reservation - .try_resize(get_reserved_byte_for_record_batch(&batch))?; + .try_resize(get_reserved_byte_for_record_batch(&batch)) + .map_err(Self::err_with_oom_context)?; let reservation = self.reservation.take(); return self.sort_batch_stream(batch, metrics, reservation); } @@ -700,7 +706,7 @@ impl ExternalSorter { .with_expressions(expressions.as_ref()) .with_metrics(metrics) .with_batch_size(self.batch_size) - .with_fetch(self.fetch) + .with_fetch(None) .with_reservation(self.merge_reservation.new_empty()) .build() } @@ -721,17 +727,30 @@ impl ExternalSorter { ); let schema = batch.schema(); - let fetch = self.fetch; let expressions: LexOrdering = self.expr.iter().cloned().collect(); - let stream = futures::stream::once(futures::future::lazy(move |_| { - let timer = metrics.elapsed_compute().timer(); - let sorted = sort_batch(&batch, &expressions, fetch)?; - timer.done(); + let row_converter = Arc::clone(&self.sort_keys_row_converter); + let stream = futures::stream::once(async move { + let _timer = metrics.elapsed_compute().timer(); + + let sort_columns = expressions + .iter() + .map(|expr| expr.evaluate_to_sort_column(&batch)) + .collect::>>()?; + + let sorted = if is_multi_column_with_lists(&sort_columns) { + // lex_sort_to_indices doesn't support List with more than one column + // https://github.com/apache/arrow-rs/issues/5454 + sort_batch_row_based(&batch, &expressions, row_converter, None)? + } else { + sort_batch(&batch, &expressions, None)? + }; + metrics.record_output(sorted.num_rows()); drop(batch); drop(reservation); Ok(sorted) - })); + }); + Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream))) } @@ -743,12 +762,51 @@ impl ExternalSorter { if self.runtime.disk_manager.tmp_files_enabled() { let size = self.sort_spill_reservation_bytes; if self.merge_reservation.size() != size { - self.merge_reservation.try_resize(size)?; + self.merge_reservation + .try_resize(size) + .map_err(Self::err_with_oom_context)?; } } Ok(()) } + + /// Reserves memory to be able to accommodate the given batch. + /// If memory is scarce, tries to spill current in-memory batches to disk first. + async fn reserve_memory_for_batch_and_maybe_spill( + &mut self, + input: &RecordBatch, + ) -> Result<()> { + let size = get_reserved_byte_for_record_batch(input); + + match self.reservation.try_grow(size) { + Ok(_) => Ok(()), + Err(e) => { + if self.in_mem_batches.is_empty() { + return Err(Self::err_with_oom_context(e)); + } + + // Spill and try again. + self.sort_and_spill_in_mem_batches().await?; + self.reservation + .try_grow(size) + .map_err(Self::err_with_oom_context) + } + } + } + + /// Wraps the error with a context message suggesting settings to tweak. + /// This is meant to be used with DataFusionError::ResourcesExhausted only. + fn err_with_oom_context(e: DataFusionError) -> DataFusionError { + match e { + DataFusionError::ResourcesExhausted(_) => e.context( + "Not enough memory to continue external sort. \ + Consider increasing the memory limit, or decreasing sort_spill_reservation_bytes" + ), + // This is not an OOM error, so just return it as is. + _ => e, + } + } } /// Estimate how much memory is needed to sort a `RecordBatch`. @@ -776,6 +834,45 @@ impl Debug for ExternalSorter { } } +/// Converts rows into a sorted array of indices based on their order. +/// This function returns the indices that represent the sorted order of the rows. +fn rows_to_indices(rows: Rows, limit: Option) -> Result { + let mut sort: Vec<_> = rows.iter().enumerate().collect(); + sort.sort_unstable_by(|(_, a), (_, b)| a.cmp(b)); + + let mut len = rows.num_rows(); + if let Some(limit) = limit { + len = limit.min(len); + } + let indices = + UInt32Array::from_iter_values(sort.iter().take(len).map(|(i, _)| *i as u32)); + Ok(indices) +} + +/// Sorts a `RecordBatch` by converting its sort columns into Arrow Row Format for faster comparison. +fn sort_batch_row_based( + batch: &RecordBatch, + expressions: &LexOrdering, + row_converter: Arc, + fetch: Option, +) -> Result { + let sort_columns = expressions + .iter() + .map(|expr| expr.evaluate_to_sort_column(batch).map(|col| col.values)) + .collect::>>()?; + let rows = row_converter.convert_columns(&sort_columns)?; + let indices = rows_to_indices(rows, fetch)?; + let columns = take_arrays(batch.columns(), &indices, None)?; + + let options = RecordBatchOptions::new().with_row_count(Some(indices.len())); + + Ok(RecordBatch::try_new_with_options( + batch.schema(), + columns, + &options, + )?) +} + pub fn sort_batch( batch: &RecordBatch, expressions: &LexOrdering, @@ -838,7 +935,9 @@ pub(crate) fn lexsort_to_indices_multi_columns( }, ); - // TODO reuse converter and rows, refer to TopK. + // Note: row converter is reused through `sort_batch_row_based()`, this function + // is not used during normal sort execution, but it's kept temporarily because + // it's inside a public interface `sort_batch()`. let converter = RowConverter::new(fields)?; let rows = converter.convert_columns(&columns)?; let mut sort: Vec<_> = rows.iter().enumerate().collect(); @@ -871,6 +970,8 @@ pub struct SortExec { preserve_partitioning: bool, /// Fetch highest/lowest n results fetch: Option, + /// Normalized common sort prefix between the input and the sort expressions (only used with fetch) + common_sort_prefix: LexOrdering, /// Cache holding plan properties like equivalences, output partitioning etc. cache: PlanProperties, } @@ -880,13 +981,15 @@ impl SortExec { /// sorted output partition. pub fn new(expr: LexOrdering, input: Arc) -> Self { let preserve_partitioning = false; - let cache = Self::compute_properties(&input, expr.clone(), preserve_partitioning); + let (cache, sort_prefix) = + Self::compute_properties(&input, expr.clone(), preserve_partitioning); Self { expr, input, metrics_set: ExecutionPlanMetricsSet::new(), preserve_partitioning, fetch: None, + common_sort_prefix: sort_prefix, cache, } } @@ -938,6 +1041,7 @@ impl SortExec { expr: self.expr.clone(), metrics_set: self.metrics_set.clone(), preserve_partitioning: self.preserve_partitioning, + common_sort_prefix: self.common_sort_prefix.clone(), fetch, cache, } @@ -971,19 +1075,21 @@ impl SortExec { } /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc. + /// It also returns the common sort prefix between the input and the sort expressions. fn compute_properties( input: &Arc, sort_exprs: LexOrdering, preserve_partitioning: bool, - ) -> PlanProperties { + ) -> (PlanProperties, LexOrdering) { // Determine execution mode: let requirement = LexRequirement::from(sort_exprs); - let sort_satisfied = input + + let (sort_prefix, sort_satisfied) = input .equivalence_properties() - .ordering_satisfy_requirement(&requirement); + .extract_common_sort_prefix(&requirement); // The emission type depends on whether the input is already sorted: - // - If already sorted, we can emit results in the same way as the input + // - If already fully sorted, we can emit results in the same way as the input // - If not sorted, we must wait until all data is processed to emit results (Final) let emission_type = if sort_satisfied { input.pipeline_behavior() @@ -1019,11 +1125,14 @@ impl SortExec { let output_partitioning = Self::output_partitioning_helper(input, preserve_partitioning); - PlanProperties::new( - eq_properties, - output_partitioning, - emission_type, - boundedness, + ( + PlanProperties::new( + eq_properties, + output_partitioning, + emission_type, + boundedness, + ), + LexOrdering::from(sort_prefix), ) } } @@ -1035,7 +1144,12 @@ impl DisplayAs for SortExec { let preserve_partitioning = self.preserve_partitioning; match self.fetch { Some(fetch) => { - write!(f, "SortExec: TopK(fetch={fetch}), expr=[{}], preserve_partitioning=[{preserve_partitioning}]", self.expr) + write!(f, "SortExec: TopK(fetch={fetch}), expr=[{}], preserve_partitioning=[{preserve_partitioning}]", self.expr)?; + if !self.common_sort_prefix.is_empty() { + write!(f, ", sort_prefix=[{}]", self.common_sort_prefix) + } else { + Ok(()) + } } None => write!(f, "SortExec: expr=[{}], preserve_partitioning=[{preserve_partitioning}]", self.expr), } @@ -1055,7 +1169,10 @@ impl DisplayAs for SortExec { impl ExecutionPlan for SortExec { fn name(&self) -> &'static str { - "SortExec" + match self.fetch { + Some(_) => "SortExec(TopK)", + None => "SortExec", + } } fn as_any(&self) -> &dyn Any { @@ -1108,10 +1225,12 @@ impl ExecutionPlan for SortExec { trace!("End SortExec's input.execute for partition: {}", partition); + let requirement = &LexRequirement::from(self.expr.clone()); + let sort_satisfied = self .input .equivalence_properties() - .ordering_satisfy_requirement(&LexRequirement::from(self.expr.clone())); + .ordering_satisfy_requirement(requirement); match (sort_satisfied, self.fetch.as_ref()) { (true, Some(fetch)) => Ok(Box::pin(LimitStream::new( @@ -1125,6 +1244,7 @@ impl ExecutionPlan for SortExec { let mut topk = TopK::try_new( partition, input.schema(), + self.common_sort_prefix.clone(), self.expr.clone(), *fetch, context.session_config().batch_size(), @@ -1137,6 +1257,9 @@ impl ExecutionPlan for SortExec { while let Some(batch) = input.next().await { let batch = batch?; topk.insert_batch(batch)?; + if topk.finished { + break; + } } topk.emit() }) @@ -1149,12 +1272,11 @@ impl ExecutionPlan for SortExec { input.schema(), self.expr.clone(), context.session_config().batch_size(), - self.fetch, execution_options.sort_spill_reservation_bytes, execution_options.sort_in_place_threshold_bytes, &self.metrics_set, context.runtime_env(), - ); + )?; Ok(Box::pin(RecordBatchStreamAdapter::new( self.schema(), futures::stream::once(async move { @@ -1247,7 +1369,7 @@ mod tests { use arrow::datatypes::*; use datafusion_common::cast::as_primitive_array; use datafusion_common::test_util::batches_to_string; - use datafusion_common::{Result, ScalarValue}; + use datafusion_common::{DataFusionError, Result, ScalarValue}; use datafusion_execution::config::SessionConfig; use datafusion_execution::runtime_env::RuntimeEnvBuilder; use datafusion_execution::RecordBatchStream; @@ -1472,6 +1594,69 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_batch_reservation_error() -> Result<()> { + // Pick a memory limit and sort_spill_reservation that make the first batch reservation fail. + // These values assume that the ExternalSorter will reserve 800 bytes for the first batch. + let expected_batch_reservation = 800; + let merge_reservation: usize = 0; // Set to 0 for simplicity + let memory_limit: usize = expected_batch_reservation + merge_reservation - 1; // Just short of what we need + + let session_config = + SessionConfig::new().with_sort_spill_reservation_bytes(merge_reservation); + let runtime = RuntimeEnvBuilder::new() + .with_memory_limit(memory_limit, 1.0) + .build_arc()?; + let task_ctx = Arc::new( + TaskContext::default() + .with_session_config(session_config) + .with_runtime(runtime), + ); + + let plan = test::scan_partitioned(1); + + // Read the first record batch to assert that our memory limit and sort_spill_reservation + // settings trigger the test scenario. + { + let mut stream = plan.execute(0, Arc::clone(&task_ctx))?; + let first_batch = stream.next().await.unwrap()?; + let batch_reservation = get_reserved_byte_for_record_batch(&first_batch); + + assert_eq!(batch_reservation, expected_batch_reservation); + assert!(memory_limit < (merge_reservation + batch_reservation)); + } + + let sort_exec = Arc::new(SortExec::new( + LexOrdering::new(vec![PhysicalSortExpr { + expr: col("i", &plan.schema())?, + options: SortOptions::default(), + }]), + plan, + )); + + let result = collect( + Arc::clone(&sort_exec) as Arc, + Arc::clone(&task_ctx), + ) + .await; + + let err = result.unwrap_err(); + assert!( + matches!(err, DataFusionError::Context(..)), + "Assertion failed: expected a Context error, but got: {:?}", + err + ); + + // Assert that the context error is wrapping a resources exhausted error. + assert!( + matches!(err.find_root(), DataFusionError::ResourcesExhausted(_)), + "Assertion failed: expected a ResourcesExhausted error, but got: {:?}", + err + ); + + Ok(()) + } + #[tokio::test] async fn test_sort_spill_utf8_strings() -> Result<()> { let session_config = SessionConfig::new() diff --git a/datafusion/physical-plan/src/spill/in_progress_spill_file.rs b/datafusion/physical-plan/src/spill/in_progress_spill_file.rs index 8c1ed77559078..7617e0a22a504 100644 --- a/datafusion/physical-plan/src/spill/in_progress_spill_file.rs +++ b/datafusion/physical-plan/src/spill/in_progress_spill_file.rs @@ -49,7 +49,12 @@ impl InProgressSpillFile { } } - /// Appends a `RecordBatch` to the file, initializing the writer if necessary. + /// Appends a `RecordBatch` to the spill file, initializing the writer if necessary. + /// + /// # Errors + /// - Returns an error if the file is not active (has been finalized) + /// - Returns an error if appending would exceed the disk usage limit configured + /// by `max_temp_directory_size` in `DiskManager` pub fn append_batch(&mut self, batch: &RecordBatch) -> Result<()> { if self.in_progress_file.is_none() { return Err(exec_datafusion_err!( @@ -70,6 +75,11 @@ impl InProgressSpillFile { } if let Some(writer) = &mut self.writer { let (spilled_rows, spilled_bytes) = writer.write(batch)?; + if let Some(in_progress_file) = &mut self.in_progress_file { + in_progress_file.update_disk_usage()?; + } else { + unreachable!() // Already checked inside current function + } // Update metrics self.spill_writer.metrics.spilled_bytes.add(spilled_bytes); diff --git a/datafusion/physical-plan/src/spill/mod.rs b/datafusion/physical-plan/src/spill/mod.rs index 88bf7953daeb4..1101616a41060 100644 --- a/datafusion/physical-plan/src/spill/mod.rs +++ b/datafusion/physical-plan/src/spill/mod.rs @@ -23,25 +23,161 @@ pub(crate) mod spill_manager; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; +use std::pin::Pin; use std::ptr::NonNull; +use std::sync::Arc; +use std::task::{Context, Poll}; use arrow::array::ArrayData; use arrow::datatypes::{Schema, SchemaRef}; use arrow::ipc::{reader::StreamReader, writer::StreamWriter}; use arrow::record_batch::RecordBatch; -use tokio::sync::mpsc::Sender; -use datafusion_common::{exec_datafusion_err, HashSet, Result}; +use datafusion_common::{exec_datafusion_err, DataFusionError, HashSet, Result}; +use datafusion_common_runtime::SpawnedTask; +use datafusion_execution::disk_manager::RefCountedTempFile; +use datafusion_execution::RecordBatchStream; +use futures::{FutureExt as _, Stream}; -fn read_spill(sender: Sender>, path: &Path) -> Result<()> { - let file = BufReader::new(File::open(path)?); - let reader = StreamReader::try_new(file, None)?; - for batch in reader { - sender - .blocking_send(batch.map_err(Into::into)) - .map_err(|e| exec_datafusion_err!("{e}"))?; +/// Stream that reads spill files from disk where each batch is read in a spawned blocking task +/// It will read one batch at a time and will not do any buffering, to buffer data use [`crate::common::spawn_buffered`] +/// +/// A simpler solution would be spawning a long-running blocking task for each +/// file read (instead of each batch). This approach does not work because when +/// the number of concurrent reads exceeds the Tokio thread pool limit, +/// deadlocks can occur and block progress. +struct SpillReaderStream { + schema: SchemaRef, + state: SpillReaderStreamState, +} + +/// When we poll for the next batch, we will get back both the batch and the reader, +/// so we can call `next` again. +type NextRecordBatchResult = Result<(StreamReader>, Option)>; + +enum SpillReaderStreamState { + /// Initial state: the stream was not initialized yet + /// and the file was not opened + Uninitialized(RefCountedTempFile), + + /// A read is in progress in a spawned blocking task for which we hold the handle. + ReadInProgress(SpawnedTask), + + /// A read has finished and we wait for being polled again in order to start reading the next batch. + Waiting(StreamReader>), + + /// The stream has finished, successfully or not. + Done, +} + +impl SpillReaderStream { + fn new(schema: SchemaRef, spill_file: RefCountedTempFile) -> Self { + Self { + schema, + state: SpillReaderStreamState::Uninitialized(spill_file), + } + } + + fn poll_next_inner( + &mut self, + cx: &mut Context<'_>, + ) -> Poll>> { + match &mut self.state { + SpillReaderStreamState::Uninitialized(_) => { + // Temporarily replace with `Done` to be able to pass the file to the task. + let SpillReaderStreamState::Uninitialized(spill_file) = + std::mem::replace(&mut self.state, SpillReaderStreamState::Done) + else { + unreachable!() + }; + + let task = SpawnedTask::spawn_blocking(move || { + let file = BufReader::new(File::open(spill_file.path())?); + // SAFETY: DataFusion's spill writer strictly follows Arrow IPC specifications + // with validated schemas and buffers. Skip redundant validation during read + // to speedup read operation. This is safe for DataFusion as input guaranteed to be correct when written. + let mut reader = unsafe { + StreamReader::try_new(file, None)?.with_skip_validation(true) + }; + + let next_batch = reader.next().transpose()?; + + Ok((reader, next_batch)) + }); + + self.state = SpillReaderStreamState::ReadInProgress(task); + + // Poll again immediately so the inner task is polled and the waker is + // registered. + self.poll_next_inner(cx) + } + + SpillReaderStreamState::ReadInProgress(task) => { + let result = futures::ready!(task.poll_unpin(cx)) + .unwrap_or_else(|err| Err(DataFusionError::External(Box::new(err)))); + + match result { + Ok((reader, batch)) => { + match batch { + Some(batch) => { + self.state = SpillReaderStreamState::Waiting(reader); + + Poll::Ready(Some(Ok(batch))) + } + None => { + // Stream is done + self.state = SpillReaderStreamState::Done; + + Poll::Ready(None) + } + } + } + Err(err) => { + self.state = SpillReaderStreamState::Done; + + Poll::Ready(Some(Err(err))) + } + } + } + + SpillReaderStreamState::Waiting(_) => { + // Temporarily replace with `Done` to be able to pass the file to the task. + let SpillReaderStreamState::Waiting(mut reader) = + std::mem::replace(&mut self.state, SpillReaderStreamState::Done) + else { + unreachable!() + }; + + let task = SpawnedTask::spawn_blocking(move || { + let next_batch = reader.next().transpose()?; + + Ok((reader, next_batch)) + }); + + self.state = SpillReaderStreamState::ReadInProgress(task); + + // Poll again immediately so the inner task is polled and the waker is + // registered. + self.poll_next_inner(cx) + } + + SpillReaderStreamState::Done => Poll::Ready(None), + } + } +} + +impl Stream for SpillReaderStream { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.get_mut().poll_next_inner(cx) + } +} + +impl RecordBatchStream for SpillReaderStream { + fn schema(&self) -> SchemaRef { + Arc::clone(&self.schema) } - Ok(()) } /// Spill the `RecordBatch` to disk as smaller batches @@ -202,6 +338,7 @@ mod tests { use arrow::record_batch::RecordBatch; use datafusion_common::Result; use datafusion_execution::runtime_env::RuntimeEnv; + use futures::StreamExt as _; use std::sync::Arc; @@ -601,4 +738,42 @@ mod tests { Ok(()) } + + #[test] + fn test_reading_more_spills_than_tokio_blocking_threads() -> Result<()> { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .max_blocking_threads(1) + .build() + .unwrap() + .block_on(async { + let batch = build_table_i32( + ("a2", &vec![0, 1, 2]), + ("b2", &vec![3, 4, 5]), + ("c2", &vec![4, 5, 6]), + ); + + let schema = batch.schema(); + + // Construct SpillManager + let env = Arc::new(RuntimeEnv::default()); + let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0); + let spill_manager = SpillManager::new(env, metrics, Arc::clone(&schema)); + let batches: [_; 10] = std::array::from_fn(|_| batch.clone()); + + let spill_file_1 = spill_manager + .spill_record_batch_and_finish(&batches, "Test1")? + .unwrap(); + let spill_file_2 = spill_manager + .spill_record_batch_and_finish(&batches, "Test2")? + .unwrap(); + + let mut stream_1 = spill_manager.read_spill_as_stream(spill_file_1)?; + let mut stream_2 = spill_manager.read_spill_as_stream(spill_file_2)?; + stream_1.next().await; + stream_2.next().await; + + Ok(()) + }) + } } diff --git a/datafusion/physical-plan/src/spill/spill_manager.rs b/datafusion/physical-plan/src/spill/spill_manager.rs index 4a8e293323f02..78cd47a8bad07 100644 --- a/datafusion/physical-plan/src/spill/spill_manager.rs +++ b/datafusion/physical-plan/src/spill/spill_manager.rs @@ -27,10 +27,9 @@ use datafusion_common::Result; use datafusion_execution::disk_manager::RefCountedTempFile; use datafusion_execution::SendableRecordBatchStream; -use crate::metrics::SpillMetrics; -use crate::stream::RecordBatchReceiverStream; +use crate::{common::spawn_buffered, metrics::SpillMetrics}; -use super::{in_progress_spill_file::InProgressSpillFile, read_spill}; +use super::{in_progress_spill_file::InProgressSpillFile, SpillReaderStream}; /// The `SpillManager` is responsible for the following tasks: /// - Reading and writing `RecordBatch`es to raw files based on the provided configurations. @@ -73,7 +72,10 @@ impl SpillManager { /// intended to incrementally write in-memory batches into the same spill file, /// use [`Self::create_in_progress_file`] instead. /// None is returned if no batches are spilled. - #[allow(dead_code)] // TODO: remove after change SMJ to use SpillManager + /// + /// # Errors + /// - Returns an error if spilling would exceed the disk usage limit configured + /// by `max_temp_directory_size` in `DiskManager` pub fn spill_record_batch_and_finish( &self, batches: &[RecordBatch], @@ -90,7 +92,10 @@ impl SpillManager { /// Refer to the documentation for [`Self::spill_record_batch_and_finish`]. This method /// additionally spills the `RecordBatch` into smaller batches, divided by `row_limit`. - #[allow(dead_code)] // TODO: remove after change aggregate to use SpillManager + /// + /// # Errors + /// - Returns an error if spilling would exceed the disk usage limit configured + /// by `max_temp_directory_size` in `DiskManager` pub fn spill_record_batch_by_size( &self, batch: &RecordBatch, @@ -120,14 +125,11 @@ impl SpillManager { &self, spill_file_path: RefCountedTempFile, ) -> Result { - let mut builder = RecordBatchReceiverStream::builder( + let stream = Box::pin(SpillReaderStream::new( Arc::clone(&self.schema), - self.batch_read_buffer_capacity, - ); - let sender = builder.tx(); + spill_file_path, + )); - builder.spawn_blocking(move || read_spill(sender, spill_file_path.path())); - - Ok(builder.build()) + Ok(spawn_buffered(stream, self.batch_read_buffer_capacity)) } } diff --git a/datafusion/physical-plan/src/topk/mod.rs b/datafusion/physical-plan/src/topk/mod.rs index 85de1eefce2e4..0b5780b9143f9 100644 --- a/datafusion/physical-plan/src/topk/mod.rs +++ b/datafusion/physical-plan/src/topk/mod.rs @@ -18,7 +18,7 @@ //! TopK: Combination of Sort / LIMIT use arrow::{ - compute::interleave, + compute::interleave_record_batch, row::{RowConverter, Rows, SortField}, }; use std::mem::size_of; @@ -27,10 +27,10 @@ use std::{cmp::Ordering, collections::BinaryHeap, sync::Arc}; use super::metrics::{BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder}; use crate::spill::get_record_batch_memory_size; use crate::{stream::RecordBatchStreamAdapter, SendableRecordBatchStream}; -use arrow::array::{Array, ArrayRef, RecordBatch}; +use arrow::array::{ArrayRef, RecordBatch}; use arrow::datatypes::SchemaRef; -use datafusion_common::HashMap; use datafusion_common::Result; +use datafusion_common::{internal_datafusion_err, HashMap}; use datafusion_execution::{ memory_pool::{MemoryConsumer, MemoryReservation}, runtime_env::RuntimeEnv, @@ -70,6 +70,25 @@ use datafusion_physical_expr_common::sort_expr::LexOrdering; /// The same answer can be produced by simply keeping track of the top /// K=3 elements, reducing the total amount of required buffer memory. /// +/// # Partial Sort Optimization +/// +/// This implementation additionally optimizes queries where the input is already +/// partially sorted by a common prefix of the requested ordering. Once the top K +/// heap is full, if subsequent rows are guaranteed to be strictly greater (in sort +/// order) on this prefix than the largest row currently stored, the operator +/// safely terminates early. +/// +/// ## Example +/// +/// For input sorted by `(day DESC)`, but not by `timestamp`, a query such as: +/// +/// ```sql +/// SELECT day, timestamp FROM sensor ORDER BY day DESC, timestamp DESC LIMIT 10; +/// ``` +/// +/// can terminate scanning early once sufficient rows from the latest days have been +/// collected, skipping older data. +/// /// # Structure /// /// This operator tracks the top K items using a `TopKHeap`. @@ -90,15 +109,43 @@ pub struct TopK { scratch_rows: Rows, /// stores the top k values and their sort key values, in order heap: TopKHeap, + /// row converter, for common keys between the sort keys and the input ordering + common_sort_prefix_converter: Option, + /// Common sort prefix between the input and the sort expressions to allow early exit optimization + common_sort_prefix: Arc<[PhysicalSortExpr]>, + /// If true, indicates that all rows of subsequent batches are guaranteed + /// to be greater (by byte order, after row conversion) than the top K, + /// which means the top K won't change and the computation can be finished early. + pub(crate) finished: bool, +} + +// Guesstimate for memory allocation: estimated number of bytes used per row in the RowConverter +const ESTIMATED_BYTES_PER_ROW: usize = 20; + +fn build_sort_fields( + ordering: &LexOrdering, + schema: &SchemaRef, +) -> Result> { + ordering + .iter() + .map(|e| { + Ok(SortField::new_with_options( + e.expr.data_type(schema)?, + e.options, + )) + }) + .collect::>() } impl TopK { /// Create a new [`TopK`] that stores the top `k` values, as /// defined by the sort expressions in `expr`. // TODO: make a builder or some other nicer API + #[allow(clippy::too_many_arguments)] pub fn try_new( partition_id: usize, schema: SchemaRef, + common_sort_prefix: LexOrdering, expr: LexOrdering, k: usize, batch_size: usize, @@ -108,35 +155,34 @@ impl TopK { let reservation = MemoryConsumer::new(format!("TopK[{partition_id}]")) .register(&runtime.memory_pool); - let expr: Arc<[PhysicalSortExpr]> = expr.into(); - - let sort_fields: Vec<_> = expr - .iter() - .map(|e| { - Ok(SortField::new_with_options( - e.expr.data_type(&schema)?, - e.options, - )) - }) - .collect::>()?; + let sort_fields: Vec<_> = build_sort_fields(&expr, &schema)?; // TODO there is potential to add special cases for single column sort fields // to improve performance let row_converter = RowConverter::new(sort_fields)?; - let scratch_rows = row_converter.empty_rows( - batch_size, - 20 * batch_size, // guesstimate 20 bytes per row - ); + let scratch_rows = + row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size); + + let prefix_row_converter = if common_sort_prefix.is_empty() { + None + } else { + let input_sort_fields: Vec<_> = + build_sort_fields(&common_sort_prefix, &schema)?; + Some(RowConverter::new(input_sort_fields)?) + }; Ok(Self { schema: Arc::clone(&schema), metrics: TopKMetrics::new(metrics, partition_id), reservation, batch_size, - expr, + expr: Arc::from(expr), row_converter, scratch_rows, - heap: TopKHeap::new(k, batch_size, schema), + heap: TopKHeap::new(k, batch_size), + common_sort_prefix_converter: prefix_row_converter, + common_sort_prefix: Arc::from(common_sort_prefix), + finished: false, }) } @@ -144,7 +190,8 @@ impl TopK { /// the top k seen so far. pub fn insert_batch(&mut self, batch: RecordBatch) -> Result<()> { // Updates on drop - let _timer = self.metrics.baseline.elapsed_compute().timer(); + let baseline = self.metrics.baseline.clone(); + let _timer = baseline.elapsed_compute().timer(); let sort_keys: Vec = self .expr @@ -163,7 +210,7 @@ impl TopK { // TODO make this algorithmically better?: // Idea: filter out rows >= self.heap.max() early (before passing to `RowConverter`) // this avoids some work and also might be better vectorizable. - let mut batch_entry = self.heap.register_batch(batch); + let mut batch_entry = self.heap.register_batch(batch.clone()); for (index, row) in rows.iter().enumerate() { match self.heap.max() { // heap has k items, and the new row is greater than the @@ -183,6 +230,87 @@ impl TopK { // update memory reservation self.reservation.try_resize(self.size())?; + + // flag the topK as finished if we know that all + // subsequent batches are guaranteed to be greater (by byte order, after row conversion) than the top K, + // which means the top K won't change and the computation can be finished early. + self.attempt_early_completion(&batch)?; + + Ok(()) + } + + /// If input ordering shares a common sort prefix with the TopK, and if the TopK's heap is full, + /// check if the computation can be finished early. + /// This is the case if the last row of the current batch is strictly greater than the max row in the heap, + /// comparing only on the shared prefix columns. + fn attempt_early_completion(&mut self, batch: &RecordBatch) -> Result<()> { + // Early exit if the batch is empty as there is no last row to extract from it. + if batch.num_rows() == 0 { + return Ok(()); + } + + // prefix_row_converter is only `Some` if the input ordering has a common prefix with the TopK, + // so early exit if it is `None`. + let Some(prefix_converter) = &self.common_sort_prefix_converter else { + return Ok(()); + }; + + // Early exit if the heap is not full (`heap.max()` only returns `Some` if the heap is full). + let Some(max_topk_row) = self.heap.max() else { + return Ok(()); + }; + + // Evaluate the prefix for the last row of the current batch. + let last_row_idx = batch.num_rows() - 1; + let mut batch_prefix_scratch = + prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW); // 1 row with capacity ESTIMATED_BYTES_PER_ROW + + self.compute_common_sort_prefix(batch, last_row_idx, &mut batch_prefix_scratch)?; + + // Retrieve the max row from the heap. + let store_entry = self + .heap + .store + .get(max_topk_row.batch_id) + .ok_or(internal_datafusion_err!("Invalid batch id in topK heap"))?; + let max_batch = &store_entry.batch; + let mut heap_prefix_scratch = + prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW); // 1 row with capacity ESTIMATED_BYTES_PER_ROW + self.compute_common_sort_prefix( + max_batch, + max_topk_row.index, + &mut heap_prefix_scratch, + )?; + + // If the last row's prefix is strictly greater than the max prefix, mark as finished. + if batch_prefix_scratch.row(0).as_ref() > heap_prefix_scratch.row(0).as_ref() { + self.finished = true; + } + + Ok(()) + } + + // Helper function to compute the prefix for a given batch and row index, storing the result in scratch. + fn compute_common_sort_prefix( + &self, + batch: &RecordBatch, + last_row_idx: usize, + scratch: &mut Rows, + ) -> Result<()> { + let last_row: Vec = self + .common_sort_prefix + .iter() + .map(|expr| { + expr.expr + .evaluate(&batch.slice(last_row_idx, 1))? + .into_array(1) + }) + .collect::>()?; + + self.common_sort_prefix_converter + .as_ref() + .unwrap() + .append(scratch, &last_row)?; Ok(()) } @@ -197,6 +325,9 @@ impl TopK { row_converter: _, scratch_rows: _, mut heap, + common_sort_prefix_converter: _, + common_sort_prefix: _, + finished: _, } = self; let _timer = metrics.baseline.elapsed_compute().timer(); // time updated on drop @@ -271,13 +402,13 @@ struct TopKHeap { } impl TopKHeap { - fn new(k: usize, batch_size: usize, schema: SchemaRef) -> Self { + fn new(k: usize, batch_size: usize) -> Self { assert!(k > 0); Self { k, batch_size, inner: BinaryHeap::new(), - store: RecordBatchStore::new(schema), + store: RecordBatchStore::new(), owned_bytes: 0, } } @@ -354,8 +485,6 @@ impl TopKHeap { /// high, as a single [`RecordBatch`], and a sorted vec of the /// current heap's contents pub fn emit_with_state(&mut self) -> Result<(Option, Vec)> { - let schema = Arc::clone(self.store.schema()); - // generate sorted rows let topk_rows = std::mem::take(&mut self.inner).into_sorted_vec(); @@ -370,30 +499,20 @@ impl TopKHeap { .map(|(i, k)| (i, k.index)) .collect(); - let num_columns = schema.fields().len(); - - // build the output columns one at time, using the - // `interleave` kernel to pick rows from different arrays - let output_columns: Vec<_> = (0..num_columns) - .map(|col| { - let input_arrays: Vec<_> = topk_rows - .iter() - .map(|k| { - let entry = - self.store.get(k.batch_id).expect("invalid stored batch id"); - entry.batch.column(col) as &dyn Array - }) - .collect(); - - // at this point `indices` contains indexes within the - // rows and `input_arrays` contains a reference to the - // relevant Array for that index. `interleave` pulls - // them together into a single new array - Ok(interleave(&input_arrays, &indices)?) + let record_batches: Vec<_> = topk_rows + .iter() + .map(|k| { + let entry = self.store.get(k.batch_id).expect("invalid stored batch id"); + &entry.batch }) - .collect::>()?; + .collect(); + + // At this point `indices` contains indexes within the + // rows and `input_arrays` contains a reference to the + // relevant RecordBatch for that index. `interleave_record_batch` pulls + // them together into a single new batch + let new_batch = interleave_record_batch(&record_batches, &indices)?; - let new_batch = RecordBatch::try_new(schema, output_columns)?; Ok((Some(new_batch), topk_rows)) } @@ -548,17 +667,14 @@ struct RecordBatchStore { batches: HashMap, /// total size of all record batches tracked by this store batches_size: usize, - /// schema of the batches - schema: SchemaRef, } impl RecordBatchStore { - fn new(schema: SchemaRef) -> Self { + fn new() -> Self { Self { next_id: 0, batches: HashMap::new(), batches_size: 0, - schema, } } @@ -609,11 +725,6 @@ impl RecordBatchStore { self.batches.is_empty() } - /// return the schema of batches stored - fn schema(&self) -> &SchemaRef { - &self.schema - } - /// remove a use from the specified batch id. If the use count /// reaches zero the batch entry is removed from the store /// @@ -649,6 +760,10 @@ mod tests { use super::*; use arrow::array::{Float64Array, Int32Array, RecordBatch}; use arrow::datatypes::{DataType, Field, Schema}; + use arrow_schema::SortOptions; + use datafusion_common::assert_batches_eq; + use datafusion_physical_expr::expressions::col; + use futures::TryStreamExt; /// This test ensures the size calculation is correct for RecordBatches with multiple columns. #[test] @@ -658,7 +773,7 @@ mod tests { Field::new("ints", DataType::Int32, true), Field::new("float64", DataType::Float64, false), ])); - let mut record_batch_store = RecordBatchStore::new(Arc::clone(&schema)); + let mut record_batch_store = RecordBatchStore::new(); let int_array = Int32Array::from(vec![Some(1), Some(2), Some(3), Some(4), Some(5)]); // 5 * 4 = 20 let float64_array = Float64Array::from(vec![1.0, 2.0, 3.0, 4.0, 5.0]); // 5 * 8 = 40 @@ -681,4 +796,98 @@ mod tests { record_batch_store.unuse(0); assert_eq!(record_batch_store.batches_size, 0); } + + /// This test validates that the `try_finish` method marks the TopK operator as finished + /// when the prefix (on column "a") of the last row in the current batch is strictly greater + /// than the max top‑k row. + /// The full sort expression is defined on both columns ("a", "b"), but the input ordering is only on "a". + #[tokio::test] + async fn test_try_finish_marks_finished_with_prefix() -> Result<()> { + // Create a schema with two columns. + let schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Float64, false), + ])); + + // Create sort expressions. + // Full sort: first by "a", then by "b". + let sort_expr_a = PhysicalSortExpr { + expr: col("a", schema.as_ref())?, + options: SortOptions::default(), + }; + let sort_expr_b = PhysicalSortExpr { + expr: col("b", schema.as_ref())?, + options: SortOptions::default(), + }; + + // Input ordering uses only column "a" (a prefix of the full sort). + let input_ordering = LexOrdering::from(vec![sort_expr_a.clone()]); + let full_expr = LexOrdering::from(vec![sort_expr_a, sort_expr_b]); + + // Create a dummy runtime environment and metrics. + let runtime = Arc::new(RuntimeEnv::default()); + let metrics = ExecutionPlanMetricsSet::new(); + + // Create a TopK instance with k = 3 and batch_size = 2. + let mut topk = TopK::try_new( + 0, + Arc::clone(&schema), + input_ordering, + full_expr, + 3, + 2, + runtime, + &metrics, + )?; + + // Create the first batch with two columns: + // Column "a": [1, 1, 2], Column "b": [20.0, 15.0, 30.0]. + let array_a1: ArrayRef = + Arc::new(Int32Array::from(vec![Some(1), Some(1), Some(2)])); + let array_b1: ArrayRef = Arc::new(Float64Array::from(vec![20.0, 15.0, 30.0])); + let batch1 = RecordBatch::try_new(Arc::clone(&schema), vec![array_a1, array_b1])?; + + // Insert the first batch. + // At this point the heap is not yet “finished” because the prefix of the last row of the batch + // is not strictly greater than the prefix of the max top‑k row (both being `2`). + topk.insert_batch(batch1)?; + assert!( + !topk.finished, + "Expected 'finished' to be false after the first batch." + ); + + // Create the second batch with two columns: + // Column "a": [2, 3], Column "b": [10.0, 20.0]. + let array_a2: ArrayRef = Arc::new(Int32Array::from(vec![Some(2), Some(3)])); + let array_b2: ArrayRef = Arc::new(Float64Array::from(vec![10.0, 20.0])); + let batch2 = RecordBatch::try_new(Arc::clone(&schema), vec![array_a2, array_b2])?; + + // Insert the second batch. + // The last row in this batch has a prefix value of `3`, + // which is strictly greater than the max top‑k row (with value `2`), + // so try_finish should mark the TopK as finished. + topk.insert_batch(batch2)?; + assert!( + topk.finished, + "Expected 'finished' to be true after the second batch." + ); + + // Verify the TopK correctly emits the top k rows from both batches + // (the value 10.0 for b is from the second batch). + let results: Vec<_> = topk.emit()?.try_collect().await?; + assert_batches_eq!( + &[ + "+---+------+", + "| a | b |", + "+---+------+", + "| 1 | 15.0 |", + "| 1 | 20.0 |", + "| 2 | 10.0 |", + "+---+------+", + ], + &results + ); + + Ok(()) + } } diff --git a/datafusion/proto-common/proto/datafusion_common.proto b/datafusion/proto-common/proto/datafusion_common.proto index bbeea5e1ec237..82f1e91d9c9b4 100644 --- a/datafusion/proto-common/proto/datafusion_common.proto +++ b/datafusion/proto-common/proto/datafusion_common.proto @@ -545,6 +545,10 @@ message ParquetOptions { uint64 max_row_group_size = 15; string created_by = 16; + + oneof coerce_int96_opt { + string coerce_int96 = 32; + } } enum JoinSide { diff --git a/datafusion/proto-common/src/common.rs b/datafusion/proto-common/src/common.rs index 61711dcf8e088..9af63e3b07365 100644 --- a/datafusion/proto-common/src/common.rs +++ b/datafusion/proto-common/src/common.rs @@ -17,6 +17,7 @@ use datafusion_common::{internal_datafusion_err, DataFusionError}; +/// Return a `DataFusionError::Internal` with the given message pub fn proto_error>(message: S) -> DataFusionError { internal_datafusion_err!("{}", message.into()) } diff --git a/datafusion/proto-common/src/from_proto/mod.rs b/datafusion/proto-common/src/from_proto/mod.rs index da43a97899565..bd969db316872 100644 --- a/datafusion/proto-common/src/from_proto/mod.rs +++ b/datafusion/proto-common/src/from_proto/mod.rs @@ -984,6 +984,9 @@ impl TryFrom<&protobuf::ParquetOptions> for ParquetOptions { maximum_buffered_record_batches_per_stream: value.maximum_buffered_record_batches_per_stream as usize, schema_force_view_types: value.schema_force_view_types, binary_as_string: value.binary_as_string, + coerce_int96: value.coerce_int96_opt.clone().map(|opt| match opt { + protobuf::parquet_options::CoerceInt96Opt::CoerceInt96(v) => Some(v), + }).unwrap_or(None), skip_arrow_metadata: value.skip_arrow_metadata, }) } diff --git a/datafusion/proto-common/src/generated/pbjson.rs b/datafusion/proto-common/src/generated/pbjson.rs index b0241fd47a26f..b44b05e9ca296 100644 --- a/datafusion/proto-common/src/generated/pbjson.rs +++ b/datafusion/proto-common/src/generated/pbjson.rs @@ -4981,6 +4981,9 @@ impl serde::Serialize for ParquetOptions { if self.bloom_filter_ndv_opt.is_some() { len += 1; } + if self.coerce_int96_opt.is_some() { + len += 1; + } let mut struct_ser = serializer.serialize_struct("datafusion_common.ParquetOptions", len)?; if self.enable_page_index { struct_ser.serialize_field("enablePageIndex", &self.enable_page_index)?; @@ -5136,6 +5139,13 @@ impl serde::Serialize for ParquetOptions { } } } + if let Some(v) = self.coerce_int96_opt.as_ref() { + match v { + parquet_options::CoerceInt96Opt::CoerceInt96(v) => { + struct_ser.serialize_field("coerceInt96", v)?; + } + } + } struct_ser.end() } } @@ -5203,6 +5213,8 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { "bloomFilterFpp", "bloom_filter_ndv", "bloomFilterNdv", + "coerce_int96", + "coerceInt96", ]; #[allow(clippy::enum_variant_names)] @@ -5237,6 +5249,7 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { Encoding, BloomFilterFpp, BloomFilterNdv, + CoerceInt96, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -5288,6 +5301,7 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { "encoding" => Ok(GeneratedField::Encoding), "bloomFilterFpp" | "bloom_filter_fpp" => Ok(GeneratedField::BloomFilterFpp), "bloomFilterNdv" | "bloom_filter_ndv" => Ok(GeneratedField::BloomFilterNdv), + "coerceInt96" | "coerce_int96" => Ok(GeneratedField::CoerceInt96), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -5337,6 +5351,7 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { let mut encoding_opt__ = None; let mut bloom_filter_fpp_opt__ = None; let mut bloom_filter_ndv_opt__ = None; + let mut coerce_int96_opt__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::EnablePageIndex => { @@ -5533,6 +5548,12 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { } bloom_filter_ndv_opt__ = map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| parquet_options::BloomFilterNdvOpt::BloomFilterNdv(x.0)); } + GeneratedField::CoerceInt96 => { + if coerce_int96_opt__.is_some() { + return Err(serde::de::Error::duplicate_field("coerceInt96")); + } + coerce_int96_opt__ = map_.next_value::<::std::option::Option<_>>()?.map(parquet_options::CoerceInt96Opt::CoerceInt96); + } } } Ok(ParquetOptions { @@ -5566,6 +5587,7 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { encoding_opt: encoding_opt__, bloom_filter_fpp_opt: bloom_filter_fpp_opt__, bloom_filter_ndv_opt: bloom_filter_ndv_opt__, + coerce_int96_opt: coerce_int96_opt__, }) } } diff --git a/datafusion/proto-common/src/generated/prost.rs b/datafusion/proto-common/src/generated/prost.rs index b6e9bc1379832..e029327d481d1 100644 --- a/datafusion/proto-common/src/generated/prost.rs +++ b/datafusion/proto-common/src/generated/prost.rs @@ -804,6 +804,8 @@ pub struct ParquetOptions { pub bloom_filter_fpp_opt: ::core::option::Option, #[prost(oneof = "parquet_options::BloomFilterNdvOpt", tags = "22")] pub bloom_filter_ndv_opt: ::core::option::Option, + #[prost(oneof = "parquet_options::CoerceInt96Opt", tags = "32")] + pub coerce_int96_opt: ::core::option::Option, } /// Nested message and enum types in `ParquetOptions`. pub mod parquet_options { @@ -857,6 +859,11 @@ pub mod parquet_options { #[prost(uint64, tag = "22")] BloomFilterNdv(u64), } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum CoerceInt96Opt { + #[prost(string, tag = "32")] + CoerceInt96(::prost::alloc::string::String), + } } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Precision { diff --git a/datafusion/proto-common/src/to_proto/mod.rs b/datafusion/proto-common/src/to_proto/mod.rs index decd0cf630388..28927cad03b4c 100644 --- a/datafusion/proto-common/src/to_proto/mod.rs +++ b/datafusion/proto-common/src/to_proto/mod.rs @@ -836,6 +836,7 @@ impl TryFrom<&ParquetOptions> for protobuf::ParquetOptions { schema_force_view_types: value.schema_force_view_types, binary_as_string: value.binary_as_string, skip_arrow_metadata: value.skip_arrow_metadata, + coerce_int96_opt: value.coerce_int96.clone().map(protobuf::parquet_options::CoerceInt96Opt::CoerceInt96), }) } } diff --git a/datafusion/proto/Cargo.toml b/datafusion/proto/Cargo.toml index 553fccf7d428e..92e697ad2d9c1 100644 --- a/datafusion/proto/Cargo.toml +++ b/datafusion/proto/Cargo.toml @@ -55,7 +55,6 @@ pbjson = { workspace = true, optional = true } prost = { workspace = true } serde = { version = "1.0", optional = true } serde_json = { workspace = true, optional = true } - [dev-dependencies] datafusion-functions = { workspace = true, default-features = true } datafusion-functions-aggregate = { workspace = true } diff --git a/datafusion/proto/proto/datafusion.proto b/datafusion/proto/proto/datafusion.proto index 2e028eb291181..39236da3b9a82 100644 --- a/datafusion/proto/proto/datafusion.proto +++ b/datafusion/proto/proto/datafusion.proto @@ -21,7 +21,7 @@ syntax = "proto3"; package datafusion; option java_multiple_files = true; -option java_package = "org.apache.arrow.datafusion.protobuf"; +option java_package = "org.apache.datafusion.protobuf"; option java_outer_classname = "DatafusionProto"; import "datafusion/proto-common/proto/datafusion_common.proto"; @@ -90,7 +90,7 @@ message ListingTableScanNode { ProjectionColumns projection = 4; datafusion_common.Schema schema = 5; repeated LogicalExprNode filters = 6; - repeated string table_partition_cols = 7; + repeated PartitionColumn table_partition_cols = 7; bool collect_stat = 8; uint32 target_partitions = 9; oneof FileFormatType { diff --git a/datafusion/proto/src/generated/datafusion_proto_common.rs b/datafusion/proto/src/generated/datafusion_proto_common.rs index b6e9bc1379832..e029327d481d1 100644 --- a/datafusion/proto/src/generated/datafusion_proto_common.rs +++ b/datafusion/proto/src/generated/datafusion_proto_common.rs @@ -804,6 +804,8 @@ pub struct ParquetOptions { pub bloom_filter_fpp_opt: ::core::option::Option, #[prost(oneof = "parquet_options::BloomFilterNdvOpt", tags = "22")] pub bloom_filter_ndv_opt: ::core::option::Option, + #[prost(oneof = "parquet_options::CoerceInt96Opt", tags = "32")] + pub coerce_int96_opt: ::core::option::Option, } /// Nested message and enum types in `ParquetOptions`. pub mod parquet_options { @@ -857,6 +859,11 @@ pub mod parquet_options { #[prost(uint64, tag = "22")] BloomFilterNdv(u64), } + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum CoerceInt96Opt { + #[prost(string, tag = "32")] + CoerceInt96(::prost::alloc::string::String), + } } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Precision { diff --git a/datafusion/proto/src/generated/prost.rs b/datafusion/proto/src/generated/prost.rs index d2165dad48501..41c60b22e3bc7 100644 --- a/datafusion/proto/src/generated/prost.rs +++ b/datafusion/proto/src/generated/prost.rs @@ -115,8 +115,8 @@ pub struct ListingTableScanNode { pub schema: ::core::option::Option, #[prost(message, repeated, tag = "6")] pub filters: ::prost::alloc::vec::Vec, - #[prost(string, repeated, tag = "7")] - pub table_partition_cols: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + #[prost(message, repeated, tag = "7")] + pub table_partition_cols: ::prost::alloc::vec::Vec, #[prost(bool, tag = "8")] pub collect_stat: bool, #[prost(uint32, tag = "9")] diff --git a/datafusion/proto/src/logical_plan/file_formats.rs b/datafusion/proto/src/logical_plan/file_formats.rs index e22738973284e..5c33277dc9f74 100644 --- a/datafusion/proto/src/logical_plan/file_formats.rs +++ b/datafusion/proto/src/logical_plan/file_formats.rs @@ -415,6 +415,9 @@ impl TableParquetOptionsProto { schema_force_view_types: global_options.global.schema_force_view_types, binary_as_string: global_options.global.binary_as_string, skip_arrow_metadata: global_options.global.skip_arrow_metadata, + coerce_int96_opt: global_options.global.coerce_int96.map(|compression| { + parquet_options::CoerceInt96Opt::CoerceInt96(compression) + }), }), column_specific_options: column_specific_options.into_iter().map(|(column_name, options)| { ParquetColumnSpecificOptions { @@ -511,6 +514,9 @@ impl From<&ParquetOptionsProto> for ParquetOptions { schema_force_view_types: proto.schema_force_view_types, binary_as_string: proto.binary_as_string, skip_arrow_metadata: proto.skip_arrow_metadata, + coerce_int96: proto.coerce_int96_opt.as_ref().map(|opt| match opt { + parquet_options::CoerceInt96Opt::CoerceInt96(coerce_int96) => coerce_int96.clone(), + }), } } } diff --git a/datafusion/proto/src/logical_plan/mod.rs b/datafusion/proto/src/logical_plan/mod.rs index c65569ef1cfbe..a39e6dac37c10 100644 --- a/datafusion/proto/src/logical_plan/mod.rs +++ b/datafusion/proto/src/logical_plan/mod.rs @@ -33,7 +33,7 @@ use crate::{ }; use crate::protobuf::{proto_error, ToProtoError}; -use arrow::datatypes::{DataType, Schema, SchemaRef}; +use arrow::datatypes::{DataType, Schema, SchemaBuilder, SchemaRef}; use datafusion::datasource::cte_worktable::CteWorkTable; #[cfg(feature = "avro")] use datafusion::datasource::file_format::avro::AvroFormat; @@ -355,10 +355,7 @@ impl AsLogicalPlan for LogicalPlanNode { .as_ref() .map(|expr| from_proto::parse_expr(expr, ctx, extension_codec)) .transpose()? - .ok_or_else(|| { - DataFusionError::Internal("expression required".to_string()) - })?; - // .try_into()?; + .ok_or_else(|| proto_error("expression required"))?; LogicalPlanBuilder::from(input).filter(expr)?.build() } LogicalPlanType::Window(window) => { @@ -458,23 +455,25 @@ impl AsLogicalPlan for LogicalPlanNode { .map(ListingTableUrl::parse) .collect::, _>>()?; + let partition_columns = scan + .table_partition_cols + .iter() + .map(|col| { + let Some(arrow_type) = col.arrow_type.as_ref() else { + return Err(proto_error( + "Missing Arrow type in partition columns", + )); + }; + let arrow_type = DataType::try_from(arrow_type).map_err(|e| { + proto_error(format!("Received an unknown ArrowType: {}", e)) + })?; + Ok((col.name.clone(), arrow_type)) + }) + .collect::>>()?; + let options = ListingOptions::new(file_format) .with_file_extension(&scan.file_extension) - .with_table_partition_cols( - scan.table_partition_cols - .iter() - .map(|col| { - ( - col.clone(), - schema - .field_with_name(col) - .unwrap() - .data_type() - .clone(), - ) - }) - .collect(), - ) + .with_table_partition_cols(partition_columns) .with_collect_stat(scan.collect_stat) .with_target_partitions(scan.target_partitions as usize) .with_file_sort_order(all_sort_orders); @@ -1046,7 +1045,6 @@ impl AsLogicalPlan for LogicalPlanNode { }) } }; - let schema: protobuf::Schema = schema.as_ref().try_into()?; let filters: Vec = serialize_exprs(filters, extension_codec)?; @@ -1099,6 +1097,21 @@ impl AsLogicalPlan for LogicalPlanNode { let options = listing_table.options(); + let mut builder = SchemaBuilder::from(schema.as_ref()); + for (idx, field) in schema.fields().iter().enumerate().rev() { + if options + .table_partition_cols + .iter() + .any(|(name, _)| name == field.name()) + { + builder.remove(idx); + } + } + + let schema = builder.finish(); + + let schema: protobuf::Schema = (&schema).try_into()?; + let mut exprs_vec: Vec = vec![]; for order in &options.file_sort_order { let expr_vec = SortExprNodeCollection { @@ -1107,6 +1120,24 @@ impl AsLogicalPlan for LogicalPlanNode { exprs_vec.push(expr_vec); } + let partition_columns = options + .table_partition_cols + .iter() + .map(|(name, arrow_type)| { + let arrow_type = protobuf::ArrowType::try_from(arrow_type) + .map_err(|e| { + proto_error(format!( + "Received an unknown ArrowType: {}", + e + )) + })?; + Ok(protobuf::PartitionColumn { + name: name.clone(), + arrow_type: Some(arrow_type), + }) + }) + .collect::>>()?; + Ok(LogicalPlanNode { logical_plan_type: Some(LogicalPlanType::ListingScan( protobuf::ListingTableScanNode { @@ -1114,11 +1145,7 @@ impl AsLogicalPlan for LogicalPlanNode { table_name: Some(table_name.clone().into()), collect_stat: options.collect_stat, file_extension: options.file_extension.clone(), - table_partition_cols: options - .table_partition_cols - .iter() - .map(|x| x.0.clone()) - .collect::>(), + table_partition_cols: partition_columns, paths: listing_table .table_paths() .iter() @@ -1133,6 +1160,7 @@ impl AsLogicalPlan for LogicalPlanNode { )), }) } else if let Some(view_table) = source.downcast_ref::() { + let schema: protobuf::Schema = schema.as_ref().try_into()?; Ok(LogicalPlanNode { logical_plan_type: Some(LogicalPlanType::ViewScan(Box::new( protobuf::ViewTableScanNode { @@ -1167,6 +1195,7 @@ impl AsLogicalPlan for LogicalPlanNode { )), }) } else { + let schema: protobuf::Schema = schema.as_ref().try_into()?; let mut bytes = vec![]; extension_codec .try_encode_table_provider(table_name, provider, &mut bytes) diff --git a/datafusion/proto/src/physical_plan/from_proto.rs b/datafusion/proto/src/physical_plan/from_proto.rs index d1141060f9e05..a886fc2425456 100644 --- a/datafusion/proto/src/physical_plan/from_proto.rs +++ b/datafusion/proto/src/physical_plan/from_proto.rs @@ -67,7 +67,7 @@ impl From<&protobuf::PhysicalColumn> for Column { /// * `proto` - Input proto with physical sort expression node /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_sort_expr( proto: &protobuf::PhysicalSortExprNode, @@ -94,7 +94,7 @@ pub fn parse_physical_sort_expr( /// * `proto` - Input proto with vector of physical sort expression node /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_sort_exprs( proto: &[protobuf::PhysicalSortExprNode], @@ -118,7 +118,7 @@ pub fn parse_physical_sort_exprs( /// * `name` - Name of the window expression. /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_window_expr( proto: &protobuf::PhysicalWindowExprNode, @@ -203,7 +203,7 @@ where /// * `proto` - Input proto with physical expression node /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_expr( proto: &protobuf::PhysicalExprNode, @@ -555,7 +555,7 @@ impl TryFrom<&protobuf::PartitionedFile> for PartitionedFile { object_meta: ObjectMeta { location: Path::from(val.path.as_str()), last_modified: Utc.timestamp_nanos(val.last_modified_ns as i64), - size: val.size as usize, + size: val.size, e_tag: None, version: None, }, @@ -565,7 +565,11 @@ impl TryFrom<&protobuf::PartitionedFile> for PartitionedFile { .map(|v| v.try_into()) .collect::, _>>()?, range: val.range.as_ref().map(|v| v.try_into()).transpose()?, - statistics: val.statistics.as_ref().map(|v| v.try_into()).transpose()?, + statistics: val + .statistics + .as_ref() + .map(|v| v.try_into().map(Arc::new)) + .transpose()?, extensions: None, metadata_size_hint: None, }) diff --git a/datafusion/proto/src/physical_plan/mod.rs b/datafusion/proto/src/physical_plan/mod.rs index 24cc0d5b3b028..90d071ab23f56 100644 --- a/datafusion/proto/src/physical_plan/mod.rs +++ b/datafusion/proto/src/physical_plan/mod.rs @@ -127,763 +127,1294 @@ impl AsExecutionPlan for protobuf::PhysicalPlanNode { )) })?; match plan { - PhysicalPlanType::Explain(explain) => Ok(Arc::new(ExplainExec::new( - Arc::new(explain.schema.as_ref().unwrap().try_into()?), - explain - .stringified_plans - .iter() - .map(|plan| plan.into()) - .collect(), - explain.verbose, - ))), - PhysicalPlanType::Projection(projection) => { - let input: Arc = into_physical_plan( - &projection.input, + PhysicalPlanType::Explain(explain) => self.try_into_explain_physical_plan( + explain, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::Projection(projection) => self + .try_into_projection_physical_plan( + projection, registry, runtime, extension_codec, - )?; - let exprs = projection - .expr - .iter() - .zip(projection.expr_name.iter()) - .map(|(expr, name)| { - Ok(( - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - )?, - name.to_string(), - )) - }) - .collect::, String)>>>()?; - Ok(Arc::new(ProjectionExec::try_new(exprs, input)?)) - } - PhysicalPlanType::Filter(filter) => { - let input: Arc = into_physical_plan( - &filter.input, + ), + PhysicalPlanType::Filter(filter) => self.try_into_filter_physical_plan( + filter, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::CsvScan(scan) => self.try_into_csv_scan_physical_plan( + scan, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::JsonScan(scan) => self.try_into_json_scan_physical_plan( + scan, + registry, + runtime, + extension_codec, + ), + #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] + PhysicalPlanType::ParquetScan(scan) => self + .try_into_parquet_scan_physical_plan( + scan, registry, runtime, extension_codec, - )?; - let predicate = filter - .expr - .as_ref() - .map(|expr| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - }) - .transpose()? - .ok_or_else(|| { - DataFusionError::Internal( - "filter (FilterExecNode) in PhysicalPlanNode is missing." - .to_owned(), - ) - })?; - let filter_selectivity = filter.default_filter_selectivity.try_into(); - let projection = if !filter.projection.is_empty() { - Some( - filter - .projection - .iter() - .map(|i| *i as usize) - .collect::>(), - ) - } else { - None - }; - let filter = - FilterExec::try_new(predicate, input)?.with_projection(projection)?; - match filter_selectivity { - Ok(filter_selectivity) => Ok(Arc::new( - filter.with_default_selectivity(filter_selectivity)?, - )), - Err(_) => Err(DataFusionError::Internal( - "filter_selectivity in PhysicalPlanNode is invalid ".to_owned(), - )), - } - } - PhysicalPlanType::CsvScan(scan) => { - let escape = if let Some( - protobuf::csv_scan_exec_node::OptionalEscape::Escape(escape), - ) = &scan.optional_escape - { - Some(str_to_byte(escape, "escape")?) - } else { - None - }; - - let comment = if let Some( - protobuf::csv_scan_exec_node::OptionalComment::Comment(comment), - ) = &scan.optional_comment - { - Some(str_to_byte(comment, "comment")?) - } else { - None - }; - - let source = Arc::new( - CsvSource::new( - scan.has_header, - str_to_byte(&scan.delimiter, "delimiter")?, - 0, - ) - .with_escape(escape) - .with_comment(comment), - ); - - let conf = FileScanConfigBuilder::from(parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), + ), + #[cfg_attr(not(feature = "avro"), allow(unused_variables))] + PhysicalPlanType::AvroScan(scan) => self.try_into_avro_scan_physical_plan( + scan, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::CoalesceBatches(coalesce_batches) => self + .try_into_coalesce_batches_physical_plan( + coalesce_batches, registry, + runtime, extension_codec, - source, - )?) - .with_newlines_in_values(scan.newlines_in_values) - .with_file_compression_type(FileCompressionType::UNCOMPRESSED) - .build(); - Ok(DataSourceExec::from_data_source(conf)) - } - PhysicalPlanType::JsonScan(scan) => { - let scan_conf = parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), + ), + PhysicalPlanType::Merge(merge) => self.try_into_merge_physical_plan( + merge, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::Repartition(repart) => self + .try_into_repartition_physical_plan( + repart, registry, + runtime, extension_codec, - Arc::new(JsonSource::new()), - )?; - Ok(DataSourceExec::from_data_source(scan_conf)) - } - #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] - PhysicalPlanType::ParquetScan(scan) => { - #[cfg(feature = "parquet")] - { - let schema = parse_protobuf_file_scan_schema( - scan.base_conf.as_ref().unwrap(), - )?; - let predicate = scan - .predicate - .as_ref() - .map(|expr| { - parse_physical_expr( - expr, - registry, - schema.as_ref(), - extension_codec, - ) - }) - .transpose()?; - let mut options = TableParquetOptions::default(); - - if let Some(table_options) = scan.parquet_options.as_ref() { - options = table_options.try_into()?; - } - let mut source = ParquetSource::new(options); - - if let Some(predicate) = predicate { - source = source.with_predicate(Arc::clone(&schema), predicate); - } - let base_config = parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), - registry, - extension_codec, - Arc::new(source), - )?; - Ok(DataSourceExec::from_data_source(base_config)) - } - #[cfg(not(feature = "parquet"))] - panic!("Unable to process a Parquet PhysicalPlan when `parquet` feature is not enabled") - } - #[cfg_attr(not(feature = "avro"), allow(unused_variables))] - PhysicalPlanType::AvroScan(scan) => { - #[cfg(feature = "avro")] - { - let conf = parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), - registry, - extension_codec, - Arc::new(AvroSource::new()), - )?; - Ok(DataSourceExec::from_data_source(conf)) - } - #[cfg(not(feature = "avro"))] - panic!("Unable to process a Avro PhysicalPlan when `avro` feature is not enabled") - } - PhysicalPlanType::CoalesceBatches(coalesce_batches) => { - let input: Arc = into_physical_plan( - &coalesce_batches.input, + ), + PhysicalPlanType::GlobalLimit(limit) => self + .try_into_global_limit_physical_plan( + limit, registry, runtime, extension_codec, - )?; - Ok(Arc::new( - CoalesceBatchesExec::new( - input, - coalesce_batches.target_batch_size as usize, - ) - .with_fetch(coalesce_batches.fetch.map(|f| f as usize)), - )) - } - PhysicalPlanType::Merge(merge) => { - let input: Arc = - into_physical_plan(&merge.input, registry, runtime, extension_codec)?; - Ok(Arc::new(CoalescePartitionsExec::new(input))) - } - PhysicalPlanType::Repartition(repart) => { - let input: Arc = into_physical_plan( - &repart.input, + ), + PhysicalPlanType::LocalLimit(limit) => self + .try_into_local_limit_physical_plan( + limit, registry, runtime, extension_codec, - )?; - let partitioning = parse_protobuf_partitioning( - repart.partitioning.as_ref(), + ), + PhysicalPlanType::Window(window_agg) => self.try_into_window_physical_plan( + window_agg, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::Aggregate(hash_agg) => self + .try_into_aggregate_physical_plan( + hash_agg, registry, - input.schema().as_ref(), + runtime, extension_codec, - )?; - Ok(Arc::new(RepartitionExec::try_new( - input, - partitioning.unwrap(), - )?)) - } - PhysicalPlanType::GlobalLimit(limit) => { - let input: Arc = - into_physical_plan(&limit.input, registry, runtime, extension_codec)?; - let fetch = if limit.fetch >= 0 { - Some(limit.fetch as usize) - } else { - None - }; - Ok(Arc::new(GlobalLimitExec::new( - input, - limit.skip as usize, - fetch, - ))) - } - PhysicalPlanType::LocalLimit(limit) => { - let input: Arc = - into_physical_plan(&limit.input, registry, runtime, extension_codec)?; - Ok(Arc::new(LocalLimitExec::new(input, limit.fetch as usize))) - } - PhysicalPlanType::Window(window_agg) => { - let input: Arc = into_physical_plan( - &window_agg.input, + ), + PhysicalPlanType::HashJoin(hashjoin) => self + .try_into_hash_join_physical_plan( + hashjoin, registry, runtime, extension_codec, - )?; - let input_schema = input.schema(); - - let physical_window_expr: Vec> = window_agg - .window_expr - .iter() - .map(|window_expr| { - parse_physical_window_expr( - window_expr, - registry, - input_schema.as_ref(), - extension_codec, - ) - }) - .collect::, _>>()?; - - let partition_keys = window_agg - .partition_keys - .iter() - .map(|expr| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - }) - .collect::>>>()?; - - if let Some(input_order_mode) = window_agg.input_order_mode.as_ref() { - let input_order_mode = match input_order_mode { - window_agg_exec_node::InputOrderMode::Linear(_) => { - InputOrderMode::Linear - } - window_agg_exec_node::InputOrderMode::PartiallySorted( - protobuf::PartiallySortedInputOrderMode { columns }, - ) => InputOrderMode::PartiallySorted( - columns.iter().map(|c| *c as usize).collect(), - ), - window_agg_exec_node::InputOrderMode::Sorted(_) => { - InputOrderMode::Sorted - } - }; - - Ok(Arc::new(BoundedWindowAggExec::try_new( - physical_window_expr, - input, - input_order_mode, - !partition_keys.is_empty(), - )?)) - } else { - Ok(Arc::new(WindowAggExec::try_new( - physical_window_expr, - input, - !partition_keys.is_empty(), - )?)) - } - } - PhysicalPlanType::Aggregate(hash_agg) => { - let input: Arc = into_physical_plan( - &hash_agg.input, + ), + PhysicalPlanType::SymmetricHashJoin(sym_join) => self + .try_into_symmetric_hash_join_physical_plan( + sym_join, registry, runtime, extension_codec, - )?; - let mode = protobuf::AggregateMode::try_from(hash_agg.mode).map_err( - |_| { - proto_error(format!( - "Received a AggregateNode message with unknown AggregateMode {}", - hash_agg.mode - )) - }, - )?; - let agg_mode: AggregateMode = match mode { - protobuf::AggregateMode::Partial => AggregateMode::Partial, - protobuf::AggregateMode::Final => AggregateMode::Final, - protobuf::AggregateMode::FinalPartitioned => { - AggregateMode::FinalPartitioned - } - protobuf::AggregateMode::Single => AggregateMode::Single, - protobuf::AggregateMode::SinglePartitioned => { - AggregateMode::SinglePartitioned - } - }; - - let num_expr = hash_agg.group_expr.len(); - - let group_expr = hash_agg - .group_expr - .iter() - .zip(hash_agg.group_expr_name.iter()) - .map(|(expr, name)| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - .map(|expr| (expr, name.to_string())) - }) - .collect::, _>>()?; - - let null_expr = hash_agg - .null_expr - .iter() - .zip(hash_agg.group_expr_name.iter()) - .map(|(expr, name)| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - .map(|expr| (expr, name.to_string())) - }) - .collect::, _>>()?; - - let groups: Vec> = if !hash_agg.groups.is_empty() { - hash_agg - .groups - .chunks(num_expr) - .map(|g| g.to_vec()) - .collect::>>() - } else { - vec![] - }; - - let input_schema = hash_agg.input_schema.as_ref().ok_or_else(|| { - DataFusionError::Internal( - "input_schema in AggregateNode is missing.".to_owned(), - ) - })?; - let physical_schema: SchemaRef = SchemaRef::new(input_schema.try_into()?); - - let physical_filter_expr = hash_agg - .filter_expr - .iter() - .map(|expr| { - expr.expr - .as_ref() - .map(|e| { - parse_physical_expr( - e, - registry, - &physical_schema, - extension_codec, - ) - }) - .transpose() - }) - .collect::, _>>()?; - - let physical_aggr_expr: Vec> = hash_agg - .aggr_expr - .iter() - .zip(hash_agg.aggr_expr_name.iter()) - .map(|(expr, name)| { - let expr_type = expr.expr_type.as_ref().ok_or_else(|| { - proto_error("Unexpected empty aggregate physical expression") - })?; - - match expr_type { - ExprType::AggregateExpr(agg_node) => { - let input_phy_expr: Vec> = agg_node.expr.iter() - .map(|e| parse_physical_expr(e, registry, &physical_schema, extension_codec)).collect::>>()?; - let ordering_req: LexOrdering = agg_node.ordering_req.iter() - .map(|e| parse_physical_sort_expr(e, registry, &physical_schema, extension_codec)) - .collect::>()?; - agg_node.aggregate_function.as_ref().map(|func| { - match func { - AggregateFunction::UserDefinedAggrFunction(udaf_name) => { - let agg_udf = match &agg_node.fun_definition { - Some(buf) => extension_codec.try_decode_udaf(udaf_name, buf)?, - None => registry.udaf(udaf_name)? - }; - - AggregateExprBuilder::new(agg_udf, input_phy_expr) - .schema(Arc::clone(&physical_schema)) - .alias(name) - .with_ignore_nulls(agg_node.ignore_nulls) - .with_distinct(agg_node.distinct) - .order_by(ordering_req) - .build() - .map(Arc::new) - } - } - }).transpose()?.ok_or_else(|| { - proto_error("Invalid AggregateExpr, missing aggregate_function") - }) - } - _ => internal_err!( - "Invalid aggregate expression for AggregateExec" - ), - } - }) - .collect::, _>>()?; - - let limit = hash_agg - .limit - .as_ref() - .map(|lit_value| lit_value.limit as usize); - - let agg = AggregateExec::try_new( - agg_mode, - PhysicalGroupBy::new(group_expr, null_expr, groups), - physical_aggr_expr, - physical_filter_expr, - input, - physical_schema, - )?; - - let agg = agg.with_limit(limit); - - Ok(Arc::new(agg)) - } - PhysicalPlanType::HashJoin(hashjoin) => { - let left: Arc = into_physical_plan( - &hashjoin.left, + ), + PhysicalPlanType::Union(union) => self.try_into_union_physical_plan( + union, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::Interleave(interleave) => self + .try_into_interleave_physical_plan( + interleave, registry, runtime, extension_codec, - )?; - let right: Arc = into_physical_plan( - &hashjoin.right, + ), + PhysicalPlanType::CrossJoin(crossjoin) => self + .try_into_cross_join_physical_plan( + crossjoin, registry, runtime, extension_codec, - )?; - let left_schema = left.schema(); - let right_schema = right.schema(); - let on: Vec<(PhysicalExprRef, PhysicalExprRef)> = hashjoin - .on - .iter() - .map(|col| { - let left = parse_physical_expr( - &col.left.clone().unwrap(), - registry, - left_schema.as_ref(), - extension_codec, - )?; - let right = parse_physical_expr( - &col.right.clone().unwrap(), - registry, - right_schema.as_ref(), - extension_codec, - )?; - Ok((left, right)) - }) - .collect::>()?; - let join_type = protobuf::JoinType::try_from(hashjoin.join_type) - .map_err(|_| { - proto_error(format!( - "Received a HashJoinNode message with unknown JoinType {}", - hashjoin.join_type - )) - })?; - let filter = hashjoin - .filter - .as_ref() - .map(|f| { - let schema = f - .schema - .as_ref() - .ok_or_else(|| proto_error("Missing JoinFilter schema"))? - .try_into()?; - - let expression = parse_physical_expr( - f.expression.as_ref().ok_or_else(|| { - proto_error("Unexpected empty filter expression") - })?, - registry, &schema, - extension_codec, - )?; - let column_indices = f.column_indices - .iter() - .map(|i| { - let side = protobuf::JoinSide::try_from(i.side) - .map_err(|_| proto_error(format!( - "Received a HashJoinNode message with JoinSide in Filter {}", - i.side)) - )?; - - Ok(ColumnIndex { - index: i.index as usize, - side: side.into(), - }) - }) - .collect::>>()?; - - Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; - - let partition_mode = protobuf::PartitionMode::try_from( - hashjoin.partition_mode, - ) - .map_err(|_| { - proto_error(format!( - "Received a HashJoinNode message with unknown PartitionMode {}", - hashjoin.partition_mode - )) - })?; - let partition_mode = match partition_mode { - protobuf::PartitionMode::CollectLeft => PartitionMode::CollectLeft, - protobuf::PartitionMode::Partitioned => PartitionMode::Partitioned, - protobuf::PartitionMode::Auto => PartitionMode::Auto, - }; - let projection = if !hashjoin.projection.is_empty() { - Some( - hashjoin - .projection - .iter() - .map(|i| *i as usize) - .collect::>(), - ) - } else { - None - }; - Ok(Arc::new(HashJoinExec::try_new( - left, - right, - on, - filter, - &join_type.into(), - projection, - partition_mode, - hashjoin.null_equals_null, - )?)) + ), + PhysicalPlanType::Empty(empty) => self.try_into_empty_physical_plan( + empty, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::PlaceholderRow(placeholder) => self + .try_into_placeholder_row_physical_plan( + placeholder, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::Sort(sort) => { + self.try_into_sort_physical_plan(sort, registry, runtime, extension_codec) } - PhysicalPlanType::SymmetricHashJoin(sym_join) => { - let left = into_physical_plan( - &sym_join.left, + PhysicalPlanType::SortPreservingMerge(sort) => self + .try_into_sort_preserving_merge_physical_plan( + sort, registry, runtime, extension_codec, - )?; - let right = into_physical_plan( - &sym_join.right, + ), + PhysicalPlanType::Extension(extension) => self + .try_into_extension_physical_plan( + extension, registry, runtime, extension_codec, - )?; - let left_schema = left.schema(); - let right_schema = right.schema(); - let on = sym_join - .on - .iter() - .map(|col| { - let left = parse_physical_expr( - &col.left.clone().unwrap(), - registry, - left_schema.as_ref(), - extension_codec, - )?; - let right = parse_physical_expr( - &col.right.clone().unwrap(), - registry, - right_schema.as_ref(), - extension_codec, - )?; - Ok((left, right)) - }) - .collect::>()?; - let join_type = protobuf::JoinType::try_from(sym_join.join_type) - .map_err(|_| { - proto_error(format!( - "Received a SymmetricHashJoin message with unknown JoinType {}", - sym_join.join_type - )) - })?; - let filter = sym_join - .filter - .as_ref() - .map(|f| { - let schema = f - .schema - .as_ref() - .ok_or_else(|| proto_error("Missing JoinFilter schema"))? - .try_into()?; + ), + PhysicalPlanType::NestedLoopJoin(join) => self + .try_into_nested_loop_join_physical_plan( + join, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::Analyze(analyze) => self.try_into_analyze_physical_plan( + analyze, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::JsonSink(sink) => self.try_into_json_sink_physical_plan( + sink, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::CsvSink(sink) => self.try_into_csv_sink_physical_plan( + sink, + registry, + runtime, + extension_codec, + ), - let expression = parse_physical_expr( - f.expression.as_ref().ok_or_else(|| { - proto_error("Unexpected empty filter expression") - })?, - registry, &schema, + #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] + PhysicalPlanType::ParquetSink(sink) => self + .try_into_parquet_sink_physical_plan( + sink, + registry, + runtime, + extension_codec, + ), + PhysicalPlanType::Unnest(unnest) => self.try_into_unnest_physical_plan( + unnest, + registry, + runtime, + extension_codec, + ), + } + } + + fn try_from_physical_plan( + plan: Arc, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result + where + Self: Sized, + { + let plan_clone = Arc::clone(&plan); + let plan = plan.as_any(); + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_explain_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_projection_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_analyze_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_filter_exec( + exec, + extension_codec, + ); + } + + if let Some(limit) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_global_limit_exec( + limit, + extension_codec, + ); + } + + if let Some(limit) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_local_limit_exec( + limit, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_hash_join_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_symmetric_hash_join_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_cross_join_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_aggregate_exec( + exec, + extension_codec, + ); + } + + if let Some(empty) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_empty_exec( + empty, + extension_codec, + ); + } + + if let Some(empty) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_placeholder_row_exec( + empty, + extension_codec, + ); + } + + if let Some(coalesce_batches) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_coalesce_batches_exec( + coalesce_batches, + extension_codec, + ); + } + + if let Some(data_source_exec) = plan.downcast_ref::() { + if let Some(node) = protobuf::PhysicalPlanNode::try_from_data_source_exec( + data_source_exec, + extension_codec, + )? { + return Ok(node); + } + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_coalesce_partitions_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_repartition_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_sort_exec(exec, extension_codec); + } + + if let Some(union) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_union_exec( + union, + extension_codec, + ); + } + + if let Some(interleave) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_interleave_exec( + interleave, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_sort_preserving_merge_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_nested_loop_join_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_window_agg_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_bounded_window_agg_exec( + exec, + extension_codec, + ); + } + + if let Some(exec) = plan.downcast_ref::() { + if let Some(node) = protobuf::PhysicalPlanNode::try_from_data_sink_exec( + exec, + extension_codec, + )? { + return Ok(node); + } + } + + if let Some(exec) = plan.downcast_ref::() { + return protobuf::PhysicalPlanNode::try_from_unnest_exec( + exec, + extension_codec, + ); + } + + let mut buf: Vec = vec![]; + match extension_codec.try_encode(Arc::clone(&plan_clone), &mut buf) { + Ok(_) => { + let inputs: Vec = plan_clone + .children() + .into_iter() + .cloned() + .map(|i| { + protobuf::PhysicalPlanNode::try_from_physical_plan( + i, extension_codec, - )?; - let column_indices = f.column_indices - .iter() - .map(|i| { - let side = protobuf::JoinSide::try_from(i.side) - .map_err(|_| proto_error(format!( - "Received a HashJoinNode message with JoinSide in Filter {}", - i.side)) - )?; + ) + }) + .collect::>()?; - Ok(ColumnIndex { - index: i.index as usize, - side: side.into(), - }) + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Extension( + protobuf::PhysicalExtensionNode { node: buf, inputs }, + )), + }) + } + Err(e) => internal_err!( + "Unsupported plan and extension codec failed with [{e}]. Plan: {plan_clone:?}" + ), + } + } +} + +impl protobuf::PhysicalPlanNode { + fn try_into_explain_physical_plan( + &self, + explain: &protobuf::ExplainExecNode, + _registry: &dyn FunctionRegistry, + _runtime: &RuntimeEnv, + _extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + Ok(Arc::new(ExplainExec::new( + Arc::new(explain.schema.as_ref().unwrap().try_into()?), + explain + .stringified_plans + .iter() + .map(|plan| plan.into()) + .collect(), + explain.verbose, + ))) + } + + fn try_into_projection_physical_plan( + &self, + projection: &protobuf::ProjectionExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&projection.input, registry, runtime, extension_codec)?; + let exprs = projection + .expr + .iter() + .zip(projection.expr_name.iter()) + .map(|(expr, name)| { + Ok(( + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + )?, + name.to_string(), + )) + }) + .collect::, String)>>>()?; + Ok(Arc::new(ProjectionExec::try_new(exprs, input)?)) + } + + fn try_into_filter_physical_plan( + &self, + filter: &protobuf::FilterExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&filter.input, registry, runtime, extension_codec)?; + let predicate = filter + .expr + .as_ref() + .map(|expr| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + }) + .transpose()? + .ok_or_else(|| { + DataFusionError::Internal( + "filter (FilterExecNode) in PhysicalPlanNode is missing.".to_owned(), + ) + })?; + let filter_selectivity = filter.default_filter_selectivity.try_into(); + let projection = if !filter.projection.is_empty() { + Some( + filter + .projection + .iter() + .map(|i| *i as usize) + .collect::>(), + ) + } else { + None + }; + let filter = + FilterExec::try_new(predicate, input)?.with_projection(projection)?; + match filter_selectivity { + Ok(filter_selectivity) => Ok(Arc::new( + filter.with_default_selectivity(filter_selectivity)?, + )), + Err(_) => Err(DataFusionError::Internal( + "filter_selectivity in PhysicalPlanNode is invalid ".to_owned(), + )), + } + } + + fn try_into_csv_scan_physical_plan( + &self, + scan: &protobuf::CsvScanExecNode, + registry: &dyn FunctionRegistry, + _runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let escape = + if let Some(protobuf::csv_scan_exec_node::OptionalEscape::Escape(escape)) = + &scan.optional_escape + { + Some(str_to_byte(escape, "escape")?) + } else { + None + }; + + let comment = if let Some( + protobuf::csv_scan_exec_node::OptionalComment::Comment(comment), + ) = &scan.optional_comment + { + Some(str_to_byte(comment, "comment")?) + } else { + None + }; + + let source = Arc::new( + CsvSource::new( + scan.has_header, + str_to_byte(&scan.delimiter, "delimiter")?, + 0, + ) + .with_escape(escape) + .with_comment(comment), + ); + + let conf = FileScanConfigBuilder::from(parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), + registry, + extension_codec, + source, + )?) + .with_newlines_in_values(scan.newlines_in_values) + .with_file_compression_type(FileCompressionType::UNCOMPRESSED) + .build(); + Ok(DataSourceExec::from_data_source(conf)) + } + + fn try_into_json_scan_physical_plan( + &self, + scan: &protobuf::JsonScanExecNode, + registry: &dyn FunctionRegistry, + _runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let scan_conf = parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), + registry, + extension_codec, + Arc::new(JsonSource::new()), + )?; + Ok(DataSourceExec::from_data_source(scan_conf)) + } + + #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] + fn try_into_parquet_scan_physical_plan( + &self, + scan: &protobuf::ParquetScanExecNode, + registry: &dyn FunctionRegistry, + _runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + #[cfg(feature = "parquet")] + { + let schema = + parse_protobuf_file_scan_schema(scan.base_conf.as_ref().unwrap())?; + let predicate = scan + .predicate + .as_ref() + .map(|expr| { + parse_physical_expr(expr, registry, schema.as_ref(), extension_codec) + }) + .transpose()?; + let mut options = TableParquetOptions::default(); + + if let Some(table_options) = scan.parquet_options.as_ref() { + options = table_options.try_into()?; + } + let mut source = ParquetSource::new(options); + + if let Some(predicate) = predicate { + source = source.with_predicate(Arc::clone(&schema), predicate); + } + let base_config = parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), + registry, + extension_codec, + Arc::new(source), + )?; + Ok(DataSourceExec::from_data_source(base_config)) + } + #[cfg(not(feature = "parquet"))] + panic!("Unable to process a Parquet PhysicalPlan when `parquet` feature is not enabled") + } + + #[cfg_attr(not(feature = "avro"), allow(unused_variables))] + fn try_into_avro_scan_physical_plan( + &self, + scan: &protobuf::AvroScanExecNode, + registry: &dyn FunctionRegistry, + _runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + #[cfg(feature = "avro")] + { + let conf = parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), + registry, + extension_codec, + Arc::new(AvroSource::new()), + )?; + Ok(DataSourceExec::from_data_source(conf)) + } + #[cfg(not(feature = "avro"))] + panic!("Unable to process a Avro PhysicalPlan when `avro` feature is not enabled") + } + + fn try_into_coalesce_batches_physical_plan( + &self, + coalesce_batches: &protobuf::CoalesceBatchesExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = into_physical_plan( + &coalesce_batches.input, + registry, + runtime, + extension_codec, + )?; + Ok(Arc::new( + CoalesceBatchesExec::new(input, coalesce_batches.target_batch_size as usize) + .with_fetch(coalesce_batches.fetch.map(|f| f as usize)), + )) + } + + fn try_into_merge_physical_plan( + &self, + merge: &protobuf::CoalescePartitionsExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&merge.input, registry, runtime, extension_codec)?; + Ok(Arc::new(CoalescePartitionsExec::new(input))) + } + + fn try_into_repartition_physical_plan( + &self, + repart: &protobuf::RepartitionExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&repart.input, registry, runtime, extension_codec)?; + let partitioning = parse_protobuf_partitioning( + repart.partitioning.as_ref(), + registry, + input.schema().as_ref(), + extension_codec, + )?; + Ok(Arc::new(RepartitionExec::try_new( + input, + partitioning.unwrap(), + )?)) + } + + fn try_into_global_limit_physical_plan( + &self, + limit: &protobuf::GlobalLimitExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&limit.input, registry, runtime, extension_codec)?; + let fetch = if limit.fetch >= 0 { + Some(limit.fetch as usize) + } else { + None + }; + Ok(Arc::new(GlobalLimitExec::new( + input, + limit.skip as usize, + fetch, + ))) + } + + fn try_into_local_limit_physical_plan( + &self, + limit: &protobuf::LocalLimitExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&limit.input, registry, runtime, extension_codec)?; + Ok(Arc::new(LocalLimitExec::new(input, limit.fetch as usize))) + } + + fn try_into_window_physical_plan( + &self, + window_agg: &protobuf::WindowAggExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&window_agg.input, registry, runtime, extension_codec)?; + let input_schema = input.schema(); + + let physical_window_expr: Vec> = window_agg + .window_expr + .iter() + .map(|window_expr| { + parse_physical_window_expr( + window_expr, + registry, + input_schema.as_ref(), + extension_codec, + ) + }) + .collect::, _>>()?; + + let partition_keys = window_agg + .partition_keys + .iter() + .map(|expr| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + }) + .collect::>>>()?; + + if let Some(input_order_mode) = window_agg.input_order_mode.as_ref() { + let input_order_mode = match input_order_mode { + window_agg_exec_node::InputOrderMode::Linear(_) => InputOrderMode::Linear, + window_agg_exec_node::InputOrderMode::PartiallySorted( + protobuf::PartiallySortedInputOrderMode { columns }, + ) => InputOrderMode::PartiallySorted( + columns.iter().map(|c| *c as usize).collect(), + ), + window_agg_exec_node::InputOrderMode::Sorted(_) => InputOrderMode::Sorted, + }; + + Ok(Arc::new(BoundedWindowAggExec::try_new( + physical_window_expr, + input, + input_order_mode, + !partition_keys.is_empty(), + )?)) + } else { + Ok(Arc::new(WindowAggExec::try_new( + physical_window_expr, + input, + !partition_keys.is_empty(), + )?)) + } + } + + fn try_into_aggregate_physical_plan( + &self, + hash_agg: &protobuf::AggregateExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&hash_agg.input, registry, runtime, extension_codec)?; + let mode = protobuf::AggregateMode::try_from(hash_agg.mode).map_err(|_| { + proto_error(format!( + "Received a AggregateNode message with unknown AggregateMode {}", + hash_agg.mode + )) + })?; + let agg_mode: AggregateMode = match mode { + protobuf::AggregateMode::Partial => AggregateMode::Partial, + protobuf::AggregateMode::Final => AggregateMode::Final, + protobuf::AggregateMode::FinalPartitioned => AggregateMode::FinalPartitioned, + protobuf::AggregateMode::Single => AggregateMode::Single, + protobuf::AggregateMode::SinglePartitioned => { + AggregateMode::SinglePartitioned + } + }; + + let num_expr = hash_agg.group_expr.len(); + + let group_expr = hash_agg + .group_expr + .iter() + .zip(hash_agg.group_expr_name.iter()) + .map(|(expr, name)| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + .map(|expr| (expr, name.to_string())) + }) + .collect::, _>>()?; + + let null_expr = hash_agg + .null_expr + .iter() + .zip(hash_agg.group_expr_name.iter()) + .map(|(expr, name)| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + .map(|expr| (expr, name.to_string())) + }) + .collect::, _>>()?; + + let groups: Vec> = if !hash_agg.groups.is_empty() { + hash_agg + .groups + .chunks(num_expr) + .map(|g| g.to_vec()) + .collect::>>() + } else { + vec![] + }; + + let input_schema = hash_agg.input_schema.as_ref().ok_or_else(|| { + DataFusionError::Internal( + "input_schema in AggregateNode is missing.".to_owned(), + ) + })?; + let physical_schema: SchemaRef = SchemaRef::new(input_schema.try_into()?); + + let physical_filter_expr = hash_agg + .filter_expr + .iter() + .map(|expr| { + expr.expr + .as_ref() + .map(|e| { + parse_physical_expr( + e, + registry, + &physical_schema, + extension_codec, + ) + }) + .transpose() + }) + .collect::, _>>()?; + + let physical_aggr_expr: Vec> = hash_agg + .aggr_expr + .iter() + .zip(hash_agg.aggr_expr_name.iter()) + .map(|(expr, name)| { + let expr_type = expr.expr_type.as_ref().ok_or_else(|| { + proto_error("Unexpected empty aggregate physical expression") + })?; + + match expr_type { + ExprType::AggregateExpr(agg_node) => { + let input_phy_expr: Vec> = agg_node + .expr + .iter() + .map(|e| { + parse_physical_expr( + e, + registry, + &physical_schema, + extension_codec, + ) }) - .collect::>()?; - - Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; + .collect::>>()?; + let ordering_req: LexOrdering = agg_node + .ordering_req + .iter() + .map(|e| { + parse_physical_sort_expr( + e, + registry, + &physical_schema, + extension_codec, + ) + }) + .collect::>()?; + agg_node + .aggregate_function + .as_ref() + .map(|func| match func { + AggregateFunction::UserDefinedAggrFunction(udaf_name) => { + let agg_udf = match &agg_node.fun_definition { + Some(buf) => extension_codec + .try_decode_udaf(udaf_name, buf)?, + None => registry.udaf(udaf_name)?, + }; + + AggregateExprBuilder::new(agg_udf, input_phy_expr) + .schema(Arc::clone(&physical_schema)) + .alias(name) + .with_ignore_nulls(agg_node.ignore_nulls) + .with_distinct(agg_node.distinct) + .order_by(ordering_req) + .build() + .map(Arc::new) + } + }) + .transpose()? + .ok_or_else(|| { + proto_error( + "Invalid AggregateExpr, missing aggregate_function", + ) + }) + } + _ => internal_err!("Invalid aggregate expression for AggregateExec"), + } + }) + .collect::, _>>()?; + + let limit = hash_agg + .limit + .as_ref() + .map(|lit_value| lit_value.limit as usize); + + let agg = AggregateExec::try_new( + agg_mode, + PhysicalGroupBy::new(group_expr, null_expr, groups), + physical_aggr_expr, + physical_filter_expr, + input, + physical_schema, + )?; + + let agg = agg.with_limit(limit); + + Ok(Arc::new(agg)) + } - let left_sort_exprs = parse_physical_sort_exprs( - &sym_join.left_sort_exprs, + fn try_into_hash_join_physical_plan( + &self, + hashjoin: &protobuf::HashJoinExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let left: Arc = + into_physical_plan(&hashjoin.left, registry, runtime, extension_codec)?; + let right: Arc = + into_physical_plan(&hashjoin.right, registry, runtime, extension_codec)?; + let left_schema = left.schema(); + let right_schema = right.schema(); + let on: Vec<(PhysicalExprRef, PhysicalExprRef)> = hashjoin + .on + .iter() + .map(|col| { + let left = parse_physical_expr( + &col.left.clone().unwrap(), registry, - &left_schema, + left_schema.as_ref(), extension_codec, )?; - let left_sort_exprs = if left_sort_exprs.is_empty() { - None - } else { - Some(left_sort_exprs) - }; - - let right_sort_exprs = parse_physical_sort_exprs( - &sym_join.right_sort_exprs, + let right = parse_physical_expr( + &col.right.clone().unwrap(), registry, - &right_schema, + right_schema.as_ref(), extension_codec, )?; - let right_sort_exprs = if right_sort_exprs.is_empty() { - None - } else { - Some(right_sort_exprs) - }; - - let partition_mode = - protobuf::StreamPartitionMode::try_from(sym_join.partition_mode).map_err(|_| { - proto_error(format!( - "Received a SymmetricHashJoin message with unknown PartitionMode {}", - sym_join.partition_mode - )) - })?; - let partition_mode = match partition_mode { - protobuf::StreamPartitionMode::SinglePartition => { - StreamJoinPartitionMode::SinglePartition - } - protobuf::StreamPartitionMode::PartitionedExec => { - StreamJoinPartitionMode::Partitioned - } - }; - SymmetricHashJoinExec::try_new( - left, - right, - on, - filter, - &join_type.into(), - sym_join.null_equals_null, - left_sort_exprs, - right_sort_exprs, - partition_mode, - ) - .map(|e| Arc::new(e) as _) - } - PhysicalPlanType::Union(union) => { - let mut inputs: Vec> = vec![]; - for input in &union.inputs { - inputs.push(input.try_into_physical_plan( - registry, - runtime, - extension_codec, - )?); - } - Ok(Arc::new(UnionExec::new(inputs))) - } - PhysicalPlanType::Interleave(interleave) => { - let mut inputs: Vec> = vec![]; - for input in &interleave.inputs { - inputs.push(input.try_into_physical_plan( - registry, - runtime, - extension_codec, - )?); - } - Ok(Arc::new(InterleaveExec::try_new(inputs)?)) - } - PhysicalPlanType::CrossJoin(crossjoin) => { - let left: Arc = into_physical_plan( - &crossjoin.left, + Ok((left, right)) + }) + .collect::>()?; + let join_type = + protobuf::JoinType::try_from(hashjoin.join_type).map_err(|_| { + proto_error(format!( + "Received a HashJoinNode message with unknown JoinType {}", + hashjoin.join_type + )) + })?; + let filter = hashjoin + .filter + .as_ref() + .map(|f| { + let schema = f + .schema + .as_ref() + .ok_or_else(|| proto_error("Missing JoinFilter schema"))? + .try_into()?; + + let expression = parse_physical_expr( + f.expression.as_ref().ok_or_else(|| { + proto_error("Unexpected empty filter expression") + })?, + registry, &schema, + extension_codec, + )?; + let column_indices = f.column_indices + .iter() + .map(|i| { + let side = protobuf::JoinSide::try_from(i.side) + .map_err(|_| proto_error(format!( + "Received a HashJoinNode message with JoinSide in Filter {}", + i.side)) + )?; + + Ok(ColumnIndex { + index: i.index as usize, + side: side.into(), + }) + }) + .collect::>>()?; + + Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; + + let partition_mode = protobuf::PartitionMode::try_from(hashjoin.partition_mode) + .map_err(|_| { + proto_error(format!( + "Received a HashJoinNode message with unknown PartitionMode {}", + hashjoin.partition_mode + )) + })?; + let partition_mode = match partition_mode { + protobuf::PartitionMode::CollectLeft => PartitionMode::CollectLeft, + protobuf::PartitionMode::Partitioned => PartitionMode::Partitioned, + protobuf::PartitionMode::Auto => PartitionMode::Auto, + }; + let projection = if !hashjoin.projection.is_empty() { + Some( + hashjoin + .projection + .iter() + .map(|i| *i as usize) + .collect::>(), + ) + } else { + None + }; + Ok(Arc::new(HashJoinExec::try_new( + left, + right, + on, + filter, + &join_type.into(), + projection, + partition_mode, + hashjoin.null_equals_null, + )?)) + } + + fn try_into_symmetric_hash_join_physical_plan( + &self, + sym_join: &protobuf::SymmetricHashJoinExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let left = + into_physical_plan(&sym_join.left, registry, runtime, extension_codec)?; + let right = + into_physical_plan(&sym_join.right, registry, runtime, extension_codec)?; + let left_schema = left.schema(); + let right_schema = right.schema(); + let on = sym_join + .on + .iter() + .map(|col| { + let left = parse_physical_expr( + &col.left.clone().unwrap(), registry, - runtime, + left_schema.as_ref(), extension_codec, )?; - let right: Arc = into_physical_plan( - &crossjoin.right, + let right = parse_physical_expr( + &col.right.clone().unwrap(), registry, - runtime, + right_schema.as_ref(), extension_codec, )?; - Ok(Arc::new(CrossJoinExec::new(left, right))) - } - PhysicalPlanType::Empty(empty) => { - let schema = Arc::new(convert_required!(empty.schema)?); - Ok(Arc::new(EmptyExec::new(schema))) + Ok((left, right)) + }) + .collect::>()?; + let join_type = + protobuf::JoinType::try_from(sym_join.join_type).map_err(|_| { + proto_error(format!( + "Received a SymmetricHashJoin message with unknown JoinType {}", + sym_join.join_type + )) + })?; + let filter = sym_join + .filter + .as_ref() + .map(|f| { + let schema = f + .schema + .as_ref() + .ok_or_else(|| proto_error("Missing JoinFilter schema"))? + .try_into()?; + + let expression = parse_physical_expr( + f.expression.as_ref().ok_or_else(|| { + proto_error("Unexpected empty filter expression") + })?, + registry, &schema, + extension_codec, + )?; + let column_indices = f.column_indices + .iter() + .map(|i| { + let side = protobuf::JoinSide::try_from(i.side) + .map_err(|_| proto_error(format!( + "Received a HashJoinNode message with JoinSide in Filter {}", + i.side)) + )?; + + Ok(ColumnIndex { + index: i.index as usize, + side: side.into(), + }) + }) + .collect::>()?; + + Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; + + let left_sort_exprs = parse_physical_sort_exprs( + &sym_join.left_sort_exprs, + registry, + &left_schema, + extension_codec, + )?; + let left_sort_exprs = if left_sort_exprs.is_empty() { + None + } else { + Some(left_sort_exprs) + }; + + let right_sort_exprs = parse_physical_sort_exprs( + &sym_join.right_sort_exprs, + registry, + &right_schema, + extension_codec, + )?; + let right_sort_exprs = if right_sort_exprs.is_empty() { + None + } else { + Some(right_sort_exprs) + }; + + let partition_mode = protobuf::StreamPartitionMode::try_from( + sym_join.partition_mode, + ) + .map_err(|_| { + proto_error(format!( + "Received a SymmetricHashJoin message with unknown PartitionMode {}", + sym_join.partition_mode + )) + })?; + let partition_mode = match partition_mode { + protobuf::StreamPartitionMode::SinglePartition => { + StreamJoinPartitionMode::SinglePartition } - PhysicalPlanType::PlaceholderRow(placeholder) => { - let schema = Arc::new(convert_required!(placeholder.schema)?); - Ok(Arc::new(PlaceholderRowExec::new(schema))) + protobuf::StreamPartitionMode::PartitionedExec => { + StreamJoinPartitionMode::Partitioned } - PhysicalPlanType::Sort(sort) => { - let input: Arc = - into_physical_plan(&sort.input, registry, runtime, extension_codec)?; - let exprs = sort + }; + SymmetricHashJoinExec::try_new( + left, + right, + on, + filter, + &join_type.into(), + sym_join.null_equals_null, + left_sort_exprs, + right_sort_exprs, + partition_mode, + ) + .map(|e| Arc::new(e) as _) + } + + fn try_into_union_physical_plan( + &self, + union: &protobuf::UnionExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let mut inputs: Vec> = vec![]; + for input in &union.inputs { + inputs.push(input.try_into_physical_plan( + registry, + runtime, + extension_codec, + )?); + } + Ok(Arc::new(UnionExec::new(inputs))) + } + + fn try_into_interleave_physical_plan( + &self, + interleave: &protobuf::InterleaveExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let mut inputs: Vec> = vec![]; + for input in &interleave.inputs { + inputs.push(input.try_into_physical_plan( + registry, + runtime, + extension_codec, + )?); + } + Ok(Arc::new(InterleaveExec::try_new(inputs)?)) + } + + fn try_into_cross_join_physical_plan( + &self, + crossjoin: &protobuf::CrossJoinExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let left: Arc = + into_physical_plan(&crossjoin.left, registry, runtime, extension_codec)?; + let right: Arc = + into_physical_plan(&crossjoin.right, registry, runtime, extension_codec)?; + Ok(Arc::new(CrossJoinExec::new(left, right))) + } + + fn try_into_empty_physical_plan( + &self, + empty: &protobuf::EmptyExecNode, + _registry: &dyn FunctionRegistry, + _runtime: &RuntimeEnv, + _extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let schema = Arc::new(convert_required!(empty.schema)?); + Ok(Arc::new(EmptyExec::new(schema))) + } + + fn try_into_placeholder_row_physical_plan( + &self, + placeholder: &protobuf::PlaceholderRowExecNode, + _registry: &dyn FunctionRegistry, + _runtime: &RuntimeEnv, + _extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let schema = Arc::new(convert_required!(placeholder.schema)?); + Ok(Arc::new(PlaceholderRowExec::new(schema))) + } + + fn try_into_sort_physical_plan( + &self, + sort: &protobuf::SortExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&sort.input, registry, runtime, extension_codec)?; + let exprs = sort .expr .iter() .map(|expr| { @@ -916,90 +1447,110 @@ impl AsExecutionPlan for protobuf::PhysicalPlanNode { } }) .collect::>()?; - let fetch = if sort.fetch < 0 { - None - } else { - Some(sort.fetch as usize) - }; - let new_sort = SortExec::new(exprs, input) - .with_fetch(fetch) - .with_preserve_partitioning(sort.preserve_partitioning); + let fetch = if sort.fetch < 0 { + None + } else { + Some(sort.fetch as usize) + }; + let new_sort = SortExec::new(exprs, input) + .with_fetch(fetch) + .with_preserve_partitioning(sort.preserve_partitioning); + + Ok(Arc::new(new_sort)) + } - Ok(Arc::new(new_sort)) - } - PhysicalPlanType::SortPreservingMerge(sort) => { - let input: Arc = - into_physical_plan(&sort.input, registry, runtime, extension_codec)?; - let exprs = sort - .expr - .iter() - .map(|expr| { - let expr = expr.expr_type.as_ref().ok_or_else(|| { + fn try_into_sort_preserving_merge_physical_plan( + &self, + sort: &protobuf::SortPreservingMergeExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&sort.input, registry, runtime, extension_codec)?; + let exprs = sort + .expr + .iter() + .map(|expr| { + let expr = expr.expr_type.as_ref().ok_or_else(|| { + proto_error(format!( + "physical_plan::from_proto() Unexpected expr {self:?}" + )) + })?; + if let ExprType::Sort(sort_expr) = expr { + let expr = sort_expr + .expr + .as_ref() + .ok_or_else(|| { proto_error(format!( - "physical_plan::from_proto() Unexpected expr {self:?}" - )) - })?; - if let ExprType::Sort(sort_expr) = expr { - let expr = sort_expr - .expr - .as_ref() - .ok_or_else(|| { - proto_error(format!( - "physical_plan::from_proto() Unexpected sort expr {self:?}" - )) - })? - .as_ref(); - Ok(PhysicalSortExpr { - expr: parse_physical_expr(expr, registry, input.schema().as_ref(), extension_codec)?, - options: SortOptions { - descending: !sort_expr.asc, - nulls_first: sort_expr.nulls_first, - }, - }) - } else { - internal_err!( - "physical_plan::from_proto() {self:?}" - ) - } + "physical_plan::from_proto() Unexpected sort expr {self:?}" + )) + })? + .as_ref(); + Ok(PhysicalSortExpr { + expr: parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + )?, + options: SortOptions { + descending: !sort_expr.asc, + nulls_first: sort_expr.nulls_first, + }, }) - .collect::>()?; - let fetch = if sort.fetch < 0 { - None } else { - Some(sort.fetch as usize) - }; - Ok(Arc::new( - SortPreservingMergeExec::new(exprs, input).with_fetch(fetch), - )) - } - PhysicalPlanType::Extension(extension) => { - let inputs: Vec> = extension - .inputs - .iter() - .map(|i| i.try_into_physical_plan(registry, runtime, extension_codec)) - .collect::>()?; + internal_err!("physical_plan::from_proto() {self:?}") + } + }) + .collect::>()?; + let fetch = if sort.fetch < 0 { + None + } else { + Some(sort.fetch as usize) + }; + Ok(Arc::new( + SortPreservingMergeExec::new(exprs, input).with_fetch(fetch), + )) + } - let extension_node = extension_codec.try_decode( - extension.node.as_slice(), - &inputs, - registry, - )?; + fn try_into_extension_physical_plan( + &self, + extension: &protobuf::PhysicalExtensionNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let inputs: Vec> = extension + .inputs + .iter() + .map(|i| i.try_into_physical_plan(registry, runtime, extension_codec)) + .collect::>()?; - Ok(extension_node) - } - PhysicalPlanType::NestedLoopJoin(join) => { - let left: Arc = - into_physical_plan(&join.left, registry, runtime, extension_codec)?; - let right: Arc = - into_physical_plan(&join.right, registry, runtime, extension_codec)?; - let join_type = - protobuf::JoinType::try_from(join.join_type).map_err(|_| { - proto_error(format!( - "Received a NestedLoopJoinExecNode message with unknown JoinType {}", - join.join_type - )) - })?; - let filter = join + let extension_node = + extension_codec.try_decode(extension.node.as_slice(), &inputs, registry)?; + + Ok(extension_node) + } + + fn try_into_nested_loop_join_physical_plan( + &self, + join: &protobuf::NestedLoopJoinExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let left: Arc = + into_physical_plan(&join.left, registry, runtime, extension_codec)?; + let right: Arc = + into_physical_plan(&join.right, registry, runtime, extension_codec)?; + let join_type = protobuf::JoinType::try_from(join.join_type).map_err(|_| { + proto_error(format!( + "Received a NestedLoopJoinExecNode message with unknown JoinType {}", + join.join_type + )) + })?; + let filter = join .filter .as_ref() .map(|f| { @@ -1036,1121 +1587,1157 @@ impl AsExecutionPlan for protobuf::PhysicalPlanNode { }) .map_or(Ok(None), |v: Result| v.map(Some))?; - let projection = if !join.projection.is_empty() { - Some( - join.projection - .iter() - .map(|i| *i as usize) - .collect::>(), - ) - } else { - None - }; - - Ok(Arc::new(NestedLoopJoinExec::try_new( - left, - right, - filter, - &join_type.into(), - projection, - )?)) - } - PhysicalPlanType::Analyze(analyze) => { - let input: Arc = into_physical_plan( - &analyze.input, - registry, - runtime, - extension_codec, - )?; - Ok(Arc::new(AnalyzeExec::new( - analyze.verbose, - analyze.show_statistics, - input, - Arc::new(convert_required!(analyze.schema)?), - ))) - } - PhysicalPlanType::JsonSink(sink) => { - let input = - into_physical_plan(&sink.input, registry, runtime, extension_codec)?; - - let data_sink: JsonSink = sink - .sink - .as_ref() - .ok_or_else(|| proto_error("Missing required field in protobuf"))? - .try_into()?; - let sink_schema = input.schema(); - let sort_order = sink - .sort_order - .as_ref() - .map(|collection| { - parse_physical_sort_exprs( - &collection.physical_sort_expr_nodes, - registry, - &sink_schema, - extension_codec, - ) - .map(LexRequirement::from) - }) - .transpose()?; - Ok(Arc::new(DataSinkExec::new( - input, - Arc::new(data_sink), - sort_order, - ))) - } - PhysicalPlanType::CsvSink(sink) => { - let input = - into_physical_plan(&sink.input, registry, runtime, extension_codec)?; + let projection = if !join.projection.is_empty() { + Some( + join.projection + .iter() + .map(|i| *i as usize) + .collect::>(), + ) + } else { + None + }; + + Ok(Arc::new(NestedLoopJoinExec::try_new( + left, + right, + filter, + &join_type.into(), + projection, + )?)) + } - let data_sink: CsvSink = sink - .sink - .as_ref() - .ok_or_else(|| proto_error("Missing required field in protobuf"))? - .try_into()?; - let sink_schema = input.schema(); - let sort_order = sink - .sort_order - .as_ref() - .map(|collection| { - parse_physical_sort_exprs( - &collection.physical_sort_expr_nodes, - registry, - &sink_schema, - extension_codec, - ) - .map(LexRequirement::from) - }) - .transpose()?; - Ok(Arc::new(DataSinkExec::new( - input, - Arc::new(data_sink), - sort_order, - ))) - } - #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] - PhysicalPlanType::ParquetSink(sink) => { - #[cfg(feature = "parquet")] - { - let input = into_physical_plan( - &sink.input, - registry, - runtime, - extension_codec, - )?; + fn try_into_analyze_physical_plan( + &self, + analyze: &protobuf::AnalyzeExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: Arc = + into_physical_plan(&analyze.input, registry, runtime, extension_codec)?; + Ok(Arc::new(AnalyzeExec::new( + analyze.verbose, + analyze.show_statistics, + input, + Arc::new(convert_required!(analyze.schema)?), + ))) + } - let data_sink: ParquetSink = sink - .sink - .as_ref() - .ok_or_else(|| proto_error("Missing required field in protobuf"))? - .try_into()?; - let sink_schema = input.schema(); - let sort_order = sink - .sort_order - .as_ref() - .map(|collection| { - parse_physical_sort_exprs( - &collection.physical_sort_expr_nodes, - registry, - &sink_schema, - extension_codec, - ) - .map(LexRequirement::from) - }) - .transpose()?; - Ok(Arc::new(DataSinkExec::new( - input, - Arc::new(data_sink), - sort_order, - ))) - } - #[cfg(not(feature = "parquet"))] - panic!("Trying to use ParquetSink without `parquet` feature enabled"); - } - PhysicalPlanType::Unnest(unnest) => { - let input = into_physical_plan( - &unnest.input, + fn try_into_json_sink_physical_plan( + &self, + sink: &protobuf::JsonSinkExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input = into_physical_plan(&sink.input, registry, runtime, extension_codec)?; + + let data_sink: JsonSink = sink + .sink + .as_ref() + .ok_or_else(|| proto_error("Missing required field in protobuf"))? + .try_into()?; + let sink_schema = input.schema(); + let sort_order = sink + .sort_order + .as_ref() + .map(|collection| { + parse_physical_sort_exprs( + &collection.physical_sort_expr_nodes, registry, - runtime, + &sink_schema, extension_codec, - )?; + ) + .map(LexRequirement::from) + }) + .transpose()?; + Ok(Arc::new(DataSinkExec::new( + input, + Arc::new(data_sink), + sort_order, + ))) + } - Ok(Arc::new(UnnestExec::new( - input, - unnest - .list_type_columns - .iter() - .map(|c| ListUnnest { - index_in_input_schema: c.index_in_input_schema as _, - depth: c.depth as _, - }) - .collect(), - unnest.struct_type_columns.iter().map(|c| *c as _).collect(), - Arc::new(convert_required!(unnest.schema)?), - into_required!(unnest.options)?, - ))) - } - } + fn try_into_csv_sink_physical_plan( + &self, + sink: &protobuf::CsvSinkExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input = into_physical_plan(&sink.input, registry, runtime, extension_codec)?; + + let data_sink: CsvSink = sink + .sink + .as_ref() + .ok_or_else(|| proto_error("Missing required field in protobuf"))? + .try_into()?; + let sink_schema = input.schema(); + let sort_order = sink + .sort_order + .as_ref() + .map(|collection| { + parse_physical_sort_exprs( + &collection.physical_sort_expr_nodes, + registry, + &sink_schema, + extension_codec, + ) + .map(LexRequirement::from) + }) + .transpose()?; + Ok(Arc::new(DataSinkExec::new( + input, + Arc::new(data_sink), + sort_order, + ))) } - fn try_from_physical_plan( - plan: Arc, + fn try_into_parquet_sink_physical_plan( + &self, + sink: &protobuf::ParquetSinkExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result - where - Self: Sized, - { - let plan_clone = Arc::clone(&plan); - let plan = plan.as_any(); + ) -> Result> { + #[cfg(feature = "parquet")] + { + let input = + into_physical_plan(&sink.input, registry, runtime, extension_codec)?; - if let Some(exec) = plan.downcast_ref::() { - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Explain( - protobuf::ExplainExecNode { - schema: Some(exec.schema().as_ref().try_into()?), - stringified_plans: exec - .stringified_plans() - .iter() - .map(|plan| plan.into()) - .collect(), - verbose: exec.verbose(), - }, - )), - }); + let data_sink: ParquetSink = sink + .sink + .as_ref() + .ok_or_else(|| proto_error("Missing required field in protobuf"))? + .try_into()?; + let sink_schema = input.schema(); + let sort_order = sink + .sort_order + .as_ref() + .map(|collection| { + parse_physical_sort_exprs( + &collection.physical_sort_expr_nodes, + registry, + &sink_schema, + extension_codec, + ) + .map(LexRequirement::from) + }) + .transpose()?; + Ok(Arc::new(DataSinkExec::new( + input, + Arc::new(data_sink), + sort_order, + ))) } + #[cfg(not(feature = "parquet"))] + panic!("Trying to use ParquetSink without `parquet` feature enabled"); + } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - let expr = exec - .expr() + fn try_into_unnest_physical_plan( + &self, + unnest: &protobuf::UnnestExecNode, + registry: &dyn FunctionRegistry, + runtime: &RuntimeEnv, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input = + into_physical_plan(&unnest.input, registry, runtime, extension_codec)?; + + Ok(Arc::new(UnnestExec::new( + input, + unnest + .list_type_columns .iter() - .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) - .collect::>>()?; - let expr_name = exec.expr().iter().map(|expr| expr.1.clone()).collect(); - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Projection(Box::new( - protobuf::ProjectionExecNode { - input: Some(Box::new(input)), - expr, - expr_name, - }, - ))), - }); - } + .map(|c| ListUnnest { + index_in_input_schema: c.index_in_input_schema as _, + depth: c.depth as _, + }) + .collect(), + unnest.struct_type_columns.iter().map(|c| *c as _).collect(), + Arc::new(convert_required!(unnest.schema)?), + into_required!(unnest.options)?, + ))) + } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Analyze(Box::new( - protobuf::AnalyzeExecNode { - verbose: exec.verbose(), - show_statistics: exec.show_statistics(), - input: Some(Box::new(input)), - schema: Some(exec.schema().as_ref().try_into()?), - }, - ))), - }); - } + fn try_from_explain_exec( + exec: &ExplainExec, + _extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Explain( + protobuf::ExplainExecNode { + schema: Some(exec.schema().as_ref().try_into()?), + stringified_plans: exec + .stringified_plans() + .iter() + .map(|plan| plan.into()) + .collect(), + verbose: exec.verbose(), + }, + )), + }) + } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Filter(Box::new( - protobuf::FilterExecNode { - input: Some(Box::new(input)), - expr: Some(serialize_physical_expr( - exec.predicate(), - extension_codec, - )?), - default_filter_selectivity: exec.default_selectivity() as u32, - projection: exec - .projection() - .as_ref() - .map_or_else(Vec::new, |v| { - v.iter().map(|x| *x as u32).collect::>() - }), - }, - ))), - }); - } + fn try_from_projection_exec( + exec: &ProjectionExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + let expr = exec + .expr() + .iter() + .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) + .collect::>>()?; + let expr_name = exec.expr().iter().map(|expr| expr.1.clone()).collect(); + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Projection(Box::new( + protobuf::ProjectionExecNode { + input: Some(Box::new(input)), + expr, + expr_name, + }, + ))), + }) + } - if let Some(limit) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - limit.input().to_owned(), - extension_codec, - )?; + fn try_from_analyze_exec( + exec: &AnalyzeExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Analyze(Box::new( + protobuf::AnalyzeExecNode { + verbose: exec.verbose(), + show_statistics: exec.show_statistics(), + input: Some(Box::new(input)), + schema: Some(exec.schema().as_ref().try_into()?), + }, + ))), + }) + } - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::GlobalLimit(Box::new( - protobuf::GlobalLimitExecNode { - input: Some(Box::new(input)), - skip: limit.skip() as u32, - fetch: match limit.fetch() { - Some(n) => n as i64, - _ => -1, // no limit - }, - }, - ))), - }); - } + fn try_from_filter_exec( + exec: &FilterExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Filter(Box::new( + protobuf::FilterExecNode { + input: Some(Box::new(input)), + expr: Some(serialize_physical_expr( + exec.predicate(), + extension_codec, + )?), + default_filter_selectivity: exec.default_selectivity() as u32, + projection: exec.projection().as_ref().map_or_else(Vec::new, |v| { + v.iter().map(|x| *x as u32).collect::>() + }), + }, + ))), + }) + } - if let Some(limit) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - limit.input().to_owned(), - extension_codec, - )?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::LocalLimit(Box::new( - protobuf::LocalLimitExecNode { - input: Some(Box::new(input)), - fetch: limit.fetch() as u32, + fn try_from_global_limit_exec( + limit: &GlobalLimitExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + limit.input().to_owned(), + extension_codec, + )?; + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::GlobalLimit(Box::new( + protobuf::GlobalLimitExecNode { + input: Some(Box::new(input)), + skip: limit.skip() as u32, + fetch: match limit.fetch() { + Some(n) => n as i64, + _ => -1, // no limit }, - ))), - }); - } + }, + ))), + }) + } - if let Some(exec) = plan.downcast_ref::() { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; - let on: Vec = exec - .on() - .iter() - .map(|tuple| { - let l = serialize_physical_expr(&tuple.0, extension_codec)?; - let r = serialize_physical_expr(&tuple.1, extension_codec)?; - Ok::<_, DataFusionError>(protobuf::JoinOn { - left: Some(l), - right: Some(r), - }) + fn try_from_local_limit_exec( + limit: &LocalLimitExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + limit.input().to_owned(), + extension_codec, + )?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::LocalLimit(Box::new( + protobuf::LocalLimitExecNode { + input: Some(Box::new(input)), + fetch: limit.fetch() as u32, + }, + ))), + }) + } + + fn try_from_hash_join_exec( + exec: &HashJoinExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; + let on: Vec = exec + .on() + .iter() + .map(|tuple| { + let l = serialize_physical_expr(&tuple.0, extension_codec)?; + let r = serialize_physical_expr(&tuple.1, extension_codec)?; + Ok::<_, DataFusionError>(protobuf::JoinOn { + left: Some(l), + right: Some(r), }) - .collect::>()?; - let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); - let filter = exec - .filter() - .as_ref() - .map(|f| { - let expression = - serialize_physical_expr(f.expression(), extension_codec)?; - let column_indices = f - .column_indices() - .iter() - .map(|i| { - let side: protobuf::JoinSide = i.side.to_owned().into(); - protobuf::ColumnIndex { - index: i.index as u32, - side: side.into(), - } - }) - .collect(); - let schema = f.schema().as_ref().try_into()?; - Ok(protobuf::JoinFilter { - expression: Some(expression), - column_indices, - schema: Some(schema), + }) + .collect::>()?; + let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); + let filter = exec + .filter() + .as_ref() + .map(|f| { + let expression = + serialize_physical_expr(f.expression(), extension_codec)?; + let column_indices = f + .column_indices() + .iter() + .map(|i| { + let side: protobuf::JoinSide = i.side.to_owned().into(); + protobuf::ColumnIndex { + index: i.index as u32, + side: side.into(), + } }) + .collect(); + let schema = f.schema().as_ref().try_into()?; + Ok(protobuf::JoinFilter { + expression: Some(expression), + column_indices, + schema: Some(schema), }) - .map_or(Ok(None), |v: Result| v.map(Some))?; - - let partition_mode = match exec.partition_mode() { - PartitionMode::CollectLeft => protobuf::PartitionMode::CollectLeft, - PartitionMode::Partitioned => protobuf::PartitionMode::Partitioned, - PartitionMode::Auto => protobuf::PartitionMode::Auto, - }; - - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::HashJoin(Box::new( - protobuf::HashJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - on, - join_type: join_type.into(), - partition_mode: partition_mode.into(), - null_equals_null: exec.null_equals_null(), - filter, - projection: exec.projection.as_ref().map_or_else(Vec::new, |v| { - v.iter().map(|x| *x as u32).collect::>() - }), - }, - ))), - }); - } + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; + + let partition_mode = match exec.partition_mode() { + PartitionMode::CollectLeft => protobuf::PartitionMode::CollectLeft, + PartitionMode::Partitioned => protobuf::PartitionMode::Partitioned, + PartitionMode::Auto => protobuf::PartitionMode::Auto, + }; + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::HashJoin(Box::new( + protobuf::HashJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + on, + join_type: join_type.into(), + partition_mode: partition_mode.into(), + null_equals_null: exec.null_equals_null(), + filter, + projection: exec.projection.as_ref().map_or_else(Vec::new, |v| { + v.iter().map(|x| *x as u32).collect::>() + }), + }, + ))), + }) + } - if let Some(exec) = plan.downcast_ref::() { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; - let on = exec - .on() - .iter() - .map(|tuple| { - let l = serialize_physical_expr(&tuple.0, extension_codec)?; - let r = serialize_physical_expr(&tuple.1, extension_codec)?; - Ok::<_, DataFusionError>(protobuf::JoinOn { - left: Some(l), - right: Some(r), - }) + fn try_from_symmetric_hash_join_exec( + exec: &SymmetricHashJoinExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; + let on = exec + .on() + .iter() + .map(|tuple| { + let l = serialize_physical_expr(&tuple.0, extension_codec)?; + let r = serialize_physical_expr(&tuple.1, extension_codec)?; + Ok::<_, DataFusionError>(protobuf::JoinOn { + left: Some(l), + right: Some(r), }) - .collect::>()?; - let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); - let filter = exec - .filter() - .as_ref() - .map(|f| { - let expression = - serialize_physical_expr(f.expression(), extension_codec)?; - let column_indices = f - .column_indices() - .iter() - .map(|i| { - let side: protobuf::JoinSide = i.side.to_owned().into(); - protobuf::ColumnIndex { - index: i.index as u32, - side: side.into(), - } - }) - .collect(); - let schema = f.schema().as_ref().try_into()?; - Ok(protobuf::JoinFilter { - expression: Some(expression), - column_indices, - schema: Some(schema), + }) + .collect::>()?; + let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); + let filter = exec + .filter() + .as_ref() + .map(|f| { + let expression = + serialize_physical_expr(f.expression(), extension_codec)?; + let column_indices = f + .column_indices() + .iter() + .map(|i| { + let side: protobuf::JoinSide = i.side.to_owned().into(); + protobuf::ColumnIndex { + index: i.index as u32, + side: side.into(), + } }) + .collect(); + let schema = f.schema().as_ref().try_into()?; + Ok(protobuf::JoinFilter { + expression: Some(expression), + column_indices, + schema: Some(schema), }) - .map_or(Ok(None), |v: Result| v.map(Some))?; + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; - let partition_mode = match exec.partition_mode() { - StreamJoinPartitionMode::SinglePartition => { - protobuf::StreamPartitionMode::SinglePartition - } - StreamJoinPartitionMode::Partitioned => { - protobuf::StreamPartitionMode::PartitionedExec - } - }; + let partition_mode = match exec.partition_mode() { + StreamJoinPartitionMode::SinglePartition => { + protobuf::StreamPartitionMode::SinglePartition + } + StreamJoinPartitionMode::Partitioned => { + protobuf::StreamPartitionMode::PartitionedExec + } + }; - let left_sort_exprs = exec - .left_sort_exprs() - .map(|exprs| { - exprs - .iter() - .map(|expr| { - Ok(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }) + let left_sort_exprs = exec + .left_sort_exprs() + .map(|exprs| { + exprs + .iter() + .map(|expr| { + Ok(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, }) - .collect::>>() - }) - .transpose()? - .unwrap_or(vec![]); - - let right_sort_exprs = exec - .right_sort_exprs() - .map(|exprs| { - exprs - .iter() - .map(|expr| { - Ok(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }) + }) + .collect::>>() + }) + .transpose()? + .unwrap_or(vec![]); + + let right_sort_exprs = exec + .right_sort_exprs() + .map(|exprs| { + exprs + .iter() + .map(|expr| { + Ok(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, }) - .collect::>>() - }) - .transpose()? - .unwrap_or(vec![]); - - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::SymmetricHashJoin(Box::new( - protobuf::SymmetricHashJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - on, - join_type: join_type.into(), - partition_mode: partition_mode.into(), - null_equals_null: exec.null_equals_null(), - left_sort_exprs, - right_sort_exprs, - filter, - }, - ))), - }); - } - - if let Some(exec) = plan.downcast_ref::() { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CrossJoin(Box::new( - protobuf::CrossJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - }, - ))), - }); - } - if let Some(exec) = plan.downcast_ref::() { - let groups: Vec = exec - .group_expr() - .groups() - .iter() - .flatten() - .copied() - .collect(); - - let group_names = exec - .group_expr() - .expr() - .iter() - .map(|expr| expr.1.to_owned()) - .collect(); - - let filter = exec - .filter_expr() - .iter() - .map(|expr| serialize_maybe_filter(expr.to_owned(), extension_codec)) - .collect::>>()?; - - let agg = exec - .aggr_expr() - .iter() - .map(|expr| { - serialize_physical_aggr_expr(expr.to_owned(), extension_codec) - }) - .collect::>>()?; - - let agg_names = exec - .aggr_expr() - .iter() - .map(|expr| expr.name().to_string()) - .collect::>(); - - let agg_mode = match exec.mode() { - AggregateMode::Partial => protobuf::AggregateMode::Partial, - AggregateMode::Final => protobuf::AggregateMode::Final, - AggregateMode::FinalPartitioned => { - protobuf::AggregateMode::FinalPartitioned - } - AggregateMode::Single => protobuf::AggregateMode::Single, - AggregateMode::SinglePartitioned => { - protobuf::AggregateMode::SinglePartitioned - } - }; - let input_schema = exec.input_schema(); - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; + }) + .collect::>>() + }) + .transpose()? + .unwrap_or(vec![]); + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::SymmetricHashJoin(Box::new( + protobuf::SymmetricHashJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + on, + join_type: join_type.into(), + partition_mode: partition_mode.into(), + null_equals_null: exec.null_equals_null(), + left_sort_exprs, + right_sort_exprs, + filter, + }, + ))), + }) + } - let null_expr = exec - .group_expr() - .null_expr() - .iter() - .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) - .collect::>>()?; + fn try_from_cross_join_exec( + exec: &CrossJoinExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CrossJoin(Box::new( + protobuf::CrossJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + }, + ))), + }) + } - let group_expr = exec - .group_expr() - .expr() - .iter() - .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) - .collect::>>()?; - - let limit = exec.limit().map(|value| protobuf::AggLimit { - limit: value as u64, - }); - - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Aggregate(Box::new( - protobuf::AggregateExecNode { - group_expr, - group_expr_name: group_names, - aggr_expr: agg, - filter_expr: filter, - aggr_expr_name: agg_names, - mode: agg_mode as i32, - input: Some(Box::new(input)), - input_schema: Some(input_schema.as_ref().try_into()?), - null_expr, - groups, - limit, - }, - ))), - }); - } + fn try_from_aggregate_exec( + exec: &AggregateExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let groups: Vec = exec + .group_expr() + .groups() + .iter() + .flatten() + .copied() + .collect(); + + let group_names = exec + .group_expr() + .expr() + .iter() + .map(|expr| expr.1.to_owned()) + .collect(); + + let filter = exec + .filter_expr() + .iter() + .map(|expr| serialize_maybe_filter(expr.to_owned(), extension_codec)) + .collect::>>()?; + + let agg = exec + .aggr_expr() + .iter() + .map(|expr| serialize_physical_aggr_expr(expr.to_owned(), extension_codec)) + .collect::>>()?; + + let agg_names = exec + .aggr_expr() + .iter() + .map(|expr| expr.name().to_string()) + .collect::>(); + + let agg_mode = match exec.mode() { + AggregateMode::Partial => protobuf::AggregateMode::Partial, + AggregateMode::Final => protobuf::AggregateMode::Final, + AggregateMode::FinalPartitioned => protobuf::AggregateMode::FinalPartitioned, + AggregateMode::Single => protobuf::AggregateMode::Single, + AggregateMode::SinglePartitioned => { + protobuf::AggregateMode::SinglePartitioned + } + }; + let input_schema = exec.input_schema(); + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + let null_expr = exec + .group_expr() + .null_expr() + .iter() + .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) + .collect::>>()?; + + let group_expr = exec + .group_expr() + .expr() + .iter() + .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) + .collect::>>()?; + + let limit = exec.limit().map(|value| protobuf::AggLimit { + limit: value as u64, + }); + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Aggregate(Box::new( + protobuf::AggregateExecNode { + group_expr, + group_expr_name: group_names, + aggr_expr: agg, + filter_expr: filter, + aggr_expr_name: agg_names, + mode: agg_mode as i32, + input: Some(Box::new(input)), + input_schema: Some(input_schema.as_ref().try_into()?), + null_expr, + groups, + limit, + }, + ))), + }) + } - if let Some(empty) = plan.downcast_ref::() { - let schema = empty.schema().as_ref().try_into()?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Empty( - protobuf::EmptyExecNode { - schema: Some(schema), - }, - )), - }); - } + fn try_from_empty_exec( + empty: &EmptyExec, + _extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let schema = empty.schema().as_ref().try_into()?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Empty(protobuf::EmptyExecNode { + schema: Some(schema), + })), + }) + } - if let Some(empty) = plan.downcast_ref::() { - let schema = empty.schema().as_ref().try_into()?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::PlaceholderRow( - protobuf::PlaceholderRowExecNode { - schema: Some(schema), - }, - )), - }); - } + fn try_from_placeholder_row_exec( + empty: &PlaceholderRowExec, + _extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let schema = empty.schema().as_ref().try_into()?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::PlaceholderRow( + protobuf::PlaceholderRowExecNode { + schema: Some(schema), + }, + )), + }) + } - if let Some(coalesce_batches) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - coalesce_batches.input().to_owned(), - extension_codec, - )?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CoalesceBatches(Box::new( - protobuf::CoalesceBatchesExecNode { - input: Some(Box::new(input)), - target_batch_size: coalesce_batches.target_batch_size() as u32, - fetch: coalesce_batches.fetch().map(|n| n as u32), - }, - ))), - }); - } + fn try_from_coalesce_batches_exec( + coalesce_batches: &CoalesceBatchesExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + coalesce_batches.input().to_owned(), + extension_codec, + )?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CoalesceBatches(Box::new( + protobuf::CoalesceBatchesExecNode { + input: Some(Box::new(input)), + target_batch_size: coalesce_batches.target_batch_size() as u32, + fetch: coalesce_batches.fetch().map(|n| n as u32), + }, + ))), + }) + } - if let Some(data_source_exec) = plan.downcast_ref::() { - let data_source = data_source_exec.data_source(); - if let Some(maybe_csv) = data_source.as_any().downcast_ref::() - { - let source = maybe_csv.file_source(); - if let Some(csv_config) = source.as_any().downcast_ref::() { - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CsvScan( - protobuf::CsvScanExecNode { - base_conf: Some(serialize_file_scan_config( - maybe_csv, - extension_codec, - )?), - has_header: csv_config.has_header(), - delimiter: byte_to_string( - csv_config.delimiter(), - "delimiter", - )?, - quote: byte_to_string(csv_config.quote(), "quote")?, - optional_escape: if let Some(escape) = csv_config.escape() - { - Some( - protobuf::csv_scan_exec_node::OptionalEscape::Escape( - byte_to_string(escape, "escape")?, - ), - ) - } else { - None - }, - optional_comment: if let Some(comment) = - csv_config.comment() - { - Some(protobuf::csv_scan_exec_node::OptionalComment::Comment( + fn try_from_data_source_exec( + data_source_exec: &DataSourceExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let data_source = data_source_exec.data_source(); + if let Some(maybe_csv) = data_source.as_any().downcast_ref::() { + let source = maybe_csv.file_source(); + if let Some(csv_config) = source.as_any().downcast_ref::() { + return Ok(Some(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CsvScan( + protobuf::CsvScanExecNode { + base_conf: Some(serialize_file_scan_config( + maybe_csv, + extension_codec, + )?), + has_header: csv_config.has_header(), + delimiter: byte_to_string( + csv_config.delimiter(), + "delimiter", + )?, + quote: byte_to_string(csv_config.quote(), "quote")?, + optional_escape: if let Some(escape) = csv_config.escape() { + Some( + protobuf::csv_scan_exec_node::OptionalEscape::Escape( + byte_to_string(escape, "escape")?, + ), + ) + } else { + None + }, + optional_comment: if let Some(comment) = csv_config.comment() + { + Some(protobuf::csv_scan_exec_node::OptionalComment::Comment( byte_to_string(comment, "comment")?, )) - } else { - None - }, - newlines_in_values: maybe_csv.newlines_in_values(), + } else { + None }, - )), - }); - } - } - } - - if let Some(data_source_exec) = plan.downcast_ref::() { - let data_source = data_source_exec.data_source(); - if let Some(scan_conf) = data_source.as_any().downcast_ref::() - { - let source = scan_conf.file_source(); - if let Some(_json_source) = source.as_any().downcast_ref::() { - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::JsonScan( - protobuf::JsonScanExecNode { - base_conf: Some(serialize_file_scan_config( - scan_conf, - extension_codec, - )?), - }, - )), - }); - } + newlines_in_values: maybe_csv.newlines_in_values(), + }, + )), + })); } } - #[cfg(feature = "parquet")] - if let Some(exec) = plan.downcast_ref::() { - if let Some((maybe_parquet, conf)) = - exec.downcast_to_file_source::() - { - let predicate = conf - .predicate() - .map(|pred| serialize_physical_expr(pred, extension_codec)) - .transpose()?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::ParquetScan( - protobuf::ParquetScanExecNode { + if let Some(scan_conf) = data_source.as_any().downcast_ref::() { + let source = scan_conf.file_source(); + if let Some(_json_source) = source.as_any().downcast_ref::() { + return Ok(Some(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::JsonScan( + protobuf::JsonScanExecNode { base_conf: Some(serialize_file_scan_config( - maybe_parquet, + scan_conf, extension_codec, )?), - predicate, - parquet_options: Some( - conf.table_parquet_options().try_into()?, - ), }, )), - }); + })); } } + #[cfg(feature = "parquet")] + if let Some((maybe_parquet, conf)) = + data_source_exec.downcast_to_file_source::() + { + let predicate = conf + .predicate() + .map(|pred| serialize_physical_expr(pred, extension_codec)) + .transpose()?; + return Ok(Some(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::ParquetScan( + protobuf::ParquetScanExecNode { + base_conf: Some(serialize_file_scan_config( + maybe_parquet, + extension_codec, + )?), + predicate, + parquet_options: Some(conf.table_parquet_options().try_into()?), + }, + )), + })); + } + #[cfg(feature = "avro")] - if let Some(data_source_exec) = plan.downcast_ref::() { - let data_source = data_source_exec.data_source(); - if let Some(maybe_avro) = - data_source.as_any().downcast_ref::() - { - let source = maybe_avro.file_source(); - if source.as_any().downcast_ref::().is_some() { - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::AvroScan( - protobuf::AvroScanExecNode { - base_conf: Some(serialize_file_scan_config( - maybe_avro, - extension_codec, - )?), - }, - )), - }); - } + if let Some(maybe_avro) = data_source.as_any().downcast_ref::() { + let source = maybe_avro.file_source(); + if source.as_any().downcast_ref::().is_some() { + return Ok(Some(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::AvroScan( + protobuf::AvroScanExecNode { + base_conf: Some(serialize_file_scan_config( + maybe_avro, + extension_codec, + )?), + }, + )), + })); } } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Merge(Box::new( - protobuf::CoalescePartitionsExecNode { - input: Some(Box::new(input)), - }, - ))), - }); - } + Ok(None) + } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; + fn try_from_coalesce_partitions_exec( + exec: &CoalescePartitionsExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Merge(Box::new( + protobuf::CoalescePartitionsExecNode { + input: Some(Box::new(input)), + }, + ))), + }) + } - let pb_partitioning = - serialize_partitioning(exec.partitioning(), extension_codec)?; + fn try_from_repartition_exec( + exec: &RepartitionExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + let pb_partitioning = + serialize_partitioning(exec.partitioning(), extension_codec)?; + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Repartition(Box::new( + protobuf::RepartitionExecNode { + input: Some(Box::new(input)), + partitioning: Some(pb_partitioning), + }, + ))), + }) + } - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Repartition(Box::new( - protobuf::RepartitionExecNode { - input: Some(Box::new(input)), - partitioning: Some(pb_partitioning), + fn try_from_sort_exec( + exec: &SortExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + let expr = exec + .expr() + .iter() + .map(|expr| { + let sort_expr = Box::new(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }); + Ok(protobuf::PhysicalExprNode { + expr_type: Some(ExprType::Sort(sort_expr)), + }) + }) + .collect::>>()?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Sort(Box::new( + protobuf::SortExecNode { + input: Some(Box::new(input)), + expr, + fetch: match exec.fetch() { + Some(n) => n as i64, + _ => -1, }, - ))), - }); - } + preserve_partitioning: exec.preserve_partitioning(), + }, + ))), + }) + } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), + fn try_from_union_exec( + union: &UnionExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let mut inputs: Vec = vec![]; + for input in union.inputs() { + inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( + input.to_owned(), extension_codec, - )?; - let expr = exec - .expr() - .iter() - .map(|expr| { - let sort_expr = Box::new(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }); - Ok(protobuf::PhysicalExprNode { - expr_type: Some(ExprType::Sort(sort_expr)), - }) - }) - .collect::>>()?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Sort(Box::new( - protobuf::SortExecNode { - input: Some(Box::new(input)), - expr, - fetch: match exec.fetch() { - Some(n) => n as i64, - _ => -1, - }, - preserve_partitioning: exec.preserve_partitioning(), - }, - ))), - }); + )?); } + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Union(protobuf::UnionExecNode { + inputs, + })), + }) + } - if let Some(union) = plan.downcast_ref::() { - let mut inputs: Vec = vec![]; - for input in union.inputs() { - inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( - input.to_owned(), - extension_codec, - )?); - } - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Union( - protobuf::UnionExecNode { inputs }, - )), - }); + fn try_from_interleave_exec( + interleave: &InterleaveExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let mut inputs: Vec = vec![]; + for input in interleave.inputs() { + inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( + input.to_owned(), + extension_codec, + )?); } + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Interleave( + protobuf::InterleaveExecNode { inputs }, + )), + }) + } - if let Some(interleave) = plan.downcast_ref::() { - let mut inputs: Vec = vec![]; - for input in interleave.inputs() { - inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( - input.to_owned(), - extension_codec, - )?); - } - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Interleave( - protobuf::InterleaveExecNode { inputs }, - )), - }); - } + fn try_from_sort_preserving_merge_exec( + exec: &SortPreservingMergeExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + let expr = exec + .expr() + .iter() + .map(|expr| { + let sort_expr = Box::new(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }); + Ok(protobuf::PhysicalExprNode { + expr_type: Some(ExprType::Sort(sort_expr)), + }) + }) + .collect::>>()?; + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::SortPreservingMerge(Box::new( + protobuf::SortPreservingMergeExecNode { + input: Some(Box::new(input)), + expr, + fetch: exec.fetch().map(|f| f as i64).unwrap_or(-1), + }, + ))), + }) + } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - let expr = exec - .expr() - .iter() - .map(|expr| { - let sort_expr = Box::new(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }); - Ok(protobuf::PhysicalExprNode { - expr_type: Some(ExprType::Sort(sort_expr)), + fn try_from_nested_loop_join_exec( + exec: &NestedLoopJoinExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; + + let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); + let filter = exec + .filter() + .as_ref() + .map(|f| { + let expression = + serialize_physical_expr(f.expression(), extension_codec)?; + let column_indices = f + .column_indices() + .iter() + .map(|i| { + let side: protobuf::JoinSide = i.side.to_owned().into(); + protobuf::ColumnIndex { + index: i.index as u32, + side: side.into(), + } }) + .collect(); + let schema = f.schema().as_ref().try_into()?; + Ok(protobuf::JoinFilter { + expression: Some(expression), + column_indices, + schema: Some(schema), }) - .collect::>>()?; - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::SortPreservingMerge( - Box::new(protobuf::SortPreservingMergeExecNode { - input: Some(Box::new(input)), - expr, - fetch: exec.fetch().map(|f| f as i64).unwrap_or(-1), + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::NestedLoopJoin(Box::new( + protobuf::NestedLoopJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + join_type: join_type.into(), + filter, + projection: exec.projection().map_or_else(Vec::new, |v| { + v.iter().map(|x| *x as u32).collect::>() }), - )), - }); - } + }, + ))), + }) + } - if let Some(exec) = plan.downcast_ref::() { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; + fn try_from_window_agg_exec( + exec: &WindowAggExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + let window_expr = exec + .window_expr() + .iter() + .map(|e| serialize_physical_window_expr(e, extension_codec)) + .collect::>>()?; + + let partition_keys = exec + .partition_keys() + .iter() + .map(|e| serialize_physical_expr(e, extension_codec)) + .collect::>>()?; + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Window(Box::new( + protobuf::WindowAggExecNode { + input: Some(Box::new(input)), + window_expr, + partition_keys, + input_order_mode: None, + }, + ))), + }) + } - let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); - let filter = exec - .filter() - .as_ref() - .map(|f| { - let expression = - serialize_physical_expr(f.expression(), extension_codec)?; - let column_indices = f - .column_indices() - .iter() - .map(|i| { - let side: protobuf::JoinSide = i.side.to_owned().into(); - protobuf::ColumnIndex { - index: i.index as u32, - side: side.into(), - } - }) - .collect(); - let schema = f.schema().as_ref().try_into()?; - Ok(protobuf::JoinFilter { - expression: Some(expression), - column_indices, - schema: Some(schema), - }) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; - - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::NestedLoopJoin(Box::new( - protobuf::NestedLoopJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - join_type: join_type.into(), - filter, - projection: exec.projection().map_or_else(Vec::new, |v| { - v.iter().map(|x| *x as u32).collect::>() - }), + fn try_from_bounded_window_agg_exec( + exec: &BoundedWindowAggExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + let window_expr = exec + .window_expr() + .iter() + .map(|e| serialize_physical_window_expr(e, extension_codec)) + .collect::>>()?; + + let partition_keys = exec + .partition_keys() + .iter() + .map(|e| serialize_physical_expr(e, extension_codec)) + .collect::>>()?; + + let input_order_mode = match &exec.input_order_mode { + InputOrderMode::Linear => { + window_agg_exec_node::InputOrderMode::Linear(protobuf::EmptyMessage {}) + } + InputOrderMode::PartiallySorted(columns) => { + window_agg_exec_node::InputOrderMode::PartiallySorted( + protobuf::PartiallySortedInputOrderMode { + columns: columns.iter().map(|c| *c as u64).collect(), }, - ))), - }); - } + ) + } + InputOrderMode::Sorted => { + window_agg_exec_node::InputOrderMode::Sorted(protobuf::EmptyMessage {}) + } + }; + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Window(Box::new( + protobuf::WindowAggExecNode { + input: Some(Box::new(input)), + window_expr, + partition_keys, + input_order_mode: Some(input_order_mode), + }, + ))), + }) + } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + fn try_from_data_sink_exec( + exec: &DataSinkExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result> { + let input: protobuf::PhysicalPlanNode = + protobuf::PhysicalPlanNode::try_from_physical_plan( exec.input().to_owned(), extension_codec, )?; + let sort_order = match exec.sort_order() { + Some(requirements) => { + let expr = requirements + .iter() + .map(|requirement| { + let expr: PhysicalSortExpr = requirement.to_owned().into(); + let sort_expr = protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }; + Ok(sort_expr) + }) + .collect::>>()?; + Some(protobuf::PhysicalSortExprNodeCollection { + physical_sort_expr_nodes: expr, + }) + } + None => None, + }; - let window_expr = exec - .window_expr() - .iter() - .map(|e| serialize_physical_window_expr(e, extension_codec)) - .collect::>>()?; - - let partition_keys = exec - .partition_keys() - .iter() - .map(|e| serialize_physical_expr(e, extension_codec)) - .collect::>>()?; - - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Window(Box::new( - protobuf::WindowAggExecNode { + if let Some(sink) = exec.sink().as_any().downcast_ref::() { + return Ok(Some(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::JsonSink(Box::new( + protobuf::JsonSinkExecNode { input: Some(Box::new(input)), - window_expr, - partition_keys, - input_order_mode: None, + sink: Some(sink.try_into()?), + sink_schema: Some(exec.schema().as_ref().try_into()?), + sort_order, }, ))), - }); + })); } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - - let window_expr = exec - .window_expr() - .iter() - .map(|e| serialize_physical_window_expr(e, extension_codec)) - .collect::>>()?; - - let partition_keys = exec - .partition_keys() - .iter() - .map(|e| serialize_physical_expr(e, extension_codec)) - .collect::>>()?; - - let input_order_mode = match &exec.input_order_mode { - InputOrderMode::Linear => window_agg_exec_node::InputOrderMode::Linear( - protobuf::EmptyMessage {}, - ), - InputOrderMode::PartiallySorted(columns) => { - window_agg_exec_node::InputOrderMode::PartiallySorted( - protobuf::PartiallySortedInputOrderMode { - columns: columns.iter().map(|c| *c as u64).collect(), - }, - ) - } - InputOrderMode::Sorted => window_agg_exec_node::InputOrderMode::Sorted( - protobuf::EmptyMessage {}, - ), - }; - - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Window(Box::new( - protobuf::WindowAggExecNode { + if let Some(sink) = exec.sink().as_any().downcast_ref::() { + return Ok(Some(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CsvSink(Box::new( + protobuf::CsvSinkExecNode { input: Some(Box::new(input)), - window_expr, - partition_keys, - input_order_mode: Some(input_order_mode), + sink: Some(sink.try_into()?), + sink_schema: Some(exec.schema().as_ref().try_into()?), + sort_order, }, ))), - }); - } - - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - let sort_order = match exec.sort_order() { - Some(requirements) => { - let expr = requirements - .iter() - .map(|requirement| { - let expr: PhysicalSortExpr = requirement.to_owned().into(); - let sort_expr = protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }; - Ok(sort_expr) - }) - .collect::>>()?; - Some(protobuf::PhysicalSortExprNodeCollection { - physical_sort_expr_nodes: expr, - }) - } - None => None, - }; - - if let Some(sink) = exec.sink().as_any().downcast_ref::() { - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::JsonSink(Box::new( - protobuf::JsonSinkExecNode { - input: Some(Box::new(input)), - sink: Some(sink.try_into()?), - sink_schema: Some(exec.schema().as_ref().try_into()?), - sort_order, - }, - ))), - }); - } - - if let Some(sink) = exec.sink().as_any().downcast_ref::() { - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CsvSink(Box::new( - protobuf::CsvSinkExecNode { - input: Some(Box::new(input)), - sink: Some(sink.try_into()?), - sink_schema: Some(exec.schema().as_ref().try_into()?), - sort_order, - }, - ))), - }); - } - - #[cfg(feature = "parquet")] - if let Some(sink) = exec.sink().as_any().downcast_ref::() { - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::ParquetSink(Box::new( - protobuf::ParquetSinkExecNode { - input: Some(Box::new(input)), - sink: Some(sink.try_into()?), - sink_schema: Some(exec.schema().as_ref().try_into()?), - sort_order, - }, - ))), - }); - } - - // If unknown DataSink then let extension handle it + })); } - if let Some(exec) = plan.downcast_ref::() { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - - return Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Unnest(Box::new( - protobuf::UnnestExecNode { + #[cfg(feature = "parquet")] + if let Some(sink) = exec.sink().as_any().downcast_ref::() { + return Ok(Some(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::ParquetSink(Box::new( + protobuf::ParquetSinkExecNode { input: Some(Box::new(input)), - schema: Some(exec.schema().try_into()?), - list_type_columns: exec - .list_column_indices() - .iter() - .map(|c| ProtoListUnnest { - index_in_input_schema: c.index_in_input_schema as _, - depth: c.depth as _, - }) - .collect(), - struct_type_columns: exec - .struct_column_indices() - .iter() - .map(|c| *c as _) - .collect(), - options: Some(exec.options().into()), + sink: Some(sink.try_into()?), + sink_schema: Some(exec.schema().as_ref().try_into()?), + sort_order, }, ))), - }); + })); } - let mut buf: Vec = vec![]; - match extension_codec.try_encode(Arc::clone(&plan_clone), &mut buf) { - Ok(_) => { - let inputs: Vec = plan_clone - .children() - .into_iter() - .cloned() - .map(|i| { - protobuf::PhysicalPlanNode::try_from_physical_plan( - i, - extension_codec, - ) - }) - .collect::>()?; + // If unknown DataSink then let extension handle it + Ok(None) + } - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Extension( - protobuf::PhysicalExtensionNode { node: buf, inputs }, - )), - }) - } - Err(e) => internal_err!( - "Unsupported plan and extension codec failed with [{e}]. Plan: {plan_clone:?}" - ), - } + fn try_from_unnest_exec( + exec: &UnnestExec, + extension_codec: &dyn PhysicalExtensionCodec, + ) -> Result { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Unnest(Box::new( + protobuf::UnnestExecNode { + input: Some(Box::new(input)), + schema: Some(exec.schema().try_into()?), + list_type_columns: exec + .list_column_indices() + .iter() + .map(|c| ProtoListUnnest { + index_in_input_schema: c.index_in_input_schema as _, + depth: c.depth as _, + }) + .collect(), + struct_type_columns: exec + .struct_column_indices() + .iter() + .map(|c| *c as _) + .collect(), + options: Some(exec.options().into()), + }, + ))), + }) } } diff --git a/datafusion/proto/src/physical_plan/to_proto.rs b/datafusion/proto/src/physical_plan/to_proto.rs index c196595eeed4a..d1b1f51ae1075 100644 --- a/datafusion/proto/src/physical_plan/to_proto.rs +++ b/datafusion/proto/src/physical_plan/to_proto.rs @@ -22,6 +22,7 @@ use datafusion::datasource::file_format::parquet::ParquetSink; use datafusion::datasource::physical_plan::FileSink; use datafusion::physical_expr::window::{SlidingAggregateWindowExpr, StandardWindowExpr}; use datafusion::physical_expr::{LexOrdering, PhysicalSortExpr, ScalarFunctionExpr}; +use datafusion::physical_expr_common::physical_expr::snapshot_physical_expr; use datafusion::physical_plan::expressions::{ BinaryExpr, CaseExpr, CastExpr, Column, InListExpr, IsNotNullExpr, IsNullExpr, Literal, NegativeExpr, NotExpr, TryCastExpr, UnKnownColumn, @@ -210,6 +211,9 @@ pub fn serialize_physical_expr( value: &Arc, codec: &dyn PhysicalExtensionCodec, ) -> Result { + // Snapshot the expr in case it has dynamic predicate state so + // it can be serialized + let value = snapshot_physical_expr(Arc::clone(value))?; let expr = value.as_any(); if let Some(expr) = expr.downcast_ref::() { @@ -368,7 +372,7 @@ pub fn serialize_physical_expr( }) } else { let mut buf: Vec = vec![]; - match codec.try_encode_expr(value, &mut buf) { + match codec.try_encode_expr(&value, &mut buf) { Ok(_) => { let inputs: Vec = value .children() @@ -441,7 +445,7 @@ impl TryFrom<&PartitionedFile> for protobuf::PartitionedFile { })? as u64; Ok(protobuf::PartitionedFile { path: pf.object_meta.location.as_ref().to_owned(), - size: pf.object_meta.size as u64, + size: pf.object_meta.size, last_modified_ns, partition_values: pf .partition_values @@ -449,7 +453,7 @@ impl TryFrom<&PartitionedFile> for protobuf::PartitionedFile { .map(|v| v.try_into()) .collect::, _>>()?, range: pf.range.as_ref().map(|r| r.try_into()).transpose()?, - statistics: pf.statistics.as_ref().map(|s| s.into()), + statistics: pf.statistics.as_ref().map(|s| s.as_ref().into()), }) } } @@ -507,7 +511,7 @@ pub fn serialize_file_scan_config( Ok(protobuf::FileScanExecConf { file_groups, - statistics: Some((&conf.statistics).into()), + statistics: Some((&conf.file_source.statistics().unwrap()).into()), limit: conf.limit.map(|l| protobuf::ScanLimit { limit: l as u32 }), projection: conf .projection diff --git a/datafusion/proto/tests/cases/roundtrip_logical_plan.rs b/datafusion/proto/tests/cases/roundtrip_logical_plan.rs index 9fa1f74ae188a..7ecb2c23a5e13 100644 --- a/datafusion/proto/tests/cases/roundtrip_logical_plan.rs +++ b/datafusion/proto/tests/cases/roundtrip_logical_plan.rs @@ -24,7 +24,10 @@ use arrow::datatypes::{ DECIMAL256_MAX_PRECISION, }; use arrow::util::pretty::pretty_format_batches; -use datafusion::datasource::file_format::json::JsonFormatFactory; +use datafusion::datasource::file_format::json::{JsonFormat, JsonFormatFactory}; +use datafusion::datasource::listing::{ + ListingOptions, ListingTable, ListingTableConfig, ListingTableUrl, +}; use datafusion::optimizer::eliminate_nested_union::EliminateNestedUnion; use datafusion::optimizer::Optimizer; use datafusion_common::parsers::CompressionTypeVariant; @@ -970,8 +973,8 @@ async fn roundtrip_expr_api() -> Result<()> { stddev_pop(lit(2.2)), approx_distinct(lit(2)), approx_median(lit(2)), - approx_percentile_cont(lit(2), lit(0.5), None), - approx_percentile_cont(lit(2), lit(0.5), Some(lit(50))), + approx_percentile_cont(lit(2).sort(true, false), lit(0.5), None), + approx_percentile_cont(lit(2).sort(true, false), lit(0.5), Some(lit(50))), approx_percentile_cont_with_weight(lit(2), lit(1), lit(0.5)), grouping(lit(1)), bit_and(lit(2)), @@ -2559,3 +2562,33 @@ async fn roundtrip_union_query() -> Result<()> { ); Ok(()) } + +#[tokio::test] +async fn roundtrip_custom_listing_tables_schema() -> Result<()> { + let ctx = SessionContext::new(); + // Make sure during round-trip, constraint information is preserved + let file_format = JsonFormat::default(); + let table_partition_cols = vec![("part".to_owned(), DataType::Int64)]; + let data = "../core/tests/data/partitioned_table_json"; + let listing_table_url = ListingTableUrl::parse(data)?; + let listing_options = ListingOptions::new(Arc::new(file_format)) + .with_table_partition_cols(table_partition_cols); + + let config = ListingTableConfig::new(listing_table_url) + .with_listing_options(listing_options) + .infer_schema(&ctx.state()) + .await?; + + ctx.register_table("hive_style", Arc::new(ListingTable::try_new(config)?))?; + + let plan = ctx + .sql("SELECT part, value FROM hive_style LIMIT 1") + .await? + .logical_plan() + .clone(); + + let bytes = logical_plan_to_bytes(&plan)?; + let new_plan = logical_plan_from_bytes(&bytes, &ctx)?; + assert_eq!(plan, new_plan); + Ok(()) +} diff --git a/datafusion/proto/tests/cases/roundtrip_physical_plan.rs b/datafusion/proto/tests/cases/roundtrip_physical_plan.rs index 6356b8b7b0cf4..6dddbb5ea0a0e 100644 --- a/datafusion/proto/tests/cases/roundtrip_physical_plan.rs +++ b/datafusion/proto/tests/cases/roundtrip_physical_plan.rs @@ -504,7 +504,7 @@ fn rountrip_aggregate_with_approx_pencentile_cont() -> Result<()> { vec![col("b", &schema)?, lit(0.5)], ) .schema(Arc::clone(&schema)) - .alias("APPROX_PERCENTILE_CONT(b, 0.5)") + .alias("APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY b)") .build() .map(Arc::new)?]; @@ -1676,3 +1676,47 @@ async fn roundtrip_empty_projection() -> Result<()> { let sql = "select 1 from alltypes_plain"; roundtrip_test_sql_with_context(sql, &ctx).await } + +#[tokio::test] +async fn roundtrip_physical_plan_node() { + use datafusion::prelude::*; + use datafusion_proto::physical_plan::{ + AsExecutionPlan, DefaultPhysicalExtensionCodec, + }; + use datafusion_proto::protobuf::PhysicalPlanNode; + + let ctx = SessionContext::new(); + + ctx.register_parquet( + "pt", + &format!( + "{}/alltypes_plain.snappy.parquet", + datafusion_common::test_util::parquet_test_data() + ), + ParquetReadOptions::default(), + ) + .await + .unwrap(); + + let plan = ctx + .sql("select id, string_col, timestamp_col from pt where id > 4 order by string_col") + .await + .unwrap() + .create_physical_plan() + .await + .unwrap(); + + let node: PhysicalPlanNode = + PhysicalPlanNode::try_from_physical_plan(plan, &DefaultPhysicalExtensionCodec {}) + .unwrap(); + + let plan = node + .try_into_physical_plan( + &ctx, + &ctx.runtime_env(), + &DefaultPhysicalExtensionCodec {}, + ) + .unwrap(); + + let _ = plan.execute(0, ctx.task_ctx()).unwrap(); +} diff --git a/datafusion/proto/tests/cases/serialize.rs b/datafusion/proto/tests/cases/serialize.rs index d1b50105d053d..d15e62909f7e4 100644 --- a/datafusion/proto/tests/cases/serialize.rs +++ b/datafusion/proto/tests/cases/serialize.rs @@ -260,7 +260,7 @@ fn test_expression_serialization_roundtrip() { for function in string::functions() { // default to 4 args (though some exprs like substr have error checking) let num_args = 4; - let args: Vec<_> = std::iter::repeat(&lit).take(num_args).cloned().collect(); + let args: Vec<_> = std::iter::repeat_n(&lit, num_args).cloned().collect(); let expr = Expr::ScalarFunction(ScalarFunction::new_udf(function, args)); let extension_codec = DefaultLogicalExtensionCodec {}; diff --git a/datafusion/sql/Cargo.toml b/datafusion/sql/Cargo.toml index 4435ee0f56cbc..b778db46769d0 100644 --- a/datafusion/sql/Cargo.toml +++ b/datafusion/sql/Cargo.toml @@ -61,5 +61,6 @@ datafusion-functions-aggregate = { workspace = true } datafusion-functions-nested = { workspace = true } datafusion-functions-window = { workspace = true } env_logger = { workspace = true } +insta = { workspace = true } paste = "^1.0" rstest = { workspace = true } diff --git a/datafusion/sql/src/expr/function.rs b/datafusion/sql/src/expr/function.rs index 436f4388d8a31..c0cb5b38ff020 100644 --- a/datafusion/sql/src/expr/function.rs +++ b/datafusion/sql/src/expr/function.rs @@ -74,7 +74,7 @@ fn find_closest_match(candidates: Vec, target: &str) -> Option { }) } -/// Arguments to for a function call extracted from the SQL AST +/// Arguments for a function call extracted from the SQL AST #[derive(Debug)] struct FunctionArgs { /// Function name @@ -91,6 +91,8 @@ struct FunctionArgs { null_treatment: Option, /// DISTINCT distinct: bool, + /// WITHIN GROUP clause, if any + within_group: Vec, } impl FunctionArgs { @@ -115,6 +117,7 @@ impl FunctionArgs { filter, null_treatment, distinct: false, + within_group, }); }; @@ -144,6 +147,9 @@ impl FunctionArgs { } FunctionArgumentClause::OrderBy(oby) => { if order_by.is_some() { + if !within_group.is_empty() { + return plan_err!("ORDER BY clause is only permitted in WITHIN GROUP clause when a WITHIN GROUP is used"); + } return not_impl_err!("Calling {name}: Duplicated ORDER BY clause in function arguments"); } order_by = Some(oby); @@ -176,8 +182,10 @@ impl FunctionArgs { } } - if !within_group.is_empty() { - return not_impl_err!("WITHIN GROUP is not supported yet: {within_group:?}"); + if within_group.len() > 1 { + return not_impl_err!( + "Only a single ordering expression is permitted in a WITHIN GROUP clause" + ); } let order_by = order_by.unwrap_or_default(); @@ -190,6 +198,7 @@ impl FunctionArgs { filter, null_treatment, distinct, + within_group, }) } } @@ -210,8 +219,14 @@ impl SqlToRel<'_, S> { filter, null_treatment, distinct, + within_group, } = function_args; + if over.is_some() && !within_group.is_empty() { + return plan_err!("OVER and WITHIN GROUP clause are can not be used together. \ + OVER is for window function, whereas WITHIN GROUP is for ordered set aggregate function"); + } + // If function is a window function (it has an OVER clause), // it shouldn't have ordering requirement as function argument // required ordering should be defined in OVER clause. @@ -356,15 +371,49 @@ impl SqlToRel<'_, S> { } else { // User defined aggregate functions (UDAF) have precedence in case it has the same name as a scalar built-in function if let Some(fm) = self.context_provider.get_aggregate_meta(&name) { - let order_by = self.order_by_to_sort_expr( - order_by, - schema, - planner_context, - true, - None, - )?; - let order_by = (!order_by.is_empty()).then_some(order_by); - let args = self.function_args_to_expr(args, schema, planner_context)?; + if fm.is_ordered_set_aggregate() && within_group.is_empty() { + return plan_err!("WITHIN GROUP clause is required when calling ordered set aggregate function({})", fm.name()); + } + + if null_treatment.is_some() && !fm.supports_null_handling_clause() { + return plan_err!( + "[IGNORE | RESPECT] NULLS are not permitted for {}", + fm.name() + ); + } + + let mut args = + self.function_args_to_expr(args, schema, planner_context)?; + + let order_by = if fm.is_ordered_set_aggregate() { + let within_group = self.order_by_to_sort_expr( + within_group, + schema, + planner_context, + false, + None, + )?; + + // add target column expression in within group clause to function arguments + if !within_group.is_empty() { + args = within_group + .iter() + .map(|sort| sort.expr.clone()) + .chain(args) + .collect::>(); + } + (!within_group.is_empty()).then_some(within_group) + } else { + let order_by = self.order_by_to_sort_expr( + order_by, + schema, + planner_context, + true, + None, + )?; + (!order_by.is_empty()).then_some(order_by) + }; + let filter: Option> = filter .map(|e| self.sql_expr_to_logical_expr(*e, schema, planner_context)) .transpose()? diff --git a/datafusion/sql/src/expr/value.rs b/datafusion/sql/src/expr/value.rs index d53691ef05d17..be4a45a25750c 100644 --- a/datafusion/sql/src/expr/value.rs +++ b/datafusion/sql/src/expr/value.rs @@ -301,7 +301,7 @@ fn interval_literal(interval_value: SQLExpr, negative: bool) -> Result { fn try_decode_hex_literal(s: &str) -> Option> { let hex_bytes = s.as_bytes(); - let mut decoded_bytes = Vec::with_capacity((hex_bytes.len() + 1) / 2); + let mut decoded_bytes = Vec::with_capacity(hex_bytes.len().div_ceil(2)); let start_idx = hex_bytes.len() % 2; if start_idx > 0 { diff --git a/datafusion/sql/src/parser.rs b/datafusion/sql/src/parser.rs index 822b651eae864..27c897f7ad608 100644 --- a/datafusion/sql/src/parser.rs +++ b/datafusion/sql/src/parser.rs @@ -20,9 +20,9 @@ //! This parser implements DataFusion specific statements such as //! `CREATE EXTERNAL TABLE` -use std::collections::VecDeque; -use std::fmt; - +use datafusion_common::config::SqlParserOptions; +use datafusion_common::DataFusionError; +use datafusion_common::{sql_err, Diagnostic, Span}; use sqlparser::ast::{ExprWithAlias, OrderByOptions}; use sqlparser::tokenizer::TokenWithSpan; use sqlparser::{ @@ -34,6 +34,8 @@ use sqlparser::{ parser::{Parser, ParserError}, tokenizer::{Token, Tokenizer, Word}, }; +use std::collections::VecDeque; +use std::fmt; // Use `Parser::expected` instead, if possible macro_rules! parser_err { @@ -42,7 +44,7 @@ macro_rules! parser_err { }; } -fn parse_file_type(s: &str) -> Result { +fn parse_file_type(s: &str) -> Result { Ok(s.to_uppercase()) } @@ -266,11 +268,9 @@ impl fmt::Display for Statement { } } -fn ensure_not_set(field: &Option, name: &str) -> Result<(), ParserError> { +fn ensure_not_set(field: &Option, name: &str) -> Result<(), DataFusionError> { if field.is_some() { - return Err(ParserError::ParserError(format!( - "{name} specified more than once", - ))); + parser_err!(format!("{name} specified more than once",))? } Ok(()) } @@ -285,6 +285,7 @@ fn ensure_not_set(field: &Option, name: &str) -> Result<(), ParserError> { /// [`Statement`] for a list of this special syntax pub struct DFParser<'a> { pub parser: Parser<'a>, + options: SqlParserOptions, } /// Same as `sqlparser` @@ -356,21 +357,28 @@ impl<'a> DFParserBuilder<'a> { self } - pub fn build(self) -> Result, ParserError> { + pub fn build(self) -> Result, DataFusionError> { let mut tokenizer = Tokenizer::new(self.dialect, self.sql); - let tokens = tokenizer.tokenize_with_location()?; + // Convert TokenizerError -> ParserError + let tokens = tokenizer + .tokenize_with_location() + .map_err(ParserError::from)?; Ok(DFParser { parser: Parser::new(self.dialect) .with_tokens_with_locations(tokens) .with_recursion_limit(self.recursion_limit), + options: SqlParserOptions { + recursion_limit: self.recursion_limit, + ..Default::default() + }, }) } } impl<'a> DFParser<'a> { #[deprecated(since = "46.0.0", note = "DFParserBuilder")] - pub fn new(sql: &'a str) -> Result { + pub fn new(sql: &'a str) -> Result { DFParserBuilder::new(sql).build() } @@ -378,13 +386,13 @@ impl<'a> DFParser<'a> { pub fn new_with_dialect( sql: &'a str, dialect: &'a dyn Dialect, - ) -> Result { + ) -> Result { DFParserBuilder::new(sql).with_dialect(dialect).build() } /// Parse a sql string into one or [`Statement`]s using the /// [`GenericDialect`]. - pub fn parse_sql(sql: &'a str) -> Result, ParserError> { + pub fn parse_sql(sql: &'a str) -> Result, DataFusionError> { let mut parser = DFParserBuilder::new(sql).build()?; parser.parse_statements() @@ -395,7 +403,7 @@ impl<'a> DFParser<'a> { pub fn parse_sql_with_dialect( sql: &str, dialect: &dyn Dialect, - ) -> Result, ParserError> { + ) -> Result, DataFusionError> { let mut parser = DFParserBuilder::new(sql).with_dialect(dialect).build()?; parser.parse_statements() } @@ -403,14 +411,14 @@ impl<'a> DFParser<'a> { pub fn parse_sql_into_expr_with_dialect( sql: &str, dialect: &dyn Dialect, - ) -> Result { + ) -> Result { let mut parser = DFParserBuilder::new(sql).with_dialect(dialect).build()?; parser.parse_expr() } /// Parse a sql string into one or [`Statement`]s - pub fn parse_statements(&mut self) -> Result, ParserError> { + pub fn parse_statements(&mut self) -> Result, DataFusionError> { let mut stmts = VecDeque::new(); let mut expecting_statement_delimiter = false; loop { @@ -438,12 +446,26 @@ impl<'a> DFParser<'a> { &self, expected: &str, found: TokenWithSpan, - ) -> Result { - parser_err!(format!("Expected {expected}, found: {found}")) + ) -> Result { + let sql_parser_span = found.span; + parser_err!(format!( + "Expected: {expected}, found: {found}{}", + found.span.start + )) + .map_err(|e| { + let e = DataFusionError::from(e); + let span = Span::try_from_sqlparser_span(sql_parser_span); + let diagnostic = Diagnostic::new_error( + format!("Expected: {expected}, found: {found}{}", found.span.start), + span, + ); + + e.with_diagnostic(diagnostic) + }) } /// Parse a new expression - pub fn parse_statement(&mut self) -> Result { + pub fn parse_statement(&mut self) -> Result { match self.parser.peek_token().token { Token::Word(w) => { match w.keyword { @@ -455,9 +477,7 @@ impl<'a> DFParser<'a> { if let Token::Word(w) = self.parser.peek_nth_token(1).token { // use native parser for COPY INTO if w.keyword == Keyword::INTO { - return Ok(Statement::Statement(Box::from( - self.parser.parse_statement()?, - ))); + return self.parse_and_handle_statement(); } } self.parser.next_token(); // COPY @@ -469,36 +489,49 @@ impl<'a> DFParser<'a> { } _ => { // use sqlparser-rs parser - Ok(Statement::Statement(Box::from( - self.parser.parse_statement()?, - ))) + self.parse_and_handle_statement() } } } _ => { // use the native parser - Ok(Statement::Statement(Box::from( - self.parser.parse_statement()?, - ))) + self.parse_and_handle_statement() } } } - pub fn parse_expr(&mut self) -> Result { + pub fn parse_expr(&mut self) -> Result { if let Token::Word(w) = self.parser.peek_token().token { match w.keyword { Keyword::CREATE | Keyword::COPY | Keyword::EXPLAIN => { - return parser_err!("Unsupported command in expression"); + return parser_err!("Unsupported command in expression")?; } _ => {} } } - self.parser.parse_expr_with_alias() + Ok(self.parser.parse_expr_with_alias()?) + } + + /// Helper method to parse a statement and handle errors consistently, especially for recursion limits + fn parse_and_handle_statement(&mut self) -> Result { + self.parser + .parse_statement() + .map(|stmt| Statement::Statement(Box::from(stmt))) + .map_err(|e| match e { + ParserError::RecursionLimitExceeded => DataFusionError::SQL( + ParserError::RecursionLimitExceeded, + Some(format!( + " (current limit: {})", + self.options.recursion_limit + )), + ), + other => DataFusionError::SQL(other, None), + }) } /// Parse a SQL `COPY TO` statement - pub fn parse_copy(&mut self) -> Result { + pub fn parse_copy(&mut self) -> Result { // parse as a query let source = if self.parser.consume_token(&Token::LParen) { let query = self.parser.parse_query()?; @@ -541,7 +574,7 @@ impl<'a> DFParser<'a> { Keyword::WITH => { self.parser.expect_keyword(Keyword::HEADER)?; self.parser.expect_keyword(Keyword::ROW)?; - return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS ('format.has_header' 'true')"); + return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS ('format.has_header' 'true')")?; } Keyword::PARTITIONED => { self.parser.expect_keyword(Keyword::BY)?; @@ -561,17 +594,13 @@ impl<'a> DFParser<'a> { if token == Token::EOF || token == Token::SemiColon { break; } else { - return Err(ParserError::ParserError(format!( - "Unexpected token {token}" - ))); + return self.expected("end of statement or ;", token)?; } } } let Some(target) = builder.target else { - return Err(ParserError::ParserError( - "Missing TO clause in COPY statement".into(), - )); + return parser_err!("Missing TO clause in COPY statement")?; }; Ok(Statement::CopyTo(CopyToStatement { @@ -589,7 +618,7 @@ impl<'a> DFParser<'a> { /// because it allows keywords as well as other non words /// /// [`parse_literal_string`]: sqlparser::parser::Parser::parse_literal_string - pub fn parse_option_key(&mut self) -> Result { + pub fn parse_option_key(&mut self) -> Result { let next_token = self.parser.next_token(); match next_token.token { Token::Word(Word { value, .. }) => { @@ -602,7 +631,7 @@ impl<'a> DFParser<'a> { // Unquoted namespaced keys have to conform to the syntax // "[\.]*". If we have a key that breaks this // pattern, error out: - return self.parser.expected("key name", next_token); + return self.expected("key name", next_token); } } Ok(parts.join(".")) @@ -610,7 +639,7 @@ impl<'a> DFParser<'a> { Token::SingleQuotedString(s) => Ok(s), Token::DoubleQuotedString(s) => Ok(s), Token::EscapedStringLiteral(s) => Ok(s), - _ => self.parser.expected("key name", next_token), + _ => self.expected("key name", next_token), } } @@ -620,7 +649,7 @@ impl<'a> DFParser<'a> { /// word or keyword in this location. /// /// [`parse_value`]: sqlparser::parser::Parser::parse_value - pub fn parse_option_value(&mut self) -> Result { + pub fn parse_option_value(&mut self) -> Result { let next_token = self.parser.next_token(); match next_token.token { // e.g. things like "snappy" or "gzip" that may be keywords @@ -629,12 +658,12 @@ impl<'a> DFParser<'a> { Token::DoubleQuotedString(s) => Ok(Value::DoubleQuotedString(s)), Token::EscapedStringLiteral(s) => Ok(Value::EscapedStringLiteral(s)), Token::Number(n, l) => Ok(Value::Number(n, l)), - _ => self.parser.expected("string or numeric value", next_token), + _ => self.expected("string or numeric value", next_token), } } /// Parse a SQL `EXPLAIN` - pub fn parse_explain(&mut self) -> Result { + pub fn parse_explain(&mut self) -> Result { let analyze = self.parser.parse_keyword(Keyword::ANALYZE); let verbose = self.parser.parse_keyword(Keyword::VERBOSE); let format = self.parse_explain_format()?; @@ -649,7 +678,7 @@ impl<'a> DFParser<'a> { })) } - pub fn parse_explain_format(&mut self) -> Result, ParserError> { + pub fn parse_explain_format(&mut self) -> Result, DataFusionError> { if !self.parser.parse_keyword(Keyword::FORMAT) { return Ok(None); } @@ -659,15 +688,13 @@ impl<'a> DFParser<'a> { Token::Word(w) => Ok(w.value), Token::SingleQuotedString(w) => Ok(w), Token::DoubleQuotedString(w) => Ok(w), - _ => self - .parser - .expected("an explain format such as TREE", next_token), + _ => self.expected("an explain format such as TREE", next_token), }?; Ok(Some(format)) } /// Parse a SQL `CREATE` statement handling `CREATE EXTERNAL TABLE` - pub fn parse_create(&mut self) -> Result { + pub fn parse_create(&mut self) -> Result { if self.parser.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(false) } else if self.parser.parse_keyword(Keyword::UNBOUNDED) { @@ -678,7 +705,7 @@ impl<'a> DFParser<'a> { } } - fn parse_partitions(&mut self) -> Result, ParserError> { + fn parse_partitions(&mut self) -> Result, DataFusionError> { let mut partitions: Vec = vec![]; if !self.parser.consume_token(&Token::LParen) || self.parser.consume_token(&Token::RParen) @@ -708,7 +735,7 @@ impl<'a> DFParser<'a> { } /// Parse the ordering clause of a `CREATE EXTERNAL TABLE` SQL statement - pub fn parse_order_by_exprs(&mut self) -> Result, ParserError> { + pub fn parse_order_by_exprs(&mut self) -> Result, DataFusionError> { let mut values = vec![]; self.parser.expect_token(&Token::LParen)?; loop { @@ -721,7 +748,7 @@ impl<'a> DFParser<'a> { } /// Parse an ORDER BY sub-expression optionally followed by ASC or DESC. - pub fn parse_order_by_expr(&mut self) -> Result { + pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parser.parse_expr()?; let asc = if self.parser.parse_keyword(Keyword::ASC) { @@ -753,7 +780,7 @@ impl<'a> DFParser<'a> { // This is a copy of the equivalent implementation in sqlparser. fn parse_columns( &mut self, - ) -> Result<(Vec, Vec), ParserError> { + ) -> Result<(Vec, Vec), DataFusionError> { let mut columns = vec![]; let mut constraints = vec![]; if !self.parser.consume_token(&Token::LParen) @@ -789,7 +816,7 @@ impl<'a> DFParser<'a> { Ok((columns, constraints)) } - fn parse_column_def(&mut self) -> Result { + fn parse_column_def(&mut self) -> Result { let name = self.parser.parse_identifier()?; let data_type = self.parser.parse_data_type()?; let mut options = vec![]; @@ -820,7 +847,7 @@ impl<'a> DFParser<'a> { fn parse_create_external_table( &mut self, unbounded: bool, - ) -> Result { + ) -> Result { let temporary = self .parser .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) @@ -868,15 +895,15 @@ impl<'a> DFParser<'a> { } else { self.parser.expect_keyword(Keyword::HEADER)?; self.parser.expect_keyword(Keyword::ROW)?; - return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS (format.has_header true)"); + return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS (format.has_header true)")?; } } Keyword::DELIMITER => { - return parser_err!("DELIMITER clause is no longer in use. Please use the OPTIONS clause with 'format.delimiter' set appropriately, e.g., OPTIONS (format.delimiter ',')"); + return parser_err!("DELIMITER clause is no longer in use. Please use the OPTIONS clause with 'format.delimiter' set appropriately, e.g., OPTIONS (format.delimiter ',')")?; } Keyword::COMPRESSION => { self.parser.expect_keyword(Keyword::TYPE)?; - return parser_err!("COMPRESSION TYPE clause is no longer in use. Please use the OPTIONS clause with 'format.compression' set appropriately, e.g., OPTIONS (format.compression gzip)"); + return parser_err!("COMPRESSION TYPE clause is no longer in use. Please use the OPTIONS clause with 'format.compression' set appropriately, e.g., OPTIONS (format.compression gzip)")?; } Keyword::PARTITIONED => { self.parser.expect_keyword(Keyword::BY)?; @@ -899,7 +926,7 @@ impl<'a> DFParser<'a> { columns.extend(cols); if !cons.is_empty() { - return Err(ParserError::ParserError( + return sql_err!(ParserError::ParserError( "Constraints on Partition Columns are not supported" .to_string(), )); @@ -919,21 +946,19 @@ impl<'a> DFParser<'a> { if token == Token::EOF || token == Token::SemiColon { break; } else { - return Err(ParserError::ParserError(format!( - "Unexpected token {token}" - ))); + return self.expected("end of statement or ;", token)?; } } } // Validations: location and file_type are required if builder.file_type.is_none() { - return Err(ParserError::ParserError( + return sql_err!(ParserError::ParserError( "Missing STORED AS clause in CREATE EXTERNAL TABLE statement".into(), )); } if builder.location.is_none() { - return Err(ParserError::ParserError( + return sql_err!(ParserError::ParserError( "Missing LOCATION clause in CREATE EXTERNAL TABLE statement".into(), )); } @@ -955,7 +980,7 @@ impl<'a> DFParser<'a> { } /// Parses the set of valid formats - fn parse_file_format(&mut self) -> Result { + fn parse_file_format(&mut self) -> Result { let token = self.parser.next_token(); match &token.token { Token::Word(w) => parse_file_type(&w.value), @@ -967,7 +992,7 @@ impl<'a> DFParser<'a> { /// /// This method supports keywords as key names as well as multiple /// value types such as Numbers as well as Strings. - fn parse_value_options(&mut self) -> Result, ParserError> { + fn parse_value_options(&mut self) -> Result, DataFusionError> { let mut options = vec![]; self.parser.expect_token(&Token::LParen)?; @@ -999,7 +1024,7 @@ mod tests { use sqlparser::dialect::SnowflakeDialect; use sqlparser::tokenizer::Span; - fn expect_parse_ok(sql: &str, expected: Statement) -> Result<(), ParserError> { + fn expect_parse_ok(sql: &str, expected: Statement) -> Result<(), DataFusionError> { let statements = DFParser::parse_sql(sql)?; assert_eq!( statements.len(), @@ -1041,7 +1066,7 @@ mod tests { } #[test] - fn create_external_table() -> Result<(), ParserError> { + fn create_external_table() -> Result<(), DataFusionError> { // positive case let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV LOCATION 'foo.csv'"; let display = None; @@ -1262,13 +1287,13 @@ mod tests { "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV PARTITIONED BY (p1 int, c1) LOCATION 'foo.csv'"; expect_parse_error( sql, - "sql parser error: Expected: a data type name, found: )", + "SQL error: ParserError(\"Expected: a data type name, found: ) at Line: 1, Column: 73\")", ); // negative case: mixed column defs and column names in `PARTITIONED BY` clause let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV PARTITIONED BY (c1, p1 int) LOCATION 'foo.csv'"; - expect_parse_error(sql, "sql parser error: Expected ',' or ')' after partition definition, found: int"); + expect_parse_error(sql, "SQL error: ParserError(\"Expected: ',' or ')' after partition definition, found: int at Line: 1, Column: 70\")"); // positive case: additional options (one entry) can be specified let sql = @@ -1514,7 +1539,7 @@ mod tests { } #[test] - fn copy_to_table_to_table() -> Result<(), ParserError> { + fn copy_to_table_to_table() -> Result<(), DataFusionError> { // positive case let sql = "COPY foo TO bar STORED AS CSV"; let expected = Statement::CopyTo(CopyToStatement { @@ -1530,7 +1555,7 @@ mod tests { } #[test] - fn skip_copy_into_snowflake() -> Result<(), ParserError> { + fn skip_copy_into_snowflake() -> Result<(), DataFusionError> { let sql = "COPY INTO foo FROM @~/staged FILE_FORMAT = (FORMAT_NAME = 'mycsv');"; let dialect = Box::new(SnowflakeDialect); let statements = DFParser::parse_sql_with_dialect(sql, dialect.as_ref())?; @@ -1547,7 +1572,7 @@ mod tests { } #[test] - fn explain_copy_to_table_to_table() -> Result<(), ParserError> { + fn explain_copy_to_table_to_table() -> Result<(), DataFusionError> { let cases = vec![ ("EXPLAIN COPY foo TO bar STORED AS PARQUET", false, false), ( @@ -1588,7 +1613,7 @@ mod tests { } #[test] - fn copy_to_query_to_table() -> Result<(), ParserError> { + fn copy_to_query_to_table() -> Result<(), DataFusionError> { let statement = verified_stmt("SELECT 1"); // unwrap the various layers @@ -1621,7 +1646,7 @@ mod tests { } #[test] - fn copy_to_options() -> Result<(), ParserError> { + fn copy_to_options() -> Result<(), DataFusionError> { let sql = "COPY foo TO bar STORED AS CSV OPTIONS ('row_group_size' '55')"; let expected = Statement::CopyTo(CopyToStatement { source: object_name("foo"), @@ -1638,7 +1663,7 @@ mod tests { } #[test] - fn copy_to_partitioned_by() -> Result<(), ParserError> { + fn copy_to_partitioned_by() -> Result<(), DataFusionError> { let sql = "COPY foo TO bar STORED AS CSV PARTITIONED BY (a) OPTIONS ('row_group_size' '55')"; let expected = Statement::CopyTo(CopyToStatement { source: object_name("foo"), @@ -1655,7 +1680,7 @@ mod tests { } #[test] - fn copy_to_multi_options() -> Result<(), ParserError> { + fn copy_to_multi_options() -> Result<(), DataFusionError> { // order of options is preserved let sql = "COPY foo TO bar STORED AS parquet OPTIONS ('format.row_group_size' 55, 'format.compression' snappy, 'execution.keep_partition_by_columns' true)"; @@ -1754,7 +1779,7 @@ mod tests { assert_contains!( err.to_string(), - "sql parser error: recursion limit exceeded" + "SQL error: RecursionLimitExceeded (current limit: 1)" ); } } diff --git a/datafusion/sql/src/planner.rs b/datafusion/sql/src/planner.rs index 180017ee9c191..3325c98aa74b6 100644 --- a/datafusion/sql/src/planner.rs +++ b/datafusion/sql/src/planner.rs @@ -331,7 +331,7 @@ impl PlannerContext { /// /// Key interfaces are: /// * [`Self::sql_statement_to_plan`]: Convert a statement -/// (e.g. `SELECT ...`) into a [`LogicalPlan`] +/// (e.g. `SELECT ...`) into a [`LogicalPlan`] /// * [`Self::sql_to_expr`]: Convert an expression (e.g. `1 + 2`) into an [`Expr`] pub struct SqlToRel<'a, S: ContextProvider> { pub(crate) context_provider: &'a S, diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index 2a2d0b3b3eb8b..33994b60b7357 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -16,6 +16,7 @@ // under the License. use std::collections::HashSet; +use std::ops::ControlFlow; use std::sync::Arc; use crate::planner::{ContextProvider, PlannerContext, SqlToRel}; @@ -45,8 +46,8 @@ use datafusion_expr::{ use indexmap::IndexMap; use sqlparser::ast::{ - Distinct, Expr as SQLExpr, GroupByExpr, NamedWindowExpr, OrderBy, - SelectItemQualifiedWildcardKind, WildcardAdditionalOptions, WindowType, + visit_expressions_mut, Distinct, Expr as SQLExpr, GroupByExpr, NamedWindowExpr, + OrderBy, SelectItemQualifiedWildcardKind, WildcardAdditionalOptions, WindowType, }; use sqlparser::ast::{NamedWindowDefinition, Select, SelectItem, TableWithJoins}; @@ -84,7 +85,7 @@ impl SqlToRel<'_, S> { // Handle named windows before processing the projection expression check_conflicting_windows(&select.named_window)?; - match_window_definitions(&mut select.projection, &select.named_window)?; + self.match_window_definitions(&mut select.projection, &select.named_window)?; // Process the SELECT expressions let select_exprs = self.prepare_select_exprs( &base_plan, @@ -758,11 +759,11 @@ impl SqlToRel<'_, S> { /// # Arguments /// /// * `input` - The input plan that will be aggregated. The grouping, aggregate, and - /// "having" expressions must all be resolvable from this plan. + /// "having" expressions must all be resolvable from this plan. /// * `select_exprs` - The projection expressions from the SELECT clause. /// * `having_expr_opt` - Optional HAVING clause. /// * `group_by_exprs` - Grouping expressions from the GROUP BY clause. These can be column - /// references or more complex expressions. + /// references or more complex expressions. /// * `aggr_exprs` - Aggregate expressions, such as `SUM(a)` or `COUNT(1)`. /// /// # Return @@ -771,9 +772,9 @@ impl SqlToRel<'_, S> { /// /// * `plan` - A [LogicalPlan::Aggregate] plan for the newly created aggregate. /// * `select_exprs_post_aggr` - The projection expressions rewritten to reference columns from - /// the aggregate + /// the aggregate /// * `having_expr_post_aggr` - The "having" expression rewritten to reference a column from - /// the aggregate + /// the aggregate fn aggregate( &self, input: &LogicalPlan, @@ -867,6 +868,61 @@ impl SqlToRel<'_, S> { Ok((plan, select_exprs_post_aggr, having_expr_post_aggr)) } + + // If the projection is done over a named window, that window + // name must be defined. Otherwise, it gives an error. + fn match_window_definitions( + &self, + projection: &mut [SelectItem], + named_windows: &[NamedWindowDefinition], + ) -> Result<()> { + let named_windows: Vec<(&NamedWindowDefinition, String)> = named_windows + .iter() + .map(|w| (w, self.ident_normalizer.normalize(w.0.clone()))) + .collect(); + for proj in projection.iter_mut() { + if let SelectItem::ExprWithAlias { expr, alias: _ } + | SelectItem::UnnamedExpr(expr) = proj + { + let mut err = None; + visit_expressions_mut(expr, |expr| { + if let SQLExpr::Function(f) = expr { + if let Some(WindowType::NamedWindow(ident)) = &f.over { + let normalized_ident = + self.ident_normalizer.normalize(ident.clone()); + for ( + NamedWindowDefinition(_, window_expr), + normalized_window_ident, + ) in named_windows.iter() + { + if normalized_ident.eq(normalized_window_ident) { + f.over = Some(match window_expr { + NamedWindowExpr::NamedWindow(ident) => { + WindowType::NamedWindow(ident.clone()) + } + NamedWindowExpr::WindowSpec(spec) => { + WindowType::WindowSpec(spec.clone()) + } + }) + } + } + // All named windows must be defined with a WindowSpec. + if let Some(WindowType::NamedWindow(ident)) = &f.over { + err = + Some(plan_err!("The window {ident} is not defined!")); + return ControlFlow::Break(()); + } + } + } + ControlFlow::Continue(()) + }); + if let Some(err) = err { + return err; + } + } + } + Ok(()) + } } // If there are any multiple-defined windows, we raise an error. @@ -883,39 +939,3 @@ fn check_conflicting_windows(window_defs: &[NamedWindowDefinition]) -> Result<() } Ok(()) } - -// If the projection is done over a named window, that window -// name must be defined. Otherwise, it gives an error. -fn match_window_definitions( - projection: &mut [SelectItem], - named_windows: &[NamedWindowDefinition], -) -> Result<()> { - for proj in projection.iter_mut() { - if let SelectItem::ExprWithAlias { - expr: SQLExpr::Function(f), - alias: _, - } - | SelectItem::UnnamedExpr(SQLExpr::Function(f)) = proj - { - for NamedWindowDefinition(window_ident, window_expr) in named_windows.iter() { - if let Some(WindowType::NamedWindow(ident)) = &f.over { - if ident.eq(window_ident) { - f.over = Some(match window_expr { - NamedWindowExpr::NamedWindow(ident) => { - WindowType::NamedWindow(ident.clone()) - } - NamedWindowExpr::WindowSpec(spec) => { - WindowType::WindowSpec(spec.clone()) - } - }) - } - } - } - // All named windows must be defined with a WindowSpec. - if let Some(WindowType::NamedWindow(ident)) = &f.over { - return plan_err!("The window {ident} is not defined!"); - } - } - } - Ok(()) -} diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index fc6cb0d32feff..1f1c235fee6f4 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -1034,7 +1034,7 @@ impl SqlToRel<'_, S> { TransactionMode::AccessMode(_) => None, TransactionMode::IsolationLevel(level) => Some(level), }) - .last() + .next_back() .copied() .unwrap_or(ast::TransactionIsolationLevel::Serializable); let access_mode: ast::TransactionAccessMode = modes @@ -1043,7 +1043,7 @@ impl SqlToRel<'_, S> { TransactionMode::AccessMode(mode) => Some(mode), TransactionMode::IsolationLevel(_) => None, }) - .last() + .next_back() .copied() .unwrap_or(ast::TransactionAccessMode::ReadWrite); let isolation_level = match isolation_level { @@ -1340,11 +1340,7 @@ impl SqlToRel<'_, S> { let options_map = self.parse_options_map(statement.options, true)?; let maybe_file_type = if let Some(stored_as) = &statement.stored_as { - if let Ok(ext_file_type) = self.context_provider.get_file_type(stored_as) { - Some(ext_file_type) - } else { - None - } + self.context_provider.get_file_type(stored_as).ok() } else { None }; @@ -1547,7 +1543,7 @@ impl SqlToRel<'_, S> { } /// Convert each [TableConstraint] to corresponding [Constraint] - fn new_constraint_from_table_constraints( + pub fn new_constraint_from_table_constraints( &self, constraints: &[TableConstraint], df_schema: &DFSchemaRef, diff --git a/datafusion/sql/src/unparser/ast.rs b/datafusion/sql/src/unparser/ast.rs index 6fcc203637cc3..117971af762a1 100644 --- a/datafusion/sql/src/unparser/ast.rs +++ b/datafusion/sql/src/unparser/ast.rs @@ -32,6 +32,8 @@ pub struct QueryBuilder { fetch: Option, locks: Vec, for_clause: Option, + // If true, we need to unparse LogicalPlan::Union as a SQL `UNION` rather than a `UNION ALL`. + distinct_union: bool, } #[allow(dead_code)] @@ -75,6 +77,13 @@ impl QueryBuilder { self.for_clause = value; self } + pub fn distinct_union(&mut self) -> &mut Self { + self.distinct_union = true; + self + } + pub fn is_distinct_union(&self) -> bool { + self.distinct_union + } pub fn build(&self) -> Result { let order_by = self .order_by_kind @@ -112,6 +121,7 @@ impl QueryBuilder { fetch: Default::default(), locks: Default::default(), for_clause: Default::default(), + distinct_union: false, } } } @@ -155,6 +165,11 @@ impl SelectBuilder { self.projection = value; self } + pub fn pop_projections(&mut self) -> Vec { + let ret = self.projection.clone(); + self.projection.clear(); + ret + } pub fn already_projected(&self) -> bool { !self.projection.is_empty() } diff --git a/datafusion/sql/src/unparser/dialect.rs b/datafusion/sql/src/unparser/dialect.rs index 05914b98f55f0..a7bde967f2fa4 100644 --- a/datafusion/sql/src/unparser/dialect.rs +++ b/datafusion/sql/src/unparser/dialect.rs @@ -17,7 +17,10 @@ use std::{collections::HashMap, sync::Arc}; -use super::{utils::character_length_to_sql, utils::date_part_to_sql, Unparser}; +use super::{ + utils::character_length_to_sql, utils::date_part_to_sql, + utils::sqlite_date_trunc_to_sql, utils::sqlite_from_unixtime_to_sql, Unparser, +}; use arrow::datatypes::TimeUnit; use datafusion_common::Result; use datafusion_expr::Expr; @@ -490,6 +493,8 @@ impl Dialect for SqliteDialect { "character_length" => { character_length_to_sql(unparser, self.character_length_style(), args) } + "from_unixtime" => sqlite_from_unixtime_to_sql(unparser, args), + "date_trunc" => sqlite_date_trunc_to_sql(unparser, args), _ => Ok(None), } } diff --git a/datafusion/sql/src/unparser/expr.rs b/datafusion/sql/src/unparser/expr.rs index 064adde55bdfd..bf3e7880bea8f 100644 --- a/datafusion/sql/src/unparser/expr.rs +++ b/datafusion/sql/src/unparser/expr.rs @@ -293,6 +293,7 @@ impl Unparser<'_> { distinct, args, filter, + order_by, .. } = &agg.params; @@ -301,6 +302,16 @@ impl Unparser<'_> { Some(filter) => Some(Box::new(self.expr_to_sql_inner(filter)?)), None => None, }; + let within_group = if agg.func.is_ordered_set_aggregate() { + order_by + .as_ref() + .unwrap_or(&Vec::new()) + .iter() + .map(|sort_expr| self.sort_to_sql(sort_expr)) + .collect::>>()? + } else { + Vec::new() + }; Ok(ast::Expr::Function(Function { name: ObjectName::from(vec![Ident { value: func_name.to_string(), @@ -316,7 +327,7 @@ impl Unparser<'_> { filter, null_treatment: None, over: None, - within_group: vec![], + within_group, parameters: ast::FunctionArguments::None, uses_odbc_syntax: false, })) @@ -1689,6 +1700,7 @@ mod tests { use std::ops::{Add, Sub}; use std::{any::Any, sync::Arc, vec}; + use crate::unparser::dialect::SqliteDialect; use arrow::array::{LargeListArray, ListArray}; use arrow::datatypes::{DataType::Int8, Field, Int32Type, Schema, TimeUnit}; use ast::ObjectName; @@ -1701,6 +1713,7 @@ mod tests { ScalarUDFImpl, Signature, Volatility, WindowFrame, WindowFunctionDefinition, }; use datafusion_expr::{interval_month_day_nano_lit, ExprFunctionExt}; + use datafusion_functions::datetime::from_unixtime::FromUnixtimeFunc; use datafusion_functions::expr_fn::{get_field, named_struct}; use datafusion_functions_aggregate::count::count_udaf; use datafusion_functions_aggregate::expr_fn::sum; @@ -1712,7 +1725,7 @@ mod tests { use crate::unparser::dialect::{ CharacterLengthStyle, CustomDialect, CustomDialectBuilder, DateFieldExtractStyle, - Dialect, DuckDBDialect, PostgreSqlDialect, ScalarFnToSqlHandler, + DefaultDialect, Dialect, DuckDBDialect, PostgreSqlDialect, ScalarFnToSqlHandler, }; use super::*; @@ -2871,6 +2884,115 @@ mod tests { Ok(()) } + #[test] + fn test_from_unixtime() -> Result<()> { + let default_dialect: Arc = Arc::new(DefaultDialect {}); + let sqlite_dialect: Arc = Arc::new(SqliteDialect {}); + + for (dialect, expected) in [ + (default_dialect, "from_unixtime(date_col)"), + (sqlite_dialect, "datetime(`date_col`, 'unixepoch')"), + ] { + let unparser = Unparser::new(dialect.as_ref()); + let expr = Expr::ScalarFunction(ScalarFunction { + func: Arc::new(ScalarUDF::from(FromUnixtimeFunc::new())), + args: vec![col("date_col")], + }); + + let ast = unparser.expr_to_sql(&expr)?; + + let actual = ast.to_string(); + let expected = expected.to_string(); + + assert_eq!(actual, expected); + } + Ok(()) + } + + #[test] + fn test_date_trunc() -> Result<()> { + let default_dialect: Arc = Arc::new(DefaultDialect {}); + let sqlite_dialect: Arc = Arc::new(SqliteDialect {}); + + for (dialect, precision, expected) in [ + ( + Arc::clone(&default_dialect), + "YEAR", + "date_trunc('YEAR', date_col)", + ), + ( + Arc::clone(&sqlite_dialect), + "YEAR", + "strftime('%Y', `date_col`)", + ), + ( + Arc::clone(&default_dialect), + "MONTH", + "date_trunc('MONTH', date_col)", + ), + ( + Arc::clone(&sqlite_dialect), + "MONTH", + "strftime('%Y-%m', `date_col`)", + ), + ( + Arc::clone(&default_dialect), + "DAY", + "date_trunc('DAY', date_col)", + ), + ( + Arc::clone(&sqlite_dialect), + "DAY", + "strftime('%Y-%m-%d', `date_col`)", + ), + ( + Arc::clone(&default_dialect), + "HOUR", + "date_trunc('HOUR', date_col)", + ), + ( + Arc::clone(&sqlite_dialect), + "HOUR", + "strftime('%Y-%m-%d %H', `date_col`)", + ), + ( + Arc::clone(&default_dialect), + "MINUTE", + "date_trunc('MINUTE', date_col)", + ), + ( + Arc::clone(&sqlite_dialect), + "MINUTE", + "strftime('%Y-%m-%d %H:%M', `date_col`)", + ), + (default_dialect, "SECOND", "date_trunc('SECOND', date_col)"), + ( + sqlite_dialect, + "SECOND", + "strftime('%Y-%m-%d %H:%M:%S', `date_col`)", + ), + ] { + let unparser = Unparser::new(dialect.as_ref()); + let expr = Expr::ScalarFunction(ScalarFunction { + func: Arc::new(ScalarUDF::from( + datafusion_functions::datetime::date_trunc::DateTruncFunc::new(), + )), + args: vec![ + Expr::Literal(ScalarValue::Utf8(Some(precision.to_string()))), + col("date_col"), + ], + }); + + let ast = unparser.expr_to_sql(&expr)?; + + let actual = ast.to_string(); + let expected = expected.to_string(); + + assert_eq!(actual, expected); + } + Ok(()) + } + #[test] fn test_dictionary_to_sql() -> Result<()> { let dialect = CustomDialectBuilder::new().build(); diff --git a/datafusion/sql/src/unparser/mod.rs b/datafusion/sql/src/unparser/mod.rs index f90efd103b0f5..05b472dc92a93 100644 --- a/datafusion/sql/src/unparser/mod.rs +++ b/datafusion/sql/src/unparser/mod.rs @@ -118,9 +118,9 @@ impl<'a> Unparser<'a> { /// The child unparsers are called iteratively. /// There are two methods in [`Unparser`] will be called: /// - `extension_to_statement`: This method is called when the custom logical node is a custom statement. - /// If multiple child unparsers return a non-None value, the last unparsing result will be returned. + /// If multiple child unparsers return a non-None value, the last unparsing result will be returned. /// - `extension_to_sql`: This method is called when the custom logical node is part of a statement. - /// If multiple child unparsers are registered for the same custom logical node, all of them will be called in order. + /// If multiple child unparsers are registered for the same custom logical node, all of them will be called in order. pub fn with_extension_unparsers( mut self, extension_unparsers: Vec>, diff --git a/datafusion/sql/src/unparser/plan.rs b/datafusion/sql/src/unparser/plan.rs index a6d89638ff41d..b849ca45d299c 100644 --- a/datafusion/sql/src/unparser/plan.rs +++ b/datafusion/sql/src/unparser/plan.rs @@ -545,6 +545,23 @@ impl Unparser<'_> { false, ); } + + // If this distinct is the parent of a Union and we're in a query context, + // then we need to unparse as a `UNION` rather than a `UNION ALL`. + if let Distinct::All(input) = distinct { + if matches!(input.as_ref(), LogicalPlan::Union(_)) { + if let Some(query_mut) = query.as_mut() { + query_mut.distinct_union(); + return self.select_to_sql_recursively( + input.as_ref(), + query, + select, + relation, + ); + } + } + } + let (select_distinct, input) = match distinct { Distinct::All(input) => (ast::Distinct::Distinct, input.as_ref()), Distinct::On(on) => { @@ -582,6 +599,10 @@ impl Unparser<'_> { } _ => (&join.left, &join.right), }; + // If there's an outer projection plan, it will already set up the projection. + // In that case, we don't need to worry about setting up the projection here. + // The outer projection plan will handle projecting the correct columns. + let already_projected = select.already_projected(); let left_plan = match try_transform_to_simple_table_scan_with_filters(left_plan)? { @@ -599,6 +620,13 @@ impl Unparser<'_> { relation, )?; + let left_projection: Option> = if !already_projected + { + Some(select.pop_projections()) + } else { + None + }; + let right_plan = match try_transform_to_simple_table_scan_with_filters(right_plan)? { Some((plan, filters)) => { @@ -657,6 +685,13 @@ impl Unparser<'_> { &mut right_relation, )?; + let right_projection: Option> = if !already_projected + { + Some(select.pop_projections()) + } else { + None + }; + match join.join_type { JoinType::LeftSemi | JoinType::LeftAnti @@ -702,6 +737,9 @@ impl Unparser<'_> { } else { select.selection(Some(exists_expr)); } + if let Some(projection) = left_projection { + select.projection(projection); + } } JoinType::Inner | JoinType::Left @@ -719,6 +757,21 @@ impl Unparser<'_> { let mut from = select.pop_from().unwrap(); from.push_join(ast_join); select.push_from(from); + if !already_projected { + let Some(left_projection) = left_projection else { + return internal_err!("Left projection is missing"); + }; + + let Some(right_projection) = right_projection else { + return internal_err!("Right projection is missing"); + }; + + let projection = left_projection + .into_iter() + .chain(right_projection.into_iter()) + .collect(); + select.projection(projection); + } } }; @@ -793,6 +846,15 @@ impl Unparser<'_> { return internal_err!("UNION operator requires at least 2 inputs"); } + let set_quantifier = + if query.as_ref().is_some_and(|q| q.is_distinct_union()) { + // Setting the SetQuantifier to None will unparse as a `UNION` + // rather than a `UNION ALL`. + ast::SetQuantifier::None + } else { + ast::SetQuantifier::All + }; + // Build the union expression tree bottom-up by reversing the order // note that we are also swapping left and right inputs because of the rev let union_expr = input_exprs @@ -800,7 +862,7 @@ impl Unparser<'_> { .rev() .reduce(|a, b| SetExpr::SetOperation { op: ast::SetOperator::Union, - set_quantifier: ast::SetQuantifier::All, + set_quantifier, left: Box::new(b), right: Box::new(a), }) @@ -900,9 +962,9 @@ impl Unparser<'_> { /// Try to find the placeholder column name generated by `RecursiveUnnestRewriter`. /// /// - If the column is a placeholder column match the pattern `Expr::Alias(Expr::Column("__unnest_placeholder(...)"))`, - /// it means it is a scalar column, return [UnnestInputType::Scalar]. + /// it means it is a scalar column, return [UnnestInputType::Scalar]. /// - If the column is a placeholder column match the pattern `Expr::Alias(Expr::Column("__unnest_placeholder(outer_ref(...)))")`, - /// it means it is an outer reference column, return [UnnestInputType::OuterReference]. + /// it means it is an outer reference column, return [UnnestInputType::OuterReference]. /// - If the column is not a placeholder column, return [None]. /// /// `outer_ref` is the display result of [Expr::OuterReferenceColumn] diff --git a/datafusion/sql/src/unparser/utils.rs b/datafusion/sql/src/unparser/utils.rs index 75038ccc43145..37f0a77972007 100644 --- a/datafusion/sql/src/unparser/utils.rs +++ b/datafusion/sql/src/unparser/utils.rs @@ -385,7 +385,7 @@ pub(crate) fn try_transform_to_simple_table_scan_with_filters( let mut builder = LogicalPlanBuilder::scan( table_scan.table_name.clone(), Arc::clone(&table_scan.source), - None, + table_scan.projection.clone(), )?; if let Some(alias) = table_alias.take() { @@ -500,3 +500,72 @@ pub(crate) fn character_length_to_sql( character_length_args, )?)) } + +/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc. +/// This remaps `from_unixtime` to `datetime(expr, 'unixepoch')`, expecting the input to be in seconds. +/// It supports no other arguments, so if any are supplied it will return an error. +/// +/// # Errors +/// +/// - If the number of arguments is not 1 - the column or expression to convert. +/// - If the scalar function cannot be converted to SQL. +pub(crate) fn sqlite_from_unixtime_to_sql( + unparser: &Unparser, + from_unixtime_args: &[Expr], +) -> Result> { + if from_unixtime_args.len() != 1 { + return internal_err!( + "from_unixtime for SQLite expects 1 argument, found {}", + from_unixtime_args.len() + ); + } + + Ok(Some(unparser.scalar_function_to_sql( + "datetime", + &[ + from_unixtime_args[0].clone(), + Expr::Literal(ScalarValue::Utf8(Some("unixepoch".to_string()))), + ], + )?)) +} + +/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc. +/// This uses the `strftime` function to format the timestamp as a string depending on the truncation unit. +/// +/// # Errors +/// +/// - If the number of arguments is not 2 - truncation unit and the column or expression to convert. +/// - If the scalar function cannot be converted to SQL. +pub(crate) fn sqlite_date_trunc_to_sql( + unparser: &Unparser, + date_trunc_args: &[Expr], +) -> Result> { + if date_trunc_args.len() != 2 { + return internal_err!( + "date_trunc for SQLite expects 2 arguments, found {}", + date_trunc_args.len() + ); + } + + if let Expr::Literal(ScalarValue::Utf8(Some(unit))) = &date_trunc_args[0] { + let format = match unit.to_lowercase().as_str() { + "year" => "%Y", + "month" => "%Y-%m", + "day" => "%Y-%m-%d", + "hour" => "%Y-%m-%d %H", + "minute" => "%Y-%m-%d %H:%M", + "second" => "%Y-%m-%d %H:%M:%S", + _ => return Ok(None), + }; + + return Ok(Some(unparser.scalar_function_to_sql( + "strftime", + &[ + Expr::Literal(ScalarValue::Utf8(Some(format.to_string()))), + date_trunc_args[1].clone(), + ], + )?)); + } + + Ok(None) +} diff --git a/datafusion/sql/tests/cases/diagnostic.rs b/datafusion/sql/tests/cases/diagnostic.rs index ebb21e9cdef53..e2e4ada9036b7 100644 --- a/datafusion/sql/tests/cases/diagnostic.rs +++ b/datafusion/sql/tests/cases/diagnostic.rs @@ -16,19 +16,21 @@ // under the License. use datafusion_functions::string; +use insta::assert_snapshot; use std::{collections::HashMap, sync::Arc}; use datafusion_common::{Diagnostic, Location, Result, Span}; -use datafusion_sql::planner::{ParserOptions, SqlToRel}; +use datafusion_sql::{ + parser::{DFParser, DFParserBuilder}, + planner::{ParserOptions, SqlToRel}, +}; use regex::Regex; -use sqlparser::{dialect::GenericDialect, parser::Parser}; use crate::{MockContextProvider, MockSessionState}; fn do_query(sql: &'static str) -> Diagnostic { - let dialect = GenericDialect {}; - let statement = Parser::new(&dialect) - .try_with_sql(sql) + let statement = DFParserBuilder::new(sql) + .build() .expect("unable to create parser") .parse_statement() .expect("unable to parse query"); @@ -40,7 +42,7 @@ fn do_query(sql: &'static str) -> Diagnostic { .with_scalar_function(Arc::new(string::concat().as_ref().clone())); let context = MockContextProvider { state }; let sql_to_rel = SqlToRel::new_with_options(&context, options); - match sql_to_rel.sql_statement_to_plan(statement) { + match sql_to_rel.statement_to_plan(statement) { Ok(_) => panic!("expected error"), Err(err) => match err.diagnostic() { Some(diag) => diag.clone(), @@ -136,7 +138,7 @@ fn test_table_not_found() -> Result<()> { let query = "SELECT * FROM /*a*/personx/*a*/"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "table 'personx' not found"); + assert_snapshot!(diag.message, @"table 'personx' not found"); assert_eq!(diag.span, Some(spans["a"])); Ok(()) } @@ -146,7 +148,7 @@ fn test_unqualified_column_not_found() -> Result<()> { let query = "SELECT /*a*/first_namex/*a*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "column 'first_namex' not found"); + assert_snapshot!(diag.message, @"column 'first_namex' not found"); assert_eq!(diag.span, Some(spans["a"])); Ok(()) } @@ -156,7 +158,7 @@ fn test_qualified_column_not_found() -> Result<()> { let query = "SELECT /*a*/person.first_namex/*a*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "column 'first_namex' not found in 'person'"); + assert_snapshot!(diag.message, @"column 'first_namex' not found in 'person'"); assert_eq!(diag.span, Some(spans["a"])); Ok(()) } @@ -166,14 +168,11 @@ fn test_union_wrong_number_of_columns() -> Result<()> { let query = "/*whole+left*/SELECT first_name FROM person/*left*/ UNION ALL /*right*/SELECT first_name, last_name FROM person/*right+whole*/"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!( - diag.message, - "UNION queries have different number of columns" - ); + assert_snapshot!(diag.message, @"UNION queries have different number of columns"); assert_eq!(diag.span, Some(spans["whole"])); - assert_eq!(diag.notes[0].message, "this side has 1 fields"); + assert_snapshot!(diag.notes[0].message, @"this side has 1 fields"); assert_eq!(diag.notes[0].span, Some(spans["left"])); - assert_eq!(diag.notes[1].message, "this side has 2 fields"); + assert_snapshot!(diag.notes[1].message, @"this side has 2 fields"); assert_eq!(diag.notes[1].span, Some(spans["right"])); Ok(()) } @@ -183,15 +182,9 @@ fn test_missing_non_aggregate_in_group_by() -> Result<()> { let query = "SELECT id, /*a*/first_name/*a*/ FROM person GROUP BY id"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!( - diag.message, - "'person.first_name' must appear in GROUP BY clause because it's not an aggregate expression" - ); + assert_snapshot!(diag.message, @"'person.first_name' must appear in GROUP BY clause because it's not an aggregate expression"); assert_eq!(diag.span, Some(spans["a"])); - assert_eq!( - diag.helps[0].message, - "Either add 'person.first_name' to GROUP BY clause, or use an aggregare function like ANY_VALUE(person.first_name)" - ); + assert_snapshot!(diag.helps[0].message, @"Either add 'person.first_name' to GROUP BY clause, or use an aggregare function like ANY_VALUE(person.first_name)"); Ok(()) } @@ -200,10 +193,10 @@ fn test_ambiguous_reference() -> Result<()> { let query = "SELECT /*a*/first_name/*a*/ FROM person a, person b"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "column 'first_name' is ambiguous"); + assert_snapshot!(diag.message, @"column 'first_name' is ambiguous"); assert_eq!(diag.span, Some(spans["a"])); - assert_eq!(diag.notes[0].message, "possible column a.first_name"); - assert_eq!(diag.notes[1].message, "possible column b.first_name"); + assert_snapshot!(diag.notes[0].message, @"possible column a.first_name"); + assert_snapshot!(diag.notes[1].message, @"possible column b.first_name"); Ok(()) } @@ -213,11 +206,11 @@ fn test_incompatible_types_binary_arithmetic() -> Result<()> { "SELECT /*whole+left*/id/*left*/ + /*right*/first_name/*right+whole*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "expressions have incompatible types"); + assert_snapshot!(diag.message, @"expressions have incompatible types"); assert_eq!(diag.span, Some(spans["whole"])); - assert_eq!(diag.notes[0].message, "has type UInt32"); + assert_snapshot!(diag.notes[0].message, @"has type UInt32"); assert_eq!(diag.notes[0].span, Some(spans["left"])); - assert_eq!(diag.notes[1].message, "has type Utf8"); + assert_snapshot!(diag.notes[1].message, @"has type Utf8"); assert_eq!(diag.notes[1].span, Some(spans["right"])); Ok(()) } @@ -227,7 +220,7 @@ fn test_field_not_found_suggestion() -> Result<()> { let query = "SELECT /*whole*/first_na/*whole*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "column 'first_na' not found"); + assert_snapshot!(diag.message, @"column 'first_na' not found"); assert_eq!(diag.span, Some(spans["whole"])); assert_eq!(diag.notes.len(), 1); @@ -243,7 +236,7 @@ fn test_field_not_found_suggestion() -> Result<()> { }) .collect(); suggested_fields.sort(); - assert_eq!(suggested_fields[0], "person.first_name"); + assert_snapshot!(suggested_fields[0], @"person.first_name"); Ok(()) } @@ -253,7 +246,7 @@ fn test_ambiguous_column_suggestion() -> Result<()> { let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "column 'id' is ambiguous"); + assert_snapshot!(diag.message, @"column 'id' is ambiguous"); assert_eq!(diag.span, Some(spans["whole"])); assert_eq!(diag.notes.len(), 2); @@ -281,8 +274,8 @@ fn test_invalid_function() -> Result<()> { let query = "SELECT /*whole*/concat_not_exist/*whole*/()"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "Invalid function 'concat_not_exist'"); - assert_eq!(diag.notes[0].message, "Possible function 'concat'"); + assert_snapshot!(diag.message, @"Invalid function 'concat_not_exist'"); + assert_snapshot!(diag.notes[0].message, @"Possible function 'concat'"); assert_eq!(diag.span, Some(spans["whole"])); Ok(()) } @@ -292,10 +285,7 @@ fn test_scalar_subquery_multiple_columns() -> Result<(), Box Result<(), Box> let spans = get_spans(query); let diag = do_query(query); - assert_eq!( - diag.message, - "Too many columns! The subquery should only return one column" - ); + assert_snapshot!(diag.message, @"Too many columns! The subquery should only return one column"); let expected_span = Some(Span { start: spans["id"].start, @@ -360,16 +347,10 @@ fn test_unary_op_plus_with_column() -> Result<()> { let query = "SELECT +/*whole*/first_name/*whole*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_eq!(diag.message, "+ cannot be used with Utf8"); + assert_snapshot!(diag.message, @"+ cannot be used with Utf8"); assert_eq!(diag.span, Some(spans["whole"])); - assert_eq!( - diag.notes[0].message, - "+ can only be used with numbers, intervals, and timestamps" - ); - assert_eq!( - diag.helps[0].message, - "perhaps you need to cast person.first_name" - ); + assert_snapshot!(diag.notes[0].message, @"+ can only be used with numbers, intervals, and timestamps"); + assert_snapshot!(diag.helps[0].message, @"perhaps you need to cast person.first_name"); Ok(()) } @@ -379,16 +360,32 @@ fn test_unary_op_plus_with_non_column() -> Result<()> { let query = "SELECT +'a'"; let diag = do_query(query); assert_eq!(diag.message, "+ cannot be used with Utf8"); - assert_eq!( - diag.notes[0].message, - "+ can only be used with numbers, intervals, and timestamps" - ); + assert_snapshot!(diag.notes[0].message, @"+ can only be used with numbers, intervals, and timestamps"); assert_eq!(diag.notes[0].span, None); - assert_eq!( - diag.helps[0].message, - "perhaps you need to cast Utf8(\"a\")" - ); + assert_snapshot!(diag.helps[0].message, @r#"perhaps you need to cast Utf8("a")"#); assert_eq!(diag.helps[0].span, None); assert_eq!(diag.span, None); Ok(()) } + +#[test] +fn test_syntax_error() -> Result<()> { + // create a table with a column of type varchar + let query = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV PARTITIONED BY (c1, p1 /*int*/int/*int*/) LOCATION 'foo.csv'"; + let spans = get_spans(query); + match DFParser::parse_sql(query) { + Ok(_) => panic!("expected error"), + Err(err) => match err.diagnostic() { + Some(diag) => { + let diag = diag.clone(); + assert_snapshot!(diag.message, @"Expected: ',' or ')' after partition definition, found: int at Line: 1, Column: 77"); + println!("{:?}", spans); + assert_eq!(diag.span, Some(spans["int"])); + Ok(()) + } + None => { + panic!("expected diagnostic") + } + }, + } +} diff --git a/datafusion/sql/tests/cases/plan_to_sql.rs b/datafusion/sql/tests/cases/plan_to_sql.rs index b7185c2d503df..a458d72d282f1 100644 --- a/datafusion/sql/tests/cases/plan_to_sql.rs +++ b/datafusion/sql/tests/cases/plan_to_sql.rs @@ -17,7 +17,8 @@ use arrow::datatypes::{DataType, Field, Schema}; use datafusion_common::{ - assert_contains, Column, DFSchema, DFSchemaRef, Result, TableReference, + assert_contains, Column, DFSchema, DFSchemaRef, DataFusionError, Result, + TableReference, }; use datafusion_expr::test::function_stub::{ count_udaf, max_udaf, min_udaf, sum, sum_udaf, @@ -38,6 +39,7 @@ use datafusion_sql::unparser::dialect::{ PostgreSqlDialect as UnparserPostgreSqlDialect, SqliteDialect, }; use datafusion_sql::unparser::{expr_to_sql, plan_to_sql, Unparser}; +use insta::assert_snapshot; use sqlparser::ast::Statement; use std::hash::Hash; use std::ops::Add; @@ -62,46 +64,44 @@ use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect}; use sqlparser::parser::Parser; #[test] -fn roundtrip_expr() { - let tests: Vec<(TableReference, &str, &str)> = vec![ - (TableReference::bare("person"), "age > 35", r#"(age > 35)"#), - ( - TableReference::bare("person"), - "id = '10'", - r#"(id = '10')"#, - ), - ( - TableReference::bare("person"), - "CAST(id AS VARCHAR)", - r#"CAST(id AS VARCHAR)"#, - ), - ( - TableReference::bare("person"), - "sum((age * 2))", - r#"sum((age * 2))"#, - ), - ]; +fn test_roundtrip_expr_1() { + let expr = roundtrip_expr(TableReference::bare("person"), "age > 35").unwrap(); + assert_snapshot!(expr, @r#"(age > 35)"#); +} - let roundtrip = |table, sql: &str| -> Result { - let dialect = GenericDialect {}; - let sql_expr = Parser::new(&dialect).try_with_sql(sql)?.parse_expr()?; - let state = MockSessionState::default().with_aggregate_function(sum_udaf()); - let context = MockContextProvider { state }; - let schema = context.get_table_source(table)?.schema(); - let df_schema = DFSchema::try_from(schema.as_ref().clone())?; - let sql_to_rel = SqlToRel::new(&context); - let expr = - sql_to_rel.sql_to_expr(sql_expr, &df_schema, &mut PlannerContext::new())?; +#[test] +fn test_roundtrip_expr_2() { + let expr = roundtrip_expr(TableReference::bare("person"), "id = '10'").unwrap(); + assert_snapshot!(expr, @r#"(id = '10')"#); +} - let ast = expr_to_sql(&expr)?; +#[test] +fn test_roundtrip_expr_3() { + let expr = + roundtrip_expr(TableReference::bare("person"), "CAST(id AS VARCHAR)").unwrap(); + assert_snapshot!(expr, @r#"CAST(id AS VARCHAR)"#); +} - Ok(ast.to_string()) - }; +#[test] +fn test_roundtrip_expr_4() { + let expr = roundtrip_expr(TableReference::bare("person"), "sum((age * 2))").unwrap(); + assert_snapshot!(expr, @r#"sum((age * 2))"#); +} - for (table, query, expected) in tests { - let actual = roundtrip(table, query).unwrap(); - assert_eq!(actual, expected); - } +fn roundtrip_expr(table: TableReference, sql: &str) -> Result { + let dialect = GenericDialect {}; + let sql_expr = Parser::new(&dialect).try_with_sql(sql)?.parse_expr()?; + let state = MockSessionState::default().with_aggregate_function(sum_udaf()); + let context = MockContextProvider { state }; + let schema = context.get_table_source(table)?.schema(); + let df_schema = DFSchema::try_from(schema.as_ref().clone())?; + let sql_to_rel = SqlToRel::new(&context); + let expr = + sql_to_rel.sql_to_expr(sql_expr, &df_schema, &mut PlannerContext::new())?; + + let ast = expr_to_sql(&expr)?; + + Ok(ast.to_string()) } #[test] @@ -170,6 +170,13 @@ fn roundtrip_statement() -> Result<()> { UNION ALL SELECT j3_string AS col1, j3_id AS id FROM j3 ) AS subquery GROUP BY col1, id ORDER BY col1 ASC, id ASC"#, + r#"SELECT col1, id FROM ( + SELECT j1_string AS col1, j1_id AS id FROM j1 + UNION + SELECT j2_string AS col1, j2_id AS id FROM j2 + UNION + SELECT j3_string AS col1, j3_id AS id FROM j3 + ) AS subquery ORDER BY col1 ASC, id ASC"#, "SELECT id, count(*) over (PARTITION BY first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), last_name, sum(id) over (PARTITION BY first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), first_name from person", @@ -236,10 +243,6 @@ fn roundtrip_statement() -> Result<()> { let roundtrip_statement = plan_to_sql(&plan)?; - let actual = &roundtrip_statement.to_string(); - println!("roundtrip sql: {actual}"); - println!("plan {}", plan.display_indent()); - let plan_roundtrip = sql_to_rel .sql_statement_to_plan(roundtrip_statement.clone()) .unwrap(); @@ -275,109 +278,185 @@ fn roundtrip_crossjoin() -> Result<()> { let plan_roundtrip = sql_to_rel .sql_statement_to_plan(roundtrip_statement) .unwrap(); + assert_snapshot!( + plan_roundtrip, + @r" + Projection: j1.j1_id, j2.j2_string + Cross Join: + TableScan: j1 + TableScan: j2 + " + ); + + Ok(()) +} - let expected = "Projection: j1.j1_id, j2.j2_string\ - \n Cross Join: \ - \n TableScan: j1\ - \n TableScan: j2"; +#[macro_export] +macro_rules! roundtrip_statement_with_dialect_helper { + ( + sql: $sql:expr, + parser_dialect: $parser_dialect:expr, + unparser_dialect: $unparser_dialect:expr, + expected: @ $expected:literal $(,)? + ) => {{ + let statement = Parser::new(&$parser_dialect) + .try_with_sql($sql)? + .parse_statement()?; - assert_eq!(plan_roundtrip.to_string(), expected); + let state = MockSessionState::default() + .with_aggregate_function(max_udaf()) + .with_aggregate_function(min_udaf()) + .with_expr_planner(Arc::new(CoreFunctionPlanner::default())) + .with_expr_planner(Arc::new(NestedFunctionPlanner)); + let context = MockContextProvider { state }; + let sql_to_rel = SqlToRel::new(&context); + let plan = sql_to_rel + .sql_statement_to_plan(statement) + .unwrap_or_else(|e| panic!("Failed to parse sql: {}\n{e}", $sql)); + + let unparser = Unparser::new(&$unparser_dialect); + let roundtrip_statement = unparser.plan_to_sql(&plan)?; + + let actual = &roundtrip_statement.to_string(); + insta::assert_snapshot!(actual, @ $expected); + }}; +} + +#[test] +fn roundtrip_statement_with_dialect_1() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", + parser_dialect: MySqlDialect {}, + unparser_dialect: UnparserMySqlDialect {}, + // top projection sort gets derived into a subquery + // for MySQL, this subquery needs an alias + expected: @"SELECT `j1_min` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, min(`ta`.`j1_id`) FROM `j1` AS `ta` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", + ); Ok(()) } #[test] -fn roundtrip_statement_with_dialect() -> Result<()> { - struct TestStatementWithDialect { - sql: &'static str, - expected: &'static str, - parser_dialect: Box, - unparser_dialect: Box, - } - let tests: Vec = vec![ - TestStatementWithDialect { - sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", - expected: - // top projection sort gets derived into a subquery - // for MySQL, this subquery needs an alias - "SELECT `j1_min` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, min(`ta`.`j1_id`) FROM `j1` AS `ta` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", - parser_dialect: Box::new(MySqlDialect {}), - unparser_dialect: Box::new(UnparserMySqlDialect {}), - }, - TestStatementWithDialect { - sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", - expected: - // top projection sort still gets derived into a subquery in default dialect - // except for the default dialect, the subquery is left non-aliased - "SELECT j1_min FROM (SELECT min(ta.j1_id) AS j1_min, min(ta.j1_id) FROM j1 AS ta ORDER BY min(ta.j1_id) ASC NULLS LAST) LIMIT 10", - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "select min(ta.j1_id) as j1_min, max(tb.j1_max) from j1 ta, (select distinct max(ta.j1_id) as j1_max from j1 ta order by max(ta.j1_id)) tb order by min(ta.j1_id) limit 10;", - expected: - "SELECT `j1_min`, `max(tb.j1_max)` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, max(`tb`.`j1_max`), min(`ta`.`j1_id`) FROM `j1` AS `ta` CROSS JOIN (SELECT `j1_max` FROM (SELECT DISTINCT max(`ta`.`j1_id`) AS `j1_max` FROM `j1` AS `ta`) AS `derived_distinct`) AS `tb` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", - parser_dialect: Box::new(MySqlDialect {}), - unparser_dialect: Box::new(UnparserMySqlDialect {}), - }, - TestStatementWithDialect { - sql: "select j1_id from (select 1 as j1_id);", - expected: - "SELECT `j1_id` FROM (SELECT 1 AS `j1_id`) AS `derived_projection`", - parser_dialect: Box::new(MySqlDialect {}), - unparser_dialect: Box::new(UnparserMySqlDialect {}), - }, - TestStatementWithDialect { - sql: "select j1_id from (select j1_id from j1 limit 10);", - expected: - "SELECT `j1`.`j1_id` FROM (SELECT `j1`.`j1_id` FROM `j1` LIMIT 10) AS `derived_limit`", - parser_dialect: Box::new(MySqlDialect {}), - unparser_dialect: Box::new(UnparserMySqlDialect {}), - }, - TestStatementWithDialect { - sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", - expected: - "SELECT `ta`.`j1_id` FROM `j1` AS `ta` ORDER BY `ta`.`j1_id` ASC LIMIT 10", - parser_dialect: Box::new(MySqlDialect {}), - unparser_dialect: Box::new(UnparserMySqlDialect {}), - }, - TestStatementWithDialect { - sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", - expected: r#"SELECT ta.j1_id FROM j1 AS ta ORDER BY ta.j1_id ASC NULLS LAST LIMIT 10"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT j1_id FROM j1 +fn roundtrip_statement_with_dialect_2() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + // top projection sort still gets derived into a subquery in default dialect + // except for the default dialect, the subquery is left non-aliased + expected: @"SELECT j1_min FROM (SELECT min(ta.j1_id) AS j1_min, min(ta.j1_id) FROM j1 AS ta ORDER BY min(ta.j1_id) ASC NULLS LAST) LIMIT 10", + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_3() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "select min(ta.j1_id) as j1_min, max(tb.j1_max) from j1 ta, (select distinct max(ta.j1_id) as j1_max from j1 ta order by max(ta.j1_id)) tb order by min(ta.j1_id) limit 10;", + parser_dialect: MySqlDialect {}, + unparser_dialect: UnparserMySqlDialect {}, + expected: @"SELECT `j1_min`, `max(tb.j1_max)` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, max(`tb`.`j1_max`), min(`ta`.`j1_id`) FROM `j1` AS `ta` CROSS JOIN (SELECT `j1_max` FROM (SELECT DISTINCT max(`ta`.`j1_id`) AS `j1_max` FROM `j1` AS `ta`) AS `derived_distinct`) AS `tb` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_4() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "select j1_id from (select 1 as j1_id);", + parser_dialect: MySqlDialect {}, + unparser_dialect: UnparserMySqlDialect {}, + expected: @"SELECT `j1_id` FROM (SELECT 1 AS `j1_id`) AS `derived_projection`", + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_5() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "select j1_id from (select j1_id from j1 limit 10);", + parser_dialect: MySqlDialect {}, + unparser_dialect: UnparserMySqlDialect {}, + expected: @"SELECT `j1`.`j1_id` FROM (SELECT `j1`.`j1_id` FROM `j1` LIMIT 10) AS `derived_limit`", + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_6() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", + parser_dialect: MySqlDialect {}, + unparser_dialect: UnparserMySqlDialect {}, + expected: @"SELECT `ta`.`j1_id` FROM `j1` AS `ta` ORDER BY `ta`.`j1_id` ASC LIMIT 10", + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_7() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT ta.j1_id FROM j1 AS ta ORDER BY ta.j1_id ASC NULLS LAST LIMIT 10"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_8() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT j1_id FROM j1 UNION ALL SELECT tb.j2_id as j1_id FROM j2 tb ORDER BY j1_id LIMIT 10;", - expected: r#"SELECT j1.j1_id FROM j1 UNION ALL SELECT tb.j2_id AS j1_id FROM j2 AS tb ORDER BY j1_id ASC NULLS LAST LIMIT 10"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - // Test query with derived tables that put distinct,sort,limit on the wrong level - TestStatementWithDialect { - sql: "SELECT j1_string from j1 order by j1_id", - expected: r#"SELECT j1.j1_string FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT j1_string AS a from j1 order by j1_id", - expected: r#"SELECT j1.j1_string AS a FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT j1_string from j1 join j2 on j1.j1_id = j2.j2_id order by j1_id", - expected: r#"SELECT j1.j1_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id ASC NULLS LAST"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: " + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT j1.j1_id FROM j1 UNION ALL SELECT tb.j2_id AS j1_id FROM j2 AS tb ORDER BY j1_id ASC NULLS LAST LIMIT 10"#, + ); + Ok(()) +} + +// Test query with derived tables that put distinct,sort,limit on the wrong level +#[test] +fn roundtrip_statement_with_dialect_9() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT j1_string from j1 order by j1_id", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT j1.j1_string FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_10() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT j1_string AS a from j1 order by j1_id", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT j1.j1_string AS a FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_11() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT j1_string from j1 join j2 on j1.j1_id = j2.j2_id order by j1_id", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT j1.j1_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id ASC NULLS LAST"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_12() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: " SELECT j1_string, j2_string @@ -397,13 +476,18 @@ fn roundtrip_statement_with_dialect() -> Result<()> { ) abc ORDER BY abc.j2_string", - expected: r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT DISTINCT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - // more tests around subquery/derived table roundtrip - TestStatementWithDialect { - sql: "SELECT string_count FROM ( + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT DISTINCT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, + ); + Ok(()) +} + +// more tests around subquery/derived table roundtrip +#[test] +fn roundtrip_statement_with_dialect_13() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT string_count FROM ( SELECT j1_id, min(j2_string) @@ -414,12 +498,17 @@ fn roundtrip_statement_with_dialect() -> Result<()> { j1_id ) AS agg (id, string_count) ", - expected: r#"SELECT agg.string_count FROM (SELECT j1.j1_id, min(j2.j2_string) FROM j1 LEFT OUTER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id) AS agg (id, string_count)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: " + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT agg.string_count FROM (SELECT j1.j1_id, min(j2.j2_string) FROM j1 LEFT OUTER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id) AS agg (id, string_count)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_14() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: " SELECT j1_string, j2_string @@ -443,13 +532,18 @@ fn roundtrip_statement_with_dialect() -> Result<()> { ) abc ORDER BY abc.j2_string", - expected: r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id, j1.j1_string, j2.j2_string ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - // Test query that order by columns are not in select columns - TestStatementWithDialect { - sql: " + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id, j1.j1_string, j2.j2_string ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, + ); + Ok(()) +} + +// Test query that order by columns are not in select columns +#[test] +fn roundtrip_statement_with_dialect_15() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: " SELECT j1_string FROM @@ -468,221 +562,364 @@ fn roundtrip_statement_with_dialect() -> Result<()> { ) abc ORDER BY j2_string", - expected: r#"SELECT abc.j1_string FROM (SELECT j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT id FROM (SELECT j1_id from j1) AS c (id)", - expected: r#"SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT id FROM (SELECT j1_id as id from j1) AS c", - expected: r#"SELECT c.id FROM (SELECT j1.j1_id AS id FROM j1) AS c"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - // Test query that has calculation in derived table with columns - TestStatementWithDialect { - sql: "SELECT id FROM (SELECT j1_id + 1 * 3 from j1) AS c (id)", - expected: r#"SELECT c.id FROM (SELECT (j1.j1_id + (1 * 3)) FROM j1) AS c (id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - // Test query that has limit/distinct/order in derived table with columns - TestStatementWithDialect { - sql: "SELECT id FROM (SELECT distinct (j1_id + 1 * 3) FROM j1 LIMIT 1) AS c (id)", - expected: r#"SELECT c.id FROM (SELECT DISTINCT (j1.j1_id + (1 * 3)) FROM j1 LIMIT 1) AS c (id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT id FROM (SELECT j1_id + 1 FROM j1 ORDER BY j1_id DESC LIMIT 1) AS c (id)", - expected: r#"SELECT c.id FROM (SELECT (j1.j1_id + 1) FROM j1 ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 1) AS c (id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT id FROM (SELECT CAST((CAST(j1_id as BIGINT) + 1) as int) * 10 FROM j1 LIMIT 1) AS c (id)", - expected: r#"SELECT c.id FROM (SELECT (CAST((CAST(j1.j1_id AS BIGINT) + 1) AS INTEGER) * 10) FROM j1 LIMIT 1) AS c (id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT id FROM (SELECT CAST(j1_id as BIGINT) + 1 FROM j1 ORDER BY j1_id LIMIT 1) AS c (id)", - expected: r#"SELECT c.id FROM (SELECT (CAST(j1.j1_id AS BIGINT) + 1) FROM j1 ORDER BY j1.j1_id ASC NULLS LAST LIMIT 1) AS c (id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", - expected: r#"SELECT temp_j.id2 FROM (SELECT j1.j1_id, j1.j1_string FROM j1) AS temp_j (id2, string2)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", - expected: r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2`, `j1`.`j1_string` AS `string2` FROM `j1`) AS `temp_j`"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(SqliteDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM (SELECT j1_id + 1 FROM j1) AS temp_j(id2)", - expected: r#"SELECT `temp_j`.`id2` FROM (SELECT (`j1`.`j1_id` + 1) AS `id2` FROM `j1`) AS `temp_j`"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(SqliteDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM (SELECT j1_id FROM j1 LIMIT 1) AS temp_j(id2)", - expected: r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2` FROM `j1` LIMIT 1) AS `temp_j`"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(SqliteDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3])", - expected: r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))" FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))")"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", - expected: r#"SELECT t1.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS t1 (c1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]), j1", - expected: r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))", j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") CROSS JOIN j1"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", - expected: r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", - expected: r#"SELECT u.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) UNION ALL SELECT u.c1 FROM (SELECT UNNEST([4, 5, 6]) AS "UNNEST(make_array(Int64(4),Int64(5),Int64(6)))") AS u (c1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3])", - expected: r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))) FROM UNNEST([1, 2, 3])"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", - expected: r#"SELECT t1.c1 FROM UNNEST([1, 2, 3]) AS t1 (c1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", - expected: r#"SELECT t1.c1 FROM UNNEST([1, 2, 3]) AS t1 (c1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]), j1", - expected: r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))), j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) CROSS JOIN j1"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", - expected: r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", - expected: r#"SELECT u.c1 FROM UNNEST([1, 2, 3]) AS u (c1) UNION ALL SELECT u.c1 FROM UNNEST([4, 5, 6]) AS u (c1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT UNNEST([1,2,3])", - expected: r#"SELECT * FROM UNNEST([1, 2, 3])"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT UNNEST([1,2,3]) as c1", - expected: r#"SELECT UNNEST([1, 2, 3]) AS c1"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT UNNEST([1,2,3]), 1", - expected: r#"SELECT UNNEST([1, 2, 3]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3))), Int64(1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", - expected: r#"SELECT u.array_col, u.struct_col, UNNEST(outer_ref(u.array_col)) FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", - expected: r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col) AS t1 (c1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT unnest([1, 2, 3, 4]) from unnest([1, 2, 3]);", - expected: r#"SELECT UNNEST([1, 2, 3, 4]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3),Int64(4))) FROM UNNEST([1, 2, 3])"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), - }, - TestStatementWithDialect { - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", - expected: r#"SELECT u.array_col, u.struct_col, "UNNEST(outer_ref(u.array_col))" FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))")"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - TestStatementWithDialect { - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", - expected: r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))") AS t1 (c1)"#, - parser_dialect: Box::new(GenericDialect {}), - unparser_dialect: Box::new(UnparserDefaultDialect {}), - }, - ]; + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT abc.j1_string FROM (SELECT j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, + ); + Ok(()) +} - for query in tests { - let statement = Parser::new(&*query.parser_dialect) - .try_with_sql(query.sql)? - .parse_statement()?; +#[test] +fn roundtrip_statement_with_dialect_16() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT id FROM (SELECT j1_id from j1) AS c (id)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)"#, + ); + Ok(()) +} - let state = MockSessionState::default() - .with_aggregate_function(max_udaf()) - .with_aggregate_function(min_udaf()) - .with_expr_planner(Arc::new(CoreFunctionPlanner::default())) - .with_expr_planner(Arc::new(NestedFunctionPlanner)); +#[test] +fn roundtrip_statement_with_dialect_17() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT id FROM (SELECT j1_id as id from j1) AS c", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT c.id FROM (SELECT j1.j1_id AS id FROM j1) AS c"#, + ); + Ok(()) +} - let context = MockContextProvider { state }; - let sql_to_rel = SqlToRel::new(&context); - let plan = sql_to_rel - .sql_statement_to_plan(statement) - .unwrap_or_else(|e| panic!("Failed to parse sql: {}\n{e}", query.sql)); +// Test query that has calculation in derived table with columns +#[test] +fn roundtrip_statement_with_dialect_18() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT id FROM (SELECT j1_id + 1 * 3 from j1) AS c (id)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT c.id FROM (SELECT (j1.j1_id + (1 * 3)) FROM j1) AS c (id)"#, + ); + Ok(()) +} - let unparser = Unparser::new(&*query.unparser_dialect); - let roundtrip_statement = unparser.plan_to_sql(&plan)?; +// Test query that has limit/distinct/order in derived table with columns +#[test] +fn roundtrip_statement_with_dialect_19() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT id FROM (SELECT distinct (j1_id + 1 * 3) FROM j1 LIMIT 1) AS c (id)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT c.id FROM (SELECT DISTINCT (j1.j1_id + (1 * 3)) FROM j1 LIMIT 1) AS c (id)"#, + ); + Ok(()) +} - let actual = &roundtrip_statement.to_string(); - println!("roundtrip sql: {actual}"); - println!("plan {}", plan.display_indent()); +#[test] +fn roundtrip_statement_with_dialect_20() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT id FROM (SELECT j1_id + 1 FROM j1 ORDER BY j1_id DESC LIMIT 1) AS c (id)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT c.id FROM (SELECT (j1.j1_id + 1) FROM j1 ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 1) AS c (id)"#, + ); + Ok(()) +} - assert_eq!(query.expected, actual); - } +#[test] +fn roundtrip_statement_with_dialect_21() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT id FROM (SELECT CAST((CAST(j1_id as BIGINT) + 1) as int) * 10 FROM j1 LIMIT 1) AS c (id)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT c.id FROM (SELECT (CAST((CAST(j1.j1_id AS BIGINT) + 1) AS INTEGER) * 10) FROM j1 LIMIT 1) AS c (id)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_22() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT id FROM (SELECT CAST(j1_id as BIGINT) + 1 FROM j1 ORDER BY j1_id LIMIT 1) AS c (id)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT c.id FROM (SELECT (CAST(j1.j1_id AS BIGINT) + 1) FROM j1 ORDER BY j1.j1_id ASC NULLS LAST LIMIT 1) AS c (id)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_23() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT temp_j.id2 FROM (SELECT j1.j1_id, j1.j1_string FROM j1) AS temp_j (id2, string2)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_24() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", + parser_dialect: GenericDialect {}, + unparser_dialect: SqliteDialect {}, + expected: @r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2`, `j1`.`j1_string` AS `string2` FROM `j1`) AS `temp_j`"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_25() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM (SELECT j1_id + 1 FROM j1) AS temp_j(id2)", + parser_dialect: GenericDialect {}, + unparser_dialect: SqliteDialect {}, + expected: @r#"SELECT `temp_j`.`id2` FROM (SELECT (`j1`.`j1_id` + 1) AS `id2` FROM `j1`) AS `temp_j`"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_26() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM (SELECT j1_id FROM j1 LIMIT 1) AS temp_j(id2)", + parser_dialect: GenericDialect {}, + unparser_dialect: SqliteDialect {}, + expected: @r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2` FROM `j1` LIMIT 1) AS `temp_j`"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_27() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3])", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))" FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))")"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_28() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT t1.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS t1 (c1)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_29() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]), j1", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))", j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") CROSS JOIN j1"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_30() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_31() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT u.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) UNION ALL SELECT u.c1 FROM (SELECT UNNEST([4, 5, 6]) AS "UNNEST(make_array(Int64(4),Int64(5),Int64(6)))") AS u (c1)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_32() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3])", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))) FROM UNNEST([1, 2, 3])"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_33() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT u.array_col, u.struct_col, "UNNEST(outer_ref(u.array_col))" FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))")"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_34() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT t1.c1 FROM UNNEST([1, 2, 3]) AS t1 (c1)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_35() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]), j1", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))), j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) CROSS JOIN j1"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_36() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_37() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT u.c1 FROM UNNEST([1, 2, 3]) AS u (c1) UNION ALL SELECT u.c1 FROM UNNEST([4, 5, 6]) AS u (c1)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_38() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT UNNEST([1,2,3])", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT * FROM UNNEST([1, 2, 3])"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_39() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT UNNEST([1,2,3]) as c1", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT UNNEST([1, 2, 3]) AS c1"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_40() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT UNNEST([1,2,3]), 1", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT UNNEST([1, 2, 3]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3))), Int64(1)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_41() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT u.array_col, u.struct_col, UNNEST(outer_ref(u.array_col)) FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_42() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col) AS t1 (c1)"#, + ); + Ok(()) +} + +#[test] +fn roundtrip_statement_with_dialect_43() -> Result<(), DataFusionError> { + let unparser = CustomDialectBuilder::default() + .with_unnest_as_table_factor(true) + .build(); + roundtrip_statement_with_dialect_helper!( + sql: "SELECT unnest([1, 2, 3, 4]) from unnest([1, 2, 3]);", + parser_dialect: GenericDialect {}, + unparser_dialect: unparser, + expected: @r#"SELECT UNNEST([1, 2, 3, 4]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3),Int64(4))) FROM UNNEST([1, 2, 3])"#, + ); + Ok(()) +} +#[test] +fn roundtrip_statement_with_dialect_45() -> Result<(), DataFusionError> { + roundtrip_statement_with_dialect_helper!( + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", + parser_dialect: GenericDialect {}, + unparser_dialect: UnparserDefaultDialect {}, + expected: @r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))") AS t1 (c1)"#, + ); Ok(()) } @@ -700,13 +937,14 @@ fn test_unnest_logical_plan() -> Result<()> { }; let sql_to_rel = SqlToRel::new(&context); let plan = sql_to_rel.sql_statement_to_plan(statement).unwrap(); - let expected = r#" + assert_snapshot!( + plan, + @r#" Projection: __unnest_placeholder(unnest_table.struct_col).field1, __unnest_placeholder(unnest_table.struct_col).field2, __unnest_placeholder(unnest_table.array_col,depth=1) AS UNNEST(unnest_table.array_col), unnest_table.struct_col, unnest_table.array_col Unnest: lists[__unnest_placeholder(unnest_table.array_col)|depth=1] structs[__unnest_placeholder(unnest_table.struct_col)] Projection: unnest_table.struct_col AS __unnest_placeholder(unnest_table.struct_col), unnest_table.array_col AS __unnest_placeholder(unnest_table.array_col), unnest_table.struct_col, unnest_table.array_col - TableScan: unnest_table"#.trim_start(); - - assert_eq!(plan.to_string(), expected); + TableScan: unnest_table"# + ); Ok(()) } @@ -726,121 +964,248 @@ fn test_aggregation_without_projection() -> Result<()> { let unparser = Unparser::default(); let statement = unparser.plan_to_sql(&plan)?; - - let actual = &statement.to_string(); - - assert_eq!( - actual, - r#"SELECT sum(users.age), users."name" FROM users GROUP BY users."name""# + assert_snapshot!( + statement, + @r#"SELECT sum(users.age), users."name" FROM users GROUP BY users."name""# ); Ok(()) } -#[test] -fn test_table_references_in_plan_to_sql() { - fn test(table_name: &str, expected_sql: &str, dialect: &impl UnparserDialect) { - let schema = Schema::new(vec![ - Field::new("id", DataType::Utf8, false), - Field::new("value", DataType::Utf8, false), - ]); - let plan = table_scan(Some(table_name), &schema, None) - .unwrap() - .project(vec![col("id"), col("value")]) - .unwrap() - .build() - .unwrap(); - - let unparser = Unparser::new(dialect); - let sql = unparser.plan_to_sql(&plan).unwrap(); - - assert_eq!(sql.to_string(), expected_sql) - } +/// return a schema with two string columns: "id" and "value" +fn test_schema() -> Schema { + Schema::new(vec![ + Field::new("id", DataType::Utf8, false), + Field::new("value", DataType::Utf8, false), + ]) +} - test( - "catalog.schema.table", - r#"SELECT "table".id, "table"."value" FROM "catalog"."schema"."table""#, +#[test] +fn test_table_references_in_plan_to_sql_1() { + let table_name = "catalog.schema.table"; + let schema = test_schema(); + let sql = table_references_in_plan_helper( + table_name, + schema, + vec![col("id"), col("value")], &DefaultDialect {}, ); - test( - "schema.table", - r#"SELECT "table".id, "table"."value" FROM "schema"."table""#, + assert_snapshot!( + sql, + @r#"SELECT "table".id, "table"."value" FROM "catalog"."schema"."table""# + ); +} + +#[test] +fn test_table_references_in_plan_to_sql_2() { + let table_name = "schema.table"; + let schema = test_schema(); + let sql = table_references_in_plan_helper( + table_name, + schema, + vec![col("id"), col("value")], &DefaultDialect {}, ); - test( - "table", - r#"SELECT "table".id, "table"."value" FROM "table""#, + assert_snapshot!( + sql, + @r#"SELECT "table".id, "table"."value" FROM "schema"."table""# + ); +} + +#[test] +fn test_table_references_in_plan_to_sql_3() { + let table_name = "table"; + let schema = test_schema(); + let sql = table_references_in_plan_helper( + table_name, + schema, + vec![col("id"), col("value")], &DefaultDialect {}, ); + assert_snapshot!( + sql, + @r#"SELECT "table".id, "table"."value" FROM "table""# + ); +} +#[test] +fn test_table_references_in_plan_to_sql_4() { + let table_name = "catalog.schema.table"; + let schema = test_schema(); let custom_dialect = CustomDialectBuilder::default() .with_full_qualified_col(true) .with_identifier_quote_style('"') .build(); - test( - "catalog.schema.table", - r#"SELECT "catalog"."schema"."table"."id", "catalog"."schema"."table"."value" FROM "catalog"."schema"."table""#, + let sql = table_references_in_plan_helper( + table_name, + schema, + vec![col("id"), col("value")], &custom_dialect, ); - test( - "schema.table", - r#"SELECT "schema"."table"."id", "schema"."table"."value" FROM "schema"."table""#, + assert_snapshot!( + sql, + @r#"SELECT "catalog"."schema"."table"."id", "catalog"."schema"."table"."value" FROM "catalog"."schema"."table""# + ); +} + +#[test] +fn test_table_references_in_plan_to_sql_5() { + let table_name = "schema.table"; + let schema = test_schema(); + let custom_dialect = CustomDialectBuilder::default() + .with_full_qualified_col(true) + .with_identifier_quote_style('"') + .build(); + + let sql = table_references_in_plan_helper( + table_name, + schema, + vec![col("id"), col("value")], &custom_dialect, ); - test( - "table", - r#"SELECT "table"."id", "table"."value" FROM "table""#, + assert_snapshot!( + sql, + @r#"SELECT "schema"."table"."id", "schema"."table"."value" FROM "schema"."table""# + ); +} + +#[test] +fn test_table_references_in_plan_to_sql_6() { + let table_name = "table"; + let schema = test_schema(); + let custom_dialect = CustomDialectBuilder::default() + .with_full_qualified_col(true) + .with_identifier_quote_style('"') + .build(); + + let sql = table_references_in_plan_helper( + table_name, + schema, + vec![col("id"), col("value")], &custom_dialect, ); + assert_snapshot!( + sql, + @r#"SELECT "table"."id", "table"."value" FROM "table""# + ); +} + +fn table_references_in_plan_helper( + table_name: &str, + table_schema: Schema, + expr: impl IntoIterator>, + dialect: &impl UnparserDialect, +) -> Statement { + let plan = table_scan(Some(table_name), &table_schema, None) + .unwrap() + .project(expr) + .unwrap() + .build() + .unwrap(); + let unparser = Unparser::new(dialect); + unparser.plan_to_sql(&plan).unwrap() } #[test] -fn test_table_scan_with_none_projection_in_plan_to_sql() { - fn test(table_name: &str, expected_sql: &str) { - let schema = Schema::new(vec![ - Field::new("id", DataType::Utf8, false), - Field::new("value", DataType::Utf8, false), - ]); +fn test_table_scan_with_none_projection_in_plan_to_sql_1() { + let schema = test_schema(); + let table_name = "catalog.schema.table"; + let plan = table_scan_with_empty_projection_and_none_projection_helper( + table_name, schema, None, + ); + let sql = plan_to_sql(&plan).unwrap(); + assert_snapshot!( + sql, + @r#"SELECT * FROM "catalog"."schema"."table""# + ); +} - let plan = table_scan(Some(table_name), &schema, None) - .unwrap() - .build() - .unwrap(); - let sql = plan_to_sql(&plan).unwrap(); - assert_eq!(sql.to_string(), expected_sql) - } +#[test] +fn test_table_scan_with_none_projection_in_plan_to_sql_2() { + let schema = test_schema(); + let table_name = "schema.table"; + let plan = table_scan_with_empty_projection_and_none_projection_helper( + table_name, schema, None, + ); + let sql = plan_to_sql(&plan).unwrap(); + assert_snapshot!( + sql, + @r#"SELECT * FROM "schema"."table""# + ); +} - test( - "catalog.schema.table", - r#"SELECT * FROM "catalog"."schema"."table""#, +#[test] +fn test_table_scan_with_none_projection_in_plan_to_sql_3() { + let schema = test_schema(); + let table_name = "table"; + let plan = table_scan_with_empty_projection_and_none_projection_helper( + table_name, schema, None, + ); + let sql = plan_to_sql(&plan).unwrap(); + assert_snapshot!( + sql, + @r#"SELECT * FROM "table""# ); - test("schema.table", r#"SELECT * FROM "schema"."table""#); - test("table", r#"SELECT * FROM "table""#); } #[test] -fn test_table_scan_with_empty_projection_in_plan_to_sql() { - fn test(table_name: &str, expected_sql: &str) { - let schema = Schema::new(vec![ - Field::new("id", DataType::Utf8, false), - Field::new("value", DataType::Utf8, false), - ]); +fn test_table_scan_with_empty_projection_in_plan_to_sql_1() { + let schema = test_schema(); + let table_name = "catalog.schema.table"; + let plan = table_scan_with_empty_projection_and_none_projection_helper( + table_name, + schema, + Some(vec![]), + ); + let sql = plan_to_sql(&plan).unwrap(); + assert_snapshot!( + sql, + @r#"SELECT 1 FROM "catalog"."schema"."table""# + ); +} - let plan = table_scan(Some(table_name), &schema, Some(vec![])) - .unwrap() - .build() - .unwrap(); - let sql = plan_to_sql(&plan).unwrap(); - assert_eq!(sql.to_string(), expected_sql) - } +#[test] +fn test_table_scan_with_empty_projection_in_plan_to_sql_2() { + let schema = test_schema(); + let table_name = "schema.table"; + let plan = table_scan_with_empty_projection_and_none_projection_helper( + table_name, + schema, + Some(vec![]), + ); + let sql = plan_to_sql(&plan).unwrap(); + assert_snapshot!( + sql, + @r#"SELECT 1 FROM "schema"."table""# + ); +} - test( - "catalog.schema.table", - r#"SELECT 1 FROM "catalog"."schema"."table""#, +#[test] +fn test_table_scan_with_empty_projection_in_plan_to_sql_3() { + let schema = test_schema(); + let table_name = "table"; + let plan = table_scan_with_empty_projection_and_none_projection_helper( + table_name, + schema, + Some(vec![]), ); - test("schema.table", r#"SELECT 1 FROM "schema"."table""#); - test("table", r#"SELECT 1 FROM "table""#); + let sql = plan_to_sql(&plan).unwrap(); + assert_snapshot!( + sql, + @r#"SELECT 1 FROM "table""# + ); +} + +fn table_scan_with_empty_projection_and_none_projection_helper( + table_name: &str, + table_schema: Schema, + projection: Option>, +) -> LogicalPlan { + table_scan(Some(table_name), &table_schema, projection) + .unwrap() + .build() + .unwrap() } #[test] @@ -920,12 +1285,12 @@ fn test_pretty_roundtrip() -> Result<()> { Ok(()) } -fn sql_round_trip(dialect: D, query: &str, expect: &str) +fn generate_round_trip_statement(dialect: D, sql: &str) -> Statement where D: Dialect, { let statement = Parser::new(&dialect) - .try_with_sql(query) + .try_with_sql(sql) .unwrap() .parse_statement() .unwrap(); @@ -942,8 +1307,7 @@ where let sql_to_rel = SqlToRel::new(&context); let plan = sql_to_rel.sql_statement_to_plan(statement).unwrap(); - let roundtrip_statement = plan_to_sql(&plan).unwrap(); - assert_eq!(roundtrip_statement.to_string(), expect); + plan_to_sql(&plan).unwrap() } #[test] @@ -958,7 +1322,10 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let sql = plan_to_sql(&plan)?; - assert_eq!(sql.to_string(), "SELECT * FROM (SELECT t1.id FROM t1) AS a"); + assert_snapshot!( + sql, + @"SELECT * FROM (SELECT t1.id FROM t1) AS a" + ); let plan = table_scan(Some("t1"), &schema, None)? .project(vec![col("id")])? @@ -966,7 +1333,10 @@ fn test_table_scan_alias() -> Result<()> { .build()?; let sql = plan_to_sql(&plan)?; - assert_eq!(sql.to_string(), "SELECT * FROM (SELECT t1.id FROM t1) AS a"); + assert_snapshot!( + sql, + @"SELECT * FROM (SELECT t1.id FROM t1) AS a" + ); let plan = table_scan(Some("t1"), &schema, None)? .filter(col("id").gt(lit(5)))? @@ -974,9 +1344,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let sql = plan_to_sql(&plan)?; - assert_eq!( - sql.to_string(), - "SELECT * FROM (SELECT t1.id FROM t1 WHERE (t1.id > 5)) AS a" + assert_snapshot!( + sql, + @r#"SELECT * FROM (SELECT t1.id FROM t1 WHERE (t1.id > 5)) AS a"# ); let table_scan_with_two_filter = table_scan_with_filters( @@ -989,9 +1359,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let table_scan_with_two_filter = plan_to_sql(&table_scan_with_two_filter)?; - assert_eq!( - table_scan_with_two_filter.to_string(), - "SELECT a.id FROM t1 AS a WHERE ((a.id > 1) AND (a.age < 2))" + assert_snapshot!( + table_scan_with_two_filter, + @r#"SELECT a.id FROM t1 AS a WHERE ((a.id > 1) AND (a.age < 2))"# ); let table_scan_with_fetch = @@ -1000,9 +1370,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let table_scan_with_fetch = plan_to_sql(&table_scan_with_fetch)?; - assert_eq!( - table_scan_with_fetch.to_string(), - "SELECT a.id FROM (SELECT * FROM t1 LIMIT 10) AS a" + assert_snapshot!( + table_scan_with_fetch, + @r#"SELECT a.id FROM (SELECT * FROM t1 LIMIT 10) AS a"# ); let table_scan_with_pushdown_all = table_scan_with_filter_and_fetch( @@ -1016,9 +1386,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let table_scan_with_pushdown_all = plan_to_sql(&table_scan_with_pushdown_all)?; - assert_eq!( - table_scan_with_pushdown_all.to_string(), - "SELECT a.id FROM (SELECT a.id, a.age FROM t1 AS a WHERE (a.id > 1) LIMIT 10) AS a" + assert_snapshot!( + table_scan_with_pushdown_all, + @r#"SELECT a.id FROM (SELECT a.id, a.age FROM t1 AS a WHERE (a.id > 1) LIMIT 10) AS a"# ); Ok(()) } @@ -1032,18 +1402,24 @@ fn test_table_scan_pushdown() -> Result<()> { let scan_with_projection = table_scan(Some("t1"), &schema, Some(vec![0, 1]))?.build()?; let scan_with_projection = plan_to_sql(&scan_with_projection)?; - assert_eq!( - scan_with_projection.to_string(), - "SELECT t1.id, t1.age FROM t1" + assert_snapshot!( + scan_with_projection, + @r#"SELECT t1.id, t1.age FROM t1"# ); let scan_with_projection = table_scan(Some("t1"), &schema, Some(vec![1]))?.build()?; let scan_with_projection = plan_to_sql(&scan_with_projection)?; - assert_eq!(scan_with_projection.to_string(), "SELECT t1.age FROM t1"); + assert_snapshot!( + scan_with_projection, + @r#"SELECT t1.age FROM t1"# + ); let scan_with_no_projection = table_scan(Some("t1"), &schema, None)?.build()?; let scan_with_no_projection = plan_to_sql(&scan_with_no_projection)?; - assert_eq!(scan_with_no_projection.to_string(), "SELECT * FROM t1"); + assert_snapshot!( + scan_with_no_projection, + @r#"SELECT * FROM t1"# + ); let table_scan_with_projection_alias = table_scan(Some("t1"), &schema, Some(vec![0, 1]))? @@ -1051,9 +1427,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_alias = plan_to_sql(&table_scan_with_projection_alias)?; - assert_eq!( - table_scan_with_projection_alias.to_string(), - "SELECT ta.id, ta.age FROM t1 AS ta" + assert_snapshot!( + table_scan_with_projection_alias, + @r#"SELECT ta.id, ta.age FROM t1 AS ta"# ); let table_scan_with_projection_alias = @@ -1062,9 +1438,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_alias = plan_to_sql(&table_scan_with_projection_alias)?; - assert_eq!( - table_scan_with_projection_alias.to_string(), - "SELECT ta.age FROM t1 AS ta" + assert_snapshot!( + table_scan_with_projection_alias, + @r#"SELECT ta.age FROM t1 AS ta"# ); let table_scan_with_no_projection_alias = table_scan(Some("t1"), &schema, None)? @@ -1072,9 +1448,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_no_projection_alias = plan_to_sql(&table_scan_with_no_projection_alias)?; - assert_eq!( - table_scan_with_no_projection_alias.to_string(), - "SELECT * FROM t1 AS ta" + assert_snapshot!( + table_scan_with_no_projection_alias, + @r#"SELECT * FROM t1 AS ta"# ); let query_from_table_scan_with_projection = LogicalPlanBuilder::from( @@ -1084,9 +1460,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let query_from_table_scan_with_projection = plan_to_sql(&query_from_table_scan_with_projection)?; - assert_eq!( - query_from_table_scan_with_projection.to_string(), - "SELECT t1.id, t1.age FROM t1" + assert_snapshot!( + query_from_table_scan_with_projection, + @r#"SELECT t1.id, t1.age FROM t1"# ); let query_from_table_scan_with_two_projections = LogicalPlanBuilder::from( @@ -1097,9 +1473,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let query_from_table_scan_with_two_projections = plan_to_sql(&query_from_table_scan_with_two_projections)?; - assert_eq!( - query_from_table_scan_with_two_projections.to_string(), - "SELECT t1.id, t1.age FROM (SELECT t1.id, t1.age FROM t1)" + assert_snapshot!( + query_from_table_scan_with_two_projections, + @r#"SELECT t1.id, t1.age FROM (SELECT t1.id, t1.age FROM t1)"# ); let table_scan_with_filter = table_scan_with_filters( @@ -1110,9 +1486,9 @@ fn test_table_scan_pushdown() -> Result<()> { )? .build()?; let table_scan_with_filter = plan_to_sql(&table_scan_with_filter)?; - assert_eq!( - table_scan_with_filter.to_string(), - "SELECT * FROM t1 WHERE (t1.id > t1.age)" + assert_snapshot!( + table_scan_with_filter, + @r#"SELECT * FROM t1 WHERE (t1.id > t1.age)"# ); let table_scan_with_two_filter = table_scan_with_filters( @@ -1123,9 +1499,9 @@ fn test_table_scan_pushdown() -> Result<()> { )? .build()?; let table_scan_with_two_filter = plan_to_sql(&table_scan_with_two_filter)?; - assert_eq!( - table_scan_with_two_filter.to_string(), - "SELECT * FROM t1 WHERE ((t1.id > 1) AND (t1.age < 2))" + assert_snapshot!( + table_scan_with_two_filter, + @r#"SELECT * FROM t1 WHERE ((t1.id > 1) AND (t1.age < 2))"# ); let table_scan_with_filter_alias = table_scan_with_filters( @@ -1137,9 +1513,9 @@ fn test_table_scan_pushdown() -> Result<()> { .alias("ta")? .build()?; let table_scan_with_filter_alias = plan_to_sql(&table_scan_with_filter_alias)?; - assert_eq!( - table_scan_with_filter_alias.to_string(), - "SELECT * FROM t1 AS ta WHERE (ta.id > ta.age)" + assert_snapshot!( + table_scan_with_filter_alias, + @r#"SELECT * FROM t1 AS ta WHERE (ta.id > ta.age)"# ); let table_scan_with_projection_and_filter = table_scan_with_filters( @@ -1151,9 +1527,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_and_filter = plan_to_sql(&table_scan_with_projection_and_filter)?; - assert_eq!( - table_scan_with_projection_and_filter.to_string(), - "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age)" + assert_snapshot!( + table_scan_with_projection_and_filter, + @r#"SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age)"# ); let table_scan_with_projection_and_filter = table_scan_with_filters( @@ -1165,18 +1541,18 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_and_filter = plan_to_sql(&table_scan_with_projection_and_filter)?; - assert_eq!( - table_scan_with_projection_and_filter.to_string(), - "SELECT t1.age FROM t1 WHERE (t1.id > t1.age)" + assert_snapshot!( + table_scan_with_projection_and_filter, + @r#"SELECT t1.age FROM t1 WHERE (t1.id > t1.age)"# ); let table_scan_with_inline_fetch = table_scan_with_filter_and_fetch(Some("t1"), &schema, None, vec![], Some(10))? .build()?; let table_scan_with_inline_fetch = plan_to_sql(&table_scan_with_inline_fetch)?; - assert_eq!( - table_scan_with_inline_fetch.to_string(), - "SELECT * FROM t1 LIMIT 10" + assert_snapshot!( + table_scan_with_inline_fetch, + @r#"SELECT * FROM t1 LIMIT 10"# ); let table_scan_with_projection_and_inline_fetch = table_scan_with_filter_and_fetch( @@ -1189,9 +1565,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_and_inline_fetch = plan_to_sql(&table_scan_with_projection_and_inline_fetch)?; - assert_eq!( - table_scan_with_projection_and_inline_fetch.to_string(), - "SELECT t1.id, t1.age FROM t1 LIMIT 10" + assert_snapshot!( + table_scan_with_projection_and_inline_fetch, + @r#"SELECT t1.id, t1.age FROM t1 LIMIT 10"# ); let table_scan_with_all = table_scan_with_filter_and_fetch( @@ -1203,9 +1579,9 @@ fn test_table_scan_pushdown() -> Result<()> { )? .build()?; let table_scan_with_all = plan_to_sql(&table_scan_with_all)?; - assert_eq!( - table_scan_with_all.to_string(), - "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age) LIMIT 10" + assert_snapshot!( + table_scan_with_all, + @r#"SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age) LIMIT 10"# ); let table_scan_with_additional_filter = table_scan_with_filters( @@ -1217,9 +1593,9 @@ fn test_table_scan_pushdown() -> Result<()> { .filter(col("id").eq(lit(5)))? .build()?; let table_scan_with_filter = plan_to_sql(&table_scan_with_additional_filter)?; - assert_eq!( - table_scan_with_filter.to_string(), - "SELECT * FROM t1 WHERE (t1.id = 5) AND (t1.id > t1.age)" + assert_snapshot!( + table_scan_with_filter, + @r#"SELECT * FROM t1 WHERE (t1.id = 5) AND (t1.id > t1.age)"# ); Ok(()) @@ -1238,9 +1614,9 @@ fn test_sort_with_push_down_fetch() -> Result<()> { .build()?; let sql = plan_to_sql(&plan)?; - assert_eq!( - format!("{}", sql), - "SELECT t1.id, t1.age FROM t1 ORDER BY t1.age ASC NULLS FIRST LIMIT 10" + assert_snapshot!( + sql, + @r#"SELECT t1.id, t1.age FROM t1 ORDER BY t1.age ASC NULLS FIRST LIMIT 10"# ); Ok(()) } @@ -1284,10 +1660,10 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_with_filter)?; - - let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND ("left"."name" LIKE 'some_name' AND (age > 10)))"#; - - assert_eq!(sql.to_string(), expected_sql); + assert_snapshot!( + sql, + @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND ("left"."name" LIKE 'some_name' AND (age > 10)))"# + ); let join_plan_no_filter = LogicalPlanBuilder::from(left_plan.clone()) .join( @@ -1299,10 +1675,10 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_no_filter)?; - - let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND ("left"."name" LIKE 'some_name' AND (age > 10))"#; - - assert_eq!(sql.to_string(), expected_sql); + assert_snapshot!( + sql, + @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND ("left"."name" LIKE 'some_name' AND (age > 10))"# + ); let right_plan_with_filter = table_scan_with_filters( Some("right_table"), @@ -1324,10 +1700,10 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_multiple_filters)?; - - let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table."name" = 'before_join_filter_val')) AND (age > 10))) WHERE ("left"."name" = 'after_join_filter_val')"#; - - assert_eq!(sql.to_string(), expected_sql); + assert_snapshot!( + sql, + @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table."name" = 'before_join_filter_val')) AND (age > 10))) WHERE ("left"."name" = 'after_join_filter_val')"# + ); let right_plan_with_filter_schema = table_scan_with_filters( Some("right_table"), @@ -1354,114 +1730,153 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_duplicated_filter)?; - - let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table.age > 10)) AND (right_table.age < 11)))"#; - - assert_eq!(sql.to_string(), expected_sql); + assert_snapshot!( + sql, + @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table.age > 10)) AND (right_table.age < 11)))"# + ); Ok(()) } #[test] fn test_interval_lhs_eq() { - sql_round_trip( + let statement = generate_round_trip_statement( GenericDialect {}, "select interval '2 seconds' = interval '2 seconds'", - "SELECT (INTERVAL '2.000000000 SECS' = INTERVAL '2.000000000 SECS')", ); + assert_snapshot!( + statement, + @r#"SELECT (INTERVAL '2.000000000 SECS' = INTERVAL '2.000000000 SECS')"# + ) } #[test] fn test_interval_lhs_lt() { - sql_round_trip( + let statement = generate_round_trip_statement( GenericDialect {}, "select interval '2 seconds' < interval '2 seconds'", - "SELECT (INTERVAL '2.000000000 SECS' < INTERVAL '2.000000000 SECS')", ); + assert_snapshot!( + statement, + @r#"SELECT (INTERVAL '2.000000000 SECS' < INTERVAL '2.000000000 SECS')"# + ) } #[test] fn test_without_offset() { - sql_round_trip(MySqlDialect {}, "select 1", "SELECT 1"); + let statement = generate_round_trip_statement(MySqlDialect {}, "select 1"); + assert_snapshot!( + statement, + @r#"SELECT 1"# + ) } #[test] fn test_with_offset0() { - sql_round_trip(MySqlDialect {}, "select 1 offset 0", "SELECT 1 OFFSET 0"); + let statement = generate_round_trip_statement(MySqlDialect {}, "select 1 offset 0"); + assert_snapshot!( + statement, + @r#"SELECT 1 OFFSET 0"# + ) } #[test] fn test_with_offset95() { - sql_round_trip(MySqlDialect {}, "select 1 offset 95", "SELECT 1 OFFSET 95"); + let statement = generate_round_trip_statement(MySqlDialect {}, "select 1 offset 95"); + assert_snapshot!( + statement, + @r#"SELECT 1 OFFSET 95"# + ) } #[test] -fn test_order_by_to_sql() { +fn test_order_by_to_sql_1() { // order by aggregation function - sql_round_trip( + let statement = generate_round_trip_statement( GenericDialect {}, r#"SELECT id, first_name, SUM(id) FROM person GROUP BY id, first_name ORDER BY SUM(id) ASC, first_name DESC, id, first_name LIMIT 10"#, - r#"SELECT person.id, person.first_name, sum(person.id) FROM person GROUP BY person.id, person.first_name ORDER BY sum(person.id) ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"#, ); + assert_snapshot!( + statement, + @r#"SELECT person.id, person.first_name, sum(person.id) FROM person GROUP BY person.id, person.first_name ORDER BY sum(person.id) ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"# + ); +} +#[test] +fn test_order_by_to_sql_2() { // order by aggregation function alias - sql_round_trip( + let statement = generate_round_trip_statement( GenericDialect {}, r#"SELECT id, first_name, SUM(id) as total_sum FROM person GROUP BY id, first_name ORDER BY total_sum ASC, first_name DESC, id, first_name LIMIT 10"#, - r#"SELECT person.id, person.first_name, sum(person.id) AS total_sum FROM person GROUP BY person.id, person.first_name ORDER BY total_sum ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"#, ); + assert_snapshot!( + statement, + @r#"SELECT person.id, person.first_name, sum(person.id) AS total_sum FROM person GROUP BY person.id, person.first_name ORDER BY total_sum ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"# + ); +} - // order by scalar function from projection - sql_round_trip( +#[test] +fn test_order_by_to_sql_3() { + let statement = generate_round_trip_statement( GenericDialect {}, r#"SELECT id, first_name, substr(first_name,0,5) FROM person ORDER BY id, substr(first_name,0,5)"#, - r#"SELECT person.id, person.first_name, substr(person.first_name, 0, 5) FROM person ORDER BY person.id ASC NULLS LAST, substr(person.first_name, 0, 5) ASC NULLS LAST"#, + ); + assert_snapshot!( + statement, + @r#"SELECT person.id, person.first_name, substr(person.first_name, 0, 5) FROM person ORDER BY person.id ASC NULLS LAST, substr(person.first_name, 0, 5) ASC NULLS LAST"# ); } #[test] fn test_aggregation_to_sql() { - sql_round_trip( - GenericDialect {}, - r#"SELECT id, first_name, + let sql = r#"SELECT id, first_name, SUM(id) AS total_sum, SUM(id) OVER (PARTITION BY first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) AS moving_sum, MAX(SUM(id)) OVER (PARTITION BY first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_total, rank() OVER (PARTITION BY grouping(id) + grouping(age), CASE WHEN grouping(age) = 0 THEN id END ORDER BY sum(id) DESC) AS rank_within_parent_1, rank() OVER (PARTITION BY grouping(age) + grouping(id), CASE WHEN (CAST(grouping(age) AS BIGINT) = 0) THEN id END ORDER BY sum(id) DESC) AS rank_within_parent_2 FROM person - GROUP BY id, first_name;"#, - r#"SELECT person.id, person.first_name, -sum(person.id) AS total_sum, sum(person.id) OVER (PARTITION BY person.first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) AS moving_sum, -max(sum(person.id)) OVER (PARTITION BY person.first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_total, -rank() OVER (PARTITION BY (grouping(person.id) + grouping(person.age)), CASE WHEN (grouping(person.age) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_1, -rank() OVER (PARTITION BY (grouping(person.age) + grouping(person.id)), CASE WHEN (CAST(grouping(person.age) AS BIGINT) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_2 -FROM person -GROUP BY person.id, person.first_name"#.replace("\n", " ").as_str(), + GROUP BY id, first_name"#; + let statement = generate_round_trip_statement(GenericDialect {}, sql); + assert_snapshot!( + statement, + @"SELECT person.id, person.first_name, sum(person.id) AS total_sum, sum(person.id) OVER (PARTITION BY person.first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) AS moving_sum, max(sum(person.id)) OVER (PARTITION BY person.first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_total, rank() OVER (PARTITION BY (grouping(person.id) + grouping(person.age)), CASE WHEN (grouping(person.age) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_1, rank() OVER (PARTITION BY (grouping(person.age) + grouping(person.id)), CASE WHEN (CAST(grouping(person.age) AS BIGINT) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_2 FROM person GROUP BY person.id, person.first_name", ); } #[test] -fn test_unnest_to_sql() { - sql_round_trip( +fn test_unnest_to_sql_1() { + let statement = generate_round_trip_statement( GenericDialect {}, r#"SELECT unnest(array_col) as u1, struct_col, array_col FROM unnest_table WHERE array_col != NULL ORDER BY struct_col, array_col"#, - r#"SELECT UNNEST(unnest_table.array_col) AS u1, unnest_table.struct_col, unnest_table.array_col FROM unnest_table WHERE (unnest_table.array_col <> NULL) ORDER BY unnest_table.struct_col ASC NULLS LAST, unnest_table.array_col ASC NULLS LAST"#, ); + assert_snapshot!( + statement, + @r#"SELECT UNNEST(unnest_table.array_col) AS u1, unnest_table.struct_col, unnest_table.array_col FROM unnest_table WHERE (unnest_table.array_col <> NULL) ORDER BY unnest_table.struct_col ASC NULLS LAST, unnest_table.array_col ASC NULLS LAST"# + ); +} - sql_round_trip( +#[test] +fn test_unnest_to_sql_2() { + let statement = generate_round_trip_statement( GenericDialect {}, r#"SELECT unnest(make_array(1, 2, 2, 5, NULL)) as u1"#, - r#"SELECT UNNEST([1, 2, 2, 5, NULL]) AS u1"#, + ); + assert_snapshot!( + statement, + @r#"SELECT UNNEST([1, 2, 2, 5, NULL]) AS u1"# ); } #[test] fn test_join_with_no_conditions() { - sql_round_trip( + let statement = generate_round_trip_statement( GenericDialect {}, "SELECT j1.j1_id, j1.j1_string FROM j1 CROSS JOIN j2", - "SELECT j1.j1_id, j1.j1_string FROM j1 CROSS JOIN j2", + ); + assert_snapshot!( + statement, + @r#"SELECT j1.j1_id, j1.j1_string FROM j1 CROSS JOIN j2"# ); } @@ -1562,8 +1977,10 @@ fn test_unparse_extension_to_statement() -> Result<()> { Arc::new(UnusedUnparser {}), ]); let sql = unparser.plan_to_sql(&extension)?; - let expected = "SELECT j1.j1_id, j1.j1_string FROM j1"; - assert_eq!(sql.to_string(), expected); + assert_snapshot!( + sql, + @r#"SELECT j1.j1_id, j1.j1_string FROM j1"# + ); if let Some(err) = plan_to_sql(&extension).err() { assert_contains!( @@ -1625,9 +2042,10 @@ fn test_unparse_extension_to_sql() -> Result<()> { Arc::new(UnusedUnparser {}), ]); let sql = unparser.plan_to_sql(&plan)?; - let expected = - "SELECT j1.j1_id AS user_id FROM (SELECT j1.j1_id, j1.j1_string FROM j1)"; - assert_eq!(sql.to_string(), expected); + assert_snapshot!( + sql, + @r#"SELECT j1.j1_id AS user_id FROM (SELECT j1.j1_id, j1.j1_string FROM j1)"# + ); if let Some(err) = plan_to_sql(&plan).err() { assert_contains!( @@ -1665,10 +2083,10 @@ fn test_unparse_optimized_multi_union() -> Result<()> { ], schema: dfschema.clone(), }); - - let sql = "SELECT 1 AS x, 'a' AS y UNION ALL SELECT 1 AS x, 'b' AS y UNION ALL SELECT 2 AS x, 'a' AS y UNION ALL SELECT 2 AS x, 'c' AS y"; - - assert_eq!(unparser.plan_to_sql(&plan)?.to_string(), sql); + assert_snapshot!( + unparser.plan_to_sql(&plan)?, + @r#"SELECT 1 AS x, 'a' AS y UNION ALL SELECT 1 AS x, 'b' AS y UNION ALL SELECT 2 AS x, 'a' AS y UNION ALL SELECT 2 AS x, 'c' AS y"# + ); let plan = LogicalPlan::Union(Union { inputs: vec![project( @@ -1746,8 +2164,10 @@ fn test_unparse_subquery_alias_with_table_pushdown() -> Result<()> { let unparser = Unparser::default(); let sql = unparser.plan_to_sql(&plan)?; - let expected = "SELECT customer_view.c_custkey, customer_view.c_name, customer_view.custkey_plus FROM (SELECT customer.c_custkey, (CAST(customer.c_custkey AS BIGINT) + 1) AS custkey_plus, customer.c_name FROM (SELECT customer.c_custkey, customer.c_name FROM customer AS customer) AS customer) AS customer_view"; - assert_eq!(sql.to_string(), expected); + assert_snapshot!( + sql, + @r#"SELECT customer_view.c_custkey, customer_view.c_name, customer_view.custkey_plus FROM (SELECT customer.c_custkey, (CAST(customer.c_custkey AS BIGINT) + 1) AS custkey_plus, customer.c_name FROM (SELECT customer.c_custkey, customer.c_name FROM customer AS customer) AS customer) AS customer_view"# + ); Ok(()) } @@ -1778,7 +2198,10 @@ fn test_unparse_left_anti_join() -> Result<()> { let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_eq!("SELECT \"t1\".\"d\" FROM \"t1\" WHERE NOT EXISTS (SELECT 1 FROM \"t2\" AS \"__correlated_sq_1\" WHERE (\"t1\".\"c\" = \"__correlated_sq_1\".\"c\"))", sql.to_string()); + assert_snapshot!( + sql, + @r#"SELECT "t1"."d" FROM "t1" WHERE NOT EXISTS (SELECT 1 FROM "t2" AS "__correlated_sq_1" WHERE ("t1"."c" = "__correlated_sq_1"."c"))"# + ); Ok(()) } @@ -1809,7 +2232,10 @@ fn test_unparse_left_semi_join() -> Result<()> { let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_eq!("SELECT \"t1\".\"d\" FROM \"t1\" WHERE EXISTS (SELECT 1 FROM \"t2\" AS \"__correlated_sq_1\" WHERE (\"t1\".\"c\" = \"__correlated_sq_1\".\"c\"))", sql.to_string()); + assert_snapshot!( + sql, + @r#"SELECT "t1"."d" FROM "t1" WHERE EXISTS (SELECT 1 FROM "t2" AS "__correlated_sq_1" WHERE ("t1"."c" = "__correlated_sq_1"."c"))"# + ); Ok(()) } @@ -1841,7 +2267,10 @@ fn test_unparse_left_mark_join() -> Result<()> { let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_eq!("SELECT \"t1\".\"d\" FROM \"t1\" WHERE (EXISTS (SELECT 1 FROM \"t2\" AS \"__correlated_sq_1\" WHERE (\"t1\".\"c\" = \"__correlated_sq_1\".\"c\")) OR (\"t1\".\"d\" < 0))", sql.to_string()); + assert_snapshot!( + sql, + @r#"SELECT "t1"."d" FROM "t1" WHERE (EXISTS (SELECT 1 FROM "t2" AS "__correlated_sq_1" WHERE ("t1"."c" = "__correlated_sq_1"."c")) OR ("t1"."d" < 0))"# + ); Ok(()) } @@ -1876,7 +2305,10 @@ fn test_unparse_right_semi_join() -> Result<()> { .build()?; let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_eq!("SELECT \"t2\".\"c\", \"t2\".\"d\" FROM \"t2\" WHERE (\"t2\".\"c\" <= 1) AND EXISTS (SELECT 1 FROM \"t1\" WHERE (\"t1\".\"c\" = \"t2\".\"c\"))", sql.to_string()); + assert_snapshot!( + sql, + @r#"SELECT "t2"."c", "t2"."d" FROM "t2" WHERE ("t2"."c" <= 1) AND EXISTS (SELECT 1 FROM "t1" WHERE ("t1"."c" = "t2"."c"))"# + ); Ok(()) } @@ -1911,6 +2343,92 @@ fn test_unparse_right_anti_join() -> Result<()> { .build()?; let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_eq!("SELECT \"t2\".\"c\", \"t2\".\"d\" FROM \"t2\" WHERE (\"t2\".\"c\" <= 1) AND NOT EXISTS (SELECT 1 FROM \"t1\" WHERE (\"t1\".\"c\" = \"t2\".\"c\"))", sql.to_string()); + assert_snapshot!( + sql, + @r#"SELECT "t2"."c", "t2"."d" FROM "t2" WHERE ("t2"."c" <= 1) AND NOT EXISTS (SELECT 1 FROM "t1" WHERE ("t1"."c" = "t2"."c"))"# + ); + Ok(()) +} + +#[test] +fn test_unparse_cross_join_with_table_scan_projection() -> Result<()> { + let schema = Schema::new(vec![ + Field::new("k", DataType::Int32, false), + Field::new("v", DataType::Int32, false), + ]); + // Cross Join: + // SubqueryAlias: t1 + // TableScan: test projection=[v] + // SubqueryAlias: t2 + // TableScan: test projection=[v] + let table_scan1 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; + let table_scan2 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; + let plan = LogicalPlanBuilder::from(subquery_alias(table_scan1, "t1")?) + .cross_join(subquery_alias(table_scan2, "t2")?)? + .build()?; + let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); + let sql = unparser.plan_to_sql(&plan)?; + assert_snapshot!( + sql, + @r#"SELECT "t1"."v", "t2"."v" FROM "test" AS "t1" CROSS JOIN "test" AS "t2""# + ); + Ok(()) +} + +#[test] +fn test_unparse_inner_join_with_table_scan_projection() -> Result<()> { + let schema = Schema::new(vec![ + Field::new("k", DataType::Int32, false), + Field::new("v", DataType::Int32, false), + ]); + // Inner Join: + // SubqueryAlias: t1 + // TableScan: test projection=[v] + // SubqueryAlias: t2 + // TableScan: test projection=[v] + let table_scan1 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; + let table_scan2 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; + let plan = LogicalPlanBuilder::from(subquery_alias(table_scan1, "t1")?) + .join_on( + subquery_alias(table_scan2, "t2")?, + datafusion_expr::JoinType::Inner, + vec![col("t1.v").eq(col("t2.v"))], + )? + .build()?; + let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); + let sql = unparser.plan_to_sql(&plan)?; + assert_snapshot!( + sql, + @r#"SELECT "t1"."v", "t2"."v" FROM "test" AS "t1" INNER JOIN "test" AS "t2" ON ("t1"."v" = "t2"."v")"# + ); + Ok(()) +} + +#[test] +fn test_unparse_left_semi_join_with_table_scan_projection() -> Result<()> { + let schema = Schema::new(vec![ + Field::new("k", DataType::Int32, false), + Field::new("v", DataType::Int32, false), + ]); + // LeftSemi Join: + // SubqueryAlias: t1 + // TableScan: test projection=[v] + // SubqueryAlias: t2 + // TableScan: test projection=[v] + let table_scan1 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; + let table_scan2 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; + let plan = LogicalPlanBuilder::from(subquery_alias(table_scan1, "t1")?) + .join_on( + subquery_alias(table_scan2, "t2")?, + datafusion_expr::JoinType::LeftSemi, + vec![col("t1.v").eq(col("t2.v"))], + )? + .build()?; + let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); + let sql = unparser.plan_to_sql(&plan)?; + assert_snapshot!( + sql, + @r#"SELECT "t1"."v" FROM "test" AS "t1" WHERE EXISTS (SELECT 1 FROM "test" AS "t2" WHERE ("t1"."v" = "t2"."v"))"# + ); Ok(()) } diff --git a/datafusion/sql/tests/sql_integration.rs b/datafusion/sql/tests/sql_integration.rs index 866c08ed0257e..2804a1de06064 100644 --- a/datafusion/sql/tests/sql_integration.rs +++ b/datafusion/sql/tests/sql_integration.rs @@ -48,6 +48,7 @@ use datafusion_functions_aggregate::{ use datafusion_functions_aggregate::{average::avg_udaf, grouping::grouping_udaf}; use datafusion_functions_nested::make_array::make_array_udf; use datafusion_functions_window::rank::rank_udwf; +use insta::{allow_duplicates, assert_snapshot}; use rstest::rstest; use sqlparser::dialect::{Dialect, GenericDialect, HiveDialect, MySqlDialect}; @@ -55,317 +56,508 @@ mod cases; mod common; #[test] -fn parse_decimals() { - let test_data = [ - ("1", "Int64(1)"), - ("001", "Int64(1)"), - ("0.1", "Decimal128(Some(1),1,1)"), - ("0.01", "Decimal128(Some(1),2,2)"), - ("1.0", "Decimal128(Some(10),2,1)"), - ("10.01", "Decimal128(Some(1001),4,2)"), - ( - "10000000000000000000.00", - "Decimal128(Some(1000000000000000000000),22,2)", - ), - ("18446744073709551615", "UInt64(18446744073709551615)"), - ( - "18446744073709551616", - "Decimal128(Some(18446744073709551616),20,0)", - ), - ]; - for (a, b) in test_data { - let sql = format!("SELECT {a}"); - let expected = format!("Projection: {b}\n EmptyRelation"); - quick_test_with_options( - &sql, - &expected, - ParserOptions { - parse_float_as_decimal: true, - enable_ident_normalization: false, - support_varchar_with_length: false, - map_varchar_to_utf8view: false, - enable_options_value_normalization: false, - collect_spans: false, - }, - ); - } +fn parse_decimals_1() { + let sql = "SELECT 1"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Int64(1) + EmptyRelation + "# + ); } #[test] -fn parse_ident_normalization() { - let test_data = [ - ( - "SELECT CHARACTER_LENGTH('str')", - "Ok(Projection: character_length(Utf8(\"str\"))\n EmptyRelation)", - false, - ), - ( - "SELECT CONCAT('Hello', 'World')", - "Ok(Projection: concat(Utf8(\"Hello\"), Utf8(\"World\"))\n EmptyRelation)", - false, - ), - ( - "SELECT age FROM person", - "Ok(Projection: person.age\n TableScan: person)", - true, - ), - ( - "SELECT AGE FROM PERSON", - "Ok(Projection: person.age\n TableScan: person)", - true, - ), - ( - "SELECT AGE FROM PERSON", - "Error during planning: No table named: PERSON found", - false, - ), - ( - "SELECT Id FROM UPPERCASE_test", - "Ok(Projection: UPPERCASE_test.Id\ - \n TableScan: UPPERCASE_test)", - false, - ), - ( - "SELECT \"Id\", lower FROM \"UPPERCASE_test\"", - "Ok(Projection: UPPERCASE_test.Id, UPPERCASE_test.lower\ - \n TableScan: UPPERCASE_test)", - true, - ), - ]; +fn parse_decimals_2() { + let sql = "SELECT 001"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Int64(1) + EmptyRelation + "# + ); +} - for (sql, expected, enable_ident_normalization) in test_data { - let plan = logical_plan_with_options( - sql, - ParserOptions { - parse_float_as_decimal: false, - enable_ident_normalization, - support_varchar_with_length: false, - map_varchar_to_utf8view: false, - enable_options_value_normalization: false, - collect_spans: false, - }, - ); - if plan.is_ok() { - let plan = plan.unwrap(); - assert_eq!(expected, format!("Ok({plan})")); - } else { - assert_eq!(expected, plan.unwrap_err().strip_backtrace()); - } - } +#[test] +fn parse_decimals_3() { + let sql = "SELECT 0.1"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Decimal128(Some(1),1,1) + EmptyRelation + "# + ); +} + +#[test] +fn parse_decimals_4() { + let sql = "SELECT 0.01"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Decimal128(Some(1),2,2) + EmptyRelation + "# + ); +} + +#[test] +fn parse_decimals_5() { + let sql = "SELECT 1.0"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Decimal128(Some(10),2,1) + EmptyRelation + "# + ); +} + +#[test] +fn parse_decimals_6() { + let sql = "SELECT 10.01"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Decimal128(Some(1001),4,2) + EmptyRelation + "# + ); +} + +#[test] +fn parse_decimals_7() { + let sql = "SELECT 10000000000000000000.00"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Decimal128(Some(1000000000000000000000),22,2) + EmptyRelation + "# + ); +} + +#[test] +fn parse_decimals_8() { + let sql = "SELECT 18446744073709551615"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: UInt64(18446744073709551615) + EmptyRelation + "# + ); +} + +#[test] +fn parse_decimals_9() { + let sql = "SELECT 18446744073709551616"; + let options = parse_decimals_parser_options(); + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Decimal128(Some(18446744073709551616),20,0) + EmptyRelation + "# + ); +} + +#[test] +fn parse_ident_normalization_1() { + let sql = "SELECT CHARACTER_LENGTH('str')"; + let parser_option = ident_normalization_parser_options_no_ident_normalization(); + let plan = logical_plan_with_options(sql, parser_option).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: character_length(Utf8("str")) + EmptyRelation + "# + ); +} + +#[test] +fn parse_ident_normalization_2() { + let sql = "SELECT CONCAT('Hello', 'World')"; + let parser_option = ident_normalization_parser_options_no_ident_normalization(); + let plan = logical_plan_with_options(sql, parser_option).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: concat(Utf8("Hello"), Utf8("World")) + EmptyRelation + "# + ); +} + +#[test] +fn parse_ident_normalization_3() { + let sql = "SELECT age FROM person"; + let parser_option = ident_normalization_parser_options_ident_normalization(); + let plan = logical_plan_with_options(sql, parser_option).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.age + TableScan: person + "# + ); +} + +#[test] +fn parse_ident_normalization_4() { + let sql = "SELECT AGE FROM PERSON"; + let parser_option = ident_normalization_parser_options_ident_normalization(); + let plan = logical_plan_with_options(sql, parser_option).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.age + TableScan: person + "# + ); +} + +#[test] +fn parse_ident_normalization_5() { + let sql = "SELECT AGE FROM PERSON"; + let parser_option = ident_normalization_parser_options_no_ident_normalization(); + let plan = logical_plan_with_options(sql, parser_option) + .unwrap_err() + .strip_backtrace(); + assert_snapshot!( + plan, + @r#" + Error during planning: No table named: PERSON found + "# + ); +} + +#[test] +fn parse_ident_normalization_6() { + let sql = "SELECT Id FROM UPPERCASE_test"; + let parser_option = ident_normalization_parser_options_no_ident_normalization(); + let plan = logical_plan_with_options(sql, parser_option).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: UPPERCASE_test.Id + TableScan: UPPERCASE_test + "# + ); +} + +#[test] +fn parse_ident_normalization_7() { + let sql = r#"SELECT "Id", lower FROM "UPPERCASE_test""#; + let parser_option = ident_normalization_parser_options_ident_normalization(); + let plan = logical_plan_with_options(sql, parser_option).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: UPPERCASE_test.Id, UPPERCASE_test.lower + TableScan: UPPERCASE_test + "# + ); } #[test] fn select_no_relation() { - quick_test( - "SELECT 1", - "Projection: Int64(1)\ - \n EmptyRelation", + let plan = logical_plan("SELECT 1").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: Int64(1) + EmptyRelation + "# ); } #[test] fn test_real_f32() { - quick_test( - "SELECT CAST(1.1 AS REAL)", - "Projection: CAST(Float64(1.1) AS Float32)\ - \n EmptyRelation", + let plan = logical_plan("SELECT CAST(1.1 AS REAL)").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: CAST(Float64(1.1) AS Float32) + EmptyRelation + "# ); } #[test] fn test_int_decimal_default() { - quick_test( - "SELECT CAST(10 AS DECIMAL)", - "Projection: CAST(Int64(10) AS Decimal128(38, 10))\ - \n EmptyRelation", + let plan = logical_plan("SELECT CAST(10 AS DECIMAL)").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: CAST(Int64(10) AS Decimal128(38, 10)) + EmptyRelation + "# ); } #[test] fn test_int_decimal_no_scale() { - quick_test( - "SELECT CAST(10 AS DECIMAL(5))", - "Projection: CAST(Int64(10) AS Decimal128(5, 0))\ - \n EmptyRelation", + let plan = logical_plan("SELECT CAST(10 AS DECIMAL(5))").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: CAST(Int64(10) AS Decimal128(5, 0)) + EmptyRelation + "# ); } #[test] fn test_tinyint() { - quick_test( - "SELECT CAST(6 AS TINYINT)", - "Projection: CAST(Int64(6) AS Int8)\ - \n EmptyRelation", + let plan = logical_plan("SELECT CAST(6 AS TINYINT)").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: CAST(Int64(6) AS Int8) + EmptyRelation + "# ); } #[test] fn cast_from_subquery() { - quick_test( - "SELECT CAST (a AS FLOAT) FROM (SELECT 1 AS a)", - "Projection: CAST(a AS Float32)\ - \n Projection: Int64(1) AS a\ - \n EmptyRelation", + let plan = logical_plan("SELECT CAST (a AS FLOAT) FROM (SELECT 1 AS a)").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: CAST(a AS Float32) + Projection: Int64(1) AS a + EmptyRelation + "# ); } #[test] fn try_cast_from_aggregation() { - quick_test( - "SELECT TRY_CAST(sum(age) AS FLOAT) FROM person", - "Projection: TRY_CAST(sum(person.age) AS Float32)\ - \n Aggregate: groupBy=[[]], aggr=[[sum(person.age)]]\ - \n TableScan: person", + let plan = logical_plan("SELECT TRY_CAST(sum(age) AS FLOAT) FROM person").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: TRY_CAST(sum(person.age) AS Float32) + Aggregate: groupBy=[[]], aggr=[[sum(person.age)]] + TableScan: person + "# ); } #[test] fn cast_to_invalid_decimal_type_precision_0() { // precision == 0 - { - let sql = "SELECT CAST(10 AS DECIMAL(0))"; - let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Decimal(precision = 0, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`.", - err.strip_backtrace() - ); - } + let sql = "SELECT CAST(10 AS DECIMAL(0))"; + let err = logical_plan(sql).expect_err("query should have failed"); + + assert_snapshot!( + err.strip_backtrace(), + @r"Error during planning: Decimal(precision = 0, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`." + ); } #[test] fn cast_to_invalid_decimal_type_precision_gt_38() { // precision > 38 - { - let sql = "SELECT CAST(10 AS DECIMAL(39))"; - let plan = "Projection: CAST(Int64(10) AS Decimal256(39, 0))\n EmptyRelation"; - quick_test(sql, plan); - } + let sql = "SELECT CAST(10 AS DECIMAL(39))"; + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: CAST(Int64(10) AS Decimal256(39, 0)) + EmptyRelation + "# + ); } #[test] fn cast_to_invalid_decimal_type_precision_gt_76() { // precision > 76 - { - let sql = "SELECT CAST(10 AS DECIMAL(79))"; - let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Decimal(precision = 79, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`.", - err.strip_backtrace() - ); - } + let sql = "SELECT CAST(10 AS DECIMAL(79))"; + let err = logical_plan(sql).expect_err("query should have failed"); + + assert_snapshot!( + err.strip_backtrace(), + @r"Error during planning: Decimal(precision = 79, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`." + ); } #[test] fn cast_to_invalid_decimal_type_precision_lt_scale() { // precision < scale - { - let sql = "SELECT CAST(10 AS DECIMAL(5, 10))"; - let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Decimal(precision = 5, scale = 10) should satisfy `0 < precision <= 76`, and `scale <= precision`.", - err.strip_backtrace() - ); - } + let sql = "SELECT CAST(10 AS DECIMAL(5, 10))"; + let err = logical_plan(sql).expect_err("query should have failed"); + + assert_snapshot!( + err.strip_backtrace(), + @r"Error during planning: Decimal(precision = 5, scale = 10) should satisfy `0 < precision <= 76`, and `scale <= precision`." + ); } #[test] fn plan_create_table_with_pk() { let sql = "create table person (id int, name string, primary key(id))"; - let plan = r#" -CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] - EmptyRelation - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] + EmptyRelation + "# + ); let sql = "create table person (id int primary key, name string)"; - let plan = r#" -CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] - EmptyRelation - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] + EmptyRelation + "# + ); let sql = "create table person (id int, name string unique not null, primary key(id))"; - let plan = r#" -CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), Unique([1])] - EmptyRelation - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), Unique([1])] + EmptyRelation + "# + ); let sql = "create table person (id int, name varchar, primary key(name, id));"; - let plan = r#" -CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([1, 0])] - EmptyRelation - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([1, 0])] + EmptyRelation + "# + ); } #[test] fn plan_create_table_with_multi_pk() { let sql = "create table person (id int, name string primary key, primary key(id))"; - let plan = r#" -CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), PrimaryKey([1])] - EmptyRelation - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), PrimaryKey([1])] + EmptyRelation + "# + ); } #[test] fn plan_create_table_with_unique() { let sql = "create table person (id int unique, name string)"; - let plan = "CreateMemoryTable: Bare { table: \"person\" } constraints=[Unique([0])]\n EmptyRelation"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } constraints=[Unique([0])] + EmptyRelation + "# + ); } #[test] fn plan_create_table_no_pk() { let sql = "create table person (id int, name string)"; - let plan = r#" -CreateMemoryTable: Bare { table: "person" } - EmptyRelation - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } + EmptyRelation + "# + ); } #[test] fn plan_create_table_check_constraint() { let sql = "create table person (id int, name string, unique(id))"; - let plan = "CreateMemoryTable: Bare { table: \"person\" } constraints=[Unique([0])]\n EmptyRelation"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateMemoryTable: Bare { table: "person" } constraints=[Unique([0])] + EmptyRelation + "# + ); } #[test] fn plan_start_transaction() { let sql = "start transaction"; - let plan = "TransactionStart: ReadWrite Serializable"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionStart: ReadWrite Serializable + "# + ); } #[test] fn plan_start_transaction_isolation() { let sql = "start transaction isolation level read committed"; - let plan = "TransactionStart: ReadWrite ReadCommitted"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionStart: ReadWrite ReadCommitted + "# + ); } #[test] fn plan_start_transaction_read_only() { let sql = "start transaction read only"; - let plan = "TransactionStart: ReadOnly Serializable"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionStart: ReadOnly Serializable + "# + ); } #[test] fn plan_start_transaction_fully_qualified() { let sql = "start transaction isolation level read committed read only"; - let plan = "TransactionStart: ReadOnly ReadCommitted"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionStart: ReadOnly ReadCommitted + "# + ); } #[test] @@ -375,95 +567,131 @@ isolation level read committed read only isolation level repeatable read "#; - let plan = "TransactionStart: ReadOnly RepeatableRead"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionStart: ReadOnly RepeatableRead + "# + ); } #[test] fn plan_commit_transaction() { let sql = "commit transaction"; - let plan = "TransactionEnd: Commit chain:=false"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionEnd: Commit chain:=false + "# + ); } #[test] fn plan_commit_transaction_chained() { let sql = "commit transaction and chain"; - let plan = "TransactionEnd: Commit chain:=true"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionEnd: Commit chain:=true + "# + ); } #[test] fn plan_rollback_transaction() { let sql = "rollback transaction"; - let plan = "TransactionEnd: Rollback chain:=false"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionEnd: Rollback chain:=false + "# + ); } #[test] fn plan_rollback_transaction_chained() { let sql = "rollback transaction and chain"; - let plan = "TransactionEnd: Rollback chain:=true"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + TransactionEnd: Rollback chain:=true + "# + ); } #[test] fn plan_copy_to() { let sql = "COPY test_decimal to 'output.csv' STORED AS CSV"; - let plan = r#" -CopyTo: format=csv output_url=output.csv options: () - TableScan: test_decimal - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CopyTo: format=csv output_url=output.csv options: () + TableScan: test_decimal + "# + ); } #[test] fn plan_explain_copy_to() { let sql = "EXPLAIN COPY test_decimal to 'output.csv'"; - let plan = r#" -Explain - CopyTo: format=csv output_url=output.csv options: () - TableScan: test_decimal - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Explain + CopyTo: format=csv output_url=output.csv options: () + TableScan: test_decimal + "# + ); } #[test] fn plan_explain_copy_to_format() { let sql = "EXPLAIN COPY test_decimal to 'output.tbl' STORED AS CSV"; - let plan = r#" -Explain - CopyTo: format=csv output_url=output.tbl options: () - TableScan: test_decimal - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Explain + CopyTo: format=csv output_url=output.tbl options: () + TableScan: test_decimal + "# + ); } #[test] fn plan_insert() { let sql = "insert into person (id, first_name, last_name) values (1, 'Alan', 'Turing')"; - let plan = "Dml: op=[Insert Into] table=[person]\ - \n Projection: column1 AS id, column2 AS first_name, column3 AS last_name, \ - CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, \ - CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀\ - \n Values: (CAST(Int64(1) AS UInt32), Utf8(\"Alan\"), Utf8(\"Turing\"))"; - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Dml: op=[Insert Into] table=[person] + Projection: column1 AS id, column2 AS first_name, column3 AS last_name, CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀 + Values: (CAST(Int64(1) AS UInt32), Utf8("Alan"), Utf8("Turing")) + "# + ); } #[test] fn plan_insert_no_target_columns() { let sql = "INSERT INTO test_decimal VALUES (1, 2), (3, 4)"; - let plan = r#" -Dml: op=[Insert Into] table=[test_decimal] - Projection: column1 AS id, column2 AS price - Values: (CAST(Int64(1) AS Int32), CAST(Int64(2) AS Decimal128(10, 2))), (CAST(Int64(3) AS Int32), CAST(Int64(4) AS Decimal128(10, 2))) - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Dml: op=[Insert Into] table=[test_decimal] + Projection: column1 AS id, column2 AS price + Values: (CAST(Int64(1) AS Int32), CAST(Int64(2) AS Decimal128(10, 2))), (CAST(Int64(3) AS Int32), CAST(Int64(4) AS Decimal128(10, 2))) + "# + ); } #[rstest] @@ -501,14 +729,16 @@ fn test_insert_schema_errors(#[case] sql: &str, #[case] error: &str) { #[test] fn plan_update() { let sql = "update person set last_name='Kay' where id=1"; - let plan = r#" -Dml: op=[Update] table=[person] - Projection: person.id AS id, person.first_name AS first_name, Utf8("Kay") AS last_name, person.age AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 - Filter: person.id = Int64(1) - TableScan: person - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Dml: op=[Update] table=[person] + Projection: person.id AS id, person.first_name AS first_name, Utf8("Kay") AS last_name, person.age AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 + Filter: person.id = Int64(1) + TableScan: person + "# + ); } #[rstest] @@ -526,26 +756,30 @@ fn update_column_does_not_exist(#[case] sql: &str) { #[test] fn plan_delete() { let sql = "delete from person where id=1"; - let plan = r#" -Dml: op=[Delete] table=[person] - Filter: id = Int64(1) - TableScan: person - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Dml: op=[Delete] table=[person] + Filter: id = Int64(1) + TableScan: person + "# + ); } #[test] fn plan_delete_quoted_identifier_case_sensitive() { let sql = "DELETE FROM \"SomeCatalog\".\"SomeSchema\".\"UPPERCASE_test\" WHERE \"Id\" = 1"; - let plan = r#" -Dml: op=[Delete] table=[SomeCatalog.SomeSchema.UPPERCASE_test] - Filter: Id = Int64(1) - TableScan: SomeCatalog.SomeSchema.UPPERCASE_test - "# - .trim(); - quick_test(sql, plan); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Dml: op=[Delete] table=[SomeCatalog.SomeSchema.UPPERCASE_test] + Filter: Id = Int64(1) + TableScan: SomeCatalog.SomeSchema.UPPERCASE_test + "# + ); } #[test] @@ -559,18 +793,24 @@ fn select_column_does_not_exist() { fn select_repeated_column() { let sql = "SELECT age, age FROM person"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Projections require unique expression names but the expression \"person.age\" at position 0 and \"person.age\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Projections require unique expression names but the expression "person.age" at position 0 and "person.age" at position 1 have the same name. Consider aliasing ("AS") one of them. + "# ); } #[test] fn select_scalar_func_with_literal_no_relation() { - quick_test( - "SELECT sqrt(9)", - "Projection: sqrt(Int64(9))\ - \n EmptyRelation", + let plan = logical_plan("SELECT sqrt(9)").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: sqrt(Int64(9)) + EmptyRelation + "# ); } @@ -578,10 +818,15 @@ fn select_scalar_func_with_literal_no_relation() { fn select_simple_filter() { let sql = "SELECT id, first_name, last_name \ FROM person WHERE state = 'CO'"; - let expected = "Projection: person.id, person.first_name, person.last_name\ - \n Filter: person.state = Utf8(\"CO\")\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.id, person.first_name, person.last_name + Filter: person.state = Utf8("CO") + TableScan: person + "# + ); } #[test] @@ -602,40 +847,58 @@ fn select_filter_cannot_use_alias() { fn select_neg_filter() { let sql = "SELECT id, first_name, last_name \ FROM person WHERE NOT state"; - let expected = "Projection: person.id, person.first_name, person.last_name\ - \n Filter: NOT person.state\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.id, person.first_name, person.last_name + Filter: NOT person.state + TableScan: person + "# + ); } #[test] fn select_compound_filter() { let sql = "SELECT id, first_name, last_name \ FROM person WHERE state = 'CO' AND age >= 21 AND age <= 65"; - let expected = "Projection: person.id, person.first_name, person.last_name\ - \n Filter: person.state = Utf8(\"CO\") AND person.age >= Int64(21) AND person.age <= Int64(65)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.id, person.first_name, person.last_name + Filter: person.state = Utf8("CO") AND person.age >= Int64(21) AND person.age <= Int64(65) + TableScan: person + "# + ); } #[test] fn test_timestamp_filter() { let sql = "SELECT state FROM person WHERE birth_date < CAST (158412331400600000 as timestamp)"; - let expected = "Projection: person.state\ - \n Filter: person.birth_date < CAST(CAST(Int64(158412331400600000) AS Timestamp(Second, None)) AS Timestamp(Nanosecond, None))\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state + Filter: person.birth_date < CAST(CAST(Int64(158412331400600000) AS Timestamp(Second, None)) AS Timestamp(Nanosecond, None)) + TableScan: person + "# + ); } #[test] fn test_date_filter() { let sql = "SELECT state FROM person WHERE birth_date < CAST ('2020-01-01' as date)"; - - let expected = "Projection: person.state\ - \n Filter: person.birth_date < CAST(Utf8(\"2020-01-01\") AS Date32)\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state + Filter: person.birth_date < CAST(Utf8("2020-01-01") AS Date32) + TableScan: person + "# + ); } #[test] @@ -648,35 +911,43 @@ fn select_all_boolean_operators() { AND age >= 21 \ AND age < 65 \ AND age <= 65"; - let expected = "Projection: person.age, person.first_name, person.last_name\ - \n Filter: person.age = Int64(21) \ - AND person.age != Int64(21) \ - AND person.age > Int64(21) \ - AND person.age >= Int64(21) \ - AND person.age < Int64(65) \ - AND person.age <= Int64(65)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.age, person.first_name, person.last_name + Filter: person.age = Int64(21) AND person.age != Int64(21) AND person.age > Int64(21) AND person.age >= Int64(21) AND person.age < Int64(65) AND person.age <= Int64(65) + TableScan: person + "# + ); } #[test] fn select_between() { let sql = "SELECT state FROM person WHERE age BETWEEN 21 AND 65"; - let expected = "Projection: person.state\ - \n Filter: person.age BETWEEN Int64(21) AND Int64(65)\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state + Filter: person.age BETWEEN Int64(21) AND Int64(65) + TableScan: person + "# + ); } #[test] fn select_between_negated() { let sql = "SELECT state FROM person WHERE age NOT BETWEEN 21 AND 65"; - let expected = "Projection: person.state\ - \n Filter: person.age NOT BETWEEN Int64(21) AND Int64(65)\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state + Filter: person.age NOT BETWEEN Int64(21) AND Int64(65) + TableScan: person + "# + ); } #[test] @@ -689,13 +960,18 @@ fn select_nested() { FROM person ) AS a ) AS b"; - let expected = "Projection: b.fn2, b.last_name\ - \n SubqueryAlias: b\ - \n Projection: a.fn1 AS fn2, a.last_name, a.birth_date\ - \n SubqueryAlias: a\ - \n Projection: person.first_name AS fn1, person.last_name, person.birth_date, person.age\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: b.fn2, b.last_name + SubqueryAlias: b + Projection: a.fn1 AS fn2, a.last_name, a.birth_date + SubqueryAlias: a + Projection: person.first_name AS fn1, person.last_name, person.birth_date, person.age + TableScan: person + "# + ); } #[test] @@ -707,27 +983,34 @@ fn select_nested_with_filters() { WHERE age > 20 ) AS a WHERE fn1 = 'X' AND age < 30"; - - let expected = "Projection: a.fn1, a.age\ - \n Filter: a.fn1 = Utf8(\"X\") AND a.age < Int64(30)\ - \n SubqueryAlias: a\ - \n Projection: person.first_name AS fn1, person.age\ - \n Filter: person.age > Int64(20)\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: a.fn1, a.age + Filter: a.fn1 = Utf8("X") AND a.age < Int64(30) + SubqueryAlias: a + Projection: person.first_name AS fn1, person.age + Filter: person.age > Int64(20) + TableScan: person + "# + ); } #[test] fn table_with_column_alias() { let sql = "SELECT a, b, c FROM lineitem l (a, b, c)"; - let expected = "Projection: l.a, l.b, l.c\ - \n SubqueryAlias: l\ - \n Projection: lineitem.l_item_id AS a, lineitem.l_description AS b, lineitem.price AS c\ - \n TableScan: lineitem"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: l.a, l.b, l.c + SubqueryAlias: l + Projection: lineitem.l_item_id AS a, lineitem.l_description AS b, lineitem.price AS c + TableScan: lineitem + "# + ); } #[test] @@ -735,9 +1018,10 @@ fn table_with_column_alias_number_cols() { let sql = "SELECT a, b, c FROM lineitem l (a, b)"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Source table contains 3 columns but only 2 names given as column alias", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r"Error during planning: Source table contains 3 columns but only 2 names given as column alias" ); } @@ -745,9 +1029,10 @@ fn table_with_column_alias_number_cols() { fn select_with_ambiguous_column() { let sql = "SELECT id FROM person a, person b"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Schema error: Ambiguous reference to unqualified field id", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r"Schema error: Ambiguous reference to unqualified field id" ); } @@ -755,37 +1040,52 @@ fn select_with_ambiguous_column() { fn join_with_ambiguous_column() { // This is legal. let sql = "SELECT id FROM person a join person b using(id)"; - let expected = "Projection: a.id\ - \n Inner Join: Using a.id = b.id\ - \n SubqueryAlias: a\ - \n TableScan: person\ - \n SubqueryAlias: b\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: a.id + Inner Join: Using a.id = b.id + SubqueryAlias: a + TableScan: person + SubqueryAlias: b + TableScan: person + "# + ); } #[test] fn natural_left_join() { let sql = "SELECT l_item_id FROM lineitem a NATURAL LEFT JOIN lineitem b"; - let expected = "Projection: a.l_item_id\ - \n Left Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price\ - \n SubqueryAlias: a\ - \n TableScan: lineitem\ - \n SubqueryAlias: b\ - \n TableScan: lineitem"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: a.l_item_id + Left Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price + SubqueryAlias: a + TableScan: lineitem + SubqueryAlias: b + TableScan: lineitem + "# + ); } #[test] fn natural_right_join() { let sql = "SELECT l_item_id FROM lineitem a NATURAL RIGHT JOIN lineitem b"; - let expected = "Projection: a.l_item_id\ - \n Right Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price\ - \n SubqueryAlias: a\ - \n TableScan: lineitem\ - \n SubqueryAlias: b\ - \n TableScan: lineitem"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: a.l_item_id + Right Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price + SubqueryAlias: a + TableScan: lineitem + SubqueryAlias: b + TableScan: lineitem + "# + ); } #[test] @@ -794,10 +1094,11 @@ fn select_with_having() { FROM person HAVING age > 100 AND age < 200"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: HAVING clause references: person.age > Int64(100) AND person.age < Int64(200) must appear in the GROUP BY clause or be used in an aggregate function", - err.strip_backtrace() - ); + + assert_snapshot!( + err.strip_backtrace(), + @r"Error during planning: HAVING clause references: person.age > Int64(100) AND person.age < Int64(200) must appear in the GROUP BY clause or be used in an aggregate function" + ); } #[test] @@ -806,10 +1107,13 @@ fn select_with_having_referencing_column_not_in_select() { FROM person HAVING first_name = 'M'"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: HAVING clause references: person.first_name = Utf8(\"M\") must appear in the GROUP BY clause or be used in an aggregate function", - err.strip_backtrace() - ); + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: HAVING clause references: person.first_name = Utf8("M") must appear in the GROUP BY clause or be used in an aggregate function + "# + ); } #[test] @@ -819,10 +1123,13 @@ fn select_with_having_refers_to_invalid_column() { GROUP BY id HAVING first_name = 'M'"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.first_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.id, max(person.age)\" appears in the SELECT clause satisfies this requirement", - err.strip_backtrace() - ); + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.first_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.id, max(person.age)" appears in the SELECT clause satisfies this requirement + "# + ); } #[test] @@ -831,10 +1138,13 @@ fn select_with_having_referencing_column_nested_in_select_expression() { FROM person HAVING age > 100"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: HAVING clause references: person.age > Int64(100) must appear in the GROUP BY clause or be used in an aggregate function", - err.strip_backtrace() - ); + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: HAVING clause references: person.age > Int64(100) must appear in the GROUP BY clause or be used in an aggregate function + "# + ); } #[test] @@ -843,10 +1153,11 @@ fn select_with_having_with_aggregate_not_in_select() { FROM person HAVING MAX(age) > 100"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.first_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"max(person.age)\" appears in the SELECT clause satisfies this requirement", - err.strip_backtrace() - ); + + assert_snapshot!( + err.strip_backtrace(), + @r#"Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.first_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "max(person.age)" appears in the SELECT clause satisfies this requirement"# + ); } #[test] @@ -854,11 +1165,16 @@ fn select_aggregate_with_having_that_reuses_aggregate() { let sql = "SELECT MAX(age) FROM person HAVING MAX(age) < 30"; - let expected = "Projection: max(person.age)\ - \n Filter: max(person.age) < Int64(30)\ - \n Aggregate: groupBy=[[]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: max(person.age) + Filter: max(person.age) < Int64(30) + Aggregate: groupBy=[[]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -866,11 +1182,16 @@ fn select_aggregate_with_having_with_aggregate_not_in_select() { let sql = "SELECT max(age) FROM person HAVING max(first_name) > 'M'"; - let expected = "Projection: max(person.age)\ - \n Filter: max(person.first_name) > Utf8(\"M\")\ - \n Aggregate: groupBy=[[]], aggr=[[max(person.age), max(person.first_name)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: max(person.age) + Filter: max(person.first_name) > Utf8("M") + Aggregate: groupBy=[[]], aggr=[[max(person.age), max(person.first_name)]] + TableScan: person + "# + ); } #[test] @@ -879,9 +1200,12 @@ fn select_aggregate_with_having_referencing_column_not_in_select() { FROM person HAVING first_name = 'M'"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.first_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"count(*)\" appears in the SELECT clause satisfies this requirement", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.first_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "count(*)" appears in the SELECT clause satisfies this requirement + "# ); } @@ -891,11 +1215,16 @@ fn select_aggregate_aliased_with_having_referencing_aggregate_by_its_alias() { FROM person HAVING max_age < 30"; // FIXME: add test for having in execution - let expected = "Projection: max(person.age) AS max_age\ - \n Filter: max(person.age) < Int64(30)\ - \n Aggregate: groupBy=[[]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: max(person.age) AS max_age + Filter: max(person.age) < Int64(30) + Aggregate: groupBy=[[]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -903,11 +1232,16 @@ fn select_aggregate_aliased_with_having_that_reuses_aggregate_but_not_by_its_ali let sql = "SELECT max(age) as max_age FROM person HAVING max(age) < 30"; - let expected = "Projection: max(person.age) AS max_age\ - \n Filter: max(person.age) < Int64(30)\ - \n Aggregate: groupBy=[[]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: max(person.age) AS max_age + Filter: max(person.age) < Int64(30) + Aggregate: groupBy=[[]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -916,11 +1250,16 @@ fn select_aggregate_with_group_by_with_having() { FROM person GROUP BY first_name HAVING first_name = 'M'"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: person.first_name = Utf8(\"M\")\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: person.first_name = Utf8("M") + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -930,12 +1269,17 @@ fn select_aggregate_with_group_by_with_having_and_where() { WHERE id > 5 GROUP BY first_name HAVING MAX(age) < 100"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: max(person.age) < Int64(100)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n Filter: person.id > Int64(5)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: max(person.age) < Int64(100) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + Filter: person.id > Int64(5) + TableScan: person + "# + ); } #[test] @@ -945,12 +1289,17 @@ fn select_aggregate_with_group_by_with_having_and_where_filtering_on_aggregate_c WHERE id > 5 AND age > 18 GROUP BY first_name HAVING MAX(age) < 100"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: max(person.age) < Int64(100)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n Filter: person.id > Int64(5) AND person.age > Int64(18)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: max(person.age) < Int64(100) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + Filter: person.id > Int64(5) AND person.age > Int64(18) + TableScan: person + "# + ); } #[test] @@ -959,11 +1308,16 @@ fn select_aggregate_with_group_by_with_having_using_column_by_alias() { FROM person GROUP BY first_name HAVING MAX(age) > 2 AND fn = 'M'"; - let expected = "Projection: person.first_name AS fn, max(person.age)\ - \n Filter: max(person.age) > Int64(2) AND person.first_name = Utf8(\"M\")\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name AS fn, max(person.age) + Filter: max(person.age) > Int64(2) AND person.first_name = Utf8("M") + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -973,11 +1327,16 @@ fn select_aggregate_with_group_by_with_having_using_columns_with_and_without_the FROM person GROUP BY first_name HAVING MAX(age) > 2 AND max_age < 5 AND first_name = 'M' AND fn = 'N'"; - let expected = "Projection: person.first_name AS fn, max(person.age) AS max_age\ - \n Filter: max(person.age) > Int64(2) AND max(person.age) < Int64(5) AND person.first_name = Utf8(\"M\") AND person.first_name = Utf8(\"N\")\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name AS fn, max(person.age) AS max_age + Filter: max(person.age) > Int64(2) AND max(person.age) < Int64(5) AND person.first_name = Utf8("M") AND person.first_name = Utf8("N") + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -986,11 +1345,16 @@ fn select_aggregate_with_group_by_with_having_that_reuses_aggregate() { FROM person GROUP BY first_name HAVING MAX(age) > 100"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: max(person.age) > Int64(100)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: max(person.age) > Int64(100) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -1000,9 +1364,12 @@ fn select_aggregate_with_group_by_with_having_referencing_column_not_in_group_by GROUP BY first_name HAVING MAX(age) > 10 AND last_name = 'M'"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.last_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.first_name, max(person.age)\" appears in the SELECT clause satisfies this requirement", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.last_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.first_name, max(person.age)" appears in the SELECT clause satisfies this requirement + "# ); } @@ -1012,11 +1379,16 @@ fn select_aggregate_with_group_by_with_having_that_reuses_aggregate_multiple_tim FROM person GROUP BY first_name HAVING MAX(age) > 100 AND MAX(age) < 200"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: max(person.age) > Int64(100) AND max(person.age) < Int64(200)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: max(person.age) > Int64(100) AND max(person.age) < Int64(200) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -1025,11 +1397,16 @@ fn select_aggregate_with_group_by_with_having_using_aggregate_not_in_select() { FROM person GROUP BY first_name HAVING MAX(age) > 100 AND MIN(id) < 50"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: max(person.age) > Int64(100) AND min(person.id) < Int64(50)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: max(person.age) > Int64(100) AND min(person.id) < Int64(50) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id)]] + TableScan: person + "# + ); } #[test] @@ -1039,11 +1416,16 @@ fn select_aggregate_aliased_with_group_by_with_having_referencing_aggregate_by_i FROM person GROUP BY first_name HAVING max_age > 100"; - let expected = "Projection: person.first_name, max(person.age) AS max_age\ - \n Filter: max(person.age) > Int64(100)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) AS max_age + Filter: max(person.age) > Int64(100) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -1053,11 +1435,16 @@ fn select_aggregate_compound_aliased_with_group_by_with_having_referencing_compo FROM person GROUP BY first_name HAVING max_age_plus_one > 100"; - let expected = "Projection: person.first_name, max(person.age) + Int64(1) AS max_age_plus_one\ - \n Filter: max(person.age) + Int64(1) > Int64(100)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Int64(1) AS max_age_plus_one + Filter: max(person.age) + Int64(1) > Int64(100) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] + TableScan: person + "# + ); } #[test] @@ -1067,11 +1454,16 @@ fn select_aggregate_with_group_by_with_having_using_derived_column_aggregate_not FROM person GROUP BY first_name HAVING MAX(age) > 100 AND MIN(id - 2) < 50"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: max(person.age) > Int64(100) AND min(person.id - Int64(2)) < Int64(50)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id - Int64(2))]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: max(person.age) > Int64(100) AND min(person.id - Int64(2)) < Int64(50) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id - Int64(2))]] + TableScan: person + "# + ); } #[test] @@ -1080,46 +1472,67 @@ fn select_aggregate_with_group_by_with_having_using_count_star_not_in_select() { FROM person GROUP BY first_name HAVING MAX(age) > 100 AND count(*) < 50"; - let expected = "Projection: person.first_name, max(person.age)\ - \n Filter: max(person.age) > Int64(100) AND count(*) < Int64(50)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), count(*)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.first_name, max(person.age) + Filter: max(person.age) > Int64(100) AND count(*) < Int64(50) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), count(*)]] + TableScan: person + "# + ); } #[test] fn select_binary_expr() { let sql = "SELECT age + salary from person"; - let expected = "Projection: person.age + person.salary\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.age + person.salary + TableScan: person + "# + ); } #[test] fn select_binary_expr_nested() { let sql = "SELECT (age + salary)/2 from person"; - let expected = "Projection: (person.age + person.salary) / Int64(2)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: (person.age + person.salary) / Int64(2) + TableScan: person + "# + ); } #[test] fn select_simple_aggregate() { - quick_test( - "SELECT MIN(age) FROM person", - "Projection: min(person.age)\ - \n Aggregate: groupBy=[[]], aggr=[[min(person.age)]]\ - \n TableScan: person", + let plan = logical_plan("SELECT MIN(age) FROM person").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: min(person.age) + Aggregate: groupBy=[[]], aggr=[[min(person.age)]] + TableScan: person + "# ); } #[test] fn test_sum_aggregate() { - quick_test( - "SELECT sum(age) from person", - "Projection: sum(person.age)\ - \n Aggregate: groupBy=[[]], aggr=[[sum(person.age)]]\ - \n TableScan: person", + let plan = logical_plan("SELECT sum(age) from person").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: sum(person.age) + Aggregate: groupBy=[[]], aggr=[[sum(person.age)]] + TableScan: person + "# ); } @@ -1134,70 +1547,97 @@ fn select_simple_aggregate_column_does_not_exist() { fn select_simple_aggregate_repeated_aggregate() { let sql = "SELECT MIN(age), MIN(age) FROM person"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Projections require unique expression names but the expression \"min(person.age)\" at position 0 and \"min(person.age)\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Projections require unique expression names but the expression "min(person.age)" at position 0 and "min(person.age)" at position 1 have the same name. Consider aliasing ("AS") one of them. + "# ); } #[test] fn select_simple_aggregate_repeated_aggregate_with_single_alias() { - quick_test( - "SELECT MIN(age), MIN(age) AS a FROM person", - "Projection: min(person.age), min(person.age) AS a\ - \n Aggregate: groupBy=[[]], aggr=[[min(person.age)]]\ - \n TableScan: person", + let plan = logical_plan("SELECT MIN(age), MIN(age) AS a FROM person").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: min(person.age), min(person.age) AS a + Aggregate: groupBy=[[]], aggr=[[min(person.age)]] + TableScan: person + "# ); } #[test] fn select_simple_aggregate_repeated_aggregate_with_unique_aliases() { - quick_test( - "SELECT MIN(age) AS a, MIN(age) AS b FROM person", - "Projection: min(person.age) AS a, min(person.age) AS b\ - \n Aggregate: groupBy=[[]], aggr=[[min(person.age)]]\ - \n TableScan: person", + let plan = logical_plan("SELECT MIN(age) AS a, MIN(age) AS b FROM person").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: min(person.age) AS a, min(person.age) AS b + Aggregate: groupBy=[[]], aggr=[[min(person.age)]] + TableScan: person + "# ); } #[test] fn select_from_typed_string_values() { - quick_test( - "SELECT col1, col2 FROM (VALUES (TIMESTAMP '2021-06-10 17:01:00Z', DATE '2004-04-09')) as t (col1, col2)", - "Projection: t.col1, t.col2\ - \n SubqueryAlias: t\ - \n Projection: column1 AS col1, column2 AS col2\ - \n Values: (CAST(Utf8(\"2021-06-10 17:01:00Z\") AS Timestamp(Nanosecond, None)), CAST(Utf8(\"2004-04-09\") AS Date32))", - ); + let plan = logical_plan( + "SELECT col1, col2 FROM (VALUES (TIMESTAMP '2021-06-10 17:01:00Z', DATE '2004-04-09')) as t (col1, col2)", + ).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: t.col1, t.col2 + SubqueryAlias: t + Projection: column1 AS col1, column2 AS col2 + Values: (CAST(Utf8("2021-06-10 17:01:00Z") AS Timestamp(Nanosecond, None)), CAST(Utf8("2004-04-09") AS Date32)) + "# + ); } #[test] fn select_simple_aggregate_repeated_aggregate_with_repeated_aliases() { let sql = "SELECT MIN(age) AS a, MIN(age) AS a FROM person"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Projections require unique expression names but the expression \"min(person.age) AS a\" at position 0 and \"min(person.age) AS a\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Projections require unique expression names but the expression "min(person.age) AS a" at position 0 and "min(person.age) AS a" at position 1 have the same name. Consider aliasing ("AS") one of them. + "# ); } #[test] fn select_simple_aggregate_with_groupby() { - quick_test( - "SELECT state, MIN(age), MAX(age) FROM person GROUP BY state", - "Projection: person.state, min(person.age), max(person.age)\ - \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT state, MIN(age), MAX(age) FROM person GROUP BY state") + .unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state, min(person.age), max(person.age) + Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]] + TableScan: person + "# ); } #[test] fn select_simple_aggregate_with_groupby_with_aliases() { - quick_test( - "SELECT state AS a, MIN(age) AS b FROM person GROUP BY state", - "Projection: person.state AS a, min(person.age) AS b\ - \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT state AS a, MIN(age) AS b FROM person GROUP BY state") + .unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state AS a, min(person.age) AS b + Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] + TableScan: person + "# ); } @@ -1205,19 +1645,26 @@ fn select_simple_aggregate_with_groupby_with_aliases() { fn select_simple_aggregate_with_groupby_with_aliases_repeated() { let sql = "SELECT state AS a, MIN(age) AS a FROM person GROUP BY state"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Projections require unique expression names but the expression \"person.state AS a\" at position 0 and \"min(person.age) AS a\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Projections require unique expression names but the expression "person.state AS a" at position 0 and "min(person.age) AS a" at position 1 have the same name. Consider aliasing ("AS") one of them. + "# ); } #[test] fn select_simple_aggregate_with_groupby_column_unselected() { - quick_test( - "SELECT MIN(age), MAX(age) FROM person GROUP BY state", - "Projection: min(person.age), max(person.age)\ - \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT MIN(age), MAX(age) FROM person GROUP BY state").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: min(person.age), max(person.age) + Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]] + TableScan: person + "# ); } @@ -1225,11 +1672,13 @@ fn select_simple_aggregate_with_groupby_column_unselected() { fn select_simple_aggregate_with_groupby_and_column_in_group_by_does_not_exist() { let sql = "SELECT sum(age) FROM person GROUP BY doesnotexist"; let err = logical_plan(sql).expect_err("query should have failed"); - let expected = "Schema error: No field named doesnotexist. \ - Valid fields are \"sum(person.age)\", \ - person.id, person.first_name, person.last_name, person.age, person.state, \ - person.salary, person.birth_date, person.\"😀\"."; - assert_eq!(err.strip_backtrace(), expected); + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Schema error: No field named doesnotexist. Valid fields are "sum(person.age)", person.id, person.first_name, person.last_name, person.age, person.state, person.salary, person.birth_date, person."😀". + "# + ); } #[test] @@ -1243,35 +1692,50 @@ fn select_simple_aggregate_with_groupby_and_column_in_aggregate_does_not_exist() fn select_interval_out_of_range() { let sql = "SELECT INTERVAL '100000000000000000 day'"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Arrow error: Invalid argument error: Unable to represent 100000000000000000 days in a signed 32-bit integer", + + assert_snapshot!( err.strip_backtrace(), + @r#" + Arrow error: Invalid argument error: Unable to represent 100000000000000000 days in a signed 32-bit integer + "# ); } #[test] fn select_simple_aggregate_with_groupby_and_column_is_in_aggregate_and_groupby() { - quick_test( - "SELECT MAX(first_name) FROM person GROUP BY first_name", - "Projection: max(person.first_name)\ - \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.first_name)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT MAX(first_name) FROM person GROUP BY first_name").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: max(person.first_name) + Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.first_name)]] + TableScan: person + "# ); } #[test] fn select_simple_aggregate_with_groupby_can_use_positions() { - quick_test( - "SELECT state, age AS b, count(1) FROM person GROUP BY 1, 2", - "Projection: person.state, person.age AS b, count(Int64(1))\ - \n Aggregate: groupBy=[[person.state, person.age]], aggr=[[count(Int64(1))]]\ - \n TableScan: person", + let plan = logical_plan("SELECT state, age AS b, count(1) FROM person GROUP BY 1, 2") + .unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state, person.age AS b, count(Int64(1)) + Aggregate: groupBy=[[person.state, person.age]], aggr=[[count(Int64(1))]] + TableScan: person + "# ); - quick_test( - "SELECT state, age AS b, count(1) FROM person GROUP BY 2, 1", - "Projection: person.state, person.age AS b, count(Int64(1))\ - \n Aggregate: groupBy=[[person.age, person.state]], aggr=[[count(Int64(1))]]\ - \n TableScan: person", + let plan = logical_plan("SELECT state, age AS b, count(1) FROM person GROUP BY 2, 1") + .unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state, person.age AS b, count(Int64(1)) + Aggregate: groupBy=[[person.age, person.state]], aggr=[[count(Int64(1))]] + TableScan: person + "# ); } @@ -1279,26 +1743,36 @@ fn select_simple_aggregate_with_groupby_can_use_positions() { fn select_simple_aggregate_with_groupby_position_out_of_range() { let sql = "SELECT state, MIN(age) FROM person GROUP BY 0"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Cannot find column with position 0 in SELECT clause. Valid columns: 1 to 2", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Cannot find column with position 0 in SELECT clause. Valid columns: 1 to 2 + "# ); let sql2 = "SELECT state, MIN(age) FROM person GROUP BY 5"; let err2 = logical_plan(sql2).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Cannot find column with position 5 in SELECT clause. Valid columns: 1 to 2", - err2.strip_backtrace() + + assert_snapshot!( + err2.strip_backtrace(), + @r#" + Error during planning: Cannot find column with position 5 in SELECT clause. Valid columns: 1 to 2 + "# ); } #[test] fn select_simple_aggregate_with_groupby_can_use_alias() { - quick_test( - "SELECT state AS a, MIN(age) AS b FROM person GROUP BY a", - "Projection: person.state AS a, min(person.age) AS b\ - \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT state AS a, MIN(age) AS b FROM person GROUP BY a").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state AS a, min(person.age) AS b + Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] + TableScan: person + "# ); } @@ -1306,56 +1780,83 @@ fn select_simple_aggregate_with_groupby_can_use_alias() { fn select_simple_aggregate_with_groupby_aggregate_repeated() { let sql = "SELECT state, MIN(age), MIN(age) FROM person GROUP BY state"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Projections require unique expression names but the expression \"min(person.age)\" at position 1 and \"min(person.age)\" at position 2 have the same name. Consider aliasing (\"AS\") one of them.", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Projections require unique expression names but the expression "min(person.age)" at position 1 and "min(person.age)" at position 2 have the same name. Consider aliasing ("AS") one of them. + "# ); } #[test] fn select_simple_aggregate_with_groupby_aggregate_repeated_and_one_has_alias() { - quick_test( - "SELECT state, MIN(age), MIN(age) AS ma FROM person GROUP BY state", - "Projection: person.state, min(person.age), min(person.age) AS ma\ - \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ - \n TableScan: person", - ) + let plan = + logical_plan("SELECT state, MIN(age), MIN(age) AS ma FROM person GROUP BY state") + .unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state, min(person.age), min(person.age) AS ma + Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] + TableScan: person + "# + ); } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_unselected() { - quick_test( - "SELECT MIN(first_name) FROM person GROUP BY age + 1", - "Projection: min(person.first_name)\ - \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT MIN(first_name) FROM person GROUP BY age + 1").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: min(person.first_name) + Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] + TableScan: person + "# ); } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_selected_and_resolvable() { - quick_test( - "SELECT age + 1, MIN(first_name) FROM person GROUP BY age + 1", - "Projection: person.age + Int64(1), min(person.first_name)\ - \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT age + 1, MIN(first_name) FROM person GROUP BY age + 1") + .unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.age + Int64(1), min(person.first_name) + Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] + TableScan: person + "# ); - quick_test( - "SELECT MIN(first_name), age + 1 FROM person GROUP BY age + 1", - "Projection: min(person.first_name), person.age + Int64(1)\ - \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT MIN(first_name), age + 1 FROM person GROUP BY age + 1") + .unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: min(person.first_name), person.age + Int64(1) + Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] + TableScan: person + "# ); } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_nested_and_resolvable() { - quick_test( - "SELECT ((age + 1) / 2) * (age + 1), MIN(first_name) FROM person GROUP BY age + 1", - "Projection: person.age + Int64(1) / Int64(2) * person.age + Int64(1), min(person.first_name)\ - \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ - \n TableScan: person", - ); + let plan = logical_plan( + "SELECT ((age + 1) / 2) * (age + 1), MIN(first_name) FROM person GROUP BY age + 1" + ).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.age + Int64(1) / Int64(2) * person.age + Int64(1), min(person.first_name) + Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] + TableScan: person + "# + ); } #[test] @@ -1364,131 +1865,192 @@ fn select_simple_aggregate_with_groupby_non_column_expression_nested_and_not_res // The query should fail, because age + 9 is not in the group by. let sql = "SELECT ((age + 1) / 2) * (age + 9), MIN(first_name) FROM person GROUP BY age + 1"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.age\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.age + Int64(1), min(person.first_name)\" appears in the SELECT clause satisfies this requirement", - err.strip_backtrace() - ); + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.age" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.age + Int64(1), min(person.first_name)" appears in the SELECT clause satisfies this requirement + "# + ); } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_and_its_column_selected() { let sql = "SELECT age, MIN(first_name) FROM person GROUP BY age + 1"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.age\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.age + Int64(1), min(person.first_name)\" appears in the SELECT clause satisfies this requirement", - err.strip_backtrace() - ); + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.age" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.age + Int64(1), min(person.first_name)" appears in the SELECT clause satisfies this requirement + "# + ); } #[test] fn select_simple_aggregate_nested_in_binary_expr_with_groupby() { - quick_test( - "SELECT state, MIN(age) < 10 FROM person GROUP BY state", - "Projection: person.state, min(person.age) < Int64(10)\ - \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT state, MIN(age) < 10 FROM person GROUP BY state").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state, min(person.age) < Int64(10) + Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] + TableScan: person + "# ); } #[test] fn select_simple_aggregate_and_nested_groupby_column() { - quick_test( - "SELECT age + 1, MAX(first_name) FROM person GROUP BY age", - "Projection: person.age + Int64(1), max(person.first_name)\ - \n Aggregate: groupBy=[[person.age]], aggr=[[max(person.first_name)]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT MAX(first_name), age + 1 FROM person GROUP BY age").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: max(person.first_name), person.age + Int64(1) + Aggregate: groupBy=[[person.age]], aggr=[[max(person.first_name)]] + TableScan: person + "# ); } #[test] fn select_aggregate_compounded_with_groupby_column() { - quick_test( - "SELECT age + MIN(salary) FROM person GROUP BY age", - "Projection: person.age + min(person.salary)\ - \n Aggregate: groupBy=[[person.age]], aggr=[[min(person.salary)]]\ - \n TableScan: person", + let plan = logical_plan("SELECT age + MIN(salary) FROM person GROUP BY age").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.age + min(person.salary) + Aggregate: groupBy=[[person.age]], aggr=[[min(person.salary)]] + TableScan: person + "# ); } #[test] fn select_aggregate_with_non_column_inner_expression_with_groupby() { - quick_test( - "SELECT state, MIN(age + 1) FROM person GROUP BY state", - "Projection: person.state, min(person.age + Int64(1))\ - \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age + Int64(1))]]\ - \n TableScan: person", + let plan = + logical_plan("SELECT state, MIN(age + 1) FROM person GROUP BY state").unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.state, min(person.age + Int64(1)) + Aggregate: groupBy=[[person.state]], aggr=[[min(person.age + Int64(1))]] + TableScan: person + "# ); } #[test] fn select_count_one() { let sql = "SELECT count(1) FROM person"; - let expected = "Projection: count(Int64(1))\ - \n Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: count(Int64(1)) + Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] + TableScan: person +"# + ); } #[test] fn select_count_column() { let sql = "SELECT count(id) FROM person"; - let expected = "Projection: count(person.id)\ - \n Aggregate: groupBy=[[]], aggr=[[count(person.id)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: count(person.id) + Aggregate: groupBy=[[]], aggr=[[count(person.id)]] + TableScan: person +"# + ); } #[test] fn select_approx_median() { let sql = "SELECT approx_median(age) FROM person"; - let expected = "Projection: approx_median(person.age)\ - \n Aggregate: groupBy=[[]], aggr=[[approx_median(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: approx_median(person.age) + Aggregate: groupBy=[[]], aggr=[[approx_median(person.age)]] + TableScan: person +"# + ); } #[test] fn select_scalar_func() { let sql = "SELECT sqrt(age) FROM person"; - let expected = "Projection: sqrt(person.age)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: sqrt(person.age) + TableScan: person +"# + ); } #[test] fn select_aliased_scalar_func() { let sql = "SELECT sqrt(person.age) AS square_people FROM person"; - let expected = "Projection: sqrt(person.age) AS square_people\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: sqrt(person.age) AS square_people + TableScan: person +"# + ); } #[test] fn select_where_nullif_division() { let sql = "SELECT c3/(c4+c5) \ FROM aggregate_test_100 WHERE c3/nullif(c4+c5, 0) > 0.1"; - let expected = "Projection: aggregate_test_100.c3 / (aggregate_test_100.c4 + aggregate_test_100.c5)\ - \n Filter: aggregate_test_100.c3 / nullif(aggregate_test_100.c4 + aggregate_test_100.c5, Int64(0)) > Float64(0.1)\ - \n TableScan: aggregate_test_100"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: aggregate_test_100.c3 / (aggregate_test_100.c4 + aggregate_test_100.c5) + Filter: aggregate_test_100.c3 / nullif(aggregate_test_100.c4 + aggregate_test_100.c5, Int64(0)) > Float64(0.1) + TableScan: aggregate_test_100 +"# + ); } #[test] fn select_where_with_negative_operator() { let sql = "SELECT c3 FROM aggregate_test_100 WHERE c3 > -0.1 AND -c4 > 0"; - let expected = "Projection: aggregate_test_100.c3\ - \n Filter: aggregate_test_100.c3 > Float64(-0.1) AND (- aggregate_test_100.c4) > Int64(0)\ - \n TableScan: aggregate_test_100"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: aggregate_test_100.c3 + Filter: aggregate_test_100.c3 > Float64(-0.1) AND (- aggregate_test_100.c4) > Int64(0) + TableScan: aggregate_test_100 +"# + ); } #[test] fn select_where_with_positive_operator() { let sql = "SELECT c3 FROM aggregate_test_100 WHERE c3 > +0.1 AND +c4 > 0"; - let expected = "Projection: aggregate_test_100.c3\ - \n Filter: aggregate_test_100.c3 > Float64(0.1) AND aggregate_test_100.c4 > Int64(0)\ - \n TableScan: aggregate_test_100"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: aggregate_test_100.c3 + Filter: aggregate_test_100.c3 > Float64(0.1) AND aggregate_test_100.c4 > Int64(0) + TableScan: aggregate_test_100 +"# + ); } #[test] @@ -1496,30 +2058,43 @@ fn select_where_compound_identifiers() { let sql = "SELECT aggregate_test_100.c3 \ FROM public.aggregate_test_100 \ WHERE aggregate_test_100.c3 > 0.1"; - let expected = "Projection: public.aggregate_test_100.c3\ - \n Filter: public.aggregate_test_100.c3 > Float64(0.1)\ - \n TableScan: public.aggregate_test_100"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: public.aggregate_test_100.c3 + Filter: public.aggregate_test_100.c3 > Float64(0.1) + TableScan: public.aggregate_test_100 +"# + ); } #[test] fn select_order_by_index() { let sql = "SELECT id FROM person ORDER BY 1"; - let expected = "Sort: person.id ASC NULLS LAST\ - \n Projection: person.id\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: person.id ASC NULLS LAST + Projection: person.id + TableScan: person +"# + ); } #[test] fn select_order_by_multiple_index() { let sql = "SELECT id, state, age FROM person ORDER BY 1, 3"; - let expected = "Sort: person.id ASC NULLS LAST, person.age ASC NULLS LAST\ - \n Projection: person.id, person.state, person.age\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: person.id ASC NULLS LAST, person.age ASC NULLS LAST + Projection: person.id, person.state, person.age + TableScan: person +"# + ); } #[test] @@ -1528,9 +2103,12 @@ fn select_order_by_index_of_0() { let err = logical_plan(sql) .expect_err("query should have failed") .strip_backtrace(); - assert_eq!( - "Error during planning: Order by index starts at 1 for column indexes", - err + + assert_snapshot!( + err, + @r#" + Error during planning: Order by index starts at 1 for column indexes + "# ); } @@ -1540,162 +2118,243 @@ fn select_order_by_index_oob() { let err = logical_plan(sql) .expect_err("query should have failed") .strip_backtrace(); - assert_eq!( - "Error during planning: Order by column out of bounds, specified: 2, max: 1", - err + + assert_snapshot!( + err, + @r#" + Error during planning: Order by column out of bounds, specified: 2, max: 1 + "# ); } #[test] -fn select_order_by() { +fn select_with_order_by() { let sql = "SELECT id FROM person ORDER BY id"; - let expected = "Sort: person.id ASC NULLS LAST\ - \n Projection: person.id\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: person.id ASC NULLS LAST + Projection: person.id + TableScan: person +"# + ); } #[test] fn select_order_by_desc() { let sql = "SELECT id FROM person ORDER BY id DESC"; - let expected = "Sort: person.id DESC NULLS FIRST\ - \n Projection: person.id\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: person.id DESC NULLS FIRST + Projection: person.id + TableScan: person +"# + ); } #[test] fn select_order_by_nulls_last() { - quick_test( - "SELECT id FROM person ORDER BY id DESC NULLS LAST", - "Sort: person.id DESC NULLS LAST\ - \n Projection: person.id\ - \n TableScan: person", + let plan = logical_plan("SELECT id FROM person ORDER BY id DESC NULLS LAST").unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: person.id DESC NULLS LAST + Projection: person.id + TableScan: person +"# ); - quick_test( - "SELECT id FROM person ORDER BY id NULLS LAST", - "Sort: person.id ASC NULLS LAST\ - \n Projection: person.id\ - \n TableScan: person", + let plan = logical_plan("SELECT id FROM person ORDER BY id NULLS LAST").unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: person.id ASC NULLS LAST + Projection: person.id + TableScan: person +"# ); } #[test] fn select_group_by() { let sql = "SELECT state FROM person GROUP BY state"; - let expected = "Projection: person.state\ - \n Aggregate: groupBy=[[person.state]], aggr=[[]]\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.state + Aggregate: groupBy=[[person.state]], aggr=[[]] + TableScan: person +"# + ); } #[test] fn select_group_by_columns_not_in_select() { let sql = "SELECT MAX(age) FROM person GROUP BY state"; - let expected = "Projection: max(person.age)\ - \n Aggregate: groupBy=[[person.state]], aggr=[[max(person.age)]]\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: max(person.age) + Aggregate: groupBy=[[person.state]], aggr=[[max(person.age)]] + TableScan: person +"# + ); } #[test] fn select_group_by_count_star() { let sql = "SELECT state, count(*) FROM person GROUP BY state"; - let expected = "Projection: person.state, count(*)\ - \n Aggregate: groupBy=[[person.state]], aggr=[[count(*)]]\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.state, count(*) + Aggregate: groupBy=[[person.state]], aggr=[[count(*)]] + TableScan: person +"# + ); } #[test] fn select_group_by_needs_projection() { let sql = "SELECT count(state), state FROM person GROUP BY state"; - let expected = "\ - Projection: count(person.state), person.state\ - \n Aggregate: groupBy=[[person.state]], aggr=[[count(person.state)]]\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: count(person.state), person.state + Aggregate: groupBy=[[person.state]], aggr=[[count(person.state)]] + TableScan: person + "# + ); } #[test] fn select_7480_1() { let sql = "SELECT c1, MIN(c12) FROM aggregate_test_100 GROUP BY c1, c13"; - let expected = "Projection: aggregate_test_100.c1, min(aggregate_test_100.c12)\ - \n Aggregate: groupBy=[[aggregate_test_100.c1, aggregate_test_100.c13]], aggr=[[min(aggregate_test_100.c12)]]\ - \n TableScan: aggregate_test_100"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: aggregate_test_100.c1, min(aggregate_test_100.c12) + Aggregate: groupBy=[[aggregate_test_100.c1, aggregate_test_100.c13]], aggr=[[min(aggregate_test_100.c12)]] + TableScan: aggregate_test_100 +"# + ); } #[test] fn select_7480_2() { let sql = "SELECT c1, c13, MIN(c12) FROM aggregate_test_100 GROUP BY c1"; let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"aggregate_test_100.c13\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"aggregate_test_100.c1, min(aggregate_test_100.c12)\" appears in the SELECT clause satisfies this requirement", - err.strip_backtrace() + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "aggregate_test_100.c13" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "aggregate_test_100.c1, min(aggregate_test_100.c12)" appears in the SELECT clause satisfies this requirement + "# ); } #[test] fn create_external_table_csv() { let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV LOCATION 'foo.csv'"; - let expected = "CreateExternalTable: Bare { table: \"t\" }"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Bare { table: "t" } +"# + ); } #[test] fn create_external_table_with_pk() { let sql = "CREATE EXTERNAL TABLE t(c1 int, primary key(c1)) STORED AS CSV LOCATION 'foo.csv'"; - let expected = - "CreateExternalTable: Bare { table: \"t\" } constraints=[PrimaryKey([0])]"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Bare { table: "t" } constraints=[PrimaryKey([0])] + "# + ); } #[test] fn create_external_table_wih_schema() { let sql = "CREATE EXTERNAL TABLE staging.foo STORED AS CSV LOCATION 'foo.csv'"; - let expected = "CreateExternalTable: Partial { schema: \"staging\", table: \"foo\" }"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Partial { schema: "staging", table: "foo" } +"# + ); } #[test] fn create_schema_with_quoted_name() { let sql = "CREATE SCHEMA \"quoted_schema_name\""; - let expected = "CreateCatalogSchema: \"quoted_schema_name\""; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateCatalogSchema: "quoted_schema_name" +"# + ); } #[test] fn create_schema_with_quoted_unnormalized_name() { let sql = "CREATE SCHEMA \"Foo\""; - let expected = "CreateCatalogSchema: \"Foo\""; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateCatalogSchema: "Foo" +"# + ); } #[test] fn create_schema_with_unquoted_normalized_name() { let sql = "CREATE SCHEMA Foo"; - let expected = "CreateCatalogSchema: \"foo\""; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateCatalogSchema: "foo" +"# + ); } #[test] fn create_external_table_custom() { let sql = "CREATE EXTERNAL TABLE dt STORED AS DELTATABLE LOCATION 's3://bucket/schema/table';"; - let expected = r#"CreateExternalTable: Bare { table: "dt" }"#; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Bare { table: "dt" } +"# + ); } #[test] fn create_external_table_csv_no_schema() { let sql = "CREATE EXTERNAL TABLE t STORED AS CSV LOCATION 'foo.csv'"; - let expected = "CreateExternalTable: Bare { table: \"t\" }"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Bare { table: "t" } +"# + ); } #[test] @@ -1708,9 +2367,18 @@ fn create_external_table_with_compression_type() { "CREATE EXTERNAL TABLE t(c1 int) STORED AS JSON LOCATION 'foo.json.bz2' OPTIONS ('format.compression' 'bzip2')", "CREATE EXTERNAL TABLE t(c1 int) STORED AS NONSTANDARD LOCATION 'foo.unk' OPTIONS ('format.compression' 'gzip')", ]; - for sql in sqls { - let expected = "CreateExternalTable: Bare { table: \"t\" }"; - quick_test(sql, expected); + + allow_duplicates! { + for sql in sqls { + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + CreateExternalTable: Bare { table: "t" } + "# + ); + } + } // negative case @@ -1722,41 +2390,66 @@ fn create_external_table_with_compression_type() { "CREATE EXTERNAL TABLE t STORED AS ARROW LOCATION 'foo.arrow' OPTIONS ('format.compression' 'gzip')", "CREATE EXTERNAL TABLE t STORED AS ARROW LOCATION 'foo.arrow' OPTIONS ('format.compression' 'bzip2')", ]; - for sql in sqls { - let err = logical_plan(sql).expect_err("query should have failed"); - assert_eq!( - "Error during planning: File compression type cannot be set for PARQUET, AVRO, or ARROW files.", - err.strip_backtrace() - ); + + allow_duplicates! { + for sql in sqls { + let err = logical_plan(sql).expect_err("query should have failed"); + + assert_snapshot!( + err.strip_backtrace(), + @r#" + Error during planning: File compression type cannot be set for PARQUET, AVRO, or ARROW files. + "# + ); + + } } } #[test] fn create_external_table_parquet() { let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS PARQUET LOCATION 'foo.parquet'"; - let expected = "CreateExternalTable: Bare { table: \"t\" }"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Bare { table: "t" } +"# + ); } #[test] fn create_external_table_parquet_sort_order() { let sql = "create external table foo(a varchar, b varchar, c timestamp) stored as parquet location '/tmp/foo' with order (c)"; - let expected = "CreateExternalTable: Bare { table: \"foo\" }"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Bare { table: "foo" } +"# + ); } #[test] fn create_external_table_parquet_no_schema() { let sql = "CREATE EXTERNAL TABLE t STORED AS PARQUET LOCATION 'foo.parquet'"; - let expected = "CreateExternalTable: Bare { table: \"t\" }"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#"CreateExternalTable: Bare { table: "t" }"# + ); } #[test] fn create_external_table_parquet_no_schema_sort_order() { let sql = "CREATE EXTERNAL TABLE t STORED AS PARQUET LOCATION 'foo.parquet' WITH ORDER (id)"; - let expected = "CreateExternalTable: Bare { table: \"t\" }"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +CreateExternalTable: Bare { table: "t" } +"# + ); } #[test] @@ -1765,11 +2458,16 @@ fn equijoin_explicit_syntax() { FROM person \ JOIN orders \ ON id = customer_id"; - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id = orders.customer_id\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -1778,12 +2476,16 @@ fn equijoin_with_condition() { FROM person \ JOIN orders \ ON id = customer_id AND order_id > 1 "; - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1)\ - \n TableScan: person\ - \n TableScan: orders"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -1792,11 +2494,16 @@ fn left_equijoin_with_conditions() { FROM person \ LEFT JOIN orders \ ON id = customer_id AND order_id > 1 AND age < 30"; - let expected = "Projection: person.id, orders.order_id\ - \n Left Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1) AND person.age < Int64(30)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Left Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1) AND person.age < Int64(30) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -1805,12 +2512,16 @@ fn right_equijoin_with_conditions() { FROM person \ RIGHT JOIN orders \ ON id = customer_id AND id > 1 AND order_id < 100"; - - let expected = "Projection: person.id, orders.order_id\ - \n Right Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Right Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -1819,11 +2530,16 @@ fn full_equijoin_with_conditions() { FROM person \ FULL JOIN orders \ ON id = customer_id AND id > 1 AND order_id < 100"; - let expected = "Projection: person.id, orders.order_id\ - \n Full Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Full Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -1832,11 +2548,16 @@ fn join_with_table_name() { FROM person \ JOIN orders \ ON person.id = orders.customer_id"; - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id = orders.customer_id\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -1845,12 +2566,17 @@ fn join_with_using() { FROM person \ JOIN person as person2 \ USING (id)"; - let expected = "Projection: person.first_name, person.id\ - \n Inner Join: Using person.id = person2.id\ - \n TableScan: person\ - \n SubqueryAlias: person2\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.first_name, person.id + Inner Join: Using person.id = person2.id + TableScan: person + SubqueryAlias: person2 + TableScan: person +"# + ); } #[test] @@ -1859,13 +2585,18 @@ fn equijoin_explicit_syntax_3_tables() { FROM person \ JOIN orders ON id = customer_id \ JOIN lineitem ON o_item_id = l_item_id"; - let expected = "Projection: person.id, orders.order_id, lineitem.l_description\ - \n Inner Join: Filter: orders.o_item_id = lineitem.l_item_id\ - \n Inner Join: Filter: person.id = orders.customer_id\ - \n TableScan: person\ - \n TableScan: orders\ - \n TableScan: lineitem"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id, lineitem.l_description + Inner Join: Filter: orders.o_item_id = lineitem.l_item_id + Inner Join: Filter: person.id = orders.customer_id + TableScan: person + TableScan: orders + TableScan: lineitem +"# + ); } #[test] @@ -1873,152 +2604,206 @@ fn boolean_literal_in_condition_expression() { let sql = "SELECT order_id \ FROM orders \ WHERE delivered = false OR delivered = true"; - let expected = "Projection: orders.order_id\ - \n Filter: orders.delivered = Boolean(false) OR orders.delivered = Boolean(true)\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id + Filter: orders.delivered = Boolean(false) OR orders.delivered = Boolean(true) + TableScan: orders +"# + ); } #[test] fn union() { let sql = "SELECT order_id from orders UNION SELECT order_id FROM orders"; - let expected = "\ - Distinct:\ - \n Union\ - \n Projection: orders.order_id\ - \n TableScan: orders\ - \n Projection: orders.order_id\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Distinct: + Union + Projection: orders.order_id + TableScan: orders + Projection: orders.order_id + TableScan: orders +"# + ); } #[test] fn union_by_name_different_columns() { let sql = "SELECT order_id from orders UNION BY NAME SELECT order_id, 1 FROM orders"; - let expected = "\ - Distinct:\ - \n Union\ - \n Projection: order_id, NULL AS Int64(1)\ - \n Projection: orders.order_id\ - \n TableScan: orders\ - \n Projection: order_id, Int64(1)\ - \n Projection: orders.order_id, Int64(1)\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Distinct: + Union + Projection: order_id, NULL AS Int64(1) + Projection: orders.order_id + TableScan: orders + Projection: order_id, Int64(1) + Projection: orders.order_id, Int64(1) + TableScan: orders +"# + ); } #[test] fn union_by_name_same_column_names() { let sql = "SELECT order_id from orders UNION SELECT order_id FROM orders"; - let expected = "\ - Distinct:\ - \n Union\ - \n Projection: orders.order_id\ - \n TableScan: orders\ - \n Projection: orders.order_id\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Distinct: + Union + Projection: orders.order_id + TableScan: orders + Projection: orders.order_id + TableScan: orders +"# + ); } #[test] fn union_all() { let sql = "SELECT order_id from orders UNION ALL SELECT order_id FROM orders"; - let expected = "Union\ - \n Projection: orders.order_id\ - \n TableScan: orders\ - \n Projection: orders.order_id\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Union + Projection: orders.order_id + TableScan: orders + Projection: orders.order_id + TableScan: orders +"# + ); } #[test] fn union_all_by_name_different_columns() { let sql = "SELECT order_id from orders UNION ALL BY NAME SELECT order_id, 1 FROM orders"; - let expected = "\ - Union\ - \n Projection: order_id, NULL AS Int64(1)\ - \n Projection: orders.order_id\ - \n TableScan: orders\ - \n Projection: order_id, Int64(1)\ - \n Projection: orders.order_id, Int64(1)\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Union + Projection: order_id, NULL AS Int64(1) + Projection: orders.order_id + TableScan: orders + Projection: order_id, Int64(1) + Projection: orders.order_id, Int64(1) + TableScan: orders +"# + ); } #[test] fn union_all_by_name_same_column_names() { let sql = "SELECT order_id from orders UNION ALL BY NAME SELECT order_id FROM orders"; - let expected = "\ - Union\ - \n Projection: order_id\ - \n Projection: orders.order_id\ - \n TableScan: orders\ - \n Projection: order_id\ - \n Projection: orders.order_id\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Union + Projection: order_id + Projection: orders.order_id + TableScan: orders + Projection: order_id + Projection: orders.order_id + TableScan: orders +"# + ); } #[test] fn empty_over() { let sql = "SELECT order_id, MAX(order_id) OVER () from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ - \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } #[test] fn empty_over_with_alias() { let sql = "SELECT order_id oid, MAX(order_id) OVER () max_oid from orders"; - let expected = "\ - Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid\ - \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid + WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } #[test] fn empty_over_dup_with_alias() { let sql = "SELECT order_id oid, MAX(order_id) OVER () max_oid, MAX(order_id) OVER () max_oid_dup from orders"; - let expected = "\ - Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid_dup\ - \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid_dup + WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } #[test] fn empty_over_dup_with_different_sort() { let sql = "SELECT order_id oid, MAX(order_id) OVER (), MAX(order_id) OVER (ORDER BY order_id) from orders"; - let expected = "\ - Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n WindowAggr: windowExpr=[[max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + WindowAggr: windowExpr=[[max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } #[test] fn empty_over_plus() { let sql = "SELECT order_id, MAX(qty * 1.1) OVER () from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ - \n WindowAggr: windowExpr=[[max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + WindowAggr: windowExpr=[[max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } #[test] fn empty_over_multiple() { let sql = "SELECT order_id, MAX(qty) OVER (), min(qty) over (), avg(qty) OVER () from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ - \n WindowAggr: windowExpr=[[max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + WindowAggr: windowExpr=[[max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } /// psql result @@ -2033,11 +2818,15 @@ fn empty_over_multiple() { #[test] fn over_partition_by() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ - \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } /// psql result @@ -2055,45 +2844,61 @@ fn over_partition_by() { #[test] fn over_order_by() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } #[test] fn over_order_by_with_window_frame_double_end() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id ROWS BETWEEN 3 PRECEDING and 3 FOLLOWING), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } #[test] fn over_order_by_with_window_frame_single_end() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id ROWS 3 PRECEDING), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } #[test] fn over_order_by_with_window_frame_single_end_groups() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id GROUPS 3 PRECEDING), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2111,12 +2916,16 @@ fn over_order_by_with_window_frame_single_end_groups() { #[test] fn over_order_by_two_sort_keys() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id), MIN(qty) OVER (ORDER BY (order_id + 1)) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2135,13 +2944,17 @@ fn over_order_by_two_sort_keys() { #[test] fn over_order_by_sort_keys_sorting() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY qty, order_id), sum(qty) OVER (), MIN(qty) OVER (ORDER BY order_id, qty) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2158,13 +2971,17 @@ fn over_order_by_sort_keys_sorting() { #[test] fn over_order_by_sort_keys_sorting_prefix_compacting() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id), sum(qty) OVER (), MIN(qty) OVER (ORDER BY order_id, qty) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2186,14 +3003,18 @@ fn over_order_by_sort_keys_sorting_prefix_compacting() { #[test] fn over_order_by_sort_keys_sorting_global_order_compacting() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY qty, order_id), sum(qty) OVER (), MIN(qty) OVER (ORDER BY order_id, qty) from orders ORDER BY order_id"; - let expected = "\ - Sort: orders.order_id ASC NULLS LAST\ - \n Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: orders.order_id ASC NULLS LAST + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2209,11 +3030,15 @@ fn over_order_by_sort_keys_sorting_global_order_compacting() { fn over_partition_by_order_by() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id ORDER BY qty) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2229,11 +3054,15 @@ fn over_partition_by_order_by() { fn over_partition_by_order_by_no_dup() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id, qty ORDER BY qty) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2252,12 +3081,16 @@ fn over_partition_by_order_by_no_dup() { fn over_partition_by_order_by_mix_up() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id, qty ORDER BY qty), MIN(qty) OVER (PARTITION BY qty ORDER BY order_id) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } /// psql result @@ -2275,90 +3108,121 @@ fn over_partition_by_order_by_mix_up() { fn over_partition_by_order_by_mix_up_prefix() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id ORDER BY qty), MIN(qty) OVER (PARTITION BY order_id, qty ORDER BY price) from orders"; - let expected = "\ - Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ - \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW + WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + TableScan: orders +"# + ); } #[test] fn approx_median_window() { let sql = "SELECT order_id, APPROX_MEDIAN(qty) OVER(PARTITION BY order_id) from orders"; - let expected = "\ - Projection: orders.order_id, approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ - \n WindowAggr: windowExpr=[[approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + WindowAggr: windowExpr=[[approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } #[test] fn select_typed_date_string() { let sql = "SELECT date '2020-12-10' AS date"; - let expected = "Projection: CAST(Utf8(\"2020-12-10\") AS Date32) AS date\ - \n EmptyRelation"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: CAST(Utf8("2020-12-10") AS Date32) AS date + EmptyRelation +"# + ); } #[test] fn select_typed_time_string() { let sql = "SELECT TIME '08:09:10.123' AS time"; - let expected = - "Projection: CAST(Utf8(\"08:09:10.123\") AS Time64(Nanosecond)) AS time\ - \n EmptyRelation"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: CAST(Utf8("08:09:10.123") AS Time64(Nanosecond)) AS time + EmptyRelation +"# + ); } #[test] fn select_multibyte_column() { let sql = r#"SELECT "😀" FROM person"#; - let expected = "Projection: person.😀\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.😀 + TableScan: person +"# + ); } #[test] fn select_groupby_orderby() { // ensure that references are correctly resolved in the order by clause // see https://github.com/apache/datafusion/issues/4854 - let sql = r#"SELECT - avg(age) AS "value", - date_trunc('month', birth_date) AS "birth_date" - FROM person GROUP BY birth_date ORDER BY birth_date; -"#; - // expect that this is not an ambiguous reference - let expected = - "Sort: birth_date ASC NULLS LAST\ - \n Projection: avg(person.age) AS value, date_trunc(Utf8(\"month\"), person.birth_date) AS birth_date\ - \n Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]]\ - \n TableScan: person"; - quick_test(sql, expected); - - // Use fully qualified `person.birth_date` as argument to date_trunc, plan should be the same - let sql = r#"SELECT - avg(age) AS "value", - date_trunc('month', person.birth_date) AS "birth_date" - FROM person GROUP BY birth_date ORDER BY birth_date; -"#; - quick_test(sql, expected); - - // Use fully qualified `person.birth_date` as group by, plan should be the same - let sql = r#"SELECT - avg(age) AS "value", - date_trunc('month', birth_date) AS "birth_date" - FROM person GROUP BY person.birth_date ORDER BY birth_date; -"#; - quick_test(sql, expected); - // Use fully qualified `person.birth_date` in both group and date_trunc, plan should be the same - let sql = r#"SELECT - avg(age) AS "value", - date_trunc('month', person.birth_date) AS "birth_date" - FROM person GROUP BY person.birth_date ORDER BY birth_date; -"#; - quick_test(sql, expected); + let sqls = vec![ + r#" + SELECT + avg(age) AS "value", + date_trunc('month', birth_date) AS "birth_date" + FROM person GROUP BY birth_date ORDER BY birth_date; + "#, + // Use fully qualified `person.birth_date` as argument to date_trunc, plan should be the same + r#" + SELECT + avg(age) AS "value", + date_trunc('month', person.birth_date) AS "birth_date" + FROM person GROUP BY birth_date ORDER BY birth_date; + "#, + // Use fully qualified `person.birth_date` as group by, plan should be the same + r#" + SELECT + avg(age) AS "value", + date_trunc('month', birth_date) AS "birth_date" + FROM person GROUP BY person.birth_date ORDER BY birth_date; + "#, + // Use fully qualified `person.birth_date` in both group and date_trunc, plan should be the same + r#" + SELECT + avg(age) AS "value", + date_trunc('month', person.birth_date) AS "birth_date" + FROM person GROUP BY person.birth_date ORDER BY birth_date; + "#, + ]; + for sql in sqls { + let plan = logical_plan(sql).unwrap(); + allow_duplicates! { + assert_snapshot!( + plan, + // expect that this is not an ambiguous reference + @r#" + Sort: birth_date ASC NULLS LAST + Projection: avg(person.age) AS value, date_trunc(Utf8("month"), person.birth_date) AS birth_date + Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]] + TableScan: person + "# + ); + } + } // Use columnized `avg(age)` in the order by let sql = r#"SELECT @@ -2367,13 +3231,16 @@ fn select_groupby_orderby() { FROM person GROUP BY person.birth_date ORDER BY avg(age) + avg(age); "#; - let expected = - "Sort: avg(person.age) + avg(person.age) ASC NULLS LAST\ - \n Projection: avg(person.age) + avg(person.age), date_trunc(Utf8(\"month\"), person.birth_date) AS birth_date\ - \n Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]]\ - \n TableScan: person"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: avg(person.age) + avg(person.age) ASC NULLS LAST + Projection: avg(person.age) + avg(person.age), date_trunc(Utf8("month"), person.birth_date) AS birth_date + Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]] + TableScan: person +"# + ); } fn logical_plan(sql: &str) -> Result { @@ -2488,114 +3355,149 @@ impl ScalarUDFImpl for DummyUDF { } } -/// Create logical plan, write with formatter, compare to expected output -fn quick_test(sql: &str, expected: &str) { - quick_test_with_options(sql, expected, ParserOptions::default()) +fn parse_decimals_parser_options() -> ParserOptions { + ParserOptions { + parse_float_as_decimal: true, + enable_ident_normalization: false, + support_varchar_with_length: false, + map_varchar_to_utf8view: false, + enable_options_value_normalization: false, + collect_spans: false, + } } -fn quick_test_with_options(sql: &str, expected: &str, options: ParserOptions) { - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_eq!(format!("{plan}"), expected); +fn ident_normalization_parser_options_no_ident_normalization() -> ParserOptions { + ParserOptions { + parse_float_as_decimal: true, + enable_ident_normalization: false, + support_varchar_with_length: false, + map_varchar_to_utf8view: false, + enable_options_value_normalization: false, + collect_spans: false, + } } -fn prepare_stmt_quick_test( - sql: &str, - expected_plan: &str, - expected_data_types: &str, -) -> LogicalPlan { - let plan = logical_plan(sql).unwrap(); - - let assert_plan = plan.clone(); - // verify plan - assert_eq!(format!("{assert_plan}"), expected_plan); - - // verify data types - if let LogicalPlan::Statement(Statement::Prepare(Prepare { data_types, .. })) = - assert_plan - { - let dt = format!("{data_types:?}"); - assert_eq!(dt, expected_data_types); +fn ident_normalization_parser_options_ident_normalization() -> ParserOptions { + ParserOptions { + parse_float_as_decimal: true, + enable_ident_normalization: true, + support_varchar_with_length: false, + map_varchar_to_utf8view: false, + enable_options_value_normalization: false, + collect_spans: false, } - - plan } -fn prepare_stmt_replace_params_quick_test( - plan: LogicalPlan, - param_values: impl Into, - expected_plan: &str, -) -> LogicalPlan { - // replace params - let plan = plan.with_param_values(param_values).unwrap(); - assert_eq!(format!("{plan}"), expected_plan); - - plan +fn generate_prepare_stmt_and_data_types(sql: &str) -> (LogicalPlan, String) { + let plan = logical_plan(sql).unwrap(); + let data_types = match &plan { + LogicalPlan::Statement(Statement::Prepare(Prepare { data_types, .. })) => { + format!("{data_types:?}") + } + _ => panic!("Expected a Prepare statement"), + }; + (plan, data_types) } #[test] fn select_partially_qualified_column() { - let sql = r#"SELECT person.first_name FROM public.person"#; - let expected = "Projection: public.person.first_name\ - \n TableScan: public.person"; - quick_test(sql, expected); + let sql = "SELECT person.first_name FROM public.person"; + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: public.person.first_name + TableScan: public.person +"# + ); } #[test] fn cross_join_not_to_inner_join() { let sql = "select person.id from person, orders, lineitem where person.id = person.age;"; - let expected = "Projection: person.id\ - \n Filter: person.id = person.age\ - \n Cross Join: \ - \n Cross Join: \ - \n TableScan: person\ - \n TableScan: orders\ - \n TableScan: lineitem"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id + Filter: person.id = person.age + Cross Join: + Cross Join: + TableScan: person + TableScan: orders + TableScan: lineitem +"# + ); } #[test] fn join_with_aliases() { let sql = "select peeps.id, folks.first_name from person as peeps join person as folks on peeps.id = folks.id"; - let expected = "Projection: peeps.id, folks.first_name\ - \n Inner Join: Filter: peeps.id = folks.id\ - \n SubqueryAlias: peeps\ - \n TableScan: person\ - \n SubqueryAlias: folks\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: peeps.id, folks.first_name + Inner Join: Filter: peeps.id = folks.id + SubqueryAlias: peeps + TableScan: person + SubqueryAlias: folks + TableScan: person +"# + ); } #[test] fn negative_interval_plus_interval_in_projection() { let sql = "select -interval '2 days' + interval '5 days';"; - let expected = - "Projection: IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\")\n EmptyRelation"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") + EmptyRelation +"# + ); } #[test] fn complex_interval_expression_in_projection() { let sql = "select -interval '2 days' + interval '5 days'+ (-interval '3 days' + interval '5 days');"; - let expected = - "Projection: IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: -3, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\")\n EmptyRelation"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: -3, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") + EmptyRelation +"# + ); } #[test] fn negative_sum_intervals_in_projection() { let sql = "select -((interval '2 days' + interval '5 days') + -(interval '4 days' + interval '7 days'));"; - let expected = - "Projection: (- IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 2, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\") + (- IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 4, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 7, nanoseconds: 0 }\")))\n EmptyRelation"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: (- IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 2, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") + (- IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 4, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 7, nanoseconds: 0 }"))) + EmptyRelation +"# + ); } #[test] fn date_plus_interval_in_projection() { let sql = "select t_date32 + interval '5 days' FROM test"; - let expected = - "Projection: test.t_date32 + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\")\n TableScan: test"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: test.t_date32 + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") + TableScan: test +"# + ); } #[test] @@ -2604,11 +3506,15 @@ fn date_plus_interval_in_filter() { WHERE t_date64 \ BETWEEN cast('1999-12-31' as date) \ AND cast('1999-12-31' as date) + interval '30 days'"; - let expected = - "Projection: test.t_date64\ - \n Filter: test.t_date64 BETWEEN CAST(Utf8(\"1999-12-31\") AS Date32) AND CAST(Utf8(\"1999-12-31\") AS Date32) + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 30, nanoseconds: 0 }\")\ - \n TableScan: test"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: test.t_date64 + Filter: test.t_date64 BETWEEN CAST(Utf8("1999-12-31") AS Date32) AND CAST(Utf8("1999-12-31") AS Date32) + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 30, nanoseconds: 0 }") + TableScan: test +"# + ); } #[test] @@ -2617,16 +3523,20 @@ fn exists_subquery() { (SELECT first_name FROM person \ WHERE last_name = p.last_name \ AND state = p.state)"; - - let expected = "Projection: p.id\ - \n Filter: EXISTS ()\ - \n Subquery:\ - \n Projection: person.first_name\ - \n Filter: person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state)\ - \n TableScan: person\ - \n SubqueryAlias: p\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: p.id + Filter: EXISTS () + Subquery: + Projection: person.first_name + Filter: person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state) + TableScan: person + SubqueryAlias: p + TableScan: person +"# + ); } #[test] @@ -2638,68 +3548,84 @@ fn exists_subquery_schema_outer_schema_overlap() { WHERE person.id = p2.id \ AND person.last_name = p.last_name \ AND person.state = p.state)"; - - let expected = "Projection: person.id\ - \n Filter: person.id = p.id AND EXISTS ()\ - \n Subquery:\ - \n Projection: person.first_name\ - \n Filter: person.id = p2.id AND person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state)\ - \n Cross Join: \ - \n TableScan: person\ - \n SubqueryAlias: p2\ - \n TableScan: person\ - \n Cross Join: \ - \n TableScan: person\ - \n SubqueryAlias: p\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id + Filter: person.id = p.id AND EXISTS () + Subquery: + Projection: person.first_name + Filter: person.id = p2.id AND person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state) + Cross Join: + TableScan: person + SubqueryAlias: p2 + TableScan: person + Cross Join: + TableScan: person + SubqueryAlias: p + TableScan: person +"# + ); } #[test] fn in_subquery_uncorrelated() { let sql = "SELECT id FROM person p WHERE id IN \ (SELECT id FROM person)"; - - let expected = "Projection: p.id\ - \n Filter: p.id IN ()\ - \n Subquery:\ - \n Projection: person.id\ - \n TableScan: person\ - \n SubqueryAlias: p\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: p.id + Filter: p.id IN () + Subquery: + Projection: person.id + TableScan: person + SubqueryAlias: p + TableScan: person +"# + ); } #[test] fn not_in_subquery_correlated() { let sql = "SELECT id FROM person p WHERE id NOT IN \ (SELECT id FROM person WHERE last_name = p.last_name AND state = 'CO')"; - - let expected = "Projection: p.id\ - \n Filter: p.id NOT IN ()\ - \n Subquery:\ - \n Projection: person.id\ - \n Filter: person.last_name = outer_ref(p.last_name) AND person.state = Utf8(\"CO\")\ - \n TableScan: person\ - \n SubqueryAlias: p\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: p.id + Filter: p.id NOT IN () + Subquery: + Projection: person.id + Filter: person.last_name = outer_ref(p.last_name) AND person.state = Utf8("CO") + TableScan: person + SubqueryAlias: p + TableScan: person +"# + ); } #[test] fn scalar_subquery() { let sql = "SELECT p.id, (SELECT MAX(id) FROM person WHERE last_name = p.last_name) FROM person p"; - - let expected = "Projection: p.id, ()\ - \n Subquery:\ - \n Projection: max(person.id)\ - \n Aggregate: groupBy=[[]], aggr=[[max(person.id)]]\ - \n Filter: person.last_name = outer_ref(p.last_name)\ - \n TableScan: person\ - \n SubqueryAlias: p\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: p.id, () + Subquery: + Projection: max(person.id) + Aggregate: groupBy=[[]], aggr=[[max(person.id)]] + Filter: person.last_name = outer_ref(p.last_name) + TableScan: person + SubqueryAlias: p + TableScan: person +"# + ); } #[test] @@ -2711,41 +3637,54 @@ fn scalar_subquery_reference_outer_field() { FROM j1, j3 \ WHERE j2_id = j1_id \ AND j1_id = j3_id)"; - - let expected = "Projection: j1.j1_string, j2.j2_string\ - \n Filter: j1.j1_id = j2.j2_id - Int64(1) AND j2.j2_id < ()\ - \n Subquery:\ - \n Projection: count(*)\ - \n Aggregate: groupBy=[[]], aggr=[[count(*)]]\ - \n Filter: outer_ref(j2.j2_id) = j1.j1_id AND j1.j1_id = j3.j3_id\ - \n Cross Join: \ - \n TableScan: j1\ - \n TableScan: j3\ - \n Cross Join: \ - \n TableScan: j1\ - \n TableScan: j2"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: j1.j1_string, j2.j2_string + Filter: j1.j1_id = j2.j2_id - Int64(1) AND j2.j2_id < () + Subquery: + Projection: count(*) + Aggregate: groupBy=[[]], aggr=[[count(*)]] + Filter: outer_ref(j2.j2_id) = j1.j1_id AND j1.j1_id = j3.j3_id + Cross Join: + TableScan: j1 + TableScan: j3 + Cross Join: + TableScan: j1 + TableScan: j2 +"# + ); } #[test] fn aggregate_with_rollup() { let sql = "SELECT id, state, age, count(*) FROM person GROUP BY id, ROLLUP (state, age)"; - let expected = "Projection: person.id, person.state, person.age, count(*)\ - \n Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[count(*)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.state, person.age, count(*) + Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[count(*)]] + TableScan: person +"# + ); } #[test] fn aggregate_with_rollup_with_grouping() { let sql = "SELECT id, state, age, grouping(state), grouping(age), grouping(state) + grouping(age), count(*) \ FROM person GROUP BY id, ROLLUP (state, age)"; - let expected = "Projection: person.id, person.state, person.age, grouping(person.state), grouping(person.age), grouping(person.state) + grouping(person.age), count(*)\ - \n Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[grouping(person.state), grouping(person.age), count(*)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.state, person.age, grouping(person.state), grouping(person.age), grouping(person.state) + grouping(person.age), count(*) + Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[grouping(person.state), grouping(person.age), count(*)]] + TableScan: person +"# + ); } #[test] @@ -2763,38 +3702,58 @@ fn rank_partition_grouping() { from person group by rollup(state, last_name)"; - let expected = "Projection: sum(person.age) AS total_sum, person.state, person.last_name, grouping(person.state) + grouping(person.last_name) AS x, rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW AS the_rank\ - \n WindowAggr: windowExpr=[[rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ - \n Aggregate: groupBy=[[ROLLUP (person.state, person.last_name)]], aggr=[[sum(person.age), grouping(person.state), grouping(person.last_name)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: sum(person.age) AS total_sum, person.state, person.last_name, grouping(person.state) + grouping(person.last_name) AS x, rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW AS the_rank + WindowAggr: windowExpr=[[rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] + Aggregate: groupBy=[[ROLLUP (person.state, person.last_name)]], aggr=[[sum(person.age), grouping(person.state), grouping(person.last_name)]] + TableScan: person +"# + ); } #[test] fn aggregate_with_cube() { let sql = "SELECT id, state, age, count(*) FROM person GROUP BY id, CUBE (state, age)"; - let expected = "Projection: person.id, person.state, person.age, count(*)\ - \n Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.age), (person.id, person.state, person.age))]], aggr=[[count(*)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.state, person.age, count(*) + Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.age), (person.id, person.state, person.age))]], aggr=[[count(*)]] + TableScan: person +"# + ); } #[test] fn round_decimal() { let sql = "SELECT round(price/3, 2) FROM test_decimal"; - let expected = "Projection: round(test_decimal.price / Int64(3), Int64(2))\ - \n TableScan: test_decimal"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: round(test_decimal.price / Int64(3), Int64(2)) + TableScan: test_decimal +"# + ); } #[test] fn aggregate_with_grouping_sets() { let sql = "SELECT id, state, age, count(*) FROM person GROUP BY id, GROUPING SETS ((state), (state, age), (id, state))"; - let expected = "Projection: person.id, person.state, person.age, count(*)\ - \n Aggregate: groupBy=[[GROUPING SETS ((person.id, person.state), (person.id, person.state, person.age), (person.id, person.id, person.state))]], aggr=[[count(*)]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.state, person.age, count(*) + Aggregate: groupBy=[[GROUPING SETS ((person.id, person.state), (person.id, person.state, person.age), (person.id, person.id, person.state))]], aggr=[[count(*)]] + TableScan: person +"# + ); } #[test] @@ -2802,11 +3761,16 @@ fn join_on_disjunction_condition() { let sql = "SELECT id, order_id \ FROM person \ JOIN orders ON id = customer_id OR person.age > 30"; - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id = orders.customer_id OR person.age > Int64(30)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id OR person.age > Int64(30) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -2814,11 +3778,16 @@ fn join_on_complex_condition() { let sql = "SELECT id, order_id \ FROM person \ JOIN orders ON id = customer_id AND (person.age > 30 OR person.last_name = 'X')"; - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id = orders.customer_id AND (person.age > Int64(30) OR person.last_name = Utf8(\"X\"))\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id AND (person.age > Int64(30) OR person.last_name = Utf8("X")) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -2826,11 +3795,16 @@ fn hive_aggregate_with_filter() -> Result<()> { let dialect = &HiveDialect {}; let sql = "SELECT sum(age) FILTER (WHERE age > 4) FROM person"; let plan = logical_plan_with_dialect(sql, dialect)?; - let expected = "Projection: sum(person.age) FILTER (WHERE person.age > Int64(4))\ - \n Aggregate: groupBy=[[]], aggr=[[sum(person.age) FILTER (WHERE person.age > Int64(4))]]\ - \n TableScan: person" - .to_string(); - assert_eq!(plan.display_indent().to_string(), expected); + + assert_snapshot!( + plan, + @r##" + Projection: sum(person.age) FILTER (WHERE person.age > Int64(4)) + Aggregate: groupBy=[[]], aggr=[[sum(person.age) FILTER (WHERE person.age > Int64(4))]] + TableScan: person + "## + ); + Ok(()) } @@ -2841,84 +3815,130 @@ fn order_by_unaliased_name() { // SchemaError(FieldNotFound { qualifier: Some("p"), name: "state", valid_fields: ["z", "q"] }) let sql = "select p.state z, sum(age) q from person p group by p.state order by p.state"; - let expected = "Projection: z, q\ - \n Sort: p.state ASC NULLS LAST\ - \n Projection: p.state AS z, sum(p.age) AS q, p.state\ - \n Aggregate: groupBy=[[p.state]], aggr=[[sum(p.age)]]\ - \n SubqueryAlias: p\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: z, q + Sort: p.state ASC NULLS LAST + Projection: p.state AS z, sum(p.age) AS q, p.state + Aggregate: groupBy=[[p.state]], aggr=[[sum(p.age)]] + SubqueryAlias: p + TableScan: person +"# + ); } #[test] fn order_by_ambiguous_name() { let sql = "select * from person a join person b using (id) order by age"; - let expected = "Schema error: Ambiguous reference to unqualified field age"; + let err = logical_plan(sql).unwrap_err().strip_backtrace(); - let err = logical_plan(sql).unwrap_err(); - assert_eq!(err.strip_backtrace(), expected); + assert_snapshot!( + err, + @r###" + Schema error: Ambiguous reference to unqualified field age + "### + ); } #[test] fn group_by_ambiguous_name() { let sql = "select max(id) from person a join person b using (id) group by age"; - let expected = "Schema error: Ambiguous reference to unqualified field age"; + let err = logical_plan(sql).unwrap_err().strip_backtrace(); - let err = logical_plan(sql).unwrap_err(); - assert_eq!(err.strip_backtrace(), expected); + assert_snapshot!( + err, + @r###" + Schema error: Ambiguous reference to unqualified field age + "### + ); } #[test] fn test_zero_offset_with_limit() { let sql = "select id from person where person.id > 100 LIMIT 5 OFFSET 0;"; - let expected = "Limit: skip=0, fetch=5\ - \n Projection: person.id\ - \n Filter: person.id > Int64(100)\ - \n TableScan: person"; - quick_test(sql, expected); - + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Limit: skip=0, fetch=5 + Projection: person.id + Filter: person.id > Int64(100) + TableScan: person +"# + ); // Flip the order of LIMIT and OFFSET in the query. Plan should remain the same. let sql = "SELECT id FROM person WHERE person.id > 100 OFFSET 0 LIMIT 5;"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Limit: skip=0, fetch=5 + Projection: person.id + Filter: person.id > Int64(100) + TableScan: person +"# + ); } #[test] fn test_offset_no_limit() { let sql = "SELECT id FROM person WHERE person.id > 100 OFFSET 5;"; - let expected = "Limit: skip=5, fetch=None\ - \n Projection: person.id\ - \n Filter: person.id > Int64(100)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Limit: skip=5, fetch=None + Projection: person.id + Filter: person.id > Int64(100) + TableScan: person +"# + ); } #[test] fn test_offset_after_limit() { let sql = "select id from person where person.id > 100 LIMIT 5 OFFSET 3;"; - let expected = "Limit: skip=3, fetch=5\ - \n Projection: person.id\ - \n Filter: person.id > Int64(100)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Limit: skip=3, fetch=5 + Projection: person.id + Filter: person.id > Int64(100) + TableScan: person +"# + ); } #[test] fn test_offset_before_limit() { let sql = "select id from person where person.id > 100 OFFSET 3 LIMIT 5;"; - let expected = "Limit: skip=3, fetch=5\ - \n Projection: person.id\ - \n Filter: person.id > Int64(100)\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Limit: skip=3, fetch=5 + Projection: person.id + Filter: person.id > Int64(100) + TableScan: person +"# + ); } #[test] fn test_distribute_by() { let sql = "select id from person distribute by state"; - let expected = "Repartition: DistributeBy(person.state)\ - \n Projection: person.id\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Repartition: DistributeBy(person.state) + Projection: person.id + TableScan: person +"# + ); } #[test] @@ -2946,12 +3966,16 @@ fn test_constant_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id = 10"; - - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id = Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = Int64(10) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -2960,13 +3984,16 @@ fn test_right_left_expr_eq_join() { FROM person \ INNER JOIN orders \ ON orders.customer_id * 2 = person.id + 10"; - - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -2975,12 +4002,16 @@ fn test_single_column_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id + 10 = orders.customer_id * 2"; - - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id + Int64(10) = orders.customer_id * Int64(2)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id + Int64(10) = orders.customer_id * Int64(2) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -2989,12 +4020,16 @@ fn test_multiple_column_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id + person.age + 10 = orders.customer_id * 2 - orders.price"; - - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id * Int64(2) - orders.price\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id * Int64(2) - orders.price + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3003,12 +4038,16 @@ fn test_left_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id + person.age + 10 = orders.customer_id"; - - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3017,12 +4056,16 @@ fn test_right_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id = orders.customer_id * 2 - orders.price"; - - let expected = "Projection: person.id, orders.order_id\ - \n Inner Join: Filter: person.id = orders.customer_id * Int64(2) - orders.price\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id * Int64(2) - orders.price + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3031,38 +4074,58 @@ fn test_noneq_with_filter_join() { let sql = "SELECT person.id, person.first_name \ FROM person INNER JOIN orders \ ON person.age > 10"; - let expected = "Projection: person.id, person.first_name\ - \n Inner Join: Filter: person.age > Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.first_name + Inner Join: Filter: person.age > Int64(10) + TableScan: person + TableScan: orders +"# + ); // left join let sql = "SELECT person.id, person.first_name \ FROM person LEFT JOIN orders \ ON person.age > 10"; - let expected = "Projection: person.id, person.first_name\ - \n Left Join: Filter: person.age > Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.first_name + Left Join: Filter: person.age > Int64(10) + TableScan: person + TableScan: orders +"# + ); // right join let sql = "SELECT person.id, person.first_name \ FROM person RIGHT JOIN orders \ ON person.age > 10"; - let expected = "Projection: person.id, person.first_name\ - \n Right Join: Filter: person.age > Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.first_name + Right Join: Filter: person.age > Int64(10) + TableScan: person + TableScan: orders +"# + ); // full join let sql = "SELECT person.id, person.first_name \ FROM person FULL JOIN orders \ ON person.age > 10"; - let expected = "Projection: person.id, person.first_name\ - \n Full Join: Filter: person.age > Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.first_name + Full Join: Filter: person.age > Int64(10) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3073,12 +4136,16 @@ fn test_one_side_constant_full_join() { FROM person \ FULL OUTER JOIN orders \ ON person.id = 10"; - - let expected = "Projection: person.id, orders.order_id\ - \n Full Join: Filter: person.id = Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, orders.order_id + Full Join: Filter: person.id = Int64(10) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3087,34 +4154,48 @@ fn test_select_join_key_inner_join() { FROM person INNER JOIN orders ON orders.customer_id * 2 = person.id + 10"; - - let expected = "Projection: orders.customer_id * Int64(2), person.id + Int64(10)\ - \n Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.customer_id * Int64(2), person.id + Int64(10) + Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10) + TableScan: person + TableScan: orders +"# + ); } #[test] fn test_select_order_by() { let sql = "SELECT '1' from person order by id"; - - let expected = "Projection: Utf8(\"1\")\n Sort: person.id ASC NULLS LAST\n Projection: Utf8(\"1\"), person.id\n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: Utf8("1") + Sort: person.id ASC NULLS LAST + Projection: Utf8("1"), person.id + TableScan: person +"# + ); } #[test] fn test_select_distinct_order_by() { let sql = "SELECT distinct '1' from person order by id"; - let expected = - "Error during planning: For SELECT DISTINCT, ORDER BY expressions person.id must appear in select list"; - // It should return error. let result = logical_plan(sql); assert!(result.is_err()); - let err = result.err().unwrap(); - assert_eq!(err.strip_backtrace(), expected); + let err = result.err().unwrap().strip_backtrace(); + + assert_snapshot!( + err, + @r###" + Error during planning: For SELECT DISTINCT, ORDER BY expressions person.id must appear in select list + "### + ); } #[rstest] @@ -3148,11 +4229,16 @@ fn test_select_unsupported_syntax_errors(#[case] sql: &str, #[case] error: &str) fn select_order_by_with_cast() { let sql = "SELECT first_name AS first_name FROM (SELECT first_name AS first_name FROM person) ORDER BY CAST(first_name as INT)"; - let expected = "Sort: CAST(person.first_name AS Int32) ASC NULLS LAST\ - \n Projection: person.first_name\ - \n Projection: person.first_name\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Sort: CAST(person.first_name AS Int32) ASC NULLS LAST + Projection: person.first_name + Projection: person.first_name + TableScan: person +"# + ); } #[test] @@ -3173,12 +4259,16 @@ fn test_duplicated_left_join_key_inner_join() { FROM person INNER JOIN orders ON person.id * 2 = orders.customer_id + 10 and person.id * 2 = orders.order_id"; - - let expected = "Projection: person.id, person.age\ - \n Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id * Int64(2) = orders.order_id\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.age + Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id * Int64(2) = orders.order_id + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3188,12 +4278,16 @@ fn test_duplicated_right_join_key_inner_join() { FROM person INNER JOIN orders ON person.id * 2 = orders.customer_id + 10 and person.id = orders.customer_id + 10"; - - let expected = "Projection: person.id, person.age\ - \n Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id = orders.customer_id + Int64(10)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.age + Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id = orders.customer_id + Int64(10) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3203,13 +4297,17 @@ fn test_ambiguous_column_references_in_on_join() { INNER JOIN person as p2 ON id = 1"; - let expected = "Schema error: Ambiguous reference to unqualified field id"; - // It should return error. let result = logical_plan(sql); assert!(result.is_err()); - let err = result.err().unwrap(); - assert_eq!(err.strip_backtrace(), expected); + let err = result.err().unwrap().strip_backtrace(); + + assert_snapshot!( + err, + @r###" + Schema error: Ambiguous reference to unqualified field id + "### + ); } #[test] @@ -3218,14 +4316,18 @@ fn test_ambiguous_column_references_with_in_using_join() { from person as p1 INNER JOIN person as p2 using(id)"; - - let expected = "Projection: p1.id, p1.age, p2.id\ - \n Inner Join: Using p1.id = p2.id\ - \n SubqueryAlias: p1\ - \n TableScan: person\ - \n SubqueryAlias: p2\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: p1.id, p1.age, p2.id + Inner Join: Using p1.id = p2.id + SubqueryAlias: p1 + TableScan: person + SubqueryAlias: p2 + TableScan: person +"# + ); } #[test] @@ -3233,9 +4335,12 @@ fn test_prepare_statement_to_plan_panic_param_format() { // param is not number following the $ sign // panic due to error returned from the parser let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = $foo"; - assert_eq!( + + assert_snapshot!( logical_plan(sql).unwrap_err().strip_backtrace(), - "Error during planning: Invalid placeholder, not a number: $foo" + @r###" + Error during planning: Invalid placeholder, not a number: $foo + "### ); } @@ -3244,9 +4349,12 @@ fn test_prepare_statement_to_plan_panic_param_zero() { // param is zero following the $ sign // panic due to error returned from the parser let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = $0"; - assert_eq!( + + assert_snapshot!( logical_plan(sql).unwrap_err().strip_backtrace(), - "Error during planning: Invalid placeholder, zero is not a valid index: $0" + @r###" + Error during planning: Invalid placeholder, zero is not a valid index: $0 + "### ); } @@ -3264,8 +4372,12 @@ fn test_prepare_statement_to_plan_panic_prepare_wrong_syntax() { #[test] fn test_prepare_statement_to_plan_panic_no_relation_and_constant_param() { let sql = "PREPARE my_plan(INT) AS SELECT id + $1"; - let expected = "Schema error: No field named id."; - assert_eq!(logical_plan(sql).unwrap_err().strip_backtrace(), expected); + + let plan = logical_plan(sql).unwrap_err().strip_backtrace(); + assert_snapshot!( + plan, + @r"Schema error: No field named id." + ); } #[test] @@ -3307,46 +4419,58 @@ fn test_prepare_statement_to_plan_panic_is_param() { fn test_prepare_statement_to_plan_no_param() { // no embedded parameter but still declare it let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = 10"; - - let expected_plan = "Prepare: \"my_plan\" [Int32] \ - \n Projection: person.id, person.age\ - \n Filter: person.age = Int64(10)\ - \n TableScan: person"; - - let expected_dt = "[Int32]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int32] + Projection: person.id, person.age + Filter: person.age = Int64(10) + TableScan: person + "# + ); + assert_snapshot!(dt, @r#"[Int32]"#); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; - let expected_plan = "Projection: person.id, person.age\ - \n Filter: person.age = Int64(10)\ - \n TableScan: person"; - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, person.age + Filter: person.age = Int64(10) + TableScan: person + " + ); ////////////////////////////////////////// // no embedded parameter and no declare it let sql = "PREPARE my_plan AS SELECT id, age FROM person WHERE age = 10"; - - let expected_plan = "Prepare: \"my_plan\" [] \ - \n Projection: person.id, person.age\ - \n Filter: person.age = Int64(10)\ - \n TableScan: person"; - - let expected_dt = "[]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [] + Projection: person.id, person.age + Filter: person.age = Int64(10) + TableScan: person + "# + ); + assert_snapshot!(dt, @r#"[]"#); /////////////////// // replace params with values let param_values: Vec = vec![]; - let expected_plan = "Projection: person.id, person.age\ - \n Filter: person.age = Int64(10)\ - \n TableScan: person"; - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, person.age + Filter: person.age = Int64(10) + TableScan: person + " + ); } #[test] @@ -3356,12 +4480,14 @@ fn test_prepare_statement_to_plan_one_param_no_value_panic() { let plan = logical_plan(sql).unwrap(); // declare 1 param but provide 0 let param_values: Vec = vec![]; - assert_eq!( + + assert_snapshot!( plan.with_param_values(param_values) - .unwrap_err() - .strip_backtrace(), - "Error during planning: Expected 1 parameters, got 0" - ); + .unwrap_err() + .strip_backtrace(), + @r###" + Error during planning: Expected 1 parameters, got 0 + "###); } #[test] @@ -3371,11 +4497,14 @@ fn test_prepare_statement_to_plan_one_param_one_value_different_type_panic() { let plan = logical_plan(sql).unwrap(); // declare 1 param but provide 0 let param_values = vec![ScalarValue::Float64(Some(20.0))]; - assert_eq!( + + assert_snapshot!( plan.with_param_values(param_values) .unwrap_err() .strip_backtrace(), - "Error during planning: Expected parameter of type Int32, got Float64 at index 0" + @r###" + Error during planning: Expected parameter of type Int32, got Float64 at index 0 + "### ); } @@ -3386,56 +4515,80 @@ fn test_prepare_statement_to_plan_no_param_on_value_panic() { let plan = logical_plan(sql).unwrap(); // declare 1 param but provide 0 let param_values = vec![ScalarValue::Int32(Some(10))]; - assert_eq!( + + assert_snapshot!( plan.with_param_values(param_values) .unwrap_err() .strip_backtrace(), - "Error during planning: Expected 0 parameters, got 1" + @r###" + Error during planning: Expected 0 parameters, got 1 + "### ); } #[test] fn test_prepare_statement_to_plan_params_as_constants() { let sql = "PREPARE my_plan(INT) AS SELECT $1"; - - let expected_plan = "Prepare: \"my_plan\" [Int32] \ - \n Projection: $1\n EmptyRelation"; - let expected_dt = "[Int32]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int32] + Projection: $1 + EmptyRelation + "# + ); + assert_snapshot!(dt, @r#"[Int32]"#); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; - let expected_plan = "Projection: Int32(10) AS $1\n EmptyRelation"; - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r" + Projection: Int32(10) AS $1 + EmptyRelation + " + ); /////////////////////////////////////// let sql = "PREPARE my_plan(INT) AS SELECT 1 + $1"; - - let expected_plan = "Prepare: \"my_plan\" [Int32] \ - \n Projection: Int64(1) + $1\n EmptyRelation"; - let expected_dt = "[Int32]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int32] + Projection: Int64(1) + $1 + EmptyRelation + "# + ); + assert_snapshot!(dt, @r#"[Int32]"#); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; - let expected_plan = - "Projection: Int64(1) + Int32(10) AS Int64(1) + $1\n EmptyRelation"; - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r" + Projection: Int64(1) + Int32(10) AS Int64(1) + $1 + EmptyRelation + " + ); /////////////////////////////////////// let sql = "PREPARE my_plan(INT, DOUBLE) AS SELECT 1 + $1 + $2"; - - let expected_plan = "Prepare: \"my_plan\" [Int32, Float64] \ - \n Projection: Int64(1) + $1 + $2\n EmptyRelation"; - let expected_dt = "[Int32, Float64]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int32, Float64] + Projection: Int64(1) + $1 + $2 + EmptyRelation + "# + ); + assert_snapshot!(dt, @r#"[Int32, Float64]"#); /////////////////// // replace params with values @@ -3443,91 +4596,95 @@ fn test_prepare_statement_to_plan_params_as_constants() { ScalarValue::Int32(Some(10)), ScalarValue::Float64(Some(10.0)), ]; - let expected_plan = - "Projection: Int64(1) + Int32(10) + Float64(10) AS Int64(1) + $1 + $2\ - \n EmptyRelation"; - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r" + Projection: Int64(1) + Int32(10) + Float64(10) AS Int64(1) + $1 + $2 + EmptyRelation + " + ); } #[test] -fn test_prepare_statement_infer_types_from_join() { +fn test_infer_types_from_join() { let sql = "SELECT id, order_id FROM person JOIN orders ON id = customer_id and age = $1"; - let expected_plan = r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id AND person.age = $1 - TableScan: person - TableScan: orders + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id AND person.age = $1 + TableScan: person + TableScan: orders "# - .trim(); - - let expected_dt = "[Int32]"; - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + ); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([("$1".to_string(), Some(DataType::Int32))]); assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::Int32(Some(10))].into(); - let expected_plan = r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id AND person.age = Int32(10) - TableScan: person - TableScan: orders - "# - .trim(); - let plan = plan.replace_params_with_values(¶m_values).unwrap(); - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let param_values = vec![ScalarValue::Int32(Some(10))]; + let plan_with_params = plan.with_param_values(param_values).unwrap(); + + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id AND person.age = Int32(10) + TableScan: person + TableScan: orders + " + ); } #[test] -fn test_prepare_statement_infer_types_from_predicate() { +fn test_infer_types_from_predicate() { let sql = "SELECT id, age FROM person WHERE age = $1"; - - let expected_plan = r#" -Projection: person.id, person.age - Filter: person.age = $1 - TableScan: person - "# - .trim(); - - let expected_dt = "[Int32]"; - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.id, person.age + Filter: person.age = $1 + TableScan: person + "# + ); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([("$1".to_string(), Some(DataType::Int32))]); assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::Int32(Some(10))].into(); - let expected_plan = r#" -Projection: person.id, person.age - Filter: person.age = Int32(10) - TableScan: person - "# - .trim(); - let plan = plan.replace_params_with_values(¶m_values).unwrap(); - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let param_values = vec![ScalarValue::Int32(Some(10))]; + let plan_with_params = plan.with_param_values(param_values).unwrap(); + + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, person.age + Filter: person.age = Int32(10) + TableScan: person + " + ); } #[test] -fn test_prepare_statement_infer_types_from_between_predicate() { +fn test_infer_types_from_between_predicate() { let sql = "SELECT id, age FROM person WHERE age BETWEEN $1 AND $2"; - let expected_plan = r#" -Projection: person.id, person.age - Filter: person.age BETWEEN $1 AND $2 - TableScan: person - "# - .trim(); - - let expected_dt = "[Int32]"; - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.id, person.age + Filter: person.age BETWEEN $1 AND $2 + TableScan: person + "# + ); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([ @@ -3537,74 +4694,75 @@ Projection: person.id, person.age assert_eq!(actual_types, expected_types); // replace params with values - let param_values = - vec![ScalarValue::Int32(Some(10)), ScalarValue::Int32(Some(30))].into(); - let expected_plan = r#" -Projection: person.id, person.age - Filter: person.age BETWEEN Int32(10) AND Int32(30) - TableScan: person - "# - .trim(); - let plan = plan.replace_params_with_values(¶m_values).unwrap(); - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let param_values = vec![ScalarValue::Int32(Some(10)), ScalarValue::Int32(Some(30))]; + let plan_with_params = plan.with_param_values(param_values).unwrap(); + + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, person.age + Filter: person.age BETWEEN Int32(10) AND Int32(30) + TableScan: person + " + ); } #[test] -fn test_prepare_statement_infer_types_subquery() { +fn test_infer_types_subquery() { let sql = "SELECT id, age FROM person WHERE age = (select max(age) from person where id = $1)"; - let expected_plan = r#" -Projection: person.id, person.age - Filter: person.age = () - Subquery: - Projection: max(person.age) - Aggregate: groupBy=[[]], aggr=[[max(person.age)]] - Filter: person.id = $1 - TableScan: person - TableScan: person - "# - .trim(); - - let expected_dt = "[Int32]"; - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: person.id, person.age + Filter: person.age = () + Subquery: + Projection: max(person.age) + Aggregate: groupBy=[[]], aggr=[[max(person.age)]] + Filter: person.id = $1 + TableScan: person + TableScan: person + "# + ); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([("$1".to_string(), Some(DataType::UInt32))]); assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::UInt32(Some(10))].into(); - let expected_plan = r#" -Projection: person.id, person.age - Filter: person.age = () - Subquery: - Projection: max(person.age) - Aggregate: groupBy=[[]], aggr=[[max(person.age)]] - Filter: person.id = UInt32(10) - TableScan: person - TableScan: person - "# - .trim(); - let plan = plan.replace_params_with_values(¶m_values).unwrap(); - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let param_values = vec![ScalarValue::UInt32(Some(10))]; + let plan_with_params = plan.with_param_values(param_values).unwrap(); + + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, person.age + Filter: person.age = () + Subquery: + Projection: max(person.age) + Aggregate: groupBy=[[]], aggr=[[max(person.age)]] + Filter: person.id = UInt32(10) + TableScan: person + TableScan: person + " + ); } #[test] -fn test_prepare_statement_update_infer() { +fn test_update_infer() { let sql = "update person set age=$1 where id=$2"; - let expected_plan = r#" -Dml: op=[Update] table=[person] - Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, $1 AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 - Filter: person.id = $2 - TableScan: person - "# - .trim(); - - let expected_dt = "[Int32]"; - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Dml: op=[Update] table=[person] + Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, $1 AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 + Filter: person.id = $2 + TableScan: person + "# + ); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([ @@ -3614,32 +4772,32 @@ Dml: op=[Update] table=[person] assert_eq!(actual_types, expected_types); // replace params with values - let param_values = - vec![ScalarValue::Int32(Some(42)), ScalarValue::UInt32(Some(1))].into(); - let expected_plan = r#" -Dml: op=[Update] table=[person] - Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, Int32(42) AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 - Filter: person.id = UInt32(1) - TableScan: person - "# - .trim(); - let plan = plan.replace_params_with_values(¶m_values).unwrap(); - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let param_values = vec![ScalarValue::Int32(Some(42)), ScalarValue::UInt32(Some(1))]; + let plan_with_params = plan.with_param_values(param_values).unwrap(); + + assert_snapshot!( + plan_with_params, + @r" + Dml: op=[Update] table=[person] + Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, Int32(42) AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 + Filter: person.id = UInt32(1) + TableScan: person + " + ); } #[test] -fn test_prepare_statement_insert_infer() { +fn test_insert_infer() { let sql = "insert into person (id, first_name, last_name) values ($1, $2, $3)"; - - let expected_plan = "Dml: op=[Insert Into] table=[person]\ - \n Projection: column1 AS id, column2 AS first_name, column3 AS last_name, \ - CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, \ - CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀\ - \n Values: ($1, $2, $3)"; - - let expected_dt = "[Int32]"; - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Dml: op=[Insert Into] table=[person] + Projection: column1 AS id, column2 AS first_name, column3 AS last_name, CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀 + Values: ($1, $2, $3) + "# + ); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([ @@ -3654,64 +4812,79 @@ fn test_prepare_statement_insert_infer() { ScalarValue::UInt32(Some(1)), ScalarValue::from("Alan"), ScalarValue::from("Turing"), - ] - .into(); - let expected_plan = "Dml: op=[Insert Into] table=[person]\ - \n Projection: column1 AS id, column2 AS first_name, column3 AS last_name, \ - CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, \ - CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀\ - \n Values: (UInt32(1) AS $1, Utf8(\"Alan\") AS $2, Utf8(\"Turing\") AS $3)"; - let plan = plan.replace_params_with_values(¶m_values).unwrap(); - - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + ]; + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r#" + Dml: op=[Insert Into] table=[person] + Projection: column1 AS id, column2 AS first_name, column3 AS last_name, CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀 + Values: (UInt32(1) AS $1, Utf8("Alan") AS $2, Utf8("Turing") AS $3) + "# + ); } #[test] fn test_prepare_statement_to_plan_one_param() { let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = $1"; - - let expected_plan = "Prepare: \"my_plan\" [Int32] \ - \n Projection: person.id, person.age\ - \n Filter: person.age = $1\ - \n TableScan: person"; - - let expected_dt = "[Int32]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int32] + Projection: person.id, person.age + Filter: person.age = $1 + TableScan: person + "# + ); + assert_snapshot!(dt, @r#"[Int32]"#); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; - let expected_plan = "Projection: person.id, person.age\ - \n Filter: person.age = Int32(10)\ - \n TableScan: person"; - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, person.age + Filter: person.age = Int32(10) + TableScan: person + " + ); } #[test] fn test_prepare_statement_to_plan_data_type() { let sql = "PREPARE my_plan(DOUBLE) AS SELECT id, age FROM person WHERE age = $1"; - // age is defined as Int32 but prepare statement declares it as DOUBLE/Float64 - // Prepare statement and its logical plan should be created successfully - let expected_plan = "Prepare: \"my_plan\" [Float64] \ - \n Projection: person.id, person.age\ - \n Filter: person.age = $1\ - \n TableScan: person"; - - let expected_dt = "[Float64]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + // age is defined as Int32 but prepare statement declares it as DOUBLE/Float64 + // Prepare statement and its logical plan should be created successfully + @r#" + Prepare: "my_plan" [Float64] + Projection: person.id, person.age + Filter: person.age = $1 + TableScan: person + "# + ); + assert_snapshot!(dt, @r#"[Float64]"#); /////////////////// // replace params with values still succeed and use Float64 let param_values = vec![ScalarValue::Float64(Some(10.0))]; - let expected_plan = "Projection: person.id, person.age\ - \n Filter: person.age = Float64(10)\ - \n TableScan: person"; - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r" + Projection: person.id, person.age + Filter: person.age = Float64(10) + TableScan: person + " + ); } #[test] @@ -3720,15 +4893,17 @@ fn test_prepare_statement_to_plan_multi_params() { SELECT id, age, $6 FROM person WHERE age IN ($1, $4) AND salary > $3 and salary < $5 OR first_name < $2"; - - let expected_plan = "Prepare: \"my_plan\" [Int32, Utf8, Float64, Int32, Float64, Utf8] \ - \n Projection: person.id, person.age, $6\ - \n Filter: person.age IN ([$1, $4]) AND person.salary > $3 AND person.salary < $5 OR person.first_name < $2\ - \n TableScan: person"; - - let expected_dt = "[Int32, Utf8, Float64, Int32, Float64, Utf8]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int32, Utf8, Float64, Int32, Float64, Utf8] + Projection: person.id, person.age, $6 + Filter: person.age IN ([$1, $4]) AND person.salary > $3 AND person.salary < $5 OR person.first_name < $2 + TableScan: person + "# + ); + assert_snapshot!(dt, @r#"[Int32, Utf8, Float64, Int32, Float64, Utf8]"#); /////////////////// // replace params with values @@ -3740,12 +4915,16 @@ fn test_prepare_statement_to_plan_multi_params() { ScalarValue::Float64(Some(200.0)), ScalarValue::from("xyz"), ]; - let expected_plan = - "Projection: person.id, person.age, Utf8(\"xyz\") AS $6\ - \n Filter: person.age IN ([Int32(10), Int32(20)]) AND person.salary > Float64(100) AND person.salary < Float64(200) OR person.first_name < Utf8(\"abc\")\ - \n TableScan: person"; - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r#" + Projection: person.id, person.age, Utf8("xyz") AS $6 + Filter: person.age IN ([Int32(10), Int32(20)]) AND person.salary > Float64(100) AND person.salary < Float64(200) OR person.first_name < Utf8("abc") + TableScan: person + "# + ); } #[test] @@ -3757,17 +4936,19 @@ fn test_prepare_statement_to_plan_having() { GROUP BY id HAVING sum(age) < $1 AND sum(age) > 10 OR sum(age) in ($3, $4)\ "; - - let expected_plan = "Prepare: \"my_plan\" [Int32, Float64, Float64, Float64] \ - \n Projection: person.id, sum(person.age)\ - \n Filter: sum(person.age) < $1 AND sum(person.age) > Int64(10) OR sum(person.age) IN ([$3, $4])\ - \n Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]]\ - \n Filter: person.salary > $2\ - \n TableScan: person"; - - let expected_dt = "[Int32, Float64, Float64, Float64]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int32, Float64, Float64, Float64] + Projection: person.id, sum(person.age) + Filter: sum(person.age) < $1 AND sum(person.age) > Int64(10) OR sum(person.age) IN ([$3, $4]) + Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]] + Filter: person.salary > $2 + TableScan: person + "# + ); + assert_snapshot!(dt, @r#"[Int32, Float64, Float64, Float64]"#); /////////////////// // replace params with values @@ -3777,14 +4958,18 @@ fn test_prepare_statement_to_plan_having() { ScalarValue::Float64(Some(200.0)), ScalarValue::Float64(Some(300.0)), ]; - let expected_plan = - "Projection: person.id, sum(person.age)\ - \n Filter: sum(person.age) < Int32(10) AND sum(person.age) > Int64(10) OR sum(person.age) IN ([Float64(200), Float64(300)])\ - \n Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]]\ - \n Filter: person.salary > Float64(100)\ - \n TableScan: person"; - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r#" + Projection: person.id, sum(person.age) + Filter: sum(person.age) < Int32(10) AND sum(person.age) > Int64(10) OR sum(person.age) IN ([Float64(200), Float64(300)]) + Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]] + Filter: person.salary > Float64(100) + TableScan: person + "# + ); } #[test] @@ -3792,22 +4977,29 @@ fn test_prepare_statement_to_plan_limit() { let sql = "PREPARE my_plan(BIGINT, BIGINT) AS SELECT id FROM person \ OFFSET $1 LIMIT $2"; - - let expected_plan = "Prepare: \"my_plan\" [Int64, Int64] \ - \n Limit: skip=$1, fetch=$2\ - \n Projection: person.id\ - \n TableScan: person"; - - let expected_dt = "[Int64, Int64]"; - - let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); + let (plan, dt) = generate_prepare_stmt_and_data_types(sql); + assert_snapshot!( + plan, + @r#" + Prepare: "my_plan" [Int64, Int64] + Limit: skip=$1, fetch=$2 + Projection: person.id + TableScan: person + "# + ); + assert_snapshot!(dt, @r#"[Int64, Int64]"#); // replace params with values let param_values = vec![ScalarValue::Int64(Some(10)), ScalarValue::Int64(Some(200))]; - let expected_plan = "Limit: skip=10, fetch=200\ - \n Projection: person.id\ - \n TableScan: person"; - prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); + let plan_with_params = plan.with_param_values(param_values).unwrap(); + assert_snapshot!( + plan_with_params, + @r#" + Limit: skip=10, fetch=200 + Projection: person.id + TableScan: person + "# + ); } #[test] @@ -3850,12 +5042,16 @@ fn test_inner_join_with_cast_key() { FROM person INNER JOIN orders ON cast(person.id as Int) = cast(orders.customer_id as Int)"; - - let expected = "Projection: person.id, person.age\ - \n Inner Join: Filter: CAST(person.id AS Int32) = CAST(orders.customer_id AS Int32)\ - \n TableScan: person\ - \n TableScan: orders"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.age + Inner Join: Filter: CAST(person.id AS Int32) = CAST(orders.customer_id AS Int32) + TableScan: person + TableScan: orders +"# + ); } #[test] @@ -3865,74 +5061,107 @@ fn test_multi_grouping_sets() { GROUP BY person.id, GROUPING SETS ((person.age,person.salary),(person.age))"; - - let expected = "Projection: person.id, person.age\ - \n Aggregate: groupBy=[[GROUPING SETS ((person.id, person.age, person.salary), (person.id, person.age))]], aggr=[[]]\ - \n TableScan: person"; - quick_test(sql, expected); - + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.age + Aggregate: groupBy=[[GROUPING SETS ((person.id, person.age, person.salary), (person.id, person.age))]], aggr=[[]] + TableScan: person +"# + ); let sql = "SELECT person.id, person.age FROM person GROUP BY person.id, GROUPING SETS ((person.age, person.salary),(person.age)), ROLLUP(person.state, person.birth_date)"; - - let expected = "Projection: person.id, person.age\ - \n Aggregate: groupBy=[[GROUPING SETS (\ - (person.id, person.age, person.salary), \ - (person.id, person.age, person.salary, person.state), \ - (person.id, person.age, person.salary, person.state, person.birth_date), \ - (person.id, person.age), \ - (person.id, person.age, person.state), \ - (person.id, person.age, person.state, person.birth_date))]], aggr=[[]]\ - \n TableScan: person"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: person.id, person.age + Aggregate: groupBy=[[GROUPING SETS ((person.id, person.age, person.salary), (person.id, person.age, person.salary, person.state), (person.id, person.age, person.salary, person.state, person.birth_date), (person.id, person.age), (person.id, person.age, person.state), (person.id, person.age, person.state, person.birth_date))]], aggr=[[]] + TableScan: person +"# + ); } #[test] fn test_field_not_found_window_function() { let order_by_sql = "SELECT count() OVER (order by a);"; - let order_by_err = logical_plan(order_by_sql).expect_err("query should have failed"); - let expected = "Schema error: No field named a."; - assert_eq!(order_by_err.strip_backtrace(), expected); + let order_by_err = logical_plan(order_by_sql) + .expect_err("query should have failed") + .strip_backtrace(); + + assert_snapshot!( + order_by_err, + @r###" + Schema error: No field named a. + "### + ); let partition_by_sql = "SELECT count() OVER (PARTITION BY a);"; - let partition_by_err = - logical_plan(partition_by_sql).expect_err("query should have failed"); - let expected = "Schema error: No field named a."; - assert_eq!(partition_by_err.strip_backtrace(), expected); + let partition_by_err = logical_plan(partition_by_sql) + .expect_err("query should have failed") + .strip_backtrace(); + + assert_snapshot!( + partition_by_err, + @r###" + Schema error: No field named a. + "### + ); - let qualified_sql = - "SELECT order_id, MAX(qty) OVER (PARTITION BY orders.order_id) from orders"; - let expected = "Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\n TableScan: orders"; - quick_test(qualified_sql, expected); + let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY orders.order_id) from orders"; + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING + WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] + TableScan: orders +"# + ); } #[test] fn test_parse_escaped_string_literal_value() { let sql = r"SELECT character_length('\r\n') AS len"; - let expected = "Projection: character_length(Utf8(\"\\r\\n\")) AS len\ - \n EmptyRelation"; - quick_test(sql, expected); - - let sql = r"SELECT character_length(E'\r\n') AS len"; - let expected = "Projection: character_length(Utf8(\"\r\n\")) AS len\ - \n EmptyRelation"; - quick_test(sql, expected); - + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" + Projection: character_length(Utf8("\r\n")) AS len + EmptyRelation + "# + ); + let sql = "SELECT character_length(E'\r\n') AS len"; + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @r#" +Projection: character_length(Utf8(" +")) AS len + EmptyRelation +"# + ); let sql = r"SELECT character_length(E'\445') AS len, E'\x4B' AS hex, E'\u0001' AS unicode"; - let expected = - "Projection: character_length(Utf8(\"%\")) AS len, Utf8(\"\u{004b}\") AS hex, Utf8(\"\u{0001}\") AS unicode\ - \n EmptyRelation"; - quick_test(sql, expected); + let plan = logical_plan(sql).unwrap(); + assert_snapshot!( + plan, + @"Projection: character_length(Utf8(\"%\")) AS len, Utf8(\"K\") AS hex, Utf8(\"\u{1}\") AS unicode\n EmptyRelation" + ); let sql = r"SELECT character_length(E'\000') AS len"; - assert_eq!( - logical_plan(sql).unwrap_err().strip_backtrace(), - "SQL error: TokenizerError(\"Unterminated encoded string literal at Line: 1, Column: 25\")" - ) + + assert_snapshot!( + logical_plan(sql).unwrap_err(), + @r###" + SQL error: TokenizerError("Unterminated encoded string literal at Line: 1, Column: 25") + "### + ); } #[test] @@ -4048,22 +5277,36 @@ fn test_custom_type_plan() -> Result<()> { } let plan = plan_sql(sql); - let expected = - "Projection: CAST(Utf8(\"2001-01-01 18:00:00\") AS Timestamp(Nanosecond, None))\ - \n EmptyRelation"; - assert_eq!(plan.to_string(), expected); + + assert_snapshot!( + plan, + @r###" + Projection: CAST(Utf8("2001-01-01 18:00:00") AS Timestamp(Nanosecond, None)) + EmptyRelation + "### + ); let plan = plan_sql("SELECT CAST(TIMESTAMP '2001-01-01 18:00:00' AS DATETIME)"); - let expected = "Projection: CAST(CAST(Utf8(\"2001-01-01 18:00:00\") AS Timestamp(Nanosecond, None)) AS Timestamp(Nanosecond, None))\ - \n EmptyRelation"; - assert_eq!(plan.to_string(), expected); + + assert_snapshot!( + plan, + @r###" + Projection: CAST(CAST(Utf8("2001-01-01 18:00:00") AS Timestamp(Nanosecond, None)) AS Timestamp(Nanosecond, None)) + EmptyRelation + "### + ); let plan = plan_sql( "SELECT ARRAY[DATETIME '2001-01-01 18:00:00', DATETIME '2001-01-02 18:00:00']", ); - let expected = "Projection: make_array(CAST(Utf8(\"2001-01-01 18:00:00\") AS Timestamp(Nanosecond, None)), CAST(Utf8(\"2001-01-02 18:00:00\") AS Timestamp(Nanosecond, None)))\ - \n EmptyRelation"; - assert_eq!(plan.to_string(), expected); + + assert_snapshot!( + plan, + @r###" + Projection: make_array(CAST(Utf8("2001-01-01 18:00:00") AS Timestamp(Nanosecond, None)), CAST(Utf8("2001-01-02 18:00:00") AS Timestamp(Nanosecond, None))) + EmptyRelation + "### + ); Ok(()) } @@ -4094,7 +5337,7 @@ fn test_error_message_invalid_scalar_function_signature() { fn test_error_message_invalid_aggregate_function_signature() { error_message_test( "select sum()", - "Error during planning: 'sum' does not support zero arguments", + "Error during planning: Execution error: Function 'sum' user-defined coercion failed with \"Execution error: sum function requires 1 argument, got 0\"", ); // We keep two different prefixes because they clarify each other. // It might be incorrect, and we should consider keeping only one. @@ -4116,7 +5359,7 @@ fn test_error_message_invalid_window_function_signature() { fn test_error_message_invalid_window_aggregate_function_signature() { error_message_test( "select sum() over()", - "Error during planning: 'sum' does not support zero arguments", + "Error during planning: Execution error: Function 'sum' user-defined coercion failed with \"Execution error: sum function requires 1 argument, got 0\"", ); } diff --git a/datafusion/sqllogictest/Cargo.toml b/datafusion/sqllogictest/Cargo.toml index 4c7ee6c1bb865..b2e02d7d5dd87 100644 --- a/datafusion/sqllogictest/Cargo.toml +++ b/datafusion/sqllogictest/Cargo.toml @@ -42,7 +42,7 @@ async-trait = { workspace = true } bigdecimal = { workspace = true } bytes = { workspace = true, optional = true } chrono = { workspace = true, optional = true } -clap = { version = "4.5.34", features = ["derive", "env"] } +clap = { version = "4.5.36", features = ["derive", "env"] } datafusion = { workspace = true, default-features = true, features = ["avro"] } futures = { workspace = true } half = { workspace = true, default-features = true } @@ -55,7 +55,7 @@ postgres-types = { version = "0.2.8", features = ["derive", "with-chrono-0_4"], rust_decimal = { version = "1.37.1", features = ["tokio-pg"] } # When updating the following dependency verify that sqlite test file regeneration works correctly # by running the regenerate_sqlite_files.sh script. -sqllogictest = "0.28.0" +sqllogictest = "0.28.1" sqlparser = { workspace = true } tempfile = { workspace = true } testcontainers = { version = "0.23", features = ["default"], optional = true } diff --git a/datafusion/sqllogictest/bin/sqllogictests.rs b/datafusion/sqllogictest/bin/sqllogictests.rs index 5894ec056a2eb..21dfe2ee08f4e 100644 --- a/datafusion/sqllogictest/bin/sqllogictests.rs +++ b/datafusion/sqllogictest/bin/sqllogictests.rs @@ -175,12 +175,7 @@ async fn run_tests() -> Result<()> { futures::stream::iter(match result { // Tokio panic error Err(e) => Some(DataFusionError::External(Box::new(e))), - Ok(thread_result) => match thread_result { - // Test run error - Err(e) => Some(e), - // success - Ok(_) => None, - }, + Ok(thread_result) => thread_result.err(), }) }) .collect() diff --git a/datafusion/sqllogictest/test_files/aggregate.slt b/datafusion/sqllogictest/test_files/aggregate.slt index 57728ef8c734a..1f63e5fcad5c7 100644 --- a/datafusion/sqllogictest/test_files/aggregate.slt +++ b/datafusion/sqllogictest/test_files/aggregate.slt @@ -133,36 +133,50 @@ SELECT approx_distinct(c9) count_c9, approx_distinct(cast(c9 as varchar)) count_ # csv_query_approx_percentile_cont_with_weight statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont_with_weight' function: coercion from \[Utf8, Int8, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont_with_weight(c1, c2, 0.95) FROM aggregate_test_100 +SELECT approx_percentile_cont_with_weight(c2, 0.95) WITHIN GROUP (ORDER BY c1) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont_with_weight' function: coercion from \[Int16, Utf8, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont_with_weight(c3, c1, 0.95) FROM aggregate_test_100 +SELECT approx_percentile_cont_with_weight(c1, 0.95) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont_with_weight' function: coercion from \[Int16, Int8, Utf8\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont_with_weight(c3, c2, c1) FROM aggregate_test_100 +SELECT approx_percentile_cont_with_weight(c2, c1) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 # csv_query_approx_percentile_cont_with_histogram_bins statement error DataFusion error: This feature is not implemented: Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be UInt > 0 literal \(got data type Int64\)\. -SELECT c1, approx_percentile_cont(c3, 0.95, -1000) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(0.95, -1000) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont' function: coercion from \[Int16, Float64, Utf8\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont(c3, 0.95, c1) FROM aggregate_test_100 +SELECT approx_percentile_cont(0.95, c1) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont' function: coercion from \[Int16, Float64, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont(c3, 0.95, 111.1) FROM aggregate_test_100 +SELECT approx_percentile_cont(0.95, 111.1) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont' function: coercion from \[Float64, Float64, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont(c12, 0.95, 111.1) FROM aggregate_test_100 +SELECT approx_percentile_cont(0.95, 111.1) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 statement error DataFusion error: This feature is not implemented: Percentile value for 'APPROX_PERCENTILE_CONT' must be a literal -SELECT approx_percentile_cont(c12, c12) FROM aggregate_test_100 +SELECT approx_percentile_cont(c12) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 statement error DataFusion error: This feature is not implemented: Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be a literal -SELECT approx_percentile_cont(c12, 0.95, c5) FROM aggregate_test_100 +SELECT approx_percentile_cont(0.95, c5) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 + +statement error DataFusion error: This feature is not implemented: Conflicting ordering requirements in aggregate functions is not supported +SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5), approx_percentile_cont(0.2) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 + +statement error DataFusion error: Error during planning: \[IGNORE | RESPECT\] NULLS are not permitted for approx_percentile_cont +SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5) IGNORE NULLS FROM aggregate_test_100 + +statement error DataFusion error: Error during planning: \[IGNORE | RESPECT\] NULLS are not permitted for approx_percentile_cont +SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5) RESPECT NULLS FROM aggregate_test_100 + +statement error DataFusion error: This feature is not implemented: Only a single ordering expression is permitted in a WITHIN GROUP clause +SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5, c12) FROM aggregate_test_100 # Not supported over sliding windows -query error This feature is not implemented: Aggregate can not be used as a sliding accumulator because `retract_batch` is not implemented -SELECT approx_percentile_cont(c3, 0.5) OVER (ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) +query error DataFusion error: Error during planning: OVER and WITHIN GROUP clause are can not be used together. OVER is for window function, whereas WITHIN GROUP is for ordered set aggregate function +SELECT approx_percentile_cont(0.5) +WITHIN GROUP (ORDER BY c3) +OVER (ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) FROM aggregate_test_100 # array agg can use order by @@ -1276,173 +1290,173 @@ SELECT approx_distinct(c9) AS a, approx_distinct(c9) AS b FROM aggregate_test_10 #csv_query_approx_percentile_cont (c2) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c2, 0.1) AS DOUBLE) / 1.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c2) AS DOUBLE) / 1.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c2, 0.5) AS DOUBLE) / 3.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c2) AS DOUBLE) / 3.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c2, 0.9) AS DOUBLE) / 5.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c2) AS DOUBLE) / 5.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c3) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c3, 0.1) AS DOUBLE) / -95.3) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c3) AS DOUBLE) / -95.3) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c3, 0.5) AS DOUBLE) / 15.5) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c3) AS DOUBLE) / 15.5) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c3, 0.9) AS DOUBLE) / 102.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c3) AS DOUBLE) / 102.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c4) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c4, 0.1) AS DOUBLE) / -22925.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c4) AS DOUBLE) / -22925.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c4, 0.5) AS DOUBLE) / 4599.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c4) AS DOUBLE) / 4599.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c4, 0.9) AS DOUBLE) / 25334.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c4) AS DOUBLE) / 25334.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c5) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c5, 0.1) AS DOUBLE) / -1882606710.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c5) AS DOUBLE) / -1882606710.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c5, 0.5) AS DOUBLE) / 377164262.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c5) AS DOUBLE) / 377164262.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c5, 0.9) AS DOUBLE) / 1991374996.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c5) AS DOUBLE) / 1991374996.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c6) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c6, 0.1) AS DOUBLE) / -7250000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c6) AS DOUBLE) / -7250000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c6, 0.5) AS DOUBLE) / 1130000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c6) AS DOUBLE) / 1130000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c6, 0.9) AS DOUBLE) / 7370000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c6) AS DOUBLE) / 7370000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c7) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c7, 0.1) AS DOUBLE) / 18.9) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c7) AS DOUBLE) / 18.9) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c7, 0.5) AS DOUBLE) / 134.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c7) AS DOUBLE) / 134.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c7, 0.9) AS DOUBLE) / 231.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c7) AS DOUBLE) / 231.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c8) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c8, 0.1) AS DOUBLE) / 2671.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c8) AS DOUBLE) / 2671.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c8, 0.5) AS DOUBLE) / 30634.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c8) AS DOUBLE) / 30634.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c8, 0.9) AS DOUBLE) / 57518.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c8) AS DOUBLE) / 57518.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c9) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c9, 0.1) AS DOUBLE) / 472608672.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c9) AS DOUBLE) / 472608672.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c9, 0.5) AS DOUBLE) / 2365817608.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c9) AS DOUBLE) / 2365817608.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c9, 0.9) AS DOUBLE) / 3776538487.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c9) AS DOUBLE) / 3776538487.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c10) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c10, 0.1) AS DOUBLE) / 1830000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c10) AS DOUBLE) / 1830000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c10, 0.5) AS DOUBLE) / 9300000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c10) AS DOUBLE) / 9300000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c10, 0.9) AS DOUBLE) / 16100000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c10) AS DOUBLE) / 16100000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c11) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c11, 0.1) AS DOUBLE) / 0.109) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c11) AS DOUBLE) / 0.109) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c11, 0.5) AS DOUBLE) / 0.491) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c11) AS DOUBLE) / 0.491) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(c11, 0.9) AS DOUBLE) / 0.834) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c11) AS DOUBLE) / 0.834) < 0.05) AS q FROM aggregate_test_100 ---- true # percentile_cont_with_nulls query I -SELECT APPROX_PERCENTILE_CONT(v, 0.5) FROM (VALUES (1), (2), (3), (NULL), (NULL), (NULL)) as t (v); +SELECT APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY v) FROM (VALUES (1), (2), (3), (NULL), (NULL), (NULL)) as t (v); ---- 2 # percentile_cont_with_nulls_only query I -SELECT APPROX_PERCENTILE_CONT(v, 0.5) FROM (VALUES (CAST(NULL as INT))) as t (v); +SELECT APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY v) FROM (VALUES (CAST(NULL as INT))) as t (v); ---- NULL @@ -1465,7 +1479,7 @@ NaN # ISSUE: https://github.com/apache/datafusion/issues/11870 query R -select APPROX_PERCENTILE_CONT(v2, 0.8) from tmp_percentile_cont; +select APPROX_PERCENTILE_CONT(0.8) WITHIN GROUP (ORDER BY v2) from tmp_percentile_cont; ---- NaN @@ -1473,10 +1487,10 @@ NaN # Note: `approx_percentile_cont_with_weight()` uses the same implementation as `approx_percentile_cont()` query R SELECT APPROX_PERCENTILE_CONT_WITH_WEIGHT( - v2, '+Inf'::Double, 0.9 ) +WITHIN GROUP (ORDER BY v2) FROM tmp_percentile_cont; ---- NaN @@ -1495,7 +1509,7 @@ INSERT INTO t1 VALUES (TRUE); # ISSUE: https://github.com/apache/datafusion/issues/12716 # This test verifies that approx_percentile_cont_with_weight does not panic when given 'NaN' and returns 'inf' query R -SELECT approx_percentile_cont_with_weight('NaN'::DOUBLE, 0, 0) FROM t1 WHERE t1.v1; +SELECT approx_percentile_cont_with_weight(0, 0) WITHIN GROUP (ORDER BY 'NaN'::DOUBLE) FROM t1 WHERE t1.v1; ---- Infinity @@ -1722,7 +1736,7 @@ b NULL NULL 7732.315789473684 # csv_query_approx_percentile_cont_with_weight query TI -SELECT c1, approx_percentile_cont(c3, 0.95) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 73 b 68 @@ -1730,9 +1744,18 @@ c 122 d 124 e 115 +query TI +SELECT c1, approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c3 DESC) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +---- +a -101 +b -114 +c -109 +d -98 +e -93 + # csv_query_approx_percentile_cont_with_weight (2) query TI -SELECT c1, approx_percentile_cont_with_weight(c3, 1, 0.95) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont_with_weight(1, 0.95) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 73 b 68 @@ -1740,9 +1763,18 @@ c 122 d 124 e 115 +query TI +SELECT c1, approx_percentile_cont_with_weight(1, 0.95) WITHIN GROUP (ORDER BY c3 DESC) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +---- +a -101 +b -114 +c -109 +d -98 +e -93 + # csv_query_approx_percentile_cont_with_histogram_bins query TI -SELECT c1, approx_percentile_cont(c3, 0.95, 200) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(0.95, 200) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 73 b 68 @@ -1751,7 +1783,7 @@ d 124 e 115 query TI -SELECT c1, approx_percentile_cont_with_weight(c3, c2, 0.95) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont_with_weight(c2, 0.95) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 74 b 68 @@ -3041,7 +3073,7 @@ SELECT COUNT(DISTINCT c1) FROM test # test_approx_percentile_cont_decimal_support query TI -SELECT c1, approx_percentile_cont(c2, cast(0.85 as decimal(10,2))) apc FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(cast(0.85 as decimal(10,2))) WITHIN GROUP (ORDER BY c2) apc FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 4 b 5 @@ -4969,6 +5001,73 @@ select count(distinct column1), count(distinct column2) from dict_test group by statement ok drop table dict_test; +# avg_duration + +statement ok +create table d as values + (arrow_cast(1, 'Duration(Second)'), arrow_cast(2, 'Duration(Millisecond)'), arrow_cast(3, 'Duration(Microsecond)'), arrow_cast(4, 'Duration(Nanosecond)'), 1), + (arrow_cast(11, 'Duration(Second)'), arrow_cast(22, 'Duration(Millisecond)'), arrow_cast(33, 'Duration(Microsecond)'), arrow_cast(44, 'Duration(Nanosecond)'), 1); + +query ???? +SELECT avg(column1), avg(column2), avg(column3), avg(column4) FROM d; +---- +0 days 0 hours 0 mins 6 secs 0 days 0 hours 0 mins 0.012 secs 0 days 0 hours 0 mins 0.000018 secs 0 days 0 hours 0 mins 0.000000024 secs + +query ????I +SELECT avg(column1), avg(column2), avg(column3), avg(column4), column5 FROM d GROUP BY column5; +---- +0 days 0 hours 0 mins 6 secs 0 days 0 hours 0 mins 0.012 secs 0 days 0 hours 0 mins 0.000018 secs 0 days 0 hours 0 mins 0.000000024 secs 1 + +statement ok +drop table d; + +statement ok +create table d as values + (arrow_cast(1, 'Duration(Second)'), arrow_cast(2, 'Duration(Millisecond)'), arrow_cast(3, 'Duration(Microsecond)'), arrow_cast(4, 'Duration(Nanosecond)'), 1), + (arrow_cast(11, 'Duration(Second)'), arrow_cast(22, 'Duration(Millisecond)'), arrow_cast(33, 'Duration(Microsecond)'), arrow_cast(44, 'Duration(Nanosecond)'), 1), + (arrow_cast(5, 'Duration(Second)'), arrow_cast(10, 'Duration(Millisecond)'), arrow_cast(15, 'Duration(Microsecond)'), arrow_cast(20, 'Duration(Nanosecond)'), 2), + (arrow_cast(25, 'Duration(Second)'), arrow_cast(50, 'Duration(Millisecond)'), arrow_cast(75, 'Duration(Microsecond)'), arrow_cast(100, 'Duration(Nanosecond)'), 2), + (NULL, NULL, NULL, NULL, 1), + (NULL, NULL, NULL, NULL, 2); + + +query I? rowsort +SELECT column5, avg(column1) FROM d GROUP BY column5; +---- +1 0 days 0 hours 0 mins 6 secs +2 0 days 0 hours 0 mins 15 secs + +query I?? rowsort +SELECT column5, column1, avg(column1) OVER (PARTITION BY column5 ORDER BY column1 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) as window_avg +FROM d WHERE column1 IS NOT NULL; +---- +1 0 days 0 hours 0 mins 1 secs 0 days 0 hours 0 mins 1 secs +1 0 days 0 hours 0 mins 11 secs 0 days 0 hours 0 mins 6 secs +2 0 days 0 hours 0 mins 25 secs 0 days 0 hours 0 mins 15 secs +2 0 days 0 hours 0 mins 5 secs 0 days 0 hours 0 mins 5 secs + +# Cumulative average window function +query I?? +SELECT column5, column1, avg(column1) OVER (ORDER BY column5, column1 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative_avg +FROM d WHERE column1 IS NOT NULL; +---- +1 0 days 0 hours 0 mins 1 secs 0 days 0 hours 0 mins 1 secs +1 0 days 0 hours 0 mins 11 secs 0 days 0 hours 0 mins 6 secs +2 0 days 0 hours 0 mins 5 secs 0 days 0 hours 0 mins 5 secs +2 0 days 0 hours 0 mins 25 secs 0 days 0 hours 0 mins 10 secs + +# Centered average window function +query I?? +SELECT column5, column1, avg(column1) OVER (ORDER BY column5 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) as centered_avg +FROM d WHERE column1 IS NOT NULL; +---- +1 0 days 0 hours 0 mins 1 secs 0 days 0 hours 0 mins 6 secs +1 0 days 0 hours 0 mins 11 secs 0 days 0 hours 0 mins 5 secs +2 0 days 0 hours 0 mins 5 secs 0 days 0 hours 0 mins 13 secs +2 0 days 0 hours 0 mins 25 secs 0 days 0 hours 0 mins 15 secs + +statement ok +drop table d; # Prepare the table with dictionary values for testing statement ok @@ -5622,6 +5721,11 @@ SELECT STRING_AGG(column1, '|') FROM (values (''), (null), ('')); ---- | +query T +SELECT STRING_AGG(DISTINCT column1, '|') FROM (values (''), (null), ('')); +---- +(empty) + statement ok CREATE TABLE strings(g INTEGER, x VARCHAR, y VARCHAR) @@ -5643,6 +5747,22 @@ SELECT STRING_AGG(x,',') FROM strings WHERE g > 100 ---- NULL +query T +SELECT STRING_AGG(DISTINCT x,',') FROM strings WHERE g > 100 +---- +NULL + +query T +SELECT STRING_AGG(DISTINCT x,'|' ORDER BY x) FROM strings +---- +a|b|i|j|p|x|y|z + +query error This feature is not implemented: The second argument of the string_agg function must be a string literal +SELECT STRING_AGG(DISTINCT x,y) FROM strings + +query error Execution error: In an aggregate with DISTINCT, ORDER BY expressions must appear in argument list +SELECT STRING_AGG(DISTINCT x,'|' ORDER BY y) FROM strings + statement ok drop table strings @@ -5657,6 +5777,17 @@ FROM my_data ---- text1, text1, text1 +query T +WITH my_data as ( +SELECT 'text1'::varchar(1000) as my_column union all +SELECT 'text1'::varchar(1000) as my_column union all +SELECT 'text1'::varchar(1000) as my_column +) +SELECT string_agg(DISTINCT my_column,', ') as my_string_agg +FROM my_data +---- +text1 + query T WITH my_data as ( SELECT 1 as dummy, 'text1'::varchar(1000) as my_column union all @@ -5669,6 +5800,18 @@ GROUP BY dummy ---- text1, text1, text1 +query T +WITH my_data as ( +SELECT 1 as dummy, 'text1'::varchar(1000) as my_column union all +SELECT 1 as dummy, 'text1'::varchar(1000) as my_column union all +SELECT 1 as dummy, 'text1'::varchar(1000) as my_column +) +SELECT string_agg(DISTINCT my_column,', ') as my_string_agg +FROM my_data +GROUP BY dummy +---- +text1 + # Tests for aggregating with NaN values statement ok CREATE TABLE float_table ( @@ -6716,7 +6859,7 @@ group1 0.0003 # median with all nulls statement ok create table group_median_all_nulls( - a STRING NOT NULL, + a STRING NOT NULL, b INT ) AS VALUES ( 'group0', NULL), @@ -6732,6 +6875,28 @@ SELECT a, median(b), arrow_typeof(median(b)) FROM group_median_all_nulls GROUP B group0 NULL Int32 group1 NULL Int32 +query I +with test AS (SELECT i as c1, i + 1 as c2 FROM generate_series(1, 10) t(i)) +select count(*) from test WHERE 1 = 1; +---- +10 + +query I +with test AS (SELECT i as c1, i + 1 as c2 FROM generate_series(1, 10) t(i)) +select count(c1) from test WHERE 1 = 1; +---- +10 + +query II rowsort +with test AS (SELECT i as c1, i + 1 as c2 FROM generate_series(1, 5) t(i)) +select c2, count(*) from test WHERE 1 = 1 group by c2; +---- +2 1 +3 1 +4 1 +5 1 +6 1 + statement ok create table t_decimal (c decimal(10, 4)) as values (100.00), (125.00), (175.00), (200.00), (200.00), (300.00), (null), (null); diff --git a/datafusion/sqllogictest/test_files/array.slt b/datafusion/sqllogictest/test_files/array.slt index cb56686b64373..9772de3db3657 100644 --- a/datafusion/sqllogictest/test_files/array.slt +++ b/datafusion/sqllogictest/test_files/array.slt @@ -2396,6 +2396,11 @@ NULL NULL NULL NULL NULL NULL +query ? +select array_sort([struct('foo', 3), struct('foo', 1), struct('bar', 1)]) +---- +[{c0: bar, c1: 1}, {c0: foo, c1: 1}, {c0: foo, c1: 3}] + ## test with argument of incorrect types query error DataFusion error: Execution error: the second parameter of array_sort expects DESC or ASC select array_sort([1, 3, null, 5, NULL, -5], 1), array_sort([1, 3, null, 5, NULL, -5], 'DESC', 1), array_sort([1, 3, null, 5, NULL, -5], 1, 1); @@ -5987,7 +5992,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6016,7 +6021,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6045,7 +6050,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6076,7 +6081,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: array_has(LargeList([7f4b18de3cfeb9b4ac78c381ee2ad278, a, b, c]), substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32))) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6105,7 +6110,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6125,7 +6130,8 @@ select count(*) from test WHERE array_has([needle], needle); ---- 100000 -# TODO: this should probably be possible to completely remove the filter as always true? +# The optimizer does not currently eliminate the filter; +# Instead, it's rewritten as `IS NULL OR NOT NULL` due to SQL null semantics query TT explain with test AS (SELECT substr(md5(i::text)::text, 1, 32) as needle FROM generate_series(1, 100000) t(i)) select count(*) from test WHERE array_has([needle], needle); @@ -6135,10 +6141,9 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: -06)----------Filter: __common_expr_3 = __common_expr_3 -07)------------Projection: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) AS __common_expr_3 -08)--------------TableScan: tmp_table projection=[value] +05)--------Projection: +06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IS NOT NULL OR Boolean(NULL) +07)------------TableScan: tmp_table projection=[value] physical_plan 01)ProjectionExec: expr=[count(Int64(1))@0 as count(*)] 02)--AggregateExec: mode=Final, gby=[], aggr=[count(Int64(1))] @@ -6146,10 +6151,9 @@ physical_plan 04)------AggregateExec: mode=Partial, gby=[], aggr=[count(Int64(1))] 05)--------ProjectionExec: expr=[] 06)----------CoalesceBatchesExec: target_batch_size=8192 -07)------------FilterExec: __common_expr_3@0 = __common_expr_3@0 -08)--------------ProjectionExec: expr=[substr(md5(CAST(value@0 AS Utf8)), 1, 32) as __common_expr_3] -09)----------------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 -10)------------------LazyMemoryExec: partitions=1, batch_generators=[generate_series: start=1, end=100000, batch_size=8192] +07)------------FilterExec: substr(md5(CAST(value@0 AS Utf8)), 1, 32) IS NOT NULL OR NULL +08)--------------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 +09)----------------LazyMemoryExec: partitions=1, batch_generators=[generate_series: start=1, end=100000, batch_size=8192] # any operator query ? @@ -7281,12 +7285,10 @@ select array_concat(column1, [7]) from arrays_values_v2; # flatten -#TODO: https://github.com/apache/datafusion/issues/7142 -# follow DuckDB -#query ? -#select flatten(NULL); -#---- -#NULL +query ? +select flatten(NULL); +---- +NULL # flatten with scalar values #1 query ??? @@ -7294,21 +7296,21 @@ select flatten(make_array(1, 2, 1, 3, 2)), flatten(make_array([1], [2, 3], [null], make_array(4, null, 5))), flatten(make_array([[1.1]], [[2.2]], [[3.3], [4.4]])); ---- -[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [1.1, 2.2, 3.3, 4.4] +[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [[1.1], [2.2], [3.3], [4.4]] query ??? select flatten(arrow_cast(make_array(1, 2, 1, 3, 2), 'LargeList(Int64)')), flatten(arrow_cast(make_array([1], [2, 3], [null], make_array(4, null, 5)), 'LargeList(LargeList(Int64))')), flatten(arrow_cast(make_array([[1.1]], [[2.2]], [[3.3], [4.4]]), 'LargeList(LargeList(LargeList(Float64)))')); ---- -[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [1.1, 2.2, 3.3, 4.4] +[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [[1.1], [2.2], [3.3], [4.4]] query ??? select flatten(arrow_cast(make_array(1, 2, 1, 3, 2), 'FixedSizeList(5, Int64)')), flatten(arrow_cast(make_array([1], [2, 3], [null], make_array(4, null, 5)), 'FixedSizeList(4, List(Int64))')), flatten(arrow_cast(make_array([[1.1], [2.2]], [[3.3], [4.4]]), 'FixedSizeList(2, List(List(Float64)))')); ---- -[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [1.1, 2.2, 3.3, 4.4] +[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [[1.1], [2.2], [3.3], [4.4]] # flatten with column values query ???? @@ -7318,8 +7320,8 @@ select flatten(column1), flatten(column4) from flatten_table; ---- -[1, 2, 3] [1, 2, 3, 4, 5, 6] [1, 2, 3] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] -[1, 2, 3, 4, 5, 6] [8] [1, 2, 3] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +[1, 2, 3] [[1, 2, 3], [4, 5], [6]] [[[1]], [[2, 3]]] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] +[1, 2, 3, 4, 5, 6] [[8]] [[[1, 2]], [[3]]] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] query ???? select flatten(column1), @@ -7328,8 +7330,8 @@ select flatten(column1), flatten(column4) from large_flatten_table; ---- -[1, 2, 3] [1, 2, 3, 4, 5, 6] [1, 2, 3] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] -[1, 2, 3, 4, 5, 6] [8] [1, 2, 3] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +[1, 2, 3] [[1, 2, 3], [4, 5], [6]] [[[1]], [[2, 3]]] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] +[1, 2, 3, 4, 5, 6] [[8]] [[[1, 2]], [[3]]] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] query ???? select flatten(column1), @@ -7338,8 +7340,19 @@ select flatten(column1), flatten(column4) from fixed_size_flatten_table; ---- -[1, 2, 3] [1, 2, 3, 4, 5, 6] [1, 2, 3] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] -[1, 2, 3, 4, 5, 6] [8, 9, 10, 11, 12, 13] [1, 2, 3] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +[1, 2, 3] [[1, 2, 3], [4, 5], [6]] [[[1]], [[2, 3]]] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] +[1, 2, 3, 4, 5, 6] [[8], [9, 10], [11, 12, 13]] [[[1, 2]], [[3]]] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + +# flatten with different inner list type +query ?????? +select flatten(arrow_cast(make_array([1, 2], [3, 4]), 'List(FixedSizeList(2, Int64))')), + flatten(arrow_cast(make_array([[1, 2]], [[3, 4]]), 'List(FixedSizeList(1, List(Int64)))')), + flatten(arrow_cast(make_array([1, 2], [3, 4]), 'LargeList(List(Int64))')), + flatten(arrow_cast(make_array([[1, 2]], [[3, 4]]), 'LargeList(List(List(Int64)))')), + flatten(arrow_cast(make_array([1, 2], [3, 4]), 'LargeList(FixedSizeList(2, Int64))')), + flatten(arrow_cast(make_array([[1, 2]], [[3, 4]]), 'LargeList(FixedSizeList(1, List(Int64)))')) +---- +[1, 2, 3, 4] [[1, 2], [3, 4]] [1, 2, 3, 4] [[1, 2], [3, 4]] [1, 2, 3, 4] [[1, 2], [3, 4]] ## empty (aliases: `array_empty`, `list_empty`) # empty scalar function #1 diff --git a/datafusion/sqllogictest/test_files/binary.slt b/datafusion/sqllogictest/test_files/binary.slt index 5c5f9d510e554..5ac13779acd74 100644 --- a/datafusion/sqllogictest/test_files/binary.slt +++ b/datafusion/sqllogictest/test_files/binary.slt @@ -147,8 +147,36 @@ query error DataFusion error: Error during planning: Cannot infer common argumen SELECT column1, column1 = arrow_cast(X'0102', 'FixedSizeBinary(2)') FROM t # Comparison to different sized Binary -query error DataFusion error: Error during planning: Cannot infer common argument type for comparison operation FixedSizeBinary\(3\) = Binary +query ?B SELECT column1, column1 = X'0102' FROM t +---- +000102 false +003102 false +NULL NULL +ff0102 false +000102 false + +query ?B +SELECT column1, column1 = X'000102' FROM t +---- +000102 true +003102 false +NULL NULL +ff0102 false +000102 true + +# Plan should not have a cast of the column (should have casted the literal +# to FixedSizeBinary as that is much faster) + +query TT +explain SELECT column1, column1 = X'000102' FROM t +---- +logical_plan +01)Projection: t.column1, t.column1 = FixedSizeBinary(3, "0,1,2") AS t.column1 = Binary("0,1,2") +02)--TableScan: t projection=[column1] +physical_plan +01)ProjectionExec: expr=[column1@0 as column1, column1@0 = 000102 as t.column1 = Binary("0,1,2")] +02)--DataSourceExec: partitions=1, partition_sizes=[1] statement ok drop table t_source diff --git a/datafusion/sqllogictest/test_files/clickbench.slt b/datafusion/sqllogictest/test_files/clickbench.slt index dfcd924758574..4c60a4365ee26 100644 --- a/datafusion/sqllogictest/test_files/clickbench.slt +++ b/datafusion/sqllogictest/test_files/clickbench.slt @@ -64,10 +64,10 @@ SELECT COUNT(DISTINCT "SearchPhrase") FROM hits; ---- 1 -query DD -SELECT MIN("EventDate"::INT::DATE), MAX("EventDate"::INT::DATE) FROM hits; +query II +SELECT MIN("EventDate"), MAX("EventDate") FROM hits; ---- -2013-07-15 2013-07-15 +15901 15901 query II SELECT "AdvEngineID", COUNT(*) FROM hits WHERE "AdvEngineID" <> 0 GROUP BY "AdvEngineID" ORDER BY COUNT(*) DESC; @@ -168,11 +168,11 @@ SELECT "SearchPhrase", MIN("URL"), MIN("Title"), COUNT(*) AS c, COUNT(DISTINCT " ---- query IITIIIIIIIIIITTIIIIIIIIIITIIITIIIITTIIITIIIIIIIIIITIIIIITIIIIIITIIIIIIIIIITTTTIIIIIIIITITTITTTTTTTTTTIIII -SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; +SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY "EventTime" LIMIT 10; ---- query T -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime" LIMIT 10; ---- query T @@ -180,7 +180,7 @@ SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhras ---- query T -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime"), "SearchPhrase" LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime", "SearchPhrase" LIMIT 10; ---- query IRI @@ -247,31 +247,31 @@ SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c 1615432634 1615432633 1615432632 1615432631 1 query TI -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; ---- query TI -SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; +SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; ---- query TI -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; ---- query IIITTI -SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; ---- -query IDI -SELECT "URLHash", "EventDate"::INT::DATE, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate"::INT::DATE ORDER BY PageViews DESC LIMIT 10 OFFSET 100; +query III +SELECT "URLHash", "EventDate", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate" ORDER BY PageViews DESC LIMIT 10 OFFSET 100; ---- query III -SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; +SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; ---- query PI -SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-14' AND "EventDate"::INT::DATE <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; +SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-14' AND "EventDate" <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; ---- # Clickbench "Extended" queries that test count distinct diff --git a/datafusion/sqllogictest/test_files/copy.slt b/datafusion/sqllogictest/test_files/copy.slt index 925f96bd4ac0c..5eeb05e814ace 100644 --- a/datafusion/sqllogictest/test_files/copy.slt +++ b/datafusion/sqllogictest/test_files/copy.slt @@ -637,7 +637,7 @@ query error DataFusion error: SQL error: ParserError\("Expected: \), found: EOF" COPY (select col2, sum(col1) from source_table # Copy from table with non literal -query error DataFusion error: SQL error: ParserError\("Unexpected token \("\) +query error DataFusion error: SQL error: ParserError\("Expected: end of statement or ;, found: \( at Line: 1, Column: 44"\) COPY source_table to '/tmp/table.parquet' (row_group_size 55 + 102); # Copy using execution.keep_partition_by_columns with an invalid value diff --git a/datafusion/sqllogictest/test_files/create_external_table.slt b/datafusion/sqllogictest/test_files/create_external_table.slt index bb66aef2514c9..03cb5edb5fcce 100644 --- a/datafusion/sqllogictest/test_files/create_external_table.slt +++ b/datafusion/sqllogictest/test_files/create_external_table.slt @@ -77,7 +77,7 @@ statement error DataFusion error: SQL error: ParserError\("Expected: HEADER, fou CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV WITH LOCATION 'foo.csv'; # Unrecognized random clause -statement error DataFusion error: SQL error: ParserError\("Unexpected token FOOBAR"\) +statement error DataFusion error: SQL error: ParserError\("Expected: end of statement or ;, found: FOOBAR at Line: 1, Column: 47"\) CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV FOOBAR BARBAR BARFOO LOCATION 'foo.csv'; # Missing partition column diff --git a/datafusion/sqllogictest/test_files/cte.slt b/datafusion/sqllogictest/test_files/cte.slt index e019af9775a42..32320a06f4fb0 100644 --- a/datafusion/sqllogictest/test_files/cte.slt +++ b/datafusion/sqllogictest/test_files/cte.slt @@ -722,7 +722,7 @@ logical_plan 03)----Projection: Int64(1) AS val 04)------EmptyRelation 05)----Projection: Int64(2) AS val -06)------Cross Join: +06)------Cross Join: 07)--------Filter: recursive_cte.val < Int64(2) 08)----------TableScan: recursive_cte 09)--------SubqueryAlias: sub_cte diff --git a/datafusion/sqllogictest/test_files/dates.slt b/datafusion/sqllogictest/test_files/dates.slt index 4425eee333735..148f0dfe64bb7 100644 --- a/datafusion/sqllogictest/test_files/dates.slt +++ b/datafusion/sqllogictest/test_files/dates.slt @@ -183,7 +183,7 @@ query error input contains invalid characters SELECT to_date('2020-09-08 12/00/00+00:00', '%c', '%+') # to_date with broken formatting -query error bad or unsupported format string +query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input SELECT to_date('2020-09-08 12/00/00+00:00', '%q') statement ok diff --git a/datafusion/sqllogictest/test_files/dictionary.slt b/datafusion/sqllogictest/test_files/dictionary.slt index 778b3537d1bff..d241e61f33ffd 100644 --- a/datafusion/sqllogictest/test_files/dictionary.slt +++ b/datafusion/sqllogictest/test_files/dictionary.slt @@ -450,3 +450,10 @@ query I select dense_rank() over (order by arrow_cast('abc', 'Dictionary(UInt16, Utf8)')); ---- 1 + +# Test dictionary encoded column to partition column casting +statement ok +CREATE TABLE test0 AS VALUES ('foo',1), ('bar',2), ('foo',3); + +statement ok +COPY (SELECT arrow_cast(column1, 'Dictionary(Int32, Utf8)') AS column1, column2 FROM test0) TO 'test_files/scratch/copy/part_dict_test' STORED AS PARQUET PARTITIONED BY (column1); diff --git a/datafusion/sqllogictest/test_files/explain.slt b/datafusion/sqllogictest/test_files/explain.slt index deff793e51106..ba2596551f1d5 100644 --- a/datafusion/sqllogictest/test_files/explain.slt +++ b/datafusion/sqllogictest/test_files/explain.slt @@ -237,6 +237,7 @@ physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after coalesce_batches SAME TEXT AS ABOVE physical_plan after OutputRequirements DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/core/tests/data/example.csv]]}, projection=[a, b, c], file_type=csv, has_header=true physical_plan after LimitAggregation SAME TEXT AS ABOVE +physical_plan after PushdownFilter SAME TEXT AS ABOVE physical_plan after LimitPushdown SAME TEXT AS ABOVE physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after SanityCheckPlan SAME TEXT AS ABOVE @@ -313,6 +314,7 @@ physical_plan after OutputRequirements 01)GlobalLimitExec: skip=0, fetch=10, statistics=[Rows=Exact(8), Bytes=Exact(671), [(Col[0]:),(Col[1]:),(Col[2]:),(Col[3]:),(Col[4]:),(Col[5]:),(Col[6]:),(Col[7]:),(Col[8]:),(Col[9]:),(Col[10]:)]] 02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet, statistics=[Rows=Exact(8), Bytes=Exact(671), [(Col[0]:),(Col[1]:),(Col[2]:),(Col[3]:),(Col[4]:),(Col[5]:),(Col[6]:),(Col[7]:),(Col[8]:),(Col[9]:),(Col[10]:)]] physical_plan after LimitAggregation SAME TEXT AS ABOVE +physical_plan after PushdownFilter SAME TEXT AS ABOVE physical_plan after LimitPushdown DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet, statistics=[Rows=Exact(8), Bytes=Exact(671), [(Col[0]:),(Col[1]:),(Col[2]:),(Col[3]:),(Col[4]:),(Col[5]:),(Col[6]:),(Col[7]:),(Col[8]:),(Col[9]:),(Col[10]:)]] physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after SanityCheckPlan SAME TEXT AS ABOVE @@ -353,6 +355,7 @@ physical_plan after OutputRequirements 01)GlobalLimitExec: skip=0, fetch=10 02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet physical_plan after LimitAggregation SAME TEXT AS ABOVE +physical_plan after PushdownFilter SAME TEXT AS ABOVE physical_plan after LimitPushdown DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after SanityCheckPlan SAME TEXT AS ABOVE diff --git a/datafusion/sqllogictest/test_files/explain_tree.slt b/datafusion/sqllogictest/test_files/explain_tree.slt index 7a0e322eb8bcd..15bf615765713 100644 --- a/datafusion/sqllogictest/test_files/explain_tree.slt +++ b/datafusion/sqllogictest/test_files/explain_tree.slt @@ -180,8 +180,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -218,8 +218,8 @@ physical_plan 18)┌─────────────┴─────────────┐ 19)│ RepartitionExec │ 20)│ -------------------- │ -21)│ output_partition_count: │ -22)│ 4 │ +21)│ partition_count(in->out): │ +22)│ 4 -> 4 │ 23)│ │ 24)│ partitioning_scheme: │ 25)│ Hash([string_col@0], 4) │ @@ -236,8 +236,8 @@ physical_plan 36)┌─────────────┴─────────────┐ 37)│ RepartitionExec │ 38)│ -------------------- │ -39)│ output_partition_count: │ -40)│ 1 │ +39)│ partition_count(in->out): │ +40)│ 1 -> 4 │ 41)│ │ 42)│ partitioning_scheme: │ 43)│ RoundRobinBatch(4) │ @@ -311,8 +311,8 @@ physical_plan 19)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 20)│ RepartitionExec ││ RepartitionExec │ 21)│ -------------------- ││ -------------------- │ -22)│ output_partition_count: ││ output_partition_count: │ -23)│ 4 ││ 4 │ +22)│ partition_count(in->out): ││ partition_count(in->out): │ +23)│ 4 -> 4 ││ 4 -> 4 │ 24)│ ││ │ 25)│ partitioning_scheme: ││ partitioning_scheme: │ 26)│ Hash([int_col@0], 4) ││ Hash([int_col@0], 4) │ @@ -320,8 +320,8 @@ physical_plan 28)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 29)│ RepartitionExec ││ RepartitionExec │ 30)│ -------------------- ││ -------------------- │ -31)│ output_partition_count: ││ output_partition_count: │ -32)│ 1 ││ 1 │ +31)│ partition_count(in->out): ││ partition_count(in->out): │ +32)│ 1 -> 4 ││ 1 -> 4 │ 33)│ ││ │ 34)│ partitioning_scheme: ││ partitioning_scheme: │ 35)│ RoundRobinBatch(4) ││ RoundRobinBatch(4) │ @@ -386,8 +386,8 @@ physical_plan 40)-----------------------------┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 41)-----------------------------│ RepartitionExec ││ RepartitionExec │ 42)-----------------------------│ -------------------- ││ -------------------- │ -43)-----------------------------│ output_partition_count: ││ output_partition_count: │ -44)-----------------------------│ 4 ││ 4 │ +43)-----------------------------│ partition_count(in->out): ││ partition_count(in->out): │ +44)-----------------------------│ 4 -> 4 ││ 4 -> 4 │ 45)-----------------------------│ ││ │ 46)-----------------------------│ partitioning_scheme: ││ partitioning_scheme: │ 47)-----------------------------│ Hash([int_col@0], 4) ││ Hash([int_col@0], 4) │ @@ -395,8 +395,8 @@ physical_plan 49)-----------------------------┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 50)-----------------------------│ RepartitionExec ││ RepartitionExec │ 51)-----------------------------│ -------------------- ││ -------------------- │ -52)-----------------------------│ output_partition_count: ││ output_partition_count: │ -53)-----------------------------│ 1 ││ 1 │ +52)-----------------------------│ partition_count(in->out): ││ partition_count(in->out): │ +53)-----------------------------│ 1 -> 4 ││ 1 -> 4 │ 54)-----------------------------│ ││ │ 55)-----------------------------│ partitioning_scheme: ││ partitioning_scheme: │ 56)-----------------------------│ RoundRobinBatch(4) ││ RoundRobinBatch(4) │ @@ -434,8 +434,8 @@ physical_plan 17)┌─────────────┴─────────────┐ 18)│ RepartitionExec │ 19)│ -------------------- │ -20)│ output_partition_count: │ -21)│ 1 │ +20)│ partition_count(in->out): │ +21)│ 1 -> 4 │ 22)│ │ 23)│ partitioning_scheme: │ 24)│ RoundRobinBatch(4) │ @@ -496,8 +496,8 @@ physical_plan 41)┌─────────────┴─────────────┐ 42)│ RepartitionExec │ 43)│ -------------------- │ -44)│ output_partition_count: │ -45)│ 1 │ +44)│ partition_count(in->out): │ +45)│ 1 -> 4 │ 46)│ │ 47)│ partitioning_scheme: │ 48)│ RoundRobinBatch(4) │ @@ -530,8 +530,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -566,8 +566,8 @@ physical_plan 15)┌─────────────┴─────────────┐ 16)│ RepartitionExec │ 17)│ -------------------- │ -18)│ output_partition_count: │ -19)│ 1 │ +18)│ partition_count(in->out): │ +19)│ 1 -> 4 │ 20)│ │ 21)│ partitioning_scheme: │ 22)│ RoundRobinBatch(4) │ @@ -599,8 +599,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -633,8 +633,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -694,8 +694,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -727,8 +727,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -889,7 +889,7 @@ explain SELECT * FROM table1 ORDER BY string_col LIMIT 1; ---- physical_plan 01)┌───────────────────────────┐ -02)│ SortExec │ +02)│ SortExec(TopK) │ 03)│ -------------------- │ 04)│ limit: 1 │ 05)│ │ @@ -922,8 +922,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -1031,8 +1031,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -1089,8 +1089,8 @@ physical_plan 12)┌─────────────┴─────────────┐ 13)│ RepartitionExec │ 14)│ -------------------- │ -15)│ output_partition_count: │ -16)│ 1 │ +15)│ partition_count(in->out): │ +16)│ 1 -> 4 │ 17)│ │ 18)│ partitioning_scheme: │ 19)│ RoundRobinBatch(4) │ @@ -1123,8 +1123,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ output_partition_count: │ -17)│ 1 │ +16)│ partition_count(in->out): │ +17)│ 1 -> 4 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -1209,8 +1209,8 @@ physical_plan 22)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 23)│ RepartitionExec ││ RepartitionExec │ 24)│ -------------------- ││ -------------------- │ -25)│ output_partition_count: ││ output_partition_count: │ -26)│ 4 ││ 4 │ +25)│ partition_count(in->out): ││ partition_count(in->out): │ +26)│ 4 -> 4 ││ 4 -> 4 │ 27)│ ││ │ 28)│ partitioning_scheme: ││ partitioning_scheme: │ 29)│ Hash([int_col@0, CAST ││ Hash([int_col@0, │ @@ -1220,8 +1220,8 @@ physical_plan 33)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 34)│ ProjectionExec ││ RepartitionExec │ 35)│ -------------------- ││ -------------------- │ -36)│ CAST(table1.string_col AS ││ output_partition_count: │ -37)│ Utf8View): ││ 1 │ +36)│ CAST(table1.string_col AS ││ partition_count(in->out): │ +37)│ Utf8View): ││ 1 -> 4 │ 38)│ CAST(string_col AS ││ │ 39)│ Utf8View) ││ partitioning_scheme: │ 40)│ ││ RoundRobinBatch(4) │ @@ -1237,8 +1237,8 @@ physical_plan 50)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 51)│ RepartitionExec ││ DataSourceExec │ 52)│ -------------------- ││ -------------------- │ -53)│ output_partition_count: ││ files: 1 │ -54)│ 1 ││ format: parquet │ +53)│ partition_count(in->out): ││ files: 1 │ +54)│ 1 -> 4 ││ format: parquet │ 55)│ ││ │ 56)│ partitioning_scheme: ││ │ 57)│ RoundRobinBatch(4) ││ │ @@ -1281,8 +1281,8 @@ physical_plan 24)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 25)│ RepartitionExec ││ RepartitionExec │ 26)│ -------------------- ││ -------------------- │ -27)│ output_partition_count: ││ output_partition_count: │ -28)│ 4 ││ 4 │ +27)│ partition_count(in->out): ││ partition_count(in->out): │ +28)│ 4 -> 4 ││ 4 -> 4 │ 29)│ ││ │ 30)│ partitioning_scheme: ││ partitioning_scheme: │ 31)│ Hash([int_col@0, CAST ││ Hash([int_col@0, │ @@ -1292,8 +1292,8 @@ physical_plan 35)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 36)│ ProjectionExec ││ RepartitionExec │ 37)│ -------------------- ││ -------------------- │ -38)│ CAST(table1.string_col AS ││ output_partition_count: │ -39)│ Utf8View): ││ 1 │ +38)│ CAST(table1.string_col AS ││ partition_count(in->out): │ +39)│ Utf8View): ││ 1 -> 4 │ 40)│ CAST(string_col AS ││ │ 41)│ Utf8View) ││ partitioning_scheme: │ 42)│ ││ RoundRobinBatch(4) │ @@ -1309,8 +1309,8 @@ physical_plan 52)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 53)│ RepartitionExec ││ DataSourceExec │ 54)│ -------------------- ││ -------------------- │ -55)│ output_partition_count: ││ files: 1 │ -56)│ 1 ││ format: parquet │ +55)│ partition_count(in->out): ││ files: 1 │ +56)│ 1 -> 4 ││ format: parquet │ 57)│ ││ │ 58)│ partitioning_scheme: ││ │ 59)│ RoundRobinBatch(4) ││ │ @@ -1356,8 +1356,8 @@ physical_plan 27)-----------------------------┌─────────────┴─────────────┐ 28)-----------------------------│ RepartitionExec │ 29)-----------------------------│ -------------------- │ -30)-----------------------------│ output_partition_count: │ -31)-----------------------------│ 1 │ +30)-----------------------------│ partition_count(in->out): │ +31)-----------------------------│ 1 -> 4 │ 32)-----------------------------│ │ 33)-----------------------------│ partitioning_scheme: │ 34)-----------------------------│ RoundRobinBatch(4) │ @@ -1380,8 +1380,8 @@ physical_plan 04)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 05)│ DataSourceExec ││ RepartitionExec │ 06)│ -------------------- ││ -------------------- │ -07)│ files: 1 ││ output_partition_count: │ -08)│ format: csv ││ 1 │ +07)│ files: 1 ││ partition_count(in->out): │ +08)│ format: csv ││ 1 -> 4 │ 09)│ ││ │ 10)│ ││ partitioning_scheme: │ 11)│ ││ RoundRobinBatch(4) │ @@ -1505,8 +1505,8 @@ physical_plan 33)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 34)│ RepartitionExec ││ RepartitionExec │ 35)│ -------------------- ││ -------------------- │ -36)│ output_partition_count: ││ output_partition_count: │ -37)│ 4 ││ 4 │ +36)│ partition_count(in->out): ││ partition_count(in->out): │ +37)│ 4 -> 4 ││ 4 -> 4 │ 38)│ ││ │ 39)│ partitioning_scheme: ││ partitioning_scheme: │ 40)│ Hash([name@0], 4) ││ Hash([name@0], 4) │ @@ -1514,8 +1514,8 @@ physical_plan 42)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 43)│ RepartitionExec ││ RepartitionExec │ 44)│ -------------------- ││ -------------------- │ -45)│ output_partition_count: ││ output_partition_count: │ -46)│ 1 ││ 1 │ +45)│ partition_count(in->out): ││ partition_count(in->out): │ +46)│ 1 -> 4 ││ 1 -> 4 │ 47)│ ││ │ 48)│ partitioning_scheme: ││ partitioning_scheme: │ 49)│ RoundRobinBatch(4) ││ RoundRobinBatch(4) │ @@ -1606,8 +1606,8 @@ physical_plan 18)┌─────────────┴─────────────┐ 19)│ RepartitionExec │ 20)│ -------------------- │ -21)│ output_partition_count: │ -22)│ 1 │ +21)│ partition_count(in->out): │ +22)│ 1 -> 4 │ 23)│ │ 24)│ partitioning_scheme: │ 25)│ RoundRobinBatch(4) │ @@ -1648,8 +1648,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ output_partition_count: │ -23)│ 1 │ +22)│ partition_count(in->out): │ +23)│ 1 -> 4 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ @@ -1689,8 +1689,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ output_partition_count: │ -23)│ 1 │ +22)│ partition_count(in->out): │ +23)│ 1 -> 4 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ @@ -1728,8 +1728,8 @@ physical_plan 17)┌─────────────┴─────────────┐ 18)│ RepartitionExec │ 19)│ -------------------- │ -20)│ output_partition_count: │ -21)│ 1 │ +20)│ partition_count(in->out): │ +21)│ 1 -> 4 │ 22)│ │ 23)│ partitioning_scheme: │ 24)│ RoundRobinBatch(4) │ @@ -1771,8 +1771,8 @@ physical_plan 20)┌─────────────┴─────────────┐ 21)│ RepartitionExec │ 22)│ -------------------- │ -23)│ output_partition_count: │ -24)│ 1 │ +23)│ partition_count(in->out): │ +24)│ 1 -> 4 │ 25)│ │ 26)│ partitioning_scheme: │ 27)│ RoundRobinBatch(4) │ @@ -1815,8 +1815,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ output_partition_count: │ -23)│ 1 │ +22)│ partition_count(in->out): │ +23)│ 1 -> 4 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ @@ -1869,8 +1869,8 @@ physical_plan 25)-----------------------------┌─────────────┴─────────────┐ 26)-----------------------------│ RepartitionExec │ 27)-----------------------------│ -------------------- │ -28)-----------------------------│ output_partition_count: │ -29)-----------------------------│ 1 │ +28)-----------------------------│ partition_count(in->out): │ +29)-----------------------------│ 1 -> 4 │ 30)-----------------------------│ │ 31)-----------------------------│ partitioning_scheme: │ 32)-----------------------------│ RoundRobinBatch(4) │ @@ -1983,8 +1983,8 @@ physical_plan 22)┌─────────────┴─────────────┐ 23)│ RepartitionExec │ 24)│ -------------------- │ -25)│ output_partition_count: │ -26)│ 1 │ +25)│ partition_count(in->out): │ +26)│ 1 -> 4 │ 27)│ │ 28)│ partitioning_scheme: │ 29)│ RoundRobinBatch(4) │ @@ -2062,8 +2062,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ output_partition_count: │ -23)│ 1 │ +22)│ partition_count(in->out): │ +23)│ 1 -> 4 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ diff --git a/datafusion/sqllogictest/test_files/expr/date_part.slt b/datafusion/sqllogictest/test_files/expr/date_part.slt index dec796aa59cb5..39c42cbe1e97f 100644 --- a/datafusion/sqllogictest/test_files/expr/date_part.slt +++ b/datafusion/sqllogictest/test_files/expr/date_part.slt @@ -884,7 +884,7 @@ SELECT extract(day from arrow_cast('14400 minutes', 'Interval(DayTime)')) query I SELECT extract(minute from arrow_cast('14400 minutes', 'Interval(DayTime)')) ---- -14400 +0 query I SELECT extract(second from arrow_cast('5.1 seconds', 'Interval(DayTime)')) @@ -894,7 +894,7 @@ SELECT extract(second from arrow_cast('5.1 seconds', 'Interval(DayTime)')) query I SELECT extract(second from arrow_cast('14400 minutes', 'Interval(DayTime)')) ---- -864000 +0 query I SELECT extract(second from arrow_cast('2 months', 'Interval(MonthDayNano)')) @@ -954,7 +954,7 @@ from t order by id; ---- 0 0 5 -1 0 15 +1 0 3 2 0 0 3 2 0 4 0 8 diff --git a/datafusion/sqllogictest/test_files/functions.slt b/datafusion/sqllogictest/test_files/functions.slt index de1dbf74c29bf..20f79622a62c6 100644 --- a/datafusion/sqllogictest/test_files/functions.slt +++ b/datafusion/sqllogictest/test_files/functions.slt @@ -858,7 +858,7 @@ SELECT greatest(-1, 1, 2.3, 123456789, 3 + 5, -(-4), abs(-9.0)) 123456789 -query error 'greatest' does not support zero argument +query error Function 'greatest' user-defined coercion failed with "Error during planning: greatest was called without any arguments. It requires at least 1." SELECT greatest() query I @@ -1056,7 +1056,7 @@ SELECT least(-1, 1, 2.3, 123456789, 3 + 5, -(-4), abs(-9.0)) -1 -query error 'least' does not support zero arguments +query error Function 'least' user-defined coercion failed with "Error during planning: least was called without any arguments. It requires at least 1." SELECT least() query I diff --git a/datafusion/sqllogictest/test_files/group_by.slt b/datafusion/sqllogictest/test_files/group_by.slt index 4c4999a364d12..9e67018ecd0b9 100644 --- a/datafusion/sqllogictest/test_files/group_by.slt +++ b/datafusion/sqllogictest/test_files/group_by.slt @@ -2232,7 +2232,7 @@ physical_plan 03)----StreamingTableExec: partition_sizes=1, projection=[a, b, c], infinite_source=true, output_ordering=[a@0 ASC NULLS LAST, b@1 ASC NULLS LAST, c@2 ASC NULLS LAST] query III -SELECT a, b, LAST_VALUE(c) as last_c +SELECT a, b, LAST_VALUE(c order by c) as last_c FROM annotated_data_infinite2 GROUP BY a, b ---- @@ -2706,6 +2706,29 @@ select k, first_value(val order by o) respect NULLS from first_null group by k; 1 1 +statement ok +CREATE TABLE last_null ( + k INT, + val INT, + o int + ) as VALUES + (0, NULL, 9), + (0, 1, 1), + (1, 1, 1); + +query II rowsort +select k, last_value(val order by o) IGNORE NULLS from last_null group by k; +---- +0 1 +1 1 + +query II rowsort +select k, last_value(val order by o) respect NULLS from last_null group by k; +---- +0 NULL +1 1 + + query TT EXPLAIN SELECT country, ARRAY_AGG(amount ORDER BY amount DESC) AS amounts, FIRST_VALUE(amount ORDER BY amount ASC) AS fv1, @@ -3775,7 +3798,7 @@ ORDER BY x; 2 2 query II -SELECT y, LAST_VALUE(x) +SELECT y, LAST_VALUE(x order by x desc) FROM FOO GROUP BY y ORDER BY y; diff --git a/datafusion/sqllogictest/test_files/information_schema.slt b/datafusion/sqllogictest/test_files/information_schema.slt index 496f24abf6ed7..87abaadb516f3 100644 --- a/datafusion/sqllogictest/test_files/information_schema.slt +++ b/datafusion/sqllogictest/test_files/information_schema.slt @@ -149,6 +149,39 @@ drop table t statement ok drop table t2 + +############ +## 0 to represent the default value (target_partitions and planning_concurrency) +########### + +statement ok +SET datafusion.execution.target_partitions = 3; + +statement ok +SET datafusion.execution.planning_concurrency = 3; + +# when setting target_partitions and planning_concurrency to 3, their values will be 3 +query TB rowsort +SELECT name, value = 3 FROM information_schema.df_settings WHERE name IN ('datafusion.execution.target_partitions', 'datafusion.execution.planning_concurrency'); +---- +datafusion.execution.planning_concurrency true +datafusion.execution.target_partitions true + +statement ok +SET datafusion.execution.target_partitions = 0; + +statement ok +SET datafusion.execution.planning_concurrency = 0; + +# when setting target_partitions and planning_concurrency to 0, their values will be equal to the +# default values, which are different from 0 (which is invalid) +query TB rowsort +SELECT name, value = 0 FROM information_schema.df_settings WHERE name IN ('datafusion.execution.target_partitions', 'datafusion.execution.planning_concurrency'); +---- +datafusion.execution.planning_concurrency false +datafusion.execution.target_partitions false + + ############ ## SHOW VARIABLES should work ########### @@ -197,6 +230,7 @@ datafusion.execution.parquet.bloom_filter_fpp NULL datafusion.execution.parquet.bloom_filter_ndv NULL datafusion.execution.parquet.bloom_filter_on_read true datafusion.execution.parquet.bloom_filter_on_write false +datafusion.execution.parquet.coerce_int96 NULL datafusion.execution.parquet.column_index_truncate_length 64 datafusion.execution.parquet.compression zstd(3) datafusion.execution.parquet.created_by datafusion @@ -296,6 +330,7 @@ datafusion.execution.parquet.bloom_filter_fpp NULL (writing) Sets bloom filter f datafusion.execution.parquet.bloom_filter_ndv NULL (writing) Sets bloom filter number of distinct values. If NULL, uses default parquet writer setting datafusion.execution.parquet.bloom_filter_on_read true (writing) Use any available bloom filters when reading parquet files datafusion.execution.parquet.bloom_filter_on_write false (writing) Write bloom filters for all columns when creating parquet files +datafusion.execution.parquet.coerce_int96 NULL (reading) If true, parquet reader will read columns of physical type int96 as originating from a different resolution than nanosecond. This is useful for reading data from systems like Spark which stores microsecond resolution timestamps in an int96 allowing it to write values with a larger date range than 64-bit timestamps with nanosecond resolution. datafusion.execution.parquet.column_index_truncate_length 64 (writing) Sets column index truncate length datafusion.execution.parquet.compression zstd(3) (writing) Sets default parquet compression codec. Valid values are: uncompressed, snappy, gzip(level), lzo, brotli(level), lz4, zstd(level), and lz4_raw. These values are not case sensitive. If NULL, uses default parquet writer setting Note that this default setting is not the same as the default parquet writer setting. datafusion.execution.parquet.created_by datafusion (writing) Sets "created by" property @@ -649,7 +684,7 @@ datafusion public date_trunc datafusion public date_trunc FUNCTION true Timestam datafusion public date_trunc datafusion public date_trunc FUNCTION true Timestamp(Second, None) SCALAR Truncates a timestamp value to a specified precision. date_trunc(precision, expression) datafusion public date_trunc datafusion public date_trunc FUNCTION true Timestamp(Second, Some("+TZ")) SCALAR Truncates a timestamp value to a specified precision. date_trunc(precision, expression) datafusion public rank datafusion public rank FUNCTION true NULL WINDOW Returns the rank of the current row within its partition, allowing gaps between ranks. This function provides a ranking similar to `row_number`, but skips ranks for identical values. rank() -datafusion public string_agg datafusion public string_agg FUNCTION true LargeUtf8 AGGREGATE Concatenates the values of string expressions and places separator values between them. string_agg(expression, delimiter) +datafusion public string_agg datafusion public string_agg FUNCTION true LargeUtf8 AGGREGATE Concatenates the values of string expressions and places separator values between them. If ordering is required, strings are concatenated in the specified order. This aggregation function can only mix DISTINCT and ORDER BY if the ordering expression is exactly the same as the first argument expression. string_agg([DISTINCT] expression, delimiter [ORDER BY expression]) query B select is_deterministic from information_schema.routines where routine_name = 'now'; @@ -717,6 +752,15 @@ datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 1 datafusion public string_agg 1 IN expression LargeUtf8 NULL false 2 datafusion public string_agg 2 IN delimiter Null NULL false 2 datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 2 +datafusion public string_agg 1 IN expression Utf8 NULL false 3 +datafusion public string_agg 2 IN delimiter Utf8 NULL false 3 +datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 3 +datafusion public string_agg 1 IN expression Utf8 NULL false 4 +datafusion public string_agg 2 IN delimiter LargeUtf8 NULL false 4 +datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 4 +datafusion public string_agg 1 IN expression Utf8 NULL false 5 +datafusion public string_agg 2 IN delimiter Null NULL false 5 +datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 5 # test variable length arguments query TTTBI rowsort diff --git a/datafusion/sqllogictest/test_files/joins.slt b/datafusion/sqllogictest/test_files/joins.slt index ca86dbfcc3c16..ddf701ba04efe 100644 --- a/datafusion/sqllogictest/test_files/joins.slt +++ b/datafusion/sqllogictest/test_files/joins.slt @@ -4742,3 +4742,51 @@ drop table person; statement count 0 drop table orders; + +# Create tables for testing compound field access in JOIN conditions +statement ok +CREATE TABLE compound_field_table_t +AS VALUES +({r: 'a', c: 1}), +({r: 'b', c: 2.3}); + +statement ok +CREATE TABLE compound_field_table_u +AS VALUES +({r: 'a', c: 1}), +({r: 'b', c: 2.3}); + +# Test compound field access in JOIN condition with table aliases +query ?? +SELECT * FROM compound_field_table_t tee JOIN compound_field_table_u you ON tee.column1['r'] = you.column1['r'] +---- +{r: a, c: 1.0} {r: a, c: 1.0} +{r: b, c: 2.3} {r: b, c: 2.3} + +# Test compound field access in JOIN condition without table aliases +query ?? +SELECT * FROM compound_field_table_t JOIN compound_field_table_u ON compound_field_table_t.column1['r'] = compound_field_table_u.column1['r'] +---- +{r: a, c: 1.0} {r: a, c: 1.0} +{r: b, c: 2.3} {r: b, c: 2.3} + +# Test compound field access with numeric field access +query ?? +SELECT * FROM compound_field_table_t tee JOIN compound_field_table_u you ON tee.column1['c'] = you.column1['c'] +---- +{r: a, c: 1.0} {r: a, c: 1.0} +{r: b, c: 2.3} {r: b, c: 2.3} + +# Test compound field access with mixed field types +query ?? +SELECT * FROM compound_field_table_t tee JOIN compound_field_table_u you ON tee.column1['r'] = you.column1['r'] AND tee.column1['c'] = you.column1['c'] +---- +{r: a, c: 1.0} {r: a, c: 1.0} +{r: b, c: 2.3} {r: b, c: 2.3} + +# Clean up compound field tables +statement ok +DROP TABLE compound_field_table_t; + +statement ok +DROP TABLE compound_field_table_u; diff --git a/datafusion/sqllogictest/test_files/parquet.slt b/datafusion/sqllogictest/test_files/parquet.slt index 2970b2effb3e9..0823a9218268e 100644 --- a/datafusion/sqllogictest/test_files/parquet.slt +++ b/datafusion/sqllogictest/test_files/parquet.slt @@ -629,3 +629,21 @@ physical_plan statement ok drop table foo + + +statement ok +set datafusion.execution.parquet.coerce_int96 = ms; + +statement ok +CREATE EXTERNAL TABLE int96_from_spark +STORED AS PARQUET +LOCATION '../../parquet-testing/data/int96_from_spark.parquet'; + +# Print schema +query TTT +describe int96_from_spark; +---- +a Timestamp(Millisecond, None) YES + +statement ok +set datafusion.execution.parquet.coerce_int96 = ns; diff --git a/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt b/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt index d325ca423daca..a10243f627209 100644 --- a/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt +++ b/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt @@ -109,7 +109,9 @@ ORDER BY int_col, bigint_col; logical_plan 01)Sort: test_table.int_col ASC NULLS LAST, test_table.bigint_col ASC NULLS LAST 02)--TableScan: test_table projection=[int_col, bigint_col] -physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet]]}, projection=[int_col, bigint_col], output_ordering=[int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet +physical_plan +01)SortPreservingMergeExec: [int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST] +02)--DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[int_col, bigint_col], output_ordering=[int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet # Another planning test, but project on a column with unsupported statistics # We should be able to ignore this and look at only the relevant statistics @@ -123,7 +125,10 @@ logical_plan 02)--Sort: test_table.int_col ASC NULLS LAST, test_table.bigint_col ASC NULLS LAST 03)----Projection: test_table.string_col, test_table.int_col, test_table.bigint_col 04)------TableScan: test_table projection=[int_col, string_col, bigint_col] -physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet]]}, projection=[string_col], file_type=parquet +physical_plan +01)ProjectionExec: expr=[string_col@0 as string_col] +02)--SortPreservingMergeExec: [int_col@1 ASC NULLS LAST, bigint_col@2 ASC NULLS LAST] +03)----DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[string_col, int_col, bigint_col], output_ordering=[int_col@1 ASC NULLS LAST, bigint_col@2 ASC NULLS LAST], file_type=parquet # Clean up & recreate but sort on descending column statement ok @@ -155,7 +160,9 @@ ORDER BY descending_col DESC NULLS LAST, bigint_col ASC NULLS LAST; logical_plan 01)Sort: test_table.descending_col DESC NULLS LAST, test_table.bigint_col ASC NULLS LAST 02)--TableScan: test_table projection=[descending_col, bigint_col] -physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet]]}, projection=[descending_col, bigint_col], output_ordering=[descending_col@0 DESC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet +physical_plan +01)SortPreservingMergeExec: [descending_col@0 DESC NULLS LAST, bigint_col@1 ASC NULLS LAST] +02)--DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[descending_col, bigint_col], output_ordering=[descending_col@0 DESC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet # Clean up & re-create with partition columns in sort order statement ok @@ -189,7 +196,9 @@ ORDER BY partition_col, int_col, bigint_col; logical_plan 01)Sort: test_table.partition_col ASC NULLS LAST, test_table.int_col ASC NULLS LAST, test_table.bigint_col ASC NULLS LAST 02)--TableScan: test_table projection=[int_col, bigint_col, partition_col] -physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet]]}, projection=[int_col, bigint_col, partition_col], output_ordering=[partition_col@2 ASC NULLS LAST, int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet +physical_plan +01)SortPreservingMergeExec: [partition_col@2 ASC NULLS LAST, int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST] +02)--DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[int_col, bigint_col, partition_col], output_ordering=[partition_col@2 ASC NULLS LAST, int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet # Clean up & re-create with overlapping column in sort order # This will test the ability to sort files with overlapping statistics diff --git a/datafusion/sqllogictest/test_files/regexp.slt b/datafusion/sqllogictest/test_files/regexp.slt deleted file mode 100644 index 44ba61e877d97..0000000000000 --- a/datafusion/sqllogictest/test_files/regexp.slt +++ /dev/null @@ -1,898 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -statement ok -CREATE TABLE t (str varchar, pattern varchar, start int, flags varchar) AS VALUES - ('abc', '^(a)', 1, 'i'), - ('ABC', '^(A).*', 1, 'i'), - ('aBc', '(b|d)', 1, 'i'), - ('AbC', '(B|D)', 2, null), - ('aBC', '^(b|c)', 3, null), - ('4000', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 1, null), - ('4010', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 2, null), - ('Düsseldorf','[\p{Letter}-]+', 3, null), - ('Москва', '[\p{L}-]+', 4, null), - ('Köln', '[a-zA-Z]ö[a-zA-Z]{2}', 1, null), - ('إسرائيل', '^\p{Arabic}+$', 2, null); - -# -# regexp_like tests -# - -query B -SELECT regexp_like(str, pattern, flags) FROM t; ----- -true -true -true -false -false -false -true -true -true -true -true - -query B -SELECT str ~ NULL FROM t; ----- -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL - -query B -select str ~ right('foo', NULL) FROM t; ----- -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL - -query B -select right('foo', NULL) !~ str FROM t; ----- -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL - -query B -SELECT regexp_like('foobarbequebaz', ''); ----- -true - -query B -SELECT regexp_like('', ''); ----- -true - -query B -SELECT regexp_like('foobarbequebaz', '(bar)(beque)'); ----- -true - -query B -SELECT regexp_like('fooBarb -eQuebaz', '(bar).*(que)', 'is'); ----- -true - -query B -SELECT regexp_like('foobarbequebaz', '(ba3r)(bequ34e)'); ----- -false - -query B -SELECT regexp_like('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); ----- -true - -query B -SELECT regexp_like('aaa-0', '.*-(\d)'); ----- -true - -query B -SELECT regexp_like('bb-1', '.*-(\d)'); ----- -true - -query B -SELECT regexp_like('aa', '.*-(\d)'); ----- -false - -query B -SELECT regexp_like(NULL, '.*-(\d)'); ----- -NULL - -query B -SELECT regexp_like('aaa-0', NULL); ----- -NULL - -query B -SELECT regexp_like(null, '.*-(\d)'); ----- -NULL - -query error Error during planning: regexp_like\(\) does not support the "global" option -SELECT regexp_like('bb-1', '.*-(\d)', 'g'); - -query error Error during planning: regexp_like\(\) does not support the "global" option -SELECT regexp_like('bb-1', '.*-(\d)', 'g'); - -query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) -SELECT regexp_like('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); - -# look-around is not supported and will just return false -query B -SELECT regexp_like('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); ----- -false - -query B -select regexp_like('aaa-555', '.*-(\d*)'); ----- -true - -# -# regexp_match tests -# - -query ? -SELECT regexp_match(str, pattern, flags) FROM t; ----- -[a] -[A] -[B] -NULL -NULL -NULL -[010] -[Düsseldorf] -[Москва] -[Köln] -[إسرائيل] - -# test string view -statement ok -CREATE TABLE t_stringview AS -SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM t; - -query ? -SELECT regexp_match(str, pattern, flags) FROM t_stringview; ----- -[a] -[A] -[B] -NULL -NULL -NULL -[010] -[Düsseldorf] -[Москва] -[Köln] -[إسرائيل] - -statement ok -DROP TABLE t_stringview; - -query ? -SELECT regexp_match('foobarbequebaz', ''); ----- -[] - -query ? -SELECT regexp_match('', ''); ----- -[] - -query ? -SELECT regexp_match('foobarbequebaz', '(bar)(beque)'); ----- -[bar, beque] - -query ? -SELECT regexp_match('fooBarb -eQuebaz', '(bar).*(que)', 'is'); ----- -[Bar, Que] - -query ? -SELECT regexp_match('foobarbequebaz', '(ba3r)(bequ34e)'); ----- -NULL - -query ? -SELECT regexp_match('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); ----- -[barbeque] - -query ? -SELECT regexp_match('aaa-0', '.*-(\d)'); ----- -[0] - -query ? -SELECT regexp_match('bb-1', '.*-(\d)'); ----- -[1] - -query ? -SELECT regexp_match('aa', '.*-(\d)'); ----- -NULL - -query ? -SELECT regexp_match(NULL, '.*-(\d)'); ----- -NULL - -query ? -SELECT regexp_match('aaa-0', NULL); ----- -NULL - -query ? -SELECT regexp_match(null, '.*-(\d)'); ----- -NULL - -query error Error during planning: regexp_match\(\) does not support the "global" option -SELECT regexp_match('bb-1', '.*-(\d)', 'g'); - -query error Error during planning: regexp_match\(\) does not support the "global" option -SELECT regexp_match('bb-1', '.*-(\d)', 'g'); - -query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) -SELECT regexp_match('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); - -# look-around is not supported and will just return null -query ? -SELECT regexp_match('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); ----- -NULL - -# ported test -query ? -SELECT regexp_match('aaa-555', '.*-(\d*)'); ----- -[555] - -query B -select 'abc' ~ null; ----- -NULL - -query B -select null ~ null; ----- -NULL - -query B -select null ~ 'abc'; ----- -NULL - -query B -select 'abc' ~* null; ----- -NULL - -query B -select null ~* null; ----- -NULL - -query B -select null ~* 'abc'; ----- -NULL - -query B -select 'abc' !~ null; ----- -NULL - -query B -select null !~ null; ----- -NULL - -query B -select null !~ 'abc'; ----- -NULL - -query B -select 'abc' !~* null; ----- -NULL - -query B -select null !~* null; ----- -NULL - -query B -select null !~* 'abc'; ----- -NULL - -# -# regexp_replace tests -# - -query T -SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM t; ----- -Xbc -X -aXc -AbC -aBC -4000 -X -X -X -X -X - -# test string view -statement ok -CREATE TABLE t_stringview AS -SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM t; - -query T -SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM t_stringview; ----- -Xbc -X -aXc -AbC -aBC -4000 -X -X -X -X -X - -statement ok -DROP TABLE t_stringview; - -query T -SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'gi'); ----- -XXX - -query T -SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'i'); ----- -XabcABC - -query T -SELECT regexp_replace('foobarbaz', 'b..', 'X', 'g'); ----- -fooXX - -query T -SELECT regexp_replace('foobarbaz', 'b..', 'X'); ----- -fooXbaz - -query T -SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', 'g'); ----- -fooXarYXazY - -query T -SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', NULL); ----- -NULL - -query T -SELECT regexp_replace('foobarbaz', 'b(..)', NULL, 'g'); ----- -NULL - -query T -SELECT regexp_replace('foobarbaz', NULL, 'X\\1Y', 'g'); ----- -NULL - -query T -SELECT regexp_replace('Thomas', '.[mN]a.', 'M'); ----- -ThM - -query T -SELECT regexp_replace(NULL, 'b(..)', 'X\\1Y', 'g'); ----- -NULL - -query T -SELECT regexp_replace('foobar', 'bar', 'xx', 'gi') ----- -fooxx - -query T -SELECT regexp_replace(arrow_cast('foobar', 'Dictionary(Int32, Utf8)'), 'bar', 'xx', 'gi') ----- -fooxx - -query TTT -select - regexp_replace(col, NULL, 'c'), - regexp_replace(col, 'a', NULL), - regexp_replace(col, 'a', 'c', NULL) -from (values ('a'), ('b')) as tbl(col); ----- -NULL NULL NULL -NULL NULL NULL - -# multiline string -query B -SELECT 'foo\nbar\nbaz' ~ 'bar'; ----- -true - -statement error -Error during planning: Cannot infer common argument type for regex operation List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata -: {} }) ~ List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata: {} }) -select [1,2] ~ [3]; - -query B -SELECT 'foo\nbar\nbaz' LIKE '%bar%'; ----- -true - -query B -SELECT NULL LIKE NULL; ----- -NULL - -query B -SELECT NULL iLIKE NULL; ----- -NULL - -query B -SELECT NULL not LIKE NULL; ----- -NULL - -query B -SELECT NULL not iLIKE NULL; ----- -NULL - -# regexp_count tests - -# regexp_count tests from postgresql -# https://github.com/postgres/postgres/blob/56d23855c864b7384970724f3ad93fb0fc319e51/src/test/regress/sql/strings.sql#L226-L235 - -query I -SELECT regexp_count('123123123123123', '(12)3'); ----- -5 - -query I -SELECT regexp_count('123123123123', '123', 1); ----- -4 - -query I -SELECT regexp_count('123123123123', '123', 3); ----- -3 - -query I -SELECT regexp_count('123123123123', '123', 33); ----- -0 - -query I -SELECT regexp_count('ABCABCABCABC', 'Abc', 1, ''); ----- -0 - -query I -SELECT regexp_count('ABCABCABCABC', 'Abc', 1, 'i'); ----- -4 - -statement error -External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based -SELECT regexp_count('123123123123', '123', 0); - -statement error -External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based -SELECT regexp_count('123123123123', '123', -3); - -statement error -External error: statement failed: DataFusion error: Arrow error: Compute error: regexp_count() does not support global flag -SELECT regexp_count('123123123123', '123', 1, 'g'); - -query I -SELECT regexp_count(str, '\w') from t; ----- -3 -3 -3 -3 -3 -4 -4 -10 -6 -4 -7 - -query I -SELECT regexp_count(str, '\w{2}', start) from t; ----- -1 -1 -1 -1 -0 -2 -1 -4 -1 -2 -3 - -query I -SELECT regexp_count(str, 'ab', 1, 'i') from t; ----- -1 -1 -1 -1 -1 -0 -0 -0 -0 -0 -0 - - -query I -SELECT regexp_count(str, pattern) from t; ----- -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start) from t; ----- -1 -1 -0 -0 -0 -0 -0 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start, flags) from t; ----- -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# test type coercion -query I -SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from t; ----- -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# test string views - -statement ok -CREATE TABLE t_stringview AS -SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(start, 'Int64') as start, arrow_cast(flags, 'Utf8View') as flags FROM t; - -query I -SELECT regexp_count(str, '\w') from t_stringview; ----- -3 -3 -3 -3 -3 -4 -4 -10 -6 -4 -7 - -query I -SELECT regexp_count(str, '\w{2}', start) from t_stringview; ----- -1 -1 -1 -1 -0 -2 -1 -4 -1 -2 -3 - -query I -SELECT regexp_count(str, 'ab', 1, 'i') from t_stringview; ----- -1 -1 -1 -1 -1 -0 -0 -0 -0 -0 -0 - - -query I -SELECT regexp_count(str, pattern) from t_stringview; ----- -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start) from t_stringview; ----- -1 -1 -0 -0 -0 -0 -0 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start, flags) from t_stringview; ----- -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# test type coercion -query I -SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from t_stringview; ----- -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# NULL tests - -query I -SELECT regexp_count(NULL, NULL); ----- -0 - -query I -SELECT regexp_count(NULL, 'a'); ----- -0 - -query I -SELECT regexp_count('a', NULL); ----- -0 - -query I -SELECT regexp_count(NULL, NULL, NULL, NULL); ----- -0 - -statement ok -CREATE TABLE empty_table (str varchar, pattern varchar, start int, flags varchar); - -query I -SELECT regexp_count(str, pattern, start, flags) from empty_table; ----- - -statement ok -INSERT INTO empty_table VALUES ('a', NULL, 1, 'i'), (NULL, 'a', 1, 'i'), (NULL, NULL, 1, 'i'), (NULL, NULL, NULL, 'i'); - -query I -SELECT regexp_count(str, pattern, start, flags) from empty_table; ----- -0 -0 -0 -0 - -statement ok -drop table t; - -statement ok -create or replace table strings as values - ('FooBar'), - ('Foo'), - ('Foo'), - ('Bar'), - ('FooBar'), - ('Bar'), - ('Baz'); - -statement ok -create or replace table dict_table as -select arrow_cast(column1, 'Dictionary(Int32, Utf8)') as column1 -from strings; - -query T -select column1 from dict_table where column1 LIKE '%oo%'; ----- -FooBar -Foo -Foo -FooBar - -query T -select column1 from dict_table where column1 NOT LIKE '%oo%'; ----- -Bar -Bar -Baz - -query T -select column1 from dict_table where column1 ILIKE '%oO%'; ----- -FooBar -Foo -Foo -FooBar - -query T -select column1 from dict_table where column1 NOT ILIKE '%oO%'; ----- -Bar -Bar -Baz - - -# plan should not cast the column, instead it should use the dictionary directly -query TT -explain select column1 from dict_table where column1 LIKE '%oo%'; ----- -logical_plan -01)Filter: dict_table.column1 LIKE Utf8("%oo%") -02)--TableScan: dict_table projection=[column1] -physical_plan -01)CoalesceBatchesExec: target_batch_size=8192 -02)--FilterExec: column1@0 LIKE %oo% -03)----DataSourceExec: partitions=1, partition_sizes=[1] - -# Ensure casting / coercion works for all operators -# (there should be no casts to Utf8) -query TT -explain select - column1 LIKE '%oo%', - column1 NOT LIKE '%oo%', - column1 ILIKE '%oo%', - column1 NOT ILIKE '%oo%' -from dict_table; ----- -logical_plan -01)Projection: dict_table.column1 LIKE Utf8("%oo%"), dict_table.column1 NOT LIKE Utf8("%oo%"), dict_table.column1 ILIKE Utf8("%oo%"), dict_table.column1 NOT ILIKE Utf8("%oo%") -02)--TableScan: dict_table projection=[column1] -physical_plan -01)ProjectionExec: expr=[column1@0 LIKE %oo% as dict_table.column1 LIKE Utf8("%oo%"), column1@0 NOT LIKE %oo% as dict_table.column1 NOT LIKE Utf8("%oo%"), column1@0 ILIKE %oo% as dict_table.column1 ILIKE Utf8("%oo%"), column1@0 NOT ILIKE %oo% as dict_table.column1 NOT ILIKE Utf8("%oo%")] -02)--DataSourceExec: partitions=1, partition_sizes=[1] - -statement ok -drop table strings - -statement ok -drop table dict_table diff --git a/datafusion/sqllogictest/test_files/regexp/README.md b/datafusion/sqllogictest/test_files/regexp/README.md new file mode 100644 index 0000000000000..7e5efc5b5ddf2 --- /dev/null +++ b/datafusion/sqllogictest/test_files/regexp/README.md @@ -0,0 +1,59 @@ + + +# Regexp Test Files + +This directory contains test files for regular expression (regexp) functions in DataFusion. + +## Directory Structure + +``` +regexp/ + - init_data.slt.part // Shared test data for regexp functions + - regexp_like.slt // Tests for regexp_like function + - regexp_count.slt // Tests for regexp_count function + - regexp_match.slt // Tests for regexp_match function + - regexp_replace.slt // Tests for regexp_replace function +``` + +## Tested Functions + +1. `regexp_like`: Check if a string matches a regular expression +2. `regexp_count`: Count occurrences of a pattern in a string +3. `regexp_match`: Extract matching substrings +4. `regexp_replace`: Replace matched substrings + +## Test Data + +Test data is centralized in the `init_data.slt.part` file and imported into each test file using the `include` directive. This approach ensures: + +Consistent test data across different regexp function tests +Easy maintenance of test data +Reduced duplication + +## Test Coverage + +Each test file covers: + +Basic functionality +Case-insensitive matching +Null handling +Start position tests +Capture group handling +Different string types (UTF-8, Unicode) diff --git a/datafusion/sqllogictest/test_files/regexp/init_data.slt.part b/datafusion/sqllogictest/test_files/regexp/init_data.slt.part new file mode 100644 index 0000000000000..ed6fb0e872df9 --- /dev/null +++ b/datafusion/sqllogictest/test_files/regexp/init_data.slt.part @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +statement ok +create table regexp_test_data (str varchar, pattern varchar, start int, flags varchar) as values + (NULL, '^(a)', 1, 'i'), + ('abc', '^(a)', 1, 'i'), + ('ABC', '^(A).*', 1, 'i'), + ('aBc', '(b|d)', 1, 'i'), + ('AbC', '(B|D)', 2, null), + ('aBC', '^(b|c)', 3, null), + ('4000', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 1, null), + ('4010', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 2, null), + ('Düsseldorf','[\p{Letter}-]+', 3, null), + ('Москва', '[\p{L}-]+', 4, null), + ('Köln', '[a-zA-Z]ö[a-zA-Z]{2}', 1, null), + ('إسرائيل', '^\p{Arabic}+$', 2, null); diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_count.slt b/datafusion/sqllogictest/test_files/regexp/regexp_count.slt new file mode 100644 index 0000000000000..f64705429bfa4 --- /dev/null +++ b/datafusion/sqllogictest/test_files/regexp/regexp_count.slt @@ -0,0 +1,344 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Import common test data +include ./init_data.slt.part + +# regexp_count tests from postgresql +# https://github.com/postgres/postgres/blob/56d23855c864b7384970724f3ad93fb0fc319e51/src/test/regress/sql/strings.sql#L226-L235 + +query I +SELECT regexp_count('123123123123123', '(12)3'); +---- +5 + +query I +SELECT regexp_count('123123123123', '123', 1); +---- +4 + +query I +SELECT regexp_count('123123123123', '123', 3); +---- +3 + +query I +SELECT regexp_count('123123123123', '123', 33); +---- +0 + +query I +SELECT regexp_count('ABCABCABCABC', 'Abc', 1, ''); +---- +0 + +query I +SELECT regexp_count('ABCABCABCABC', 'Abc', 1, 'i'); +---- +4 + +statement error +External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based +SELECT regexp_count('123123123123', '123', 0); + +statement error +External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based +SELECT regexp_count('123123123123', '123', -3); + +statement error +External error: statement failed: DataFusion error: Arrow error: Compute error: regexp_count() does not support global flag +SELECT regexp_count('123123123123', '123', 1, 'g'); + +query I +SELECT regexp_count(str, '\w') from regexp_test_data; +---- +0 +3 +3 +3 +3 +3 +4 +4 +10 +6 +4 +7 + +query I +SELECT regexp_count(str, '\w{2}', start) from regexp_test_data; +---- +0 +1 +1 +1 +1 +0 +2 +1 +4 +1 +2 +3 + +query I +SELECT regexp_count(str, 'ab', 1, 'i') from regexp_test_data; +---- +0 +1 +1 +1 +1 +1 +0 +0 +0 +0 +0 +0 + + +query I +SELECT regexp_count(str, pattern) from regexp_test_data; +---- +0 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start) from regexp_test_data; +---- +0 +1 +1 +0 +0 +0 +0 +0 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start, flags) from regexp_test_data; +---- +0 +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# test type coercion +query I +SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from regexp_test_data; +---- +0 +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# test string views + +statement ok +CREATE TABLE t_stringview AS +SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(start, 'Int64') as start, arrow_cast(flags, 'Utf8View') as flags FROM regexp_test_data; + +query I +SELECT regexp_count(str, '\w') from t_stringview; +---- +0 +3 +3 +3 +3 +3 +4 +4 +10 +6 +4 +7 + +query I +SELECT regexp_count(str, '\w{2}', start) from t_stringview; +---- +0 +1 +1 +1 +1 +0 +2 +1 +4 +1 +2 +3 + +query I +SELECT regexp_count(str, 'ab', 1, 'i') from t_stringview; +---- +0 +1 +1 +1 +1 +1 +0 +0 +0 +0 +0 +0 + + +query I +SELECT regexp_count(str, pattern) from t_stringview; +---- +0 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start) from t_stringview; +---- +0 +1 +1 +0 +0 +0 +0 +0 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start, flags) from t_stringview; +---- +0 +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# test type coercion +query I +SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from t_stringview; +---- +0 +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# NULL tests + +query I +SELECT regexp_count(NULL, NULL); +---- +0 + +query I +SELECT regexp_count(NULL, 'a'); +---- +0 + +query I +SELECT regexp_count('a', NULL); +---- +0 + +query I +SELECT regexp_count(NULL, NULL, NULL, NULL); +---- +0 + +statement ok +CREATE TABLE empty_table (str varchar, pattern varchar, start int, flags varchar); + +query I +SELECT regexp_count(str, pattern, start, flags) from empty_table; +---- + +statement ok +INSERT INTO empty_table VALUES ('a', NULL, 1, 'i'), (NULL, 'a', 1, 'i'), (NULL, NULL, 1, 'i'), (NULL, NULL, NULL, 'i'); + +query I +SELECT regexp_count(str, pattern, start, flags) from empty_table; +---- +0 +0 +0 +0 + +statement ok +drop table t_stringview; + +statement ok +drop table empty_table; \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_like.slt b/datafusion/sqllogictest/test_files/regexp/regexp_like.slt new file mode 100644 index 0000000000000..ec48d62499c84 --- /dev/null +++ b/datafusion/sqllogictest/test_files/regexp/regexp_like.slt @@ -0,0 +1,280 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Import common test data +include ./init_data.slt.part + +query B +SELECT regexp_like(str, pattern, flags) FROM regexp_test_data; +---- +NULL +true +true +true +false +false +false +true +true +true +true +true + +query B +SELECT str ~ NULL FROM regexp_test_data; +---- +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL + +query B +select str ~ right('foo', NULL) FROM regexp_test_data; +---- +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL + +query B +select right('foo', NULL) !~ str FROM regexp_test_data; +---- +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL + +query B +SELECT regexp_like('foobarbequebaz', ''); +---- +true + +query B +SELECT regexp_like('', ''); +---- +true + +query B +SELECT regexp_like('foobarbequebaz', '(bar)(beque)'); +---- +true + +query B +SELECT regexp_like('fooBarbeQuebaz', '(bar).*(que)', 'is'); +---- +true + +query B +SELECT regexp_like('foobarbequebaz', '(ba3r)(bequ34e)'); +---- +false + +query B +SELECT regexp_like('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); +---- +true + +query B +SELECT regexp_like('aaa-0', '.*-(\d)'); +---- +true + +query B +SELECT regexp_like('bb-1', '.*-(\d)'); +---- +true + +query B +SELECT regexp_like('aa', '.*-(\d)'); +---- +false + +query B +SELECT regexp_like(NULL, '.*-(\d)'); +---- +NULL + +query B +SELECT regexp_like('aaa-0', NULL); +---- +NULL + +query B +SELECT regexp_like(null, '.*-(\d)'); +---- +NULL + +query error Error during planning: regexp_like\(\) does not support the "global" option +SELECT regexp_like('bb-1', '.*-(\d)', 'g'); + +query error Error during planning: regexp_like\(\) does not support the "global" option +SELECT regexp_like('bb-1', '.*-(\d)', 'g'); + +query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) +SELECT regexp_like('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); + +# look-around is not supported and will just return false +query B +SELECT regexp_like('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); +---- +false + +query B +select regexp_like('aaa-555', '.*-(\d*)'); +---- +true + +# multiline string +query B +SELECT 'foo\nbar\nbaz' ~ 'bar'; +---- +true + +statement error +Error during planning: Cannot infer common argument type for regex operation List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata +: {} }) ~ List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata: {} }) +select [1,2] ~ [3]; + +query B +SELECT 'foo\nbar\nbaz' LIKE '%bar%'; +---- +true + +query B +SELECT NULL LIKE NULL; +---- +NULL + +query B +SELECT NULL iLIKE NULL; +---- +NULL + +query B +SELECT NULL not LIKE NULL; +---- +NULL + +query B +SELECT NULL not iLIKE NULL; +---- +NULL + +statement ok +create or replace table strings as values + ('FooBar'), + ('Foo'), + ('Foo'), + ('Bar'), + ('FooBar'), + ('Bar'), + ('Baz'); + +statement ok +create or replace table dict_table as +select arrow_cast(column1, 'Dictionary(Int32, Utf8)') as column1 +from strings; + +query T +select column1 from dict_table where column1 LIKE '%oo%'; +---- +FooBar +Foo +Foo +FooBar + +query T +select column1 from dict_table where column1 NOT LIKE '%oo%'; +---- +Bar +Bar +Baz + +query T +select column1 from dict_table where column1 ILIKE '%oO%'; +---- +FooBar +Foo +Foo +FooBar + +query T +select column1 from dict_table where column1 NOT ILIKE '%oO%'; +---- +Bar +Bar +Baz + + +# plan should not cast the column, instead it should use the dictionary directly +query TT +explain select column1 from dict_table where column1 LIKE '%oo%'; +---- +logical_plan +01)Filter: dict_table.column1 LIKE Utf8("%oo%") +02)--TableScan: dict_table projection=[column1] +physical_plan +01)CoalesceBatchesExec: target_batch_size=8192 +02)--FilterExec: column1@0 LIKE %oo% +03)----DataSourceExec: partitions=1, partition_sizes=[1] + +# Ensure casting / coercion works for all operators +# (there should be no casts to Utf8) +query TT +explain select + column1 LIKE '%oo%', + column1 NOT LIKE '%oo%', + column1 ILIKE '%oo%', + column1 NOT ILIKE '%oo%' +from dict_table; +---- +logical_plan +01)Projection: dict_table.column1 LIKE Utf8("%oo%"), dict_table.column1 NOT LIKE Utf8("%oo%"), dict_table.column1 ILIKE Utf8("%oo%"), dict_table.column1 NOT ILIKE Utf8("%oo%") +02)--TableScan: dict_table projection=[column1] +physical_plan +01)ProjectionExec: expr=[column1@0 LIKE %oo% as dict_table.column1 LIKE Utf8("%oo%"), column1@0 NOT LIKE %oo% as dict_table.column1 NOT LIKE Utf8("%oo%"), column1@0 ILIKE %oo% as dict_table.column1 ILIKE Utf8("%oo%"), column1@0 NOT ILIKE %oo% as dict_table.column1 NOT ILIKE Utf8("%oo%")] +02)--DataSourceExec: partitions=1, partition_sizes=[1] + +statement ok +drop table strings + +statement ok +drop table dict_table \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_match.slt b/datafusion/sqllogictest/test_files/regexp/regexp_match.slt new file mode 100644 index 0000000000000..4b4cf4f134d8e --- /dev/null +++ b/datafusion/sqllogictest/test_files/regexp/regexp_match.slt @@ -0,0 +1,201 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Import common test data +include ./init_data.slt.part + +query ? +SELECT regexp_match(str, pattern, flags) FROM regexp_test_data; +---- +NULL +[a] +[A] +[B] +NULL +NULL +NULL +[010] +[Düsseldorf] +[Москва] +[Köln] +[إسرائيل] + +# test string view +statement ok +CREATE TABLE t_stringview AS +SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM regexp_test_data; + +query ? +SELECT regexp_match(str, pattern, flags) FROM t_stringview; +---- +NULL +[a] +[A] +[B] +NULL +NULL +NULL +[010] +[Düsseldorf] +[Москва] +[Köln] +[إسرائيل] + +statement ok +DROP TABLE t_stringview; + +query ? +SELECT regexp_match('foobarbequebaz', ''); +---- +[] + +query ? +SELECT regexp_match('', ''); +---- +[] + +query ? +SELECT regexp_match('foobarbequebaz', '(bar)(beque)'); +---- +[bar, beque] + +query ? +SELECT regexp_match('fooBarb +eQuebaz', '(bar).*(que)', 'is'); +---- +[Bar, Que] + +query ? +SELECT regexp_match('foobarbequebaz', '(ba3r)(bequ34e)'); +---- +NULL + +query ? +SELECT regexp_match('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); +---- +[barbeque] + +query ? +SELECT regexp_match('aaa-0', '.*-(\d)'); +---- +[0] + +query ? +SELECT regexp_match('bb-1', '.*-(\d)'); +---- +[1] + +query ? +SELECT regexp_match('aa', '.*-(\d)'); +---- +NULL + +query ? +SELECT regexp_match(NULL, '.*-(\d)'); +---- +NULL + +query ? +SELECT regexp_match('aaa-0', NULL); +---- +NULL + +query ? +SELECT regexp_match(null, '.*-(\d)'); +---- +NULL + +query error Error during planning: regexp_match\(\) does not support the "global" option +SELECT regexp_match('bb-1', '.*-(\d)', 'g'); + +query error Error during planning: regexp_match\(\) does not support the "global" option +SELECT regexp_match('bb-1', '.*-(\d)', 'g'); + +query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) +SELECT regexp_match('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); + +# look-around is not supported and will just return null +query ? +SELECT regexp_match('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); +---- +NULL + +# ported test +query ? +SELECT regexp_match('aaa-555', '.*-(\d*)'); +---- +[555] + +query B +select 'abc' ~ null; +---- +NULL + +query B +select null ~ null; +---- +NULL + +query B +select null ~ 'abc'; +---- +NULL + +query B +select 'abc' ~* null; +---- +NULL + +query B +select null ~* null; +---- +NULL + +query B +select null ~* 'abc'; +---- +NULL + +query B +select 'abc' !~ null; +---- +NULL + +query B +select null !~ null; +---- +NULL + +query B +select null !~ 'abc'; +---- +NULL + +query B +select 'abc' !~* null; +---- +NULL + +query B +select null !~* null; +---- +NULL + +query B +select null !~* 'abc'; +---- +NULL \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_replace.slt b/datafusion/sqllogictest/test_files/regexp/regexp_replace.slt new file mode 100644 index 0000000000000..d54261f02b81a --- /dev/null +++ b/datafusion/sqllogictest/test_files/regexp/regexp_replace.slt @@ -0,0 +1,129 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Import common test data +include ./init_data.slt.part + +query T +SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM regexp_test_data; +---- +NULL +Xbc +X +aXc +AbC +aBC +4000 +X +X +X +X +X + +# test string view +statement ok +CREATE TABLE t_stringview AS +SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM regexp_test_data; + +query T +SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM t_stringview; +---- +NULL +Xbc +X +aXc +AbC +aBC +4000 +X +X +X +X +X + +statement ok +DROP TABLE t_stringview; + +query T +SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'gi'); +---- +XXX + +query T +SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'i'); +---- +XabcABC + +query T +SELECT regexp_replace('foobarbaz', 'b..', 'X', 'g'); +---- +fooXX + +query T +SELECT regexp_replace('foobarbaz', 'b..', 'X'); +---- +fooXbaz + +query T +SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', 'g'); +---- +fooXarYXazY + +query T +SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', NULL); +---- +NULL + +query T +SELECT regexp_replace('foobarbaz', 'b(..)', NULL, 'g'); +---- +NULL + +query T +SELECT regexp_replace('foobarbaz', NULL, 'X\\1Y', 'g'); +---- +NULL + +query T +SELECT regexp_replace('Thomas', '.[mN]a.', 'M'); +---- +ThM + +query T +SELECT regexp_replace(NULL, 'b(..)', 'X\\1Y', 'g'); +---- +NULL + +query T +SELECT regexp_replace('foobar', 'bar', 'xx', 'gi') +---- +fooxx + +query T +SELECT regexp_replace(arrow_cast('foobar', 'Dictionary(Int32, Utf8)'), 'bar', 'xx', 'gi') +---- +fooxx + +query TTT +select + regexp_replace(col, NULL, 'c'), + regexp_replace(col, 'a', NULL), + regexp_replace(col, 'a', 'c', NULL) +from (values ('a'), ('b')) as tbl(col); +---- +NULL NULL NULL +NULL NULL NULL \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/simplify_expr.slt b/datafusion/sqllogictest/test_files/simplify_expr.slt index 43193fb41cfad..075ccafcfd2e0 100644 --- a/datafusion/sqllogictest/test_files/simplify_expr.slt +++ b/datafusion/sqllogictest/test_files/simplify_expr.slt @@ -63,5 +63,47 @@ query T select b from t where b !~ '.*' ---- +query TT +explain select * from t where a = a; +---- +logical_plan +01)Filter: t.a IS NOT NULL OR Boolean(NULL) +02)--TableScan: t projection=[a, b] +physical_plan +01)CoalesceBatchesExec: target_batch_size=8192 +02)--FilterExec: a@0 IS NOT NULL OR NULL +03)----DataSourceExec: partitions=1, partition_sizes=[1] + statement ok drop table t; + +# test decimal precision +query B +SELECT a * 1.000::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); +---- +false + +query B +SELECT 1.000::DECIMAL(4,3) * a > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); +---- +false + +query B +SELECT NULL::DECIMAL(4,3) * a > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); +---- +NULL + +query B +SELECT a * NULL::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); +---- +NULL + +query B +SELECT a / 1.000::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); +---- +false + +query B +SELECT a / NULL::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); +---- +NULL diff --git a/datafusion/sqllogictest/test_files/subquery.slt b/datafusion/sqllogictest/test_files/subquery.slt index aaccaaa43ce49..a0ac15b740d72 100644 --- a/datafusion/sqllogictest/test_files/subquery.slt +++ b/datafusion/sqllogictest/test_files/subquery.slt @@ -921,7 +921,7 @@ query TT explain SELECT t1_id, (SELECT count(*) + 2 as cnt_plus_2 FROM t2 WHERE t2.t2_int = t1.t1_int having count(*) = 0) from t1 ---- logical_plan -01)Projection: t1.t1_id, CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN NULL ELSE __scalar_sq_1.cnt_plus_2 END AS cnt_plus_2 +01)Projection: t1.t1_id, CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN Int64(NULL) ELSE __scalar_sq_1.cnt_plus_2 END AS cnt_plus_2 02)--Left Join: t1.t1_int = __scalar_sq_1.t2_int 03)----TableScan: t1 projection=[t1_id, t1_int] 04)----SubqueryAlias: __scalar_sq_1 @@ -995,7 +995,7 @@ select t1.t1_int from t1 where ( ---- logical_plan 01)Projection: t1.t1_int -02)--Filter: CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN NULL ELSE __scalar_sq_1.cnt_plus_two END = Int64(2) +02)--Filter: CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN Int64(NULL) ELSE __scalar_sq_1.cnt_plus_two END = Int64(2) 03)----Projection: t1.t1_int, __scalar_sq_1.cnt_plus_two, __scalar_sq_1.count(Int64(1)), __scalar_sq_1.__always_true 04)------Left Join: t1.t1_int = __scalar_sq_1.t2_int 05)--------TableScan: t1 projection=[t1_int] @@ -1049,6 +1049,46 @@ false true true +query IT rowsort +SELECT t1_id, (SELECT case when max(t2.t2_id) > 1 then 'a' else 'b' end FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 +---- +11 a +22 b +33 a +44 b + +query IB rowsort +SELECT t1_id, (SELECT max(t2.t2_id) is null FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 +---- +11 false +22 true +33 false +44 true + +query TT +explain SELECT t1_id, (SELECT max(t2.t2_id) is null FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 +---- +logical_plan +01)Projection: t1.t1_id, __scalar_sq_1.__always_true IS NULL OR __scalar_sq_1.__always_true IS NOT NULL AND __scalar_sq_1.max(t2.t2_id) IS NULL AS x +02)--Left Join: t1.t1_int = __scalar_sq_1.t2_int +03)----TableScan: t1 projection=[t1_id, t1_int] +04)----SubqueryAlias: __scalar_sq_1 +05)------Projection: max(t2.t2_id) IS NULL, t2.t2_int, Boolean(true) AS __always_true +06)--------Aggregate: groupBy=[[t2.t2_int]], aggr=[[max(t2.t2_id)]] +07)----------TableScan: t2 projection=[t2_id, t2_int] + +query TT +explain SELECT t1_id, (SELECT max(t2.t2_id) FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 +---- +logical_plan +01)Projection: t1.t1_id, __scalar_sq_1.max(t2.t2_id) AS x +02)--Left Join: t1.t1_int = __scalar_sq_1.t2_int +03)----TableScan: t1 projection=[t1_id, t1_int] +04)----SubqueryAlias: __scalar_sq_1 +05)------Projection: max(t2.t2_id), t2.t2_int +06)--------Aggregate: groupBy=[[t2.t2_int]], aggr=[[max(t2.t2_id)]] +07)----------TableScan: t2 projection=[t2_id, t2_int] + # in_subquery_to_join_with_correlated_outer_filter_disjunction query TT explain select t1.t1_id, diff --git a/datafusion/sqllogictest/test_files/timestamps.slt b/datafusion/sqllogictest/test_files/timestamps.slt index dcbcfbfa439d5..44d0f1f97d4d5 100644 --- a/datafusion/sqllogictest/test_files/timestamps.slt +++ b/datafusion/sqllogictest/test_files/timestamps.slt @@ -416,6 +416,33 @@ SELECT to_timestamp(123456789.123456789) as c1, cast(123456789.123456789 as time ---- 1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 +# to_timestamp Decimal128 inputs + +query PPP +SELECT to_timestamp(arrow_cast(1.1, 'Decimal128(2,1)')) as c1, cast(arrow_cast(1.1, 'Decimal128(2,1)') as timestamp) as c2, arrow_cast(1.1, 'Decimal128(2,1)')::timestamp as c3; +---- +1970-01-01T00:00:01.100 1970-01-01T00:00:01.100 1970-01-01T00:00:01.100 + +query PPP +SELECT to_timestamp(arrow_cast(-1.1, 'Decimal128(2,1)')) as c1, cast(arrow_cast(-1.1, 'Decimal128(2,1)') as timestamp) as c2, arrow_cast(-1.1, 'Decimal128(2,1)')::timestamp as c3; +---- +1969-12-31T23:59:58.900 1969-12-31T23:59:58.900 1969-12-31T23:59:58.900 + +query PPP +SELECT to_timestamp(arrow_cast(0.0, 'Decimal128(2,1)')) as c1, cast(arrow_cast(0.0, 'Decimal128(2,1)') as timestamp) as c2, arrow_cast(0.0, 'Decimal128(2,1)')::timestamp as c3; +---- +1970-01-01T00:00:00 1970-01-01T00:00:00 1970-01-01T00:00:00 + +query PPP +SELECT to_timestamp(arrow_cast(1.23456789, 'Decimal128(9,8)')) as c1, cast(arrow_cast(1.23456789, 'Decimal128(9,8)') as timestamp) as c2, arrow_cast(1.23456789, 'Decimal128(9,8)')::timestamp as c3; +---- +1970-01-01T00:00:01.234567890 1970-01-01T00:00:01.234567890 1970-01-01T00:00:01.234567890 + +query PPP +SELECT to_timestamp(arrow_cast(123456789.123456789, 'Decimal128(18,9)')) as c1, cast(arrow_cast(123456789.123456789, 'Decimal128(18,9)') as timestamp) as c2, arrow_cast(123456789.123456789, 'Decimal128(18,9)')::timestamp as c3; +---- +1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 + # from_unixtime @@ -2241,23 +2268,23 @@ query error input contains invalid characters SELECT to_timestamp_seconds('2020-09-08 12/00/00+00:00', '%c', '%+') # to_timestamp with broken formatting -query error bad or unsupported format string +query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input SELECT to_timestamp('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_nanos with broken formatting -query error bad or unsupported format string +query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input SELECT to_timestamp_nanos('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_millis with broken formatting -query error bad or unsupported format string +query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input SELECT to_timestamp_millis('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_micros with broken formatting -query error bad or unsupported format string +query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input SELECT to_timestamp_micros('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_seconds with broken formatting -query error bad or unsupported format string +query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input SELECT to_timestamp_seconds('2020-09-08 12/00/00+00:00', '%q') # Create string timestamp table with different formats @@ -2815,6 +2842,11 @@ select to_char(arrow_cast(TIMESTAMP '2023-08-03 14:38:50Z', 'Timestamp(Second, N ---- 03-08-2023 14-38-50 +query T +select to_char(arrow_cast('2023-09-04'::date, 'Timestamp(Second, Some("UTC"))'), '%Y-%m-%dT%H:%M:%S%.3f'); +---- +2023-09-04T00:00:00.000 + query T select to_char(arrow_cast(123456, 'Duration(Second)'), 'pretty'); ---- diff --git a/datafusion/sqllogictest/test_files/topk.slt b/datafusion/sqllogictest/test_files/topk.slt index b5ff95c358d8e..ce23fe26528c3 100644 --- a/datafusion/sqllogictest/test_files/topk.slt +++ b/datafusion/sqllogictest/test_files/topk.slt @@ -233,3 +233,165 @@ d 1 -98 y7C453hRWd4E7ImjNDWlpexB8nUqjh y7C453hRWd4E7ImjNDWlpexB8nUqjh e 2 52 xipQ93429ksjNcXPX5326VSg1xJZcW xipQ93429ksjNcXPX5326VSg1xJZcW d 1 -72 wwXqSGKLyBQyPkonlzBNYUJTCo4LRS wwXqSGKLyBQyPkonlzBNYUJTCo4LRS a 1 -5 waIGbOGl1PM6gnzZ4uuZt4E2yDWRHs waIGbOGl1PM6gnzZ4uuZt4E2yDWRHs + +##################################### +## Test TopK with Partially Sorted Inputs +##################################### + + +# Create an external table where data is pre-sorted by (number DESC, letter ASC) only. +statement ok +CREATE EXTERNAL TABLE partial_sorted ( + number INT, + letter VARCHAR, + age INT +) +STORED AS parquet +LOCATION 'test_files/scratch/topk/partial_sorted/1.parquet' +WITH ORDER (number DESC, letter ASC); + +# Insert test data into the external table. +query I +COPY ( + SELECT * + FROM ( + VALUES + (1, 'F', 100), + (1, 'B', 50), + (2, 'C', 70), + (2, 'D', 80), + (3, 'A', 60), + (3, 'E', 90) + ) AS t(number, letter, age) + ORDER BY number DESC, letter ASC +) +TO 'test_files/scratch/topk/partial_sorted/1.parquet'; +---- +6 + +## explain physical_plan only +statement ok +set datafusion.explain.physical_plan_only = true + +## batch size smaller than number of rows in the table and result +statement ok +set datafusion.execution.batch_size = 2 + +# Run a TopK query that orders by all columns. +# Although the table is only guaranteed to be sorted by (number DESC, letter ASC), +# DataFusion should use the common prefix optimization +# and return the correct top 3 rows when ordering by all columns. +query ITI +select number, letter, age from partial_sorted order by number desc, letter asc, age desc limit 3; +---- +3 A 60 +3 E 90 +2 C 70 + +# A more complex example with a projection that includes an expression (see further down for the explained plan) +query IIITI +select + number + 1 as number_plus, + number, + number + 1 as other_number_plus, + letter, + age +from partial_sorted +order by + number_plus desc, + number desc, + other_number_plus desc, + letter asc, + age desc +limit 3; +---- +4 3 4 A 60 +4 3 4 E 90 +3 2 3 C 70 + +# Verify that the physical plan includes the sort prefix. +# The output should display a "sort_prefix" in the SortExec node. +query TT +explain select number, letter, age from partial_sorted order by number desc, letter asc, age desc limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[number@0 DESC, letter@1 ASC NULLS LAST, age@2 DESC], preserve_partitioning=[false], sort_prefix=[number@0 DESC, letter@1 ASC NULLS LAST] +02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + + +# Explain variations of the above query with different orderings, and different sort prefixes. +# The "sort_prefix" in the SortExec node should only be present if the TopK's ordering starts with either (number DESC, letter ASC) or just (number DESC). +query TT +explain select number, letter, age from partial_sorted order by age desc limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[age@2 DESC], preserve_partitioning=[false] +02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + +query TT +explain select number, letter, age from partial_sorted order by number desc, letter desc limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[number@0 DESC, letter@1 DESC], preserve_partitioning=[false], sort_prefix=[number@0 DESC] +02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + +query TT +explain select number, letter, age from partial_sorted order by number asc limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[number@0 ASC NULLS LAST], preserve_partitioning=[false] +02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + +query TT +explain select number, letter, age from partial_sorted order by letter asc, number desc limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[letter@1 ASC NULLS LAST, number@0 DESC], preserve_partitioning=[false] +02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + +# Explicit NULLS ordering cases (reversing the order of the NULLS on the number and letter orderings) +query TT +explain select number, letter, age from partial_sorted order by number desc, letter asc NULLS FIRST limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[number@0 DESC, letter@1 ASC], preserve_partitioning=[false], sort_prefix=[number@0 DESC] +02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + +query TT +explain select number, letter, age from partial_sorted order by number desc NULLS LAST, letter asc limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[number@0 DESC NULLS LAST, letter@1 ASC NULLS LAST], preserve_partitioning=[false] +02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + + +# Verify that the sort prefix is correctly computed on the normalized ordering (removing redundant aliased columns) +query TT +explain select number, letter, age, number as column4, letter as column5 from partial_sorted order by number desc, column4 desc, letter asc, column5 asc, age desc limit 3; +---- +physical_plan +01)SortExec: TopK(fetch=3), expr=[number@0 DESC, column4@3 DESC, letter@1 ASC NULLS LAST, column5@4 ASC NULLS LAST, age@2 DESC], preserve_partitioning=[false], sort_prefix=[number@0 DESC, letter@1 ASC NULLS LAST] +02)--ProjectionExec: expr=[number@0 as number, letter@1 as letter, age@2 as age, number@0 as column4, letter@1 as column5] +03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet + +# Verify that the sort prefix is correctly computed over normalized, order-maintaining projections (number + 1, number, number + 1, age) +query TT +explain select number + 1 as number_plus, number, number + 1 as other_number_plus, age from partial_sorted order by number_plus desc, number desc, other_number_plus desc, age asc limit 3; +---- +physical_plan +01)SortPreservingMergeExec: [number_plus@0 DESC, number@1 DESC, other_number_plus@2 DESC, age@3 ASC NULLS LAST], fetch=3 +02)--SortExec: TopK(fetch=3), expr=[number_plus@0 DESC, number@1 DESC, other_number_plus@2 DESC, age@3 ASC NULLS LAST], preserve_partitioning=[true], sort_prefix=[number_plus@0 DESC, number@1 DESC] +03)----ProjectionExec: expr=[__common_expr_1@0 as number_plus, number@1 as number, __common_expr_1@0 as other_number_plus, age@2 as age] +04)------ProjectionExec: expr=[CAST(number@0 AS Int64) + 1 as __common_expr_1, number@0 as number, age@1 as age] +05)--------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 +06)----------DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, age], output_ordering=[number@0 DESC], file_type=parquet + +# Cleanup +statement ok +DROP TABLE partial_sorted; + +statement ok +set datafusion.explain.physical_plan_only = false + +statement ok +set datafusion.execution.batch_size = 8192 diff --git a/datafusion/sqllogictest/test_files/window.slt b/datafusion/sqllogictest/test_files/window.slt index 76e3751e4b8e4..52cc80eae1c8a 100644 --- a/datafusion/sqllogictest/test_files/window.slt +++ b/datafusion/sqllogictest/test_files/window.slt @@ -2356,7 +2356,7 @@ logical_plan 03)----WindowAggr: windowExpr=[[row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] 04)------TableScan: aggregate_test_100 projection=[c9] physical_plan -01)SortExec: TopK(fetch=5), expr=[rn1@1 ASC NULLS LAST, c9@0 ASC NULLS LAST], preserve_partitioning=[false] +01)SortExec: TopK(fetch=5), expr=[rn1@1 ASC NULLS LAST, c9@0 ASC NULLS LAST], preserve_partitioning=[false], sort_prefix=[rn1@1 ASC NULLS LAST] 02)--ProjectionExec: expr=[c9@0 as c9, row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW@1 as rn1] 03)----BoundedWindowAggExec: wdw=[row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: Ok(Field { name: "row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW", data_type: UInt64, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(UInt64(NULL)), end_bound: CurrentRow, is_causal: false }], mode=[Sorted] 04)------SortExec: expr=[c9@0 DESC], preserve_partitioning=[false] @@ -5537,6 +5537,21 @@ physical_plan 02)--WindowAggExec: wdw=[max(aggregate_test_100_ordered.c5) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING: Ok(Field { name: "max(aggregate_test_100_ordered.c5) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING", data_type: Int32, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Rows, start_bound: Preceding(UInt64(NULL)), end_bound: Following(UInt64(NULL)), is_causal: false }] 03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/testing/data/csv/aggregate_test_100.csv]]}, projection=[c5], file_type=csv, has_header=true +query II rowsort +SELECT + t1.v1, + SUM(t1.v1) OVER w + 1 +FROM + generate_series(1, 5) AS t1(v1) +WINDOW + w AS (ORDER BY t1.v1); +---- +1 2 +2 4 +3 7 +4 11 +5 16 + # Testing Utf8View with window statement ok CREATE TABLE aggregate_test_100_utf8view AS SELECT @@ -5595,3 +5610,35 @@ DROP TABLE aggregate_test_100_utf8view; statement ok DROP TABLE aggregate_test_100 + +# window definitions with aliases +query II rowsort +SELECT + t1.v1, + SUM(t1.v1) OVER W + 1 +FROM + generate_series(1, 5) AS t1(v1) +WINDOW + w AS (ORDER BY t1.v1); +---- +1 2 +2 4 +3 7 +4 11 +5 16 + +# window definitions with aliases +query II rowsort +SELECT + t1.v1, + SUM(t1.v1) OVER w + 1 +FROM + generate_series(1, 5) AS t1(v1) +WINDOW + W AS (ORDER BY t1.v1); +---- +1 2 +2 4 +3 7 +4 11 +5 16 diff --git a/datafusion/substrait/src/logical_plan/consumer.rs b/datafusion/substrait/src/logical_plan/consumer.rs index 61f3379735c7d..1442267d3dbb6 100644 --- a/datafusion/substrait/src/logical_plan/consumer.rs +++ b/datafusion/substrait/src/logical_plan/consumer.rs @@ -1835,8 +1835,7 @@ fn requalify_sides_if_needed( }) }) { // These names have no connection to the original plan, but they'll make the columns - // (mostly) unique. There may be cases where this still causes duplicates, if either left - // or right side itself contains duplicate names with different qualifiers. + // (mostly) unique. Ok(( left.alias(TableReference::bare("left"))?, right.alias(TableReference::bare("right"))?, diff --git a/datafusion/substrait/src/physical_plan/producer.rs b/datafusion/substrait/src/physical_plan/producer.rs index 9ba0e0c964e9e..cb725a7277fd3 100644 --- a/datafusion/substrait/src/physical_plan/producer.rs +++ b/datafusion/substrait/src/physical_plan/producer.rs @@ -61,7 +61,7 @@ pub fn to_substrait_rel( substrait_files.push(FileOrFiles { partition_index: partition_index.try_into().unwrap(), start: 0, - length: file.object_meta.size as u64, + length: file.object_meta.size, path_type: Some(PathType::UriPath( file.object_meta.location.as_ref().to_string(), )), diff --git a/datafusion/substrait/tests/cases/consumer_integration.rs b/datafusion/substrait/tests/cases/consumer_integration.rs index af9d92378298a..bdeeeb585c0cb 100644 --- a/datafusion/substrait/tests/cases/consumer_integration.rs +++ b/datafusion/substrait/tests/cases/consumer_integration.rs @@ -519,6 +519,33 @@ mod tests { Ok(()) } + #[tokio::test] + async fn test_multiple_joins() -> Result<()> { + let plan_str = test_plan_to_string("multiple_joins.json").await?; + assert_eq!( + plan_str, + "Projection: left.count(Int64(1)) AS count_first, left.category, left.count(Int64(1)):1 AS count_second, right.count(Int64(1)) AS count_third\ + \n Left Join: left.id = right.id\ + \n SubqueryAlias: left\ + \n Left Join: left.id = right.id\ + \n SubqueryAlias: left\ + \n Left Join: left.id = right.id\ + \n SubqueryAlias: left\ + \n Aggregate: groupBy=[[id]], aggr=[[count(Int64(1))]]\ + \n Values: (Int64(1)), (Int64(2))\ + \n SubqueryAlias: right\ + \n Aggregate: groupBy=[[id, category]], aggr=[[]]\ + \n Values: (Int64(1), Utf8(\"info\")), (Int64(2), Utf8(\"low\"))\ + \n SubqueryAlias: right\ + \n Aggregate: groupBy=[[id]], aggr=[[count(Int64(1))]]\ + \n Values: (Int64(1)), (Int64(2))\ + \n SubqueryAlias: right\ + \n Aggregate: groupBy=[[id]], aggr=[[count(Int64(1))]]\ + \n Values: (Int64(1)), (Int64(2))" + ); + Ok(()) + } + #[tokio::test] async fn test_select_window_count() -> Result<()> { let plan_str = test_plan_to_string("select_window_count.substrait.json").await?; diff --git a/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs b/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs index f989d05c80dd1..9a85f3e6c4dc4 100644 --- a/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs +++ b/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs @@ -37,6 +37,7 @@ use datafusion::logical_expr::{ }; use datafusion::optimizer::simplify_expressions::expr_simplifier::THRESHOLD_INLINE_INLIST; use datafusion::prelude::*; +use insta::assert_snapshot; use std::hash::Hash; use std::sync::Arc; use substrait::proto::extensions::simple_extension_declaration::MappingType; @@ -188,13 +189,16 @@ async fn simple_select() -> Result<()> { #[tokio::test] async fn wildcard_select() -> Result<()> { - assert_expected_plan_unoptimized( - "SELECT * FROM data", - "Projection: data.a, data.b, data.c, data.d, data.e, data.f\ - \n TableScan: data", - true, - ) - .await + let plan = generate_plan_from_sql("SELECT * FROM data", true, false).await?; + + assert_snapshot!( + plan, + @r#" + Projection: data.a, data.b, data.c, data.d, data.e, data.f + TableScan: data + "# + ); + Ok(()) } #[tokio::test] @@ -299,24 +303,42 @@ async fn aggregate_grouping_sets() -> Result<()> { #[tokio::test] async fn aggregate_grouping_rollup() -> Result<()> { - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT a, c, e, avg(b) FROM data GROUP BY ROLLUP (a, c, e)", - "Projection: data.a, data.c, data.e, avg(data.b)\ - \n Aggregate: groupBy=[[GROUPING SETS ((data.a, data.c, data.e), (data.a, data.c), (data.a), ())]], aggr=[[avg(data.b)]]\ - \n TableScan: data projection=[a, b, c, e]", - true - ).await + true, + true, + ) + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: data.a, data.c, data.e, avg(data.b) + Aggregate: groupBy=[[GROUPING SETS ((data.a, data.c, data.e), (data.a, data.c), (data.a), ())]], aggr=[[avg(data.b)]] + TableScan: data projection=[a, b, c, e] + "# + ); + Ok(()) } #[tokio::test] async fn multilayer_aggregate() -> Result<()> { - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT a, sum(partial_count_b) FROM (SELECT a, count(b) as partial_count_b FROM data GROUP BY a) GROUP BY a", - "Aggregate: groupBy=[[data.a]], aggr=[[sum(count(data.b)) AS sum(partial_count_b)]]\ - \n Aggregate: groupBy=[[data.a]], aggr=[[count(data.b)]]\ - \n TableScan: data projection=[a, b]", - true - ).await + true, + true, + ) + .await?; + + assert_snapshot!( + plan, + @r#" + Aggregate: groupBy=[[data.a]], aggr=[[sum(count(data.b)) AS sum(partial_count_b)]] + Aggregate: groupBy=[[data.a]], aggr=[[count(data.b)]] + TableScan: data projection=[a, b] + "# + ); + Ok(()) } #[tokio::test] @@ -454,13 +476,21 @@ async fn try_cast_decimal_to_string() -> Result<()> { #[tokio::test] async fn aggregate_case() -> Result<()> { - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT sum(CASE WHEN a > 0 THEN 1 ELSE NULL END) FROM data", - "Aggregate: groupBy=[[]], aggr=[[sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE Int64(NULL) END) AS sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE NULL END)]]\ - \n TableScan: data projection=[a]", - true + true, + true, ) - .await + .await?; + + assert_snapshot!( + plan, + @r#" + Aggregate: groupBy=[[]], aggr=[[sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE Int64(NULL) END) AS sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE NULL END)]] + TableScan: data projection=[a] + "# + ); + Ok(()) } #[tokio::test] @@ -493,18 +523,27 @@ async fn roundtrip_inlist_4() -> Result<()> { #[tokio::test] async fn roundtrip_inlist_5() -> Result<()> { // on roundtrip there is an additional projection during TableScan which includes all column of the table, - // using assert_expected_plan here as a workaround - assert_expected_plan( + // using assert_and_generate_plan and assert_snapshot! here as a workaround + let plan = generate_plan_from_sql( "SELECT a, f FROM data WHERE (f IN ('a', 'b', 'c') OR a in (SELECT data2.a FROM data2 WHERE f IN ('b', 'c', 'd')))", + true, + true, + ) + .await?; - "Projection: data.a, data.f\ - \n Filter: data.f = Utf8(\"a\") OR data.f = Utf8(\"b\") OR data.f = Utf8(\"c\") OR data2.mark\ - \n LeftMark Join: data.a = data2.a\ - \n TableScan: data projection=[a, f]\ - \n Projection: data2.a\ - \n Filter: data2.f = Utf8(\"b\") OR data2.f = Utf8(\"c\") OR data2.f = Utf8(\"d\")\ - \n TableScan: data2 projection=[a, f], partial_filters=[data2.f = Utf8(\"b\") OR data2.f = Utf8(\"c\") OR data2.f = Utf8(\"d\")]", - true).await + assert_snapshot!( + plan, + @r#" + Projection: data.a, data.f + Filter: data.f = Utf8("a") OR data.f = Utf8("b") OR data.f = Utf8("c") OR data2.mark + LeftMark Join: data.a = data2.a + TableScan: data projection=[a, f] + Projection: data2.a + Filter: data2.f = Utf8("b") OR data2.f = Utf8("c") OR data2.f = Utf8("d") + TableScan: data2 projection=[a, f], partial_filters=[data2.f = Utf8("b") OR data2.f = Utf8("c") OR data2.f = Utf8("d")] + "# + ); + Ok(()) } #[tokio::test] @@ -535,27 +574,44 @@ async fn roundtrip_non_equi_join() -> Result<()> { #[tokio::test] async fn roundtrip_exists_filter() -> Result<()> { - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT b FROM data d1 WHERE EXISTS (SELECT * FROM data2 d2 WHERE d2.a = d1.a AND d2.e != d1.e)", - "Projection: data.b\ - \n LeftSemi Join: data.a = data2.a Filter: data2.e != CAST(data.e AS Int64)\ - \n TableScan: data projection=[a, b, e]\ - \n TableScan: data2 projection=[a, e]", - false // "d1" vs "data" field qualifier - ).await + false, + true, + ) + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: data.b + LeftSemi Join: data.a = data2.a Filter: data2.e != CAST(data.e AS Int64) + TableScan: data projection=[a, b, e] + TableScan: data2 projection=[a, e] + "# + ); + Ok(()) } #[tokio::test] async fn inner_join() -> Result<()> { - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT data.a FROM data JOIN data2 ON data.a = data2.a", - "Projection: data.a\ - \n Inner Join: data.a = data2.a\ - \n TableScan: data projection=[a]\ - \n TableScan: data2 projection=[a]", + true, true, ) - .await + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: data.a + Inner Join: data.a = data2.a + TableScan: data projection=[a] + TableScan: data2 projection=[a] + "# + ); + Ok(()) } #[tokio::test] @@ -592,17 +648,25 @@ async fn roundtrip_self_implicit_cross_join() -> Result<()> { #[tokio::test] async fn self_join_introduces_aliases() -> Result<()> { - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT d1.b, d2.c FROM data d1 JOIN data d2 ON d1.b = d2.b", - "Projection: left.b, right.c\ - \n Inner Join: left.b = right.b\ - \n SubqueryAlias: left\ - \n TableScan: data projection=[b]\ - \n SubqueryAlias: right\ - \n TableScan: data projection=[b, c]", false, + true, ) - .await + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: left.b, right.c + Inner Join: left.b = right.b + SubqueryAlias: left + TableScan: data projection=[b] + SubqueryAlias: right + TableScan: data projection=[b, c] + "# + ); + Ok(()) } #[tokio::test] @@ -747,12 +811,15 @@ async fn aggregate_wo_projection_consume() -> Result<()> { let proto_plan = read_json("tests/testdata/test_plans/aggregate_no_project.substrait.json"); - assert_expected_plan_substrait( - proto_plan, - "Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]]\ - \n TableScan: data projection=[a]", - ) - .await + let plan = generate_plan_from_substrait(proto_plan).await?; + assert_snapshot!( + plan, + @r#" + Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]] + TableScan: data projection=[a] + "# + ); + Ok(()) } #[tokio::test] @@ -760,12 +827,15 @@ async fn aggregate_wo_projection_group_expression_ref_consume() -> Result<()> { let proto_plan = read_json("tests/testdata/test_plans/aggregate_no_project_group_expression_ref.substrait.json"); - assert_expected_plan_substrait( - proto_plan, - "Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]]\ - \n TableScan: data projection=[a]", - ) - .await + let plan = generate_plan_from_substrait(proto_plan).await?; + assert_snapshot!( + plan, + @r#" + Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]] + TableScan: data projection=[a] + "# + ); + Ok(()) } #[tokio::test] @@ -773,12 +843,15 @@ async fn aggregate_wo_projection_sorted_consume() -> Result<()> { let proto_plan = read_json("tests/testdata/test_plans/aggregate_sorted_no_project.substrait.json"); - assert_expected_plan_substrait( - proto_plan, - "Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) ORDER BY [data.a DESC NULLS FIRST] AS countA]]\ - \n TableScan: data projection=[a]", - ) - .await + let plan = generate_plan_from_substrait(proto_plan).await?; + assert_snapshot!( + plan, + @r#" + Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) ORDER BY [data.a DESC NULLS FIRST] AS countA]] + TableScan: data projection=[a] + "# + ); + Ok(()) } #[tokio::test] @@ -986,19 +1059,67 @@ async fn roundtrip_literal_list() -> Result<()> { #[tokio::test] async fn roundtrip_literal_struct() -> Result<()> { - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT STRUCT(1, true, CAST(NULL AS STRING)) FROM data", - "Projection: Struct({c0:1,c1:true,c2:}) AS struct(Int64(1),Boolean(true),NULL)\ - \n TableScan: data projection=[]", - false, // "Struct(..)" vs "struct(..)" + true, + true, ) - .await + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: Struct({c0:1,c1:true,c2:}) AS struct(Int64(1),Boolean(true),NULL) + TableScan: data projection=[] + "# + ); + Ok(()) +} + +#[tokio::test] +async fn roundtrip_literal_named_struct() -> Result<()> { + let plan = generate_plan_from_sql( + "SELECT STRUCT(1 as int_field, true as boolean_field, CAST(NULL AS STRING) as string_field) FROM data", + true, + true, + ) + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: Struct({int_field:1,boolean_field:true,string_field:}) AS named_struct(Utf8("int_field"),Int64(1),Utf8("boolean_field"),Boolean(true),Utf8("string_field"),NULL) + TableScan: data projection=[] + "# + ); + Ok(()) +} + +#[tokio::test] +async fn roundtrip_literal_renamed_struct() -> Result<()> { + // This test aims to hit a case where the struct column itself has the expected name, but its + // inner field needs to be renamed. + let plan = generate_plan_from_sql( + "SELECT CAST((STRUCT(1)) AS Struct<\"int_field\"Int>) AS 'Struct({c0:1})' FROM data", + true, + true, + ) + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: Struct({int_field:1}) AS Struct({c0:1}) + TableScan: data projection=[] + "# + ); + Ok(()) } #[tokio::test] async fn roundtrip_values() -> Result<()> { // TODO: would be nice to have a struct inside the LargeList, but arrow_cast doesn't support that currently - assert_expected_plan( + let plan = generate_plan_from_sql( "VALUES \ (\ 1, \ @@ -1009,17 +1130,18 @@ async fn roundtrip_values() -> Result<()> { [STRUCT(STRUCT('a' AS string_field) AS struct_field), STRUCT(STRUCT('b' AS string_field) AS struct_field)]\ ), \ (NULL, NULL, NULL, NULL, NULL, NULL)", - "Values: \ - (\ - Int64(1), \ - Utf8(\"a\"), \ - List([[-213.1, , 5.5, 2.0, 1.0], []]), \ - LargeList([1, 2, 3]), \ - Struct({c0:true,int_field:1,c2:}), \ - List([{struct_field: {string_field: a}}, {struct_field: {string_field: b}}])\ - ), \ - (Int64(NULL), Utf8(NULL), List(), LargeList(), Struct({c0:,int_field:,c2:}), List())", - true).await + true, + true, + ) + .await?; + + assert_snapshot!( + plan, + @r#" + Values: (Int64(1), Utf8("a"), List([[-213.1, , 5.5, 2.0, 1.0], []]), LargeList([1, 2, 3]), Struct({c0:true,int_field:1,c2:}), List([{struct_field: {string_field: a}}, {struct_field: {string_field: b}}])), (Int64(NULL), Utf8(NULL), List(), LargeList(), Struct({c0:,int_field:,c2:}), List()) + "# + ); + Ok(()) } #[tokio::test] @@ -1061,14 +1183,22 @@ async fn duplicate_column() -> Result<()> { // only. DataFusion however, is strict about not having duplicate column names appear in the plan. // This test confirms that we generate aliases for columns in the plan which would otherwise have // colliding names. - assert_expected_plan( + let plan = generate_plan_from_sql( "SELECT a + 1 as sum_a, a + 1 as sum_a_2 FROM data", - "Projection: data.a + Int64(1) AS sum_a, data.a + Int64(1) AS data.a + Int64(1)__temp__0 AS sum_a_2\ - \n Projection: data.a + Int64(1)\ - \n TableScan: data projection=[a]", + true, true, ) - .await + .await?; + + assert_snapshot!( + plan, + @r#" + Projection: data.a + Int64(1) AS sum_a, data.a + Int64(1) AS data.a + Int64(1)__temp__0 AS sum_a_2 + Projection: data.a + Int64(1) + TableScan: data projection=[a] + "# + ); + Ok(()) } /// Construct a plan that cast columns. Only those SQL types are supported for now. @@ -1374,30 +1504,32 @@ async fn assert_read_filter_count( Ok(()) } -async fn assert_expected_plan_unoptimized( +async fn generate_plan_from_sql( sql: &str, - expected_plan_str: &str, assert_schema: bool, -) -> Result<()> { + optimized: bool, +) -> Result { let ctx = create_context().await?; - let df = ctx.sql(sql).await?; - let plan = df.into_unoptimized_plan(); - let proto = to_substrait_plan(&plan, &ctx.state())?; - let plan2 = from_substrait_plan(&ctx.state(), &proto).await?; - - println!("{plan}"); - println!("{plan2}"); + let df: DataFrame = ctx.sql(sql).await?; - println!("{proto:?}"); + let plan = if optimized { + df.into_optimized_plan()? + } else { + df.into_unoptimized_plan() + }; + let proto = to_substrait_plan(&plan, &ctx.state())?; + let plan2 = if optimized { + let temp = from_substrait_plan(&ctx.state(), &proto).await?; + ctx.state().optimize(&temp)? + } else { + from_substrait_plan(&ctx.state(), &proto).await? + }; if assert_schema { assert_eq!(plan.schema(), plan2.schema()); } - let plan2str = format!("{plan2}"); - assert_eq!(expected_plan_str, &plan2str); - - Ok(()) + Ok(plan2) } async fn assert_expected_plan( @@ -1412,11 +1544,6 @@ async fn assert_expected_plan( let plan2 = from_substrait_plan(&ctx.state(), &proto).await?; let plan2 = ctx.state().optimize(&plan2)?; - println!("{plan}"); - println!("{plan2}"); - - println!("{proto:?}"); - if assert_schema { assert_eq!(plan.schema(), plan2.schema()); } @@ -1427,20 +1554,14 @@ async fn assert_expected_plan( Ok(()) } -async fn assert_expected_plan_substrait( - substrait_plan: Plan, - expected_plan_str: &str, -) -> Result<()> { +async fn generate_plan_from_substrait(substrait_plan: Plan) -> Result { let ctx = create_context().await?; let plan = from_substrait_plan(&ctx.state(), &substrait_plan).await?; let plan = ctx.state().optimize(&plan)?; - let planstr = format!("{plan}"); - assert_eq!(planstr, expected_plan_str); - - Ok(()) + Ok(plan) } async fn assert_substrait_sql(substrait_plan: Plan, sql: &str) -> Result<()> { @@ -1491,9 +1612,6 @@ async fn test_alias(sql_with_alias: &str, sql_no_alias: &str) -> Result<()> { let proto = to_substrait_plan(&df.into_optimized_plan()?, &ctx.state())?; let plan = from_substrait_plan(&ctx.state(), &proto).await?; - println!("{plan_with_alias}"); - println!("{plan}"); - let plan1str = format!("{plan_with_alias}"); let plan2str = format!("{plan}"); assert_eq!(plan1str, plan2str); @@ -1510,11 +1628,6 @@ async fn roundtrip_logical_plan_with_ctx( let plan2 = from_substrait_plan(&ctx.state(), &proto).await?; let plan2 = ctx.state().optimize(&plan2)?; - println!("{plan}"); - println!("{plan2}"); - - println!("{proto:?}"); - let plan1str = format!("{plan}"); let plan2str = format!("{plan2}"); assert_eq!(plan1str, plan2str); diff --git a/datafusion/substrait/tests/testdata/test_plans/multiple_joins.json b/datafusion/substrait/tests/testdata/test_plans/multiple_joins.json new file mode 100644 index 0000000000000..e88cce648da7c --- /dev/null +++ b/datafusion/substrait/tests/testdata/test_plans/multiple_joins.json @@ -0,0 +1,536 @@ +{ + "extensionUris": [{ + "extensionUriAnchor": 1, + "uri": "/functions_aggregate_generic.yaml" + }, { + "extensionUriAnchor": 2, + "uri": "/functions_comparison.yaml" + }], + "extensions": [{ + "extensionFunction": { + "extensionUriReference": 1, + "functionAnchor": 0, + "name": "count:" + } + }, { + "extensionFunction": { + "extensionUriReference": 2, + "functionAnchor": 1, + "name": "equal:any_any" + } + }], + "relations": [{ + "root": { + "input": { + "project": { + "common": { + "emit": { + "outputMapping": [8, 9, 10, 11] + } + }, + "input": { + "join": { + "common": { + "direct": { + } + }, + "left": { + "join": { + "common": { + "direct": { + } + }, + "left": { + "join": { + "common": { + "direct": { + } + }, + "left": { + "aggregate": { + "common": { + "direct": { + } + }, + "input": { + "read": { + "common": { + "direct": { + } + }, + "baseSchema": { + "names": ["id"], + "struct": { + "types": [{ + "i64": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }], + "typeVariationReference": 0, + "nullability": "NULLABILITY_REQUIRED" + } + }, + "virtualTable": { + "values": [{ + "fields": [{ + "i64": "1", + "nullable": true, + "typeVariationReference": 0 + }] + }, { + "fields": [{ + "i64": "2", + "nullable": true, + "typeVariationReference": 0 + }] + }] + } + } + }, + "groupings": [{ + "groupingExpressions": [{ + "selection": { + "directReference": { + "structField": { + "field": 0 + } + }, + "rootReference": { + } + } + }], + "expressionReferences": [] + }], + "measures": [{ + "measure": { + "functionReference": 0, + "args": [], + "sorts": [], + "phase": "AGGREGATION_PHASE_INITIAL_TO_RESULT", + "outputType": { + "i64": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_REQUIRED" + } + }, + "invocation": "AGGREGATION_INVOCATION_ALL", + "arguments": [], + "options": [] + } + }], + "groupingExpressions": [] + } + }, + "right": { + "aggregate": { + "common": { + "direct": { + } + }, + "input": { + "read": { + "common": { + "direct": { + } + }, + "baseSchema": { + "names": ["id", "category"], + "struct": { + "types": [{ + "i64": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }, { + "string": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }], + "typeVariationReference": 0, + "nullability": "NULLABILITY_REQUIRED" + } + }, + "virtualTable": { + "values": [{ + "fields": [{ + "i64": "1", + "nullable": true, + "typeVariationReference": 0 + }, { + "string": "info", + "nullable": true, + "typeVariationReference": 0 + }] + }, { + "fields": [{ + "i64": "2", + "nullable": true, + "typeVariationReference": 0 + }, { + "string": "low", + "nullable": true, + "typeVariationReference": 0 + }] + }] + } + } + }, + "groupings": [{ + "groupingExpressions": [{ + "selection": { + "directReference": { + "structField": { + "field": 0 + } + }, + "rootReference": { + } + } + }, { + "selection": { + "directReference": { + "structField": { + "field": 1 + } + }, + "rootReference": { + } + } + }], + "expressionReferences": [] + }], + "measures": [], + "groupingExpressions": [] + } + }, + "expression": { + "scalarFunction": { + "functionReference": 1, + "args": [], + "outputType": { + "bool": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }, + "arguments": [{ + "value": { + "selection": { + "directReference": { + "structField": { + "field": 0 + } + }, + "rootReference": { + } + } + } + }, { + "value": { + "selection": { + "directReference": { + "structField": { + "field": 2 + } + }, + "rootReference": { + } + } + } + }], + "options": [] + } + }, + "type": "JOIN_TYPE_LEFT" + } + }, + "right": { + "aggregate": { + "common": { + "direct": { + } + }, + "input": { + "read": { + "common": { + "direct": { + } + }, + "baseSchema": { + "names": ["id"], + "struct": { + "types": [{ + "i64": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }], + "typeVariationReference": 0, + "nullability": "NULLABILITY_REQUIRED" + } + }, + "virtualTable": { + "values": [{ + "fields": [{ + "i64": "1", + "nullable": true, + "typeVariationReference": 0 + }] + }, { + "fields": [{ + "i64": "2", + "nullable": true, + "typeVariationReference": 0 + }] + }] + } + } + }, + "groupings": [{ + "groupingExpressions": [{ + "selection": { + "directReference": { + "structField": { + "field": 0 + } + }, + "rootReference": { + } + } + }], + "expressionReferences": [] + }], + "measures": [{ + "measure": { + "functionReference": 0, + "args": [], + "sorts": [], + "phase": "AGGREGATION_PHASE_INITIAL_TO_RESULT", + "outputType": { + "i64": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_REQUIRED" + } + }, + "invocation": "AGGREGATION_INVOCATION_ALL", + "arguments": [], + "options": [] + } + }], + "groupingExpressions": [] + } + }, + "expression": { + "scalarFunction": { + "functionReference": 1, + "args": [], + "outputType": { + "bool": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }, + "arguments": [{ + "value": { + "selection": { + "directReference": { + "structField": { + "field": 0 + } + }, + "rootReference": { + } + } + } + }, { + "value": { + "selection": { + "directReference": { + "structField": { + "field": 4 + } + }, + "rootReference": { + } + } + } + }], + "options": [] + } + }, + "type": "JOIN_TYPE_LEFT" + } + }, + "right": { + "aggregate": { + "common": { + "direct": { + } + }, + "input": { + "read": { + "common": { + "direct": { + } + }, + "baseSchema": { + "names": ["id"], + "struct": { + "types": [{ + "i64": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }], + "typeVariationReference": 0, + "nullability": "NULLABILITY_REQUIRED" + } + }, + "virtualTable": { + "values": [{ + "fields": [{ + "i64": "1", + "nullable": true, + "typeVariationReference": 0 + }] + }, { + "fields": [{ + "i64": "2", + "nullable": true, + "typeVariationReference": 0 + }] + }] + } + } + }, + "groupings": [{ + "groupingExpressions": [{ + "selection": { + "directReference": { + "structField": { + "field": 0 + } + }, + "rootReference": { + } + } + }], + "expressionReferences": [] + }], + "measures": [{ + "measure": { + "functionReference": 0, + "args": [], + "sorts": [], + "phase": "AGGREGATION_PHASE_INITIAL_TO_RESULT", + "outputType": { + "i64": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_REQUIRED" + } + }, + "invocation": "AGGREGATION_INVOCATION_ALL", + "arguments": [], + "options": [] + } + }], + "groupingExpressions": [] + } + }, + "expression": { + "scalarFunction": { + "functionReference": 1, + "args": [], + "outputType": { + "bool": { + "typeVariationReference": 0, + "nullability": "NULLABILITY_NULLABLE" + } + }, + "arguments": [{ + "value": { + "selection": { + "directReference": { + "structField": { + "field": 0 + } + }, + "rootReference": { + } + } + } + }, { + "value": { + "selection": { + "directReference": { + "structField": { + "field": 6 + } + }, + "rootReference": { + } + } + } + }], + "options": [] + } + }, + "type": "JOIN_TYPE_LEFT" + } + }, + "expressions": [{ + "selection": { + "directReference": { + "structField": { + "field": 1 + } + }, + "rootReference": { + } + } + }, { + "selection": { + "directReference": { + "structField": { + "field": 3 + } + }, + "rootReference": { + } + } + }, { + "selection": { + "directReference": { + "structField": { + "field": 5 + } + }, + "rootReference": { + } + } + }, { + "selection": { + "directReference": { + "structField": { + "field": 7 + } + }, + "rootReference": { + } + } + }] + } + }, + "names": ["count_first", "category", "count_second", "count_third"] + } + }], + "expectedTypeUrls": [], + "version": { + "majorNumber": 0, + "minorNumber": 52, + "patchNumber": 0, + "gitHash": "" + } +} \ No newline at end of file diff --git a/datafusion/wasmtest/README.md b/datafusion/wasmtest/README.md index 8843eed697eca..70f4daef91034 100644 --- a/datafusion/wasmtest/README.md +++ b/datafusion/wasmtest/README.md @@ -71,8 +71,6 @@ wasm-pack test --headless --chrome wasm-pack test --headless --safari ``` -**Note:** In GitHub Actions we test the compilation with `wasm-build`, but we don't currently invoke `wasm-pack test`. This is because the headless mode is not yet working. Document of adding a GitHub Action job: https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/continuous-integration.html#github-actions. - To tweak timeout setting, use `WASM_BINDGEN_TEST_TIMEOUT` environment variable. E.g., `WASM_BINDGEN_TEST_TIMEOUT=300 wasm-pack test --firefox --headless`. ## Compatibility diff --git a/datafusion/wasmtest/datafusion-wasm-app/package-lock.json b/datafusion/wasmtest/datafusion-wasm-app/package-lock.json index 65d8bdbb5e931..c018e779fcbf3 100644 --- a/datafusion/wasmtest/datafusion-wasm-app/package-lock.json +++ b/datafusion/wasmtest/datafusion-wasm-app/package-lock.json @@ -2007,10 +2007,11 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -5562,9 +5563,9 @@ } }, "http-proxy-middleware": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", - "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, "requires": { "@types/http-proxy": "^1.17.8", diff --git a/datafusion/wasmtest/src/lib.rs b/datafusion/wasmtest/src/lib.rs index 6c7be9056eb43..0a7e546b4b18d 100644 --- a/datafusion/wasmtest/src/lib.rs +++ b/datafusion/wasmtest/src/lib.rs @@ -82,7 +82,6 @@ pub fn basic_parse() { #[cfg(test)] mod test { use super::*; - use datafusion::execution::options::ParquetReadOptions; use datafusion::{ arrow::{ array::{ArrayRef, Int32Array, RecordBatch, StringArray}, @@ -98,7 +97,6 @@ mod test { }; use datafusion_physical_plan::collect; use datafusion_sql::parser::DFParser; - use insta::assert_snapshot; use object_store::{memory::InMemory, path::Path, ObjectStore}; use url::Url; use wasm_bindgen_test::wasm_bindgen_test; @@ -240,22 +238,24 @@ mod test { let url = Url::parse("memory://").unwrap(); session_ctx.register_object_store(&url, Arc::new(store)); - - let df = session_ctx - .read_parquet("memory:///", ParquetReadOptions::new()) + session_ctx + .register_parquet("a", "memory:///a.parquet", Default::default()) .await .unwrap(); + let df = session_ctx.sql("SELECT * FROM a").await.unwrap(); + let result = df.collect().await.unwrap(); - assert_snapshot!(batches_to_string(&result), @r" - +----+-------+ - | id | value | - +----+-------+ - | 1 | a | - | 2 | b | - | 3 | c | - +----+-------+ - "); + assert_eq!( + batches_to_string(&result), + "+----+-------+\n\ + | id | value |\n\ + +----+-------+\n\ + | 1 | a |\n\ + | 2 | b |\n\ + | 3 | c |\n\ + +----+-------+" + ); } } diff --git a/datafusion/wasmtest/webdriver.json b/datafusion/wasmtest/webdriver.json new file mode 100644 index 0000000000000..f59a2be9955f1 --- /dev/null +++ b/datafusion/wasmtest/webdriver.json @@ -0,0 +1,15 @@ +{ + "moz:firefoxOptions": { + "prefs": { + "media.navigator.streams.fake": true, + "media.navigator.permission.disabled": true + }, + "args": [] + }, + "goog:chromeOptions": { + "args": [ + "--use-fake-device-for-media-stream", + "--use-fake-ui-for-media-stream" + ] + } +} \ No newline at end of file diff --git a/dev/changelog/47.0.0.md b/dev/changelog/47.0.0.md new file mode 100644 index 0000000000000..64ca2e157a9e3 --- /dev/null +++ b/dev/changelog/47.0.0.md @@ -0,0 +1,506 @@ + + +# Apache DataFusion 47.0.0 Changelog + +This release consists of 364 commits from 94 contributors. See credits at the end of this changelog for more information. + +**Breaking changes:** + +- chore: cleanup deprecated API since `version <= 40` [#15027](https://github.com/apache/datafusion/pull/15027) (qazxcdswe123) +- fix: mark ScalarUDFImpl::invoke_batch as deprecated [#15049](https://github.com/apache/datafusion/pull/15049) (Blizzara) +- feat: support customize metadata in alias for dataframe api [#15120](https://github.com/apache/datafusion/pull/15120) (chenkovsky) +- Refactor: add `FileGroup` structure for `Vec` [#15379](https://github.com/apache/datafusion/pull/15379) (xudong963) +- Change default `EXPLAIN` format in `datafusion-cli` to `tree` format [#15427](https://github.com/apache/datafusion/pull/15427) (alamb) +- Support computing statistics for FileGroup [#15432](https://github.com/apache/datafusion/pull/15432) (xudong963) +- Remove redundant statistics from FileScanConfig [#14955](https://github.com/apache/datafusion/pull/14955) (Standing-Man) +- parquet reader: move pruning predicate creation from ParquetSource to ParquetOpener [#15561](https://github.com/apache/datafusion/pull/15561) (adriangb) +- feat: Add unique id for every memory consumer [#15613](https://github.com/apache/datafusion/pull/15613) (EmilyMatt) + +**Performance related:** + +- Fix sequential metadata fetching in ListingTable causing high latency [#14918](https://github.com/apache/datafusion/pull/14918) (geoffreyclaude) +- Implement GroupsAccumulator for min/max Duration [#15322](https://github.com/apache/datafusion/pull/15322) (shruti2522) +- [Minor] Remove/reorder logical plan rules [#15421](https://github.com/apache/datafusion/pull/15421) (Dandandan) +- Improve performance of `first_value` by implementing special `GroupsAccumulator` [#15266](https://github.com/apache/datafusion/pull/15266) (UBarney) +- perf: unwrap cast for comparing ints =/!= strings [#15110](https://github.com/apache/datafusion/pull/15110) (alan910127) +- Improve performance sort TPCH q3 with Utf8Vew ( Sort-preserving mergi… [#15447](https://github.com/apache/datafusion/pull/15447) (zhuqi-lucas) +- perf: Reuse row converter during sort [#15302](https://github.com/apache/datafusion/pull/15302) (2010YOUY01) +- perf: Add TopK benchmarks as variation over the `sort_tpch` benchmarks [#15560](https://github.com/apache/datafusion/pull/15560) (geoffreyclaude) +- Perf: remove `clone` on `uninitiated_partitions` in SortPreservingMergeStream [#15562](https://github.com/apache/datafusion/pull/15562) (rluvaton) +- Add short circuit evaluation for `AND` and `OR` [#15462](https://github.com/apache/datafusion/pull/15462) (acking-you) +- perf: Introduce sort prefix computation for early TopK exit optimization on partially sorted input (10x speedup on top10 bench) [#15563](https://github.com/apache/datafusion/pull/15563) (geoffreyclaude) +- Improve performance of `last_value` by implementing special `GroupsAccumulator` [#15542](https://github.com/apache/datafusion/pull/15542) (UBarney) +- Enhance: simplify `x=x` --> `x IS NOT NULL OR NULL` [#15589](https://github.com/apache/datafusion/pull/15589) (ding-young) + +**Implemented enhancements:** + +- feat: Add `tree` / pretty explain mode [#14677](https://github.com/apache/datafusion/pull/14677) (irenjj) +- feat: Add `array_max` function support [#14470](https://github.com/apache/datafusion/pull/14470) (erenavsarogullari) +- feat: implement tree explain for `ProjectionExec` [#15082](https://github.com/apache/datafusion/pull/15082) (Standing-Man) +- feat: support ApproxDistinct with utf8view [#15200](https://github.com/apache/datafusion/pull/15200) (zhuqi-lucas) +- feat: Attach `Diagnostic` to more than one column errors in scalar_subquery and in_subquery [#15143](https://github.com/apache/datafusion/pull/15143) (changsun20) +- feat: topk functionality for aggregates should support utf8view and largeutf8 [#15152](https://github.com/apache/datafusion/pull/15152) (zhuqi-lucas) +- feat: Native support utf8view for regex string operators [#15275](https://github.com/apache/datafusion/pull/15275) (zhuqi-lucas) +- feat: introduce `JoinSetTracer` trait for tracing context propagation in spawned tasks [#14547](https://github.com/apache/datafusion/pull/14547) (geoffreyclaude) +- feat: Support serde for JsonSource PhysicalPlan [#15311](https://github.com/apache/datafusion/pull/15311) (westhide) +- feat: Support serde for FileScanConfig `batch_size` [#15335](https://github.com/apache/datafusion/pull/15335) (westhide) +- feat: simplify regex wildcard pattern [#15299](https://github.com/apache/datafusion/pull/15299) (waynexia) +- feat: Add union_by_name, union_by_name_distinct to DataFrame api [#15489](https://github.com/apache/datafusion/pull/15489) (Omega359) +- feat: Add config `max_temp_directory_size` to limit max disk usage for spilling queries [#15520](https://github.com/apache/datafusion/pull/15520) (2010YOUY01) +- feat: Add tracing regression tests [#15673](https://github.com/apache/datafusion/pull/15673) (geoffreyclaude) + +**Fixed bugs:** + +- fix: External sort failing on an edge case [#15017](https://github.com/apache/datafusion/pull/15017) (2010YOUY01) +- fix: graceful NULL and type error handling in array functions [#14737](https://github.com/apache/datafusion/pull/14737) (alan910127) +- fix: Support datatype cast for insert api same as insert into sql [#15091](https://github.com/apache/datafusion/pull/15091) (zhuqi-lucas) +- fix: unparse for subqueryalias [#15068](https://github.com/apache/datafusion/pull/15068) (chenkovsky) +- fix: date_trunc bench broken by #15049 [#15169](https://github.com/apache/datafusion/pull/15169) (Blizzara) +- fix: compound_field_access doesn't identifier qualifier. [#15153](https://github.com/apache/datafusion/pull/15153) (chenkovsky) +- fix: unparsing left/ right semi/mark join [#15212](https://github.com/apache/datafusion/pull/15212) (chenkovsky) +- fix: handle duplicate WindowFunction expressions in Substrait consumer [#15211](https://github.com/apache/datafusion/pull/15211) (Blizzara) +- fix: write hive partitions for any int/uint/float [#15337](https://github.com/apache/datafusion/pull/15337) (christophermcdermott) +- fix: `core_expressions` feature flag broken, move `overlay` into `core` functions [#15217](https://github.com/apache/datafusion/pull/15217) (shruti2522) +- fix: Redundant files spilled during external sort + introduce `SpillManager` [#15355](https://github.com/apache/datafusion/pull/15355) (2010YOUY01) +- fix: typo of DropFunction [#15434](https://github.com/apache/datafusion/pull/15434) (chenkovsky) +- fix: Unconditionally wrap UNION BY NAME input nodes w/ `Projection` [#15242](https://github.com/apache/datafusion/pull/15242) (rkrishn7) +- fix: the average time for clickbench query compute should use new vec to make it compute for each query [#15472](https://github.com/apache/datafusion/pull/15472) (zhuqi-lucas) +- fix: Assertion fail in external sort [#15469](https://github.com/apache/datafusion/pull/15469) (2010YOUY01) +- fix: aggregation corner case [#15457](https://github.com/apache/datafusion/pull/15457) (chenkovsky) +- fix: update group by columns for merge phase after spill [#15531](https://github.com/apache/datafusion/pull/15531) (rluvaton) +- fix: Queries similar to `count-bug` produce incorrect results [#15281](https://github.com/apache/datafusion/pull/15281) (suibianwanwank) +- fix: ffi aggregation [#15576](https://github.com/apache/datafusion/pull/15576) (chenkovsky) +- fix: nested window function [#15033](https://github.com/apache/datafusion/pull/15033) (chenkovsky) +- fix: dictionary encoded column to partition column casting bug [#15652](https://github.com/apache/datafusion/pull/15652) (haruband) +- fix: recursion protection for physical plan node [#15600](https://github.com/apache/datafusion/pull/15600) (chenkovsky) +- fix: add map coercion for binary ops [#15551](https://github.com/apache/datafusion/pull/15551) (alexwilcoxson-rel) +- fix: Rewrite `date_trunc` and `from_unixtime` for the SQLite unparser [#15630](https://github.com/apache/datafusion/pull/15630) (peasee) +- fix(substrait): fix regressed edge case in renaming inner struct fields [#15634](https://github.com/apache/datafusion/pull/15634) (Blizzara) +- fix: normalize window ident [#15639](https://github.com/apache/datafusion/pull/15639) (chenkovsky) +- fix: unparse join without projection [#15693](https://github.com/apache/datafusion/pull/15693) (chenkovsky) + +**Documentation updates:** + +- MINOR fix(docs): set the proper link for dev-env setup in contrib guide [#14960](https://github.com/apache/datafusion/pull/14960) (clflushopt) +- Add Upgrade Guide for DataFusion 46.0.0 [#14891](https://github.com/apache/datafusion/pull/14891) (alamb) +- Improve `SessionStateBuilder::new` documentation [#14980](https://github.com/apache/datafusion/pull/14980) (alamb) +- Minor: Replace Star and Fork buttons in docs with static versions [#14988](https://github.com/apache/datafusion/pull/14988) (amoeba) +- Fix documentation warnings and error if anymore occur [#14952](https://github.com/apache/datafusion/pull/14952) (AmosAidoo) +- docs: Improve docs on AggregateFunctionExpr construction [#15044](https://github.com/apache/datafusion/pull/15044) (ctsk) +- Minor: More comment to aggregation fuzzer [#15048](https://github.com/apache/datafusion/pull/15048) (2010YOUY01) +- Improve benchmark documentation [#15054](https://github.com/apache/datafusion/pull/15054) (carols10cents) +- doc: update RecordBatchReceiverStreamBuilder::spawn_blocking task behaviour [#14995](https://github.com/apache/datafusion/pull/14995) (shruti2522) +- doc: Correct benchmark command [#15094](https://github.com/apache/datafusion/pull/15094) (qazxcdswe123) +- Add `insta` / snapshot testing to CLI & set up AWS mock [#13672](https://github.com/apache/datafusion/pull/13672) (blaginin) +- Config: Add support default sql varchar to view types [#15104](https://github.com/apache/datafusion/pull/15104) (zhuqi-lucas) +- Support `EXPLAIN ... FORMAT ...` [#15166](https://github.com/apache/datafusion/pull/15166) (alamb) +- Update version to 46.0.1, add CHANGELOG (#15243) [#15244](https://github.com/apache/datafusion/pull/15244) (xudong963) +- docs: update documentation for Final GroupBy in accumulator.rs [#15279](https://github.com/apache/datafusion/pull/15279) (qazxcdswe123) +- minor: fix `data/sqlite` link [#15286](https://github.com/apache/datafusion/pull/15286) (sdht0) +- Add upgrade notes for array signatures [#15237](https://github.com/apache/datafusion/pull/15237) (jkosh44) +- Add doc for the `statistics_from_parquet_meta_calc method` [#15330](https://github.com/apache/datafusion/pull/15330) (xudong963) +- added explaination for Schema and DFSchema to documentation [#15329](https://github.com/apache/datafusion/pull/15329) (Jiashu-Hu) +- Documentation: Plan custom expressions [#15353](https://github.com/apache/datafusion/pull/15353) (Jiashu-Hu) +- Update concepts-readings-events.md [#15440](https://github.com/apache/datafusion/pull/15440) (berkaysynnada) +- Add support for DISTINCT + ORDER BY in `ARRAY_AGG` [#14413](https://github.com/apache/datafusion/pull/14413) (gabotechs) +- Update the copyright year [#15453](https://github.com/apache/datafusion/pull/15453) (omkenge) +- Docs: Formatting and Added Extra resources [#15450](https://github.com/apache/datafusion/pull/15450) (2SpaceMasterRace) +- Add documentation for `Run extended tests` command [#15463](https://github.com/apache/datafusion/pull/15463) (alamb) +- bench: Document how to use cross platform Samply profiler [#15481](https://github.com/apache/datafusion/pull/15481) (comphead) +- Update user guide to note decimal is not experimental anymore [#15515](https://github.com/apache/datafusion/pull/15515) (Jiashu-Hu) +- datafusion-cli: document reading partitioned parquet [#15505](https://github.com/apache/datafusion/pull/15505) (marvelshan) +- Update concepts-readings-events.md [#15541](https://github.com/apache/datafusion/pull/15541) (oznur-synnada) +- Add documentation example for `AggregateExprBuilder` [#15504](https://github.com/apache/datafusion/pull/15504) (Shreyaskr1409) +- Docs : Added Sql examples for window Functions : `nth_val` , etc [#15555](https://github.com/apache/datafusion/pull/15555) (Adez017) +- Add disk usage limit configuration to datafusion-cli [#15586](https://github.com/apache/datafusion/pull/15586) (jsai28) +- Bug fix : fix the bug in docs in 'cum_dist()' Example [#15618](https://github.com/apache/datafusion/pull/15618) (Adez017) +- Make tree the Default EXPLAIN Format and Reorder Documentation Sections [#15706](https://github.com/apache/datafusion/pull/15706) (kosiew) +- Add coerce int96 option for Parquet to support different TimeUnits, test int96_from_spark.parquet from parquet-testing [#15537](https://github.com/apache/datafusion/pull/15537) (mbutrovich) +- STRING_AGG missing functionality [#14412](https://github.com/apache/datafusion/pull/14412) (gabotechs) +- doc : update RepartitionExec display tree [#15710](https://github.com/apache/datafusion/pull/15710) (getChan) +- Update version to 47.0.0, add CHANGELOG [#15731](https://github.com/apache/datafusion/pull/15731) (xudong963) + +**Other:** + +- Improve documentation for `DataSourceExec`, `FileScanConfig`, `DataSource` etc [#14941](https://github.com/apache/datafusion/pull/14941) (alamb) +- Do not swap with projection when file is partitioned [#14956](https://github.com/apache/datafusion/pull/14956) (blaginin) +- Minor: Add more projection pushdown tests, clarify comments [#14963](https://github.com/apache/datafusion/pull/14963) (alamb) +- Update labeler components [#14942](https://github.com/apache/datafusion/pull/14942) (alamb) +- Deprecate `Expr::Wildcard` [#14959](https://github.com/apache/datafusion/pull/14959) (linhr) +- Minor: use FileScanConfig builder API in some tests [#14938](https://github.com/apache/datafusion/pull/14938) (alamb) +- Minor: improve documentation of `AggregateMode` [#14946](https://github.com/apache/datafusion/pull/14946) (alamb) +- chore(deps): bump thiserror from 2.0.11 to 2.0.12 [#14971](https://github.com/apache/datafusion/pull/14971) (dependabot[bot]) +- chore(deps): bump pyo3 from 0.23.4 to 0.23.5 [#14972](https://github.com/apache/datafusion/pull/14972) (dependabot[bot]) +- chore(deps): bump async-trait from 0.1.86 to 0.1.87 [#14973](https://github.com/apache/datafusion/pull/14973) (dependabot[bot]) +- Fix verification script and extended tests due to `rustup` changes [#14990](https://github.com/apache/datafusion/pull/14990) (alamb) +- Split out avro, parquet, json and csv into individual crates [#14951](https://github.com/apache/datafusion/pull/14951) (AdamGS) +- Minor: Add `backtrace` feature in datafusion-cli [#14997](https://github.com/apache/datafusion/pull/14997) (2010YOUY01) +- chore: Update `SessionStateBuilder::with_default_features` does not replace existing features [#14935](https://github.com/apache/datafusion/pull/14935) (irenjj) +- Make `create_ordering` pub and add doc for it [#14996](https://github.com/apache/datafusion/pull/14996) (xudong963) +- Simplify Between expression to Eq [#14994](https://github.com/apache/datafusion/pull/14994) (jayzhan211) +- Count wildcard alias [#14927](https://github.com/apache/datafusion/pull/14927) (jayzhan211) +- replace TypeSignature::String with TypeSignature::Coercible [#14917](https://github.com/apache/datafusion/pull/14917) (zjregee) +- Minor: Add indentation to EnforceDistribution test plans. [#15007](https://github.com/apache/datafusion/pull/15007) (wiedld) +- Minor: add method `SessionStateBuilder::new_with_default_features()` [#14998](https://github.com/apache/datafusion/pull/14998) (shruti2522) +- Implement `tree` explain for FilterExec [#15001](https://github.com/apache/datafusion/pull/15001) (alamb) +- Unparser add `AtArrow` and `ArrowAt` conversion to BinaryOperator [#14968](https://github.com/apache/datafusion/pull/14968) (cetra3) +- Add dependency checks to verify-release-candidate script [#15009](https://github.com/apache/datafusion/pull/15009) (waynexia) +- Fix: to_char Function Now Correctly Handles DATE Values in DataFusion [#14970](https://github.com/apache/datafusion/pull/14970) (kosiew) +- Make Substrait Schema Structs always non-nullable [#15011](https://github.com/apache/datafusion/pull/15011) (amoeba) +- Adjust physical optimizer rule order, put `ProjectionPushdown` at last [#15040](https://github.com/apache/datafusion/pull/15040) (xudong963) +- Move `UnwrapCastInComparison` into `Simplifier` [#15012](https://github.com/apache/datafusion/pull/15012) (jayzhan211) +- chore(deps): bump aws-config from 1.5.17 to 1.5.18 [#15041](https://github.com/apache/datafusion/pull/15041) (dependabot[bot]) +- chore(deps): bump bytes from 1.10.0 to 1.10.1 [#15042](https://github.com/apache/datafusion/pull/15042) (dependabot[bot]) +- Minor: Deprecate `ScalarValue::raw_data` [#15016](https://github.com/apache/datafusion/pull/15016) (qazxcdswe123) +- Implement tree explain for `DataSourceExec` [#15029](https://github.com/apache/datafusion/pull/15029) (alamb) +- Refactor test suite in EnforceDistribution, to use standard test config. [#15010](https://github.com/apache/datafusion/pull/15010) (wiedld) +- Update ring to v0.17.13 [#15063](https://github.com/apache/datafusion/pull/15063) (alamb) +- Remove deprecated function `OptimizerRule::try_optimize` [#15051](https://github.com/apache/datafusion/pull/15051) (qazxcdswe123) +- Minor: fix CI to make the sqllogic testing result consistent [#15059](https://github.com/apache/datafusion/pull/15059) (zhuqi-lucas) +- Refactor SortPushdown using the standard top-down visitor and using `EquivalenceProperties` [#14821](https://github.com/apache/datafusion/pull/14821) (wiedld) +- Improve explain tree formatting for longer lines / word wrap [#15031](https://github.com/apache/datafusion/pull/15031) (irenjj) +- chore(deps): bump sqllogictest from 0.27.2 to 0.28.0 [#15060](https://github.com/apache/datafusion/pull/15060) (dependabot[bot]) +- chore(deps): bump async-compression from 0.4.18 to 0.4.19 [#15061](https://github.com/apache/datafusion/pull/15061) (dependabot[bot]) +- Handle columns in with_new_exprs with a Join [#15055](https://github.com/apache/datafusion/pull/15055) (delamarch3) +- Minor: Improve documentation of `need_handle_count_bug` [#15050](https://github.com/apache/datafusion/pull/15050) (suibianwanwank) +- Implement `tree` explain for `HashJoinExec` [#15079](https://github.com/apache/datafusion/pull/15079) (irenjj) +- Implement tree explain for PartialSortExec [#15066](https://github.com/apache/datafusion/pull/15066) (irenjj) +- Implement `tree` explain for `SortExec` [#15077](https://github.com/apache/datafusion/pull/15077) (irenjj) +- Minor: final `46.0.0` release tweaks: changelog + instructions [#15073](https://github.com/apache/datafusion/pull/15073) (alamb) +- Implement tree explain for `NestedLoopJoinExec`, `CrossJoinExec`, `So… [#15081](https://github.com/apache/datafusion/pull/15081) (irenjj) +- Implement `tree` explain for `BoundedWindowAggExec` and `WindowAggExec` [#15084](https://github.com/apache/datafusion/pull/15084) (irenjj) +- implement tree rendering for StreamingTableExec [#15085](https://github.com/apache/datafusion/pull/15085) (Standing-Man) +- chore(deps): bump semver from 1.0.25 to 1.0.26 [#15116](https://github.com/apache/datafusion/pull/15116) (dependabot[bot]) +- chore(deps): bump clap from 4.5.30 to 4.5.31 [#15115](https://github.com/apache/datafusion/pull/15115) (dependabot[bot]) +- implement tree explain for GlobalLimitExec [#15100](https://github.com/apache/datafusion/pull/15100) (zjregee) +- Minor: Cleanup useless/duplicated code in gen tools [#15113](https://github.com/apache/datafusion/pull/15113) (lewiszlw) +- Refactor EnforceDistribution test cases to demonstrate dependencies across optimizer runs. [#15074](https://github.com/apache/datafusion/pull/15074) (wiedld) +- Improve parsing `extra_info` in tree explain [#15125](https://github.com/apache/datafusion/pull/15125) (irenjj) +- Add tests for simplification and coercion of `SessionContext::create_physical_expr` [#15034](https://github.com/apache/datafusion/pull/15034) (alamb) +- Minor: Fix invalid query in test [#15131](https://github.com/apache/datafusion/pull/15131) (alamb) +- Do not display logical_plan win explain `tree` mode 🧹 [#15132](https://github.com/apache/datafusion/pull/15132) (alamb) +- Substrait support for propagating TableScan.filters to Substrait ReadRel.filter [#14194](https://github.com/apache/datafusion/pull/14194) (jamxia155) +- Fix wasm32 build on version 46 [#15102](https://github.com/apache/datafusion/pull/15102) (XiangpengHao) +- Fix broken `serde` feature [#15124](https://github.com/apache/datafusion/pull/15124) (vadimpiven) +- chore(deps): bump tempfile from 3.17.1 to 3.18.0 [#15146](https://github.com/apache/datafusion/pull/15146) (dependabot[bot]) +- chore(deps): bump syn from 2.0.98 to 2.0.100 [#15147](https://github.com/apache/datafusion/pull/15147) (dependabot[bot]) +- Implement tree explain for AggregateExec [#15103](https://github.com/apache/datafusion/pull/15103) (zebsme) +- Implement tree explain for `RepartitionExec` and `WorkTableExec` [#15137](https://github.com/apache/datafusion/pull/15137) (Standing-Man) +- Expand wildcard to actual expressions in `prepare_select_exprs` [#15090](https://github.com/apache/datafusion/pull/15090) (jayzhan211) +- fixed PushDownFilter bug [15047] [#15142](https://github.com/apache/datafusion/pull/15142) (Jiashu-Hu) +- Bump `env_logger` from `0.11.6` to `0.11.7` [#15148](https://github.com/apache/datafusion/pull/15148) (mbrobbel) +- Minor: fix extend sqllogical consistent with main test [#15145](https://github.com/apache/datafusion/pull/15145) (zhuqi-lucas) +- Implement tree rendering for `SortPreservingMergeExec` [#15140](https://github.com/apache/datafusion/pull/15140) (Standing-Man) +- Remove expand wildcard rule [#15170](https://github.com/apache/datafusion/pull/15170) (jayzhan211) +- chore: remove ScalarUDFImpl::return_type_from_exprs [#15130](https://github.com/apache/datafusion/pull/15130) (Blizzara) +- chore(deps): bump libc from 0.2.170 to 0.2.171 [#15176](https://github.com/apache/datafusion/pull/15176) (dependabot[bot]) +- chore(deps): bump serde_json from 1.0.139 to 1.0.140 [#15175](https://github.com/apache/datafusion/pull/15175) (dependabot[bot]) +- chore(deps): bump substrait from 0.53.2 to 0.54.0 [#15043](https://github.com/apache/datafusion/pull/15043) (dependabot[bot]) +- Minor: split EXPLAIN and ANALYZE planning into different functions [#15188](https://github.com/apache/datafusion/pull/15188) (alamb) +- Implement `tree` explain for `JsonSink` [#15185](https://github.com/apache/datafusion/pull/15185) (irenjj) +- Split out `datafusion-substrait` and `datafusion-proto` CI feature checks, increase coverage [#15156](https://github.com/apache/datafusion/pull/15156) (alamb) +- Remove unused wildcard expanding methods [#15180](https://github.com/apache/datafusion/pull/15180) (goldmedal) +- #15108 issue: "Non Panic Task error" is not an internal error [#15109](https://github.com/apache/datafusion/pull/15109) (Satyam018) +- Implement tree explain for LazyMemoryExec [#15187](https://github.com/apache/datafusion/pull/15187) (zebsme) +- implement tree explain for CoalesceBatchesExec [#15194](https://github.com/apache/datafusion/pull/15194) (Standing-Man) +- Implement `tree` explain for `CsvSink` [#15204](https://github.com/apache/datafusion/pull/15204) (irenjj) +- chore(deps): bump blake3 from 1.6.0 to 1.6.1 [#15198](https://github.com/apache/datafusion/pull/15198) (dependabot[bot]) +- chore(deps): bump clap from 4.5.31 to 4.5.32 [#15199](https://github.com/apache/datafusion/pull/15199) (dependabot[bot]) +- chore(deps): bump serde from 1.0.218 to 1.0.219 [#15197](https://github.com/apache/datafusion/pull/15197) (dependabot[bot]) +- Fix datafusion proto crate `json` feature [#15172](https://github.com/apache/datafusion/pull/15172) (Owen-CH-Leung) +- Add blog link to `EquivalenceProperties` docs [#15215](https://github.com/apache/datafusion/pull/15215) (alamb) +- Minor: split datafusion-cli testing into its own CI job [#15075](https://github.com/apache/datafusion/pull/15075) (alamb) +- Implement tree explain for InterleaveExec [#15219](https://github.com/apache/datafusion/pull/15219) (zebsme) +- Move catalog_common out of core [#15193](https://github.com/apache/datafusion/pull/15193) (logan-keede) +- chore(deps): bump tokio-util from 0.7.13 to 0.7.14 [#15223](https://github.com/apache/datafusion/pull/15223) (dependabot[bot]) +- chore(deps): bump aws-config from 1.5.18 to 1.6.0 [#15222](https://github.com/apache/datafusion/pull/15222) (dependabot[bot]) +- chore(deps): bump bzip2 from 0.5.1 to 0.5.2 [#15221](https://github.com/apache/datafusion/pull/15221) (dependabot[bot]) +- Document guidelines for physical operator yielding [#15030](https://github.com/apache/datafusion/pull/15030) (carols10cents) +- Implement `tree` explain for `ArrowFileSink`, fix original URL [#15206](https://github.com/apache/datafusion/pull/15206) (irenjj) +- Implement tree explain for `LocalLimitExec` [#15232](https://github.com/apache/datafusion/pull/15232) (shruti2522) +- Use insta for `DataFrame` tests [#15165](https://github.com/apache/datafusion/pull/15165) (blaginin) +- Re-enable github discussion [#15241](https://github.com/apache/datafusion/pull/15241) (2010YOUY01) +- Minor: exclude datafusion-cli testing for mac [#15240](https://github.com/apache/datafusion/pull/15240) (zhuqi-lucas) +- Implement tree explain for CoalescePartitionsExec [#15225](https://github.com/apache/datafusion/pull/15225) (Shreyaskr1409) +- Enable `used_underscore_binding` clippy lint [#15189](https://github.com/apache/datafusion/pull/15189) (Shreyaskr1409) +- Simpler to see expressions in explain `tree` mode [#15163](https://github.com/apache/datafusion/pull/15163) (irenjj) +- Fix invalid schema for unions in ViewTables [#15135](https://github.com/apache/datafusion/pull/15135) (Friede80) +- Make `ListingTableUrl::try_new` public [#15250](https://github.com/apache/datafusion/pull/15250) (linhr) +- Fix wildcard dataframe case [#15230](https://github.com/apache/datafusion/pull/15230) (jayzhan211) +- Simplify the printing of all plans containing `expr` in `tree` mode [#15249](https://github.com/apache/datafusion/pull/15249) (irenjj) +- Support utf8view datatype for window [#15257](https://github.com/apache/datafusion/pull/15257) (zhuqi-lucas) +- chore: remove deprecated variants of UDF's invoke (invoke, invoke_no_args, invoke_batch) [#15123](https://github.com/apache/datafusion/pull/15123) (Blizzara) +- Improve feature flag CI coverage `datafusion` and `datafusion-functions` [#15203](https://github.com/apache/datafusion/pull/15203) (alamb) +- Add debug logging for default catalog overwrite in SessionState build [#15251](https://github.com/apache/datafusion/pull/15251) (byte-sourcerer) +- Implement tree explain for PlaceholderRowExec [#15270](https://github.com/apache/datafusion/pull/15270) (zebsme) +- Implement tree explain for UnionExec [#15278](https://github.com/apache/datafusion/pull/15278) (zebsme) +- Migrate dataframe tests to `insta` [#15262](https://github.com/apache/datafusion/pull/15262) (jsai28) +- Minor: consistently apply `clippy::clone_on_ref_ptr` in all crates [#15284](https://github.com/apache/datafusion/pull/15284) (alamb) +- chore(deps): bump async-trait from 0.1.87 to 0.1.88 [#15294](https://github.com/apache/datafusion/pull/15294) (dependabot[bot]) +- chore(deps): bump uuid from 1.15.1 to 1.16.0 [#15292](https://github.com/apache/datafusion/pull/15292) (dependabot[bot]) +- Add CatalogProvider and SchemaProvider to FFI Crate [#15280](https://github.com/apache/datafusion/pull/15280) (timsaucer) +- Refactor file schema type coercions [#15268](https://github.com/apache/datafusion/pull/15268) (xudong963) +- chore(deps): bump rust_decimal from 1.36.0 to 1.37.0 [#15293](https://github.com/apache/datafusion/pull/15293) (dependabot[bot]) +- chore: Attach Diagnostic to "incompatible type in unary expression" error [#15209](https://github.com/apache/datafusion/pull/15209) (onlyjackfrost) +- Support logic optimize rule to pass the case that Utf8view datatype combined with Utf8 datatype [#15239](https://github.com/apache/datafusion/pull/15239) (zhuqi-lucas) +- Migrate user_defined tests to insta [#15255](https://github.com/apache/datafusion/pull/15255) (shruti2522) +- Remove inline table scan analyzer rule [#15201](https://github.com/apache/datafusion/pull/15201) (jayzhan211) +- CI Red: Fix union in view table test [#15300](https://github.com/apache/datafusion/pull/15300) (jayzhan211) +- refactor: Move view and stream from `datasource` to `catalog`, deprecate `View::try_new` [#15260](https://github.com/apache/datafusion/pull/15260) (logan-keede) +- chore(deps): bump substrait from 0.54.0 to 0.55.0 [#15305](https://github.com/apache/datafusion/pull/15305) (dependabot[bot]) +- chore(deps): bump half from 2.4.1 to 2.5.0 [#15303](https://github.com/apache/datafusion/pull/15303) (dependabot[bot]) +- chore(deps): bump mimalloc from 0.1.43 to 0.1.44 [#15304](https://github.com/apache/datafusion/pull/15304) (dependabot[bot]) +- Fix predicate pushdown for custom SchemaAdapters [#15263](https://github.com/apache/datafusion/pull/15263) (adriangb) +- Fix extended tests by restore datafusion-testing submodule [#15318](https://github.com/apache/datafusion/pull/15318) (alamb) +- Support Duration in min/max agg functions [#15310](https://github.com/apache/datafusion/pull/15310) (svranesevic) +- Migrate tests to insta [#15288](https://github.com/apache/datafusion/pull/15288) (jsai28) +- chore(deps): bump quote from 1.0.38 to 1.0.40 [#15332](https://github.com/apache/datafusion/pull/15332) (dependabot[bot]) +- chore(deps): bump blake3 from 1.6.1 to 1.7.0 [#15331](https://github.com/apache/datafusion/pull/15331) (dependabot[bot]) +- Simplify display format of `AggregateFunctionExpr`, add `Expr::sql_name` [#15253](https://github.com/apache/datafusion/pull/15253) (irenjj) +- chore(deps): bump indexmap from 2.7.1 to 2.8.0 [#15333](https://github.com/apache/datafusion/pull/15333) (dependabot[bot]) +- chore(deps): bump tokio from 1.43.0 to 1.44.1 [#15347](https://github.com/apache/datafusion/pull/15347) (dependabot[bot]) +- chore(deps): bump tempfile from 3.18.0 to 3.19.1 [#15346](https://github.com/apache/datafusion/pull/15346) (dependabot[bot]) +- Minor: Keep debug symbols for `release-nonlto` build [#15350](https://github.com/apache/datafusion/pull/15350) (2010YOUY01) +- Use `any` instead of `for_each` [#15289](https://github.com/apache/datafusion/pull/15289) (xudong963) +- refactor: move `CteWorkTable`, `default_table_source` a bunch of files out of core [#15316](https://github.com/apache/datafusion/pull/15316) (logan-keede) +- Fix empty aggregation function count() in Substrait [#15345](https://github.com/apache/datafusion/pull/15345) (gabotechs) +- Improved error for expand wildcard rule [#15287](https://github.com/apache/datafusion/pull/15287) (Jiashu-Hu) +- Added tests with are writing into parquet files in memory for issue #… [#15325](https://github.com/apache/datafusion/pull/15325) (pranavJibhakate) +- Migrate physical plan tests to `insta` (Part-1) [#15313](https://github.com/apache/datafusion/pull/15313) (Shreyaskr1409) +- Fix array_has_all and array_has_any with empty array [#15039](https://github.com/apache/datafusion/pull/15039) (LuQQiu) +- Update datafusion-testing pin to fix extended tests [#15368](https://github.com/apache/datafusion/pull/15368) (alamb) +- chore(deps): Update sqlparser to 0.55.0 [#15183](https://github.com/apache/datafusion/pull/15183) (PokIsemaine) +- Only unnest source for `EmptyRelation` [#15159](https://github.com/apache/datafusion/pull/15159) (blaginin) +- chore(deps): bump rust_decimal from 1.37.0 to 1.37.1 [#15378](https://github.com/apache/datafusion/pull/15378) (dependabot[bot]) +- chore(deps): bump chrono-tz from 0.10.1 to 0.10.2 [#15377](https://github.com/apache/datafusion/pull/15377) (dependabot[bot]) +- remove the duplicate test for unparser [#15385](https://github.com/apache/datafusion/pull/15385) (goldmedal) +- Minor: add average time for clickbench benchmark query [#15381](https://github.com/apache/datafusion/pull/15381) (zhuqi-lucas) +- include some BinaryOperator from sqlparser [#15327](https://github.com/apache/datafusion/pull/15327) (waynexia) +- Add "end to end parquet reading test" for WASM [#15362](https://github.com/apache/datafusion/pull/15362) (jsai28) +- Migrate physical plan tests to `insta` (Part-2) [#15364](https://github.com/apache/datafusion/pull/15364) (Shreyaskr1409) +- Migrate physical plan tests to `insta` (Part-3 / Final) [#15399](https://github.com/apache/datafusion/pull/15399) (Shreyaskr1409) +- Restore lazy evaluation of fallible CASE [#15390](https://github.com/apache/datafusion/pull/15390) (findepi) +- chore(deps): bump log from 0.4.26 to 0.4.27 [#15410](https://github.com/apache/datafusion/pull/15410) (dependabot[bot]) +- chore(deps): bump chrono-tz from 0.10.2 to 0.10.3 [#15412](https://github.com/apache/datafusion/pull/15412) (dependabot[bot]) +- Perf: Support Utf8View datatype single column comparisons for SortPreservingMergeStream [#15348](https://github.com/apache/datafusion/pull/15348) (zhuqi-lucas) +- Enforce JOIN plan to require condition [#15334](https://github.com/apache/datafusion/pull/15334) (goldmedal) +- Fix type coercion for unsigned and signed integers (`Int64` vs `UInt64`, etc) [#15341](https://github.com/apache/datafusion/pull/15341) (Omega359) +- simplify `array_has` UDF to `InList` expr when haystack is constant [#15354](https://github.com/apache/datafusion/pull/15354) (davidhewitt) +- Move `DataSink` to `datasource` and add session crate [#15371](https://github.com/apache/datafusion/pull/15371) (jayzhan-synnada) +- refactor: SpillManager into a separate file [#15407](https://github.com/apache/datafusion/pull/15407) (Weijun-H) +- Always use `PartitionMode::Auto` in planner [#15339](https://github.com/apache/datafusion/pull/15339) (Dandandan) +- Fix link to Volcano paper [#15437](https://github.com/apache/datafusion/pull/15437) (JackKelly) +- minor: Add new crates to labeler [#15426](https://github.com/apache/datafusion/pull/15426) (logan-keede) +- refactor: Use SpillManager for all spilling scenarios [#15405](https://github.com/apache/datafusion/pull/15405) (2010YOUY01) +- refactor(hash_join): Move JoinHashMap to separate mod [#15419](https://github.com/apache/datafusion/pull/15419) (ctsk) +- Migrate datasource tests to insta [#15258](https://github.com/apache/datafusion/pull/15258) (shruti2522) +- Add `downcast_to_source` method for `DataSourceExec` [#15416](https://github.com/apache/datafusion/pull/15416) (xudong963) +- refactor: use TypeSignature::Coercible for crypto functions [#14826](https://github.com/apache/datafusion/pull/14826) (Chen-Yuan-Lai) +- Minor: fix doc for `FileGroupPartitioner` [#15448](https://github.com/apache/datafusion/pull/15448) (xudong963) +- chore(deps): bump clap from 4.5.32 to 4.5.34 [#15452](https://github.com/apache/datafusion/pull/15452) (dependabot[bot]) +- Fix roundtrip bug with empty projection in DataSourceExec [#15449](https://github.com/apache/datafusion/pull/15449) (XiangpengHao) +- Triggering extended tests through PR comment: `Run extended tests` [#15101](https://github.com/apache/datafusion/pull/15101) (danila-b) +- Use `equals_datatype` to compare type when type coercion [#15366](https://github.com/apache/datafusion/pull/15366) (goldmedal) +- Fix no effect metrics bug in ParquetSource [#15460](https://github.com/apache/datafusion/pull/15460) (XiangpengHao) +- chore(deps): bump aws-config from 1.6.0 to 1.6.1 [#15470](https://github.com/apache/datafusion/pull/15470) (dependabot[bot]) +- minor: Allow to run TPCH bench for a specific query [#15467](https://github.com/apache/datafusion/pull/15467) (comphead) +- Migrate subtraits tests to insta, part1 [#15444](https://github.com/apache/datafusion/pull/15444) (qstommyshu) +- Add `FileScanConfigBuilder` [#15352](https://github.com/apache/datafusion/pull/15352) (blaginin) +- Update ClickBench queries to avoid to_timestamp_seconds [#15475](https://github.com/apache/datafusion/pull/15475) (acking-you) +- Remove CoalescePartitions insertion from HashJoinExec [#15476](https://github.com/apache/datafusion/pull/15476) (ctsk) +- Migrate-substrait-tests-to-insta, part2 [#15480](https://github.com/apache/datafusion/pull/15480) (qstommyshu) +- Revert #15476 to fix the datafusion-examples CI fail [#15496](https://github.com/apache/datafusion/pull/15496) (goldmedal) +- Migrate datafusion/sql tests to insta, part1 [#15497](https://github.com/apache/datafusion/pull/15497) (qstommyshu) +- Allow type coersion of zero input arrays to nullary [#15487](https://github.com/apache/datafusion/pull/15487) (timsaucer) +- Decimal type support for `to_timestamp` [#15486](https://github.com/apache/datafusion/pull/15486) (jatin510) +- refactor: Move `Memtable` to catalog [#15459](https://github.com/apache/datafusion/pull/15459) (logan-keede) +- Migrate optimizer tests to insta [#15446](https://github.com/apache/datafusion/pull/15446) (qstommyshu) +- FIX : some benchmarks are failing [#15367](https://github.com/apache/datafusion/pull/15367) (getChan) +- Add query to extended clickbench suite for "complex filter" [#15500](https://github.com/apache/datafusion/pull/15500) (acking-you) +- Extract tokio runtime creation from hot loop in benchmarks [#15508](https://github.com/apache/datafusion/pull/15508) (Omega359) +- chore(deps): bump blake3 from 1.7.0 to 1.8.0 [#15502](https://github.com/apache/datafusion/pull/15502) (dependabot[bot]) +- Minor: clone and debug for FileSinkConfig [#15516](https://github.com/apache/datafusion/pull/15516) (jayzhan211) +- use state machine to refactor the `get_files_with_limit` method [#15521](https://github.com/apache/datafusion/pull/15521) (xudong963) +- Migrate `datafusion/sql` tests to insta, part2 [#15499](https://github.com/apache/datafusion/pull/15499) (qstommyshu) +- Disable sccache action to fix gh cache issue [#15536](https://github.com/apache/datafusion/pull/15536) (Omega359) +- refactor: Cleanup unused `fetch` field inside `ExternalSorter` [#15525](https://github.com/apache/datafusion/pull/15525) (2010YOUY01) +- Fix duplicate unqualified Field name (schema error) on join queries [#15438](https://github.com/apache/datafusion/pull/15438) (LiaCastaneda) +- Add utf8view benchmark for aggregate topk [#15518](https://github.com/apache/datafusion/pull/15518) (zhuqi-lucas) +- ArraySort: support structs [#15527](https://github.com/apache/datafusion/pull/15527) (cht42) +- Migrate datafusion/sql tests to insta, part3 [#15533](https://github.com/apache/datafusion/pull/15533) (qstommyshu) +- Migrate datafusion/sql tests to insta, part4 [#15548](https://github.com/apache/datafusion/pull/15548) (qstommyshu) +- Add topk information into tree explain plans [#15547](https://github.com/apache/datafusion/pull/15547) (kumarlokesh) +- Minor: add Arc for statistics in FileGroup [#15564](https://github.com/apache/datafusion/pull/15564) (xudong963) +- Test: configuration fuzzer for (external) sort queries [#15501](https://github.com/apache/datafusion/pull/15501) (2010YOUY01) +- minor: Organize fields inside SortMergeJoinStream [#15557](https://github.com/apache/datafusion/pull/15557) (suibianwanwank) +- Minor: rm session downcast [#15575](https://github.com/apache/datafusion/pull/15575) (jayzhan211) +- Migrate datafusion/sql tests to insta, part5 [#15567](https://github.com/apache/datafusion/pull/15567) (qstommyshu) +- Add SQL logic tests for compound field access in JOIN conditions [#15556](https://github.com/apache/datafusion/pull/15556) (kosiew) +- Run audit CI check on all pushes to main [#15572](https://github.com/apache/datafusion/pull/15572) (alamb) +- Introduce load-balanced `split_groups_by_statistics` method [#15473](https://github.com/apache/datafusion/pull/15473) (xudong963) +- chore: update clickbench [#15574](https://github.com/apache/datafusion/pull/15574) (chenkovsky) +- Improve spill performance: Disable re-validation of spilled files [#15454](https://github.com/apache/datafusion/pull/15454) (zebsme) +- chore: rm duplicated `JoinOn` type [#15590](https://github.com/apache/datafusion/pull/15590) (jayzhan211) +- Chore: Call arrow's methods `row_count` and `skipped_row_count` [#15587](https://github.com/apache/datafusion/pull/15587) (jayzhan211) +- Actually run wasm test in ci [#15595](https://github.com/apache/datafusion/pull/15595) (XiangpengHao) +- Migrate datafusion/sql tests to insta, part6 [#15578](https://github.com/apache/datafusion/pull/15578) (qstommyshu) +- Add test case for new casting feature from date to tz-aware timestamps [#15609](https://github.com/apache/datafusion/pull/15609) (friendlymatthew) +- Remove CoalescePartitions insertion from Joins [#15570](https://github.com/apache/datafusion/pull/15570) (ctsk) +- fix doc and broken api [#15602](https://github.com/apache/datafusion/pull/15602) (logan-keede) +- Migrate datafusion/sql tests to insta, part7 [#15621](https://github.com/apache/datafusion/pull/15621) (qstommyshu) +- ignore security_audit CI check proc-macro-error warning [#15626](https://github.com/apache/datafusion/pull/15626) (Jiashu-Hu) +- chore(deps): bump tokio from 1.44.1 to 1.44.2 [#15627](https://github.com/apache/datafusion/pull/15627) (dependabot[bot]) +- Upgrade toolchain to Rust-1.86 [#15625](https://github.com/apache/datafusion/pull/15625) (jsai28) +- chore(deps): bump bigdecimal from 0.4.7 to 0.4.8 [#15523](https://github.com/apache/datafusion/pull/15523) (dependabot[bot]) +- chore(deps): bump the arrow-parquet group across 1 directory with 7 updates [#15593](https://github.com/apache/datafusion/pull/15593) (dependabot[bot]) +- chore: improve RepartitionExec display tree [#15606](https://github.com/apache/datafusion/pull/15606) (getChan) +- Move back schema not matching check and workaround [#15580](https://github.com/apache/datafusion/pull/15580) (LiaCastaneda) +- Minor: refine comments for statistics compution [#15647](https://github.com/apache/datafusion/pull/15647) (xudong963) +- Remove uneeded binary_op benchmarks [#15632](https://github.com/apache/datafusion/pull/15632) (alamb) +- chore(deps): bump blake3 from 1.8.0 to 1.8.1 [#15650](https://github.com/apache/datafusion/pull/15650) (dependabot[bot]) +- chore(deps): bump mimalloc from 0.1.44 to 0.1.46 [#15651](https://github.com/apache/datafusion/pull/15651) (dependabot[bot]) +- chore: avoid erroneuous warning for FFI table operation (only not default value) [#15579](https://github.com/apache/datafusion/pull/15579) (chenkovsky) +- Update datafusion-testing pin (to fix extended test on main) [#15655](https://github.com/apache/datafusion/pull/15655) (alamb) +- Ignore false positive only_used_in_recursion Clippy warning [#15635](https://github.com/apache/datafusion/pull/15635) (DerGut) +- chore: Rename protobuf Java package [#15658](https://github.com/apache/datafusion/pull/15658) (andygrove) +- Remove redundant `Precision` combination code in favor of `Precision::min/max/add` [#15659](https://github.com/apache/datafusion/pull/15659) (alamb) +- Introduce DynamicFilterSource and DynamicPhysicalExpr [#15568](https://github.com/apache/datafusion/pull/15568) (adriangb) +- Public some projected methods in `FileScanConfig` [#15671](https://github.com/apache/datafusion/pull/15671) (xudong963) +- fix decimal precision issue in simplify expression optimize rule [#15588](https://github.com/apache/datafusion/pull/15588) (jayzhan211) +- Implement Future for SpawnedTask. [#15653](https://github.com/apache/datafusion/pull/15653) (ashdnazg) +- chore(deps): bump crossbeam-channel from 0.5.14 to 0.5.15 [#15674](https://github.com/apache/datafusion/pull/15674) (dependabot[bot]) +- chore(deps): bump clap from 4.5.34 to 4.5.35 [#15668](https://github.com/apache/datafusion/pull/15668) (dependabot[bot]) +- [Minor] Use interleave_record_batch in TopK implementation [#15677](https://github.com/apache/datafusion/pull/15677) (Dandandan) +- Consolidate statistics merging code (try 2) [#15661](https://github.com/apache/datafusion/pull/15661) (alamb) +- Add Table Functions to FFI Crate [#15581](https://github.com/apache/datafusion/pull/15581) (timsaucer) +- Remove waits from blocking threads reading spill files. [#15654](https://github.com/apache/datafusion/pull/15654) (ashdnazg) +- chore(deps): bump sysinfo from 0.33.1 to 0.34.2 [#15682](https://github.com/apache/datafusion/pull/15682) (dependabot[bot]) +- Minor: add order by arg for last value [#15695](https://github.com/apache/datafusion/pull/15695) (jayzhan211) +- Upgrade to arrow/parquet 55, and `object_store` to `0.12.0` and pyo3 to `0.24.0` [#15466](https://github.com/apache/datafusion/pull/15466) (alamb) +- tests: only refresh the minimum sysinfo in mem limit tests. [#15702](https://github.com/apache/datafusion/pull/15702) (ashdnazg) +- ci: fix workflow triggering extended tests from pr comments. [#15704](https://github.com/apache/datafusion/pull/15704) (ashdnazg) +- chore(deps): bump flate2 from 1.1.0 to 1.1.1 [#15703](https://github.com/apache/datafusion/pull/15703) (dependabot[bot]) +- Fix internal error in sort when hitting memory limit [#15692](https://github.com/apache/datafusion/pull/15692) (DerGut) +- Update checked in Cargo.lock file to get clean CI [#15725](https://github.com/apache/datafusion/pull/15725) (alamb) +- chore(deps): bump indexmap from 2.8.0 to 2.9.0 [#15732](https://github.com/apache/datafusion/pull/15732) (dependabot[bot]) +- Minor: include output partition count of `RepartitionExec` to tree explain [#15717](https://github.com/apache/datafusion/pull/15717) (2010YOUY01) + +## Credits + +Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. + +``` + 48 dependabot[bot] + 34 Andrew Lamb + 16 xudong.w + 15 Jay Zhan + 15 Qi Zhu + 15 irenjj + 13 Chen Chongchen + 13 Yongting You + 10 Tommy shu + 7 Shruti Sharma + 6 Alan Tang + 6 Arttu + 6 Jiashu Hu + 6 Shreyas (Lua) + 6 logan-keede + 6 zeb + 5 Dmitrii Blaginin + 5 Geoffrey Claude + 5 Jax Liu + 5 YuNing Chen + 4 Bruce Ritchie + 4 Christian + 4 Eshed Schacham + 4 Xiangpeng Hao + 4 wiedld + 3 Adrian Garcia Badaracco + 3 Daniël Heres + 3 Gabriel + 3 LB7666 + 3 Namgung Chan + 3 Ruihang Xia + 3 Tim Saucer + 3 jsai28 + 3 kosiew + 3 suibianwanwan + 2 Bryce Mecum + 2 Carol (Nichols || Goulding) + 2 Heran Lin + 2 Jannik Steinmann + 2 Jyotir Sai + 2 Li-Lun Lin + 2 Lía Adriana + 2 Oleks V + 2 Raz Luvaton + 2 UBarney + 2 aditya singh rathore + 2 westhide + 2 zjregee + 1 @clflushopt + 1 Adam Gutglick + 1 Alex Huang + 1 Alex Wilcoxson + 1 Amos Aidoo + 1 Andy Grove + 1 Andy Yen + 1 Berkay Şahin + 1 Chang + 1 Danila Baklazhenko + 1 David Hewitt + 1 Emily Matheys + 1 Eren Avsarogullari + 1 Hari Varsha + 1 Ian Lai + 1 Jack Kelly + 1 Jagdish Parihar + 1 Joseph Koshakow + 1 Lokesh + 1 LuQQiu + 1 Matt Butrovich + 1 Matt Friede + 1 Matthew Kim + 1 Matthijs Brobbel + 1 Om Kenge + 1 Owen Leung + 1 Peter L + 1 Piotr Findeisen + 1 Rohan Krishnaswamy + 1 Satyam018 + 1 Sava Vranešević + 1 Siddhartha Sahu + 1 Sile Zhou + 1 Vadim Piven + 1 Zaki + 1 christophermcdermott + 1 cht42 + 1 cjw + 1 delamarch3 + 1 ding-young + 1 haruband + 1 jamxia155 + 1 oznur-synnada + 1 peasee + 1 pranavJibhakate + 1 张林伟 +``` + +Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. diff --git a/dev/update_runtime_config_docs.sh b/dev/update_runtime_config_docs.sh new file mode 100755 index 0000000000000..0d9d0f1033236 --- /dev/null +++ b/dev/update_runtime_config_docs.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +set -e + +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "${SOURCE_DIR}/../" && pwd + +TARGET_FILE="docs/source/user-guide/runtime_configs.md" +PRINT_CONFIG_DOCS_COMMAND="cargo run --manifest-path datafusion/core/Cargo.toml --bin print_runtime_config_docs" + +echo "Inserting header" +cat <<'EOF' > "$TARGET_FILE" + + + + +# Runtime Environment Configurations + +DataFusion runtime configurations can be set via SQL using the `SET` command. + +For example, to configure `datafusion.runtime.memory_limit`: + +```sql +SET datafusion.runtime.memory_limit = '2G'; +``` + +The following runtime configuration settings are available: + +EOF + +echo "Running CLI and inserting runtime config docs table" +$PRINT_CONFIG_DOCS_COMMAND >> "$TARGET_FILE" + +echo "Running prettier" +npx prettier@2.3.2 --write "$TARGET_FILE" + +echo "'$TARGET_FILE' successfully updated!" diff --git a/docs/source/index.rst b/docs/source/index.rst index 0dc947fdea579..e920a0f036cbe 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -116,6 +116,7 @@ To get started, see user-guide/expressions user-guide/sql/index user-guide/configs + user-guide/runtime_configs user-guide/explain-usage user-guide/faq diff --git a/docs/source/library-user-guide/profiling.md b/docs/source/library-user-guide/profiling.md index 40fae6f447056..61e848a2b7d9b 100644 --- a/docs/source/library-user-guide/profiling.md +++ b/docs/source/library-user-guide/profiling.md @@ -21,7 +21,7 @@ The section contains examples how to perform CPU profiling for Apache DataFusion on different operating systems. -## Building a flamegraph +## Building a flame graph [Video: how to CPU profile DataFusion with a Flamegraph](https://youtu.be/2z11xtYw_xs) @@ -82,6 +82,43 @@ CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --root --bench sql_planner -- [Video: how to CPU profile DataFusion with XCode Instruments](https://youtu.be/P3dXH61Kr5U) -## Linux +## Profiling using Samply cross platform profiler -## Windows +There is an opportunity to build flamegraphs, call trees and stack charts on any platform using +[Samply](https://github.com/mstange/samply) + +Install Samply profiler + +```shell +cargo install --locked samply +``` + +More Samply [installation options](https://github.com/mstange/samply?tab=readme-ov-file#installation) + +Run the profiler + +```shell +samply record --profile profiling ./my-application my-arguments +``` + +### Profile the benchmark + +[Set up benchmarks](https://github.com/apache/datafusion/blob/main/benchmarks/README.md#running-the-benchmarks) if not yet done + +Example: Profile Q22 query from TPC-H benchmark. +Note: `--profile profiling` to profile release optimized artifact with debug symbols + +```shell +cargo build --profile profiling --bin tpch +samply record ./target/profiling/tpch benchmark datafusion --iterations 5 --path datafusion/benchmarks/data/tpch_sf10 --prefer_hash_join true --format parquet -o datafusion/benchmarks/results/dev2/tpch_sf10.json --query 22 +``` + +After sampling has completed the Samply starts a local server and navigates to the profiler + +```shell +Local server listening at http://127.0.0.1:3000 +``` + +![img.png](samply_profiler.png) + +Note: The Firefox profiler cannot be opened in Safari, please use Chrome or Firefox instead diff --git a/docs/source/library-user-guide/samply_profiler.png b/docs/source/library-user-guide/samply_profiler.png new file mode 100644 index 0000000000000000000000000000000000000000..08b99074585682f7d86f0b98faffdc10ae39912f GIT binary patch literal 605887 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%0Dy;6$4Sgqv8ijJF?zw)&%aRIRC8SH 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 #$rQ9tzq2 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�zG1{yMp=X41OloFBDT41iW>8X=Ce9qiZM4`XEDS zc`Qx=)L~7VvT@n%%!hVSTjw4Y`XSW2jov8R&cZ*8 zv^M@5Rw6=qZAE3YvBJDDTW&1|FS^j4NTD(I{wqIG4ogyR;jE@6IHM=jfW>7jR0>5+HScy2Oi`fEu%$K|wR*FIJfYG{2}Ej|8W zCbzMS0{A((WnJ8!Ci>o|oMyb8!u@EVFV~gQwr=LthYxXoU{LFzEm8Gc_ip+rq=v_1 zfN3Fvro0qSt^B;Kl1^5T3Rqdmr`tL1Z;wVY|1YAKzs%w1)zKXscgCG8$K1Y^<(#do zyl(jTV=(7AW>9(;!%T{wgJ5|=sm z?jXAG{t&wA;czbJM;CD1MGu6~-yR94^X_HE>7F1uhs#_(D~$66bGcx;iqkHc8On9~ z)0MNsxl9O;5%%6glv&(C3E4144^VhYBbC*3^ROMDw1Rd@%4_5H?-wPhzkeU~?ChiG zKg*;ax0Z8z{VAukgVXyZeWK&Q{(dU2?WS9vi>8jQU9=&rnij1sqOUiwlGxP4^IR?c z`YEVV(jIv)m2P+{lE*ZV$C~BY87$}C>Pu&Hzt6bSkA8iN5B-km;m`L3&~;BlP*G(k ztq!bUd8>|EJ9g3KiDW{}^4jt^5^w$<=)4cC;=&N!31 zx{$7UESzq8F^1mxE|)S2+qlpBM9Do_aA;t^^nDSJ%@>UCxv%rMFS%uIIs+Yc_w>@n z@EYpdV-$ENkS{T(g))oUslH_g`SW}nVtTgeD37_>)w@UL>XTIkJkFUk|NC6}aCts$ zj;s+n`D9fgMWr>#9KHKP9`ibuch(eAYJMwyv$2HfBZX{@BZOx0`ug8GXn7dUHt*^PD`ok@Kt!sAS&Q%)GLZ)&y7a z9G5d6sixIIRowSZS`}EuG*T<&L6^`WQRcDt(^$9L&S^DNSP4GZKn3Mp^d{4UAM>V^ z{5HDf`53yI*ZyFQArGknsO3s7aeTerMmJ>FWQ$y=6DGxrqI;fiY&R(wn0PkOQEN?bY zMg0zL*MJyoz~e(xn(6TcnN(WS!(|Q$kKem*zqI$NMnE;7py3z zoO1lHuR~%uazZs27%(Qo0E&S4t5`?C;R-lrVMn;{?X*Ed`G4H# zM{}2Wb|(Ft0gkWNj^KcUV;Tp%JO${W(ECG@T1A$#jxXq17;~$S!d3(0?}kXX;|q?l zpBUr#+Q70n#v3|1dMb2Lt6Rd61VdtjxXqrPqRJ;#}n)zpdr{VU_3_qUS*yAmKPF*R&hWAHZ)J6v~CygrODzfMnGBU zqB!h|gXABrER}JGP8i@iT;a>QFzEA~dqYJ>nes}q=!Vy@&Wtu($Lj#=0lFf7BklYd z5n?;Lf%U_wuO-XcL!Do)uM~#?oOIA*;M}_Dxp=Y1;1C}UEkM`A@3Zd-m3m-I#{pB= zh@eA3mj&=Fu@%9&Z=80bX(sO`xW>K z@`+-d5r7xo()@G<~w0nq>E|4>ZtFE5n?NuVj%ES_fF7W@Z}+10^y;)o69XTQs|2get5 z%jZ7Nr7!skyUAA8qj0DK2QX0%0S5$O55c^`z60kc=Ibub11JB3?`F`)Ysv@PG}aKz zbcywQG~sRT^N}21I0zZeJQ{Y%*LeL{#~0RvJ$R!7c$+<-0_QQxSU%wlurc7kH5^|! znELkj#c}`>w#sW*kH3T05Dp)KXW#&)*@F&>?F<2R!twRuqC7chi2%nJ_81%*!uVRp z7wlZ%6L5UNX@vlu1jiR_6mWdOegU5F=!aQiGl%^QHa6^Wus7hqAnYAz1NbBkkHj(0 zg%dE3c_sG7LVhmjW9il!IWVw_=?3;6@Q!Qd#K=0v`h;C?ZCC@-PaaKsJw?_9=mIuc ztif}555?NRI>T?QAFPonuO!L1Kl51*UCHtdXmw-I_&B~0CLLdsVcf!aJHBv=YP=j@ z|MhPO{$_(Tr$o%g25_lAXhA!5wNZ)|Ef8S+1PE6jD`1D@)OT#hd|Wq#`_ z3!Hg(mN9Fk9GT8Xw_`2n#L>`Y&I%fpOmlY5&{_yylx4LkWhsSxU znIAb#!)aIWrV4;DqCThN3x-|1Nr4kVcpii-2w-^2E>(Ym#>2y{U&T~1wph!UY`Wp)?7tM?k$1)sWXR*?Oyl|#~&z;K( z3{KfuK?Q5*Pj`jLn!>uZjxVgixij=Cn5G z;-gh&?cxoECqB-i=!`bn-M62WUgP|58fP{|)YCT`D_F=f!WEn@%l)hAjV1Z4h-{~a z=BLs9Z>6yCSx-dgT2y79Jz$sZTPgx@o5v+_XW@Tfjh=kkd*t=O@#iz4(+sKlS%}hXV)t>62AO zlv}oKL}vo(dw*F0Wfr&7XKRXROH{2Giw6$&i!mI=_xhF|+8j|Man_*R&t)51chK69 zDlxz#4#wqxxH<|;YM`jJMlMrLzR|T5$@P4`rjXL|Tg0#q-v^KbcYMZ)g#+&i<(A)7edwwEKeJO_9Q$X$8cF+p{ zQd$*IMvuIkBym2GRnpF_QPuSI`a-(*^*E-rO8RtFJ~g#&rvv-c=C zU9{Y{Yw&xYx2?0{!{!t#r}-0rbZyKJ>fWHq)P``q0(0S@F5opU%9~m(IA;hyHNK7CLL1 zFP(PlW;&1K|H5U?=HKVv<40G_4B&E`=sb?Q;9mayP#~Rq7aTLbT;7K+c_4t!=d|~jN{Gux;HMfDsaVIVFEuxn`Po>uv zr_yJua_FA9QFPILTj{5uVqPEBwA#Oz;xg)J>6Suzb8#9y^Kl~QPoe9d2&E+(3K;qH z&H8)_imR0NKk{xIz4~P;KeLQJT9!>$%?_lSpAD0;Nm=#W=N+t*_M9}>yJt5o^(mBj z`GoNe_xFv(>Ac1p2kC6*&K=a!*2QyFO5NLghRrkbMW@yZU02k!)8_DUu6Ng9I?7`n zI=i+@+6POs=-H2xccqg<3kdv>7Li3>FsYa=*ypSd5p7UesDi) zUM%yVTw2BJ{lg!#Y3?GPuaA;wrC$kk@H$ztp-|?{H>yJBs=2M38k)P9*X7eG@1oKE+HUWz^Wx#m|lBvD(39is<2Y z;<>GDoL0*8*GyZZD=CY4&})m*xo;H|&ubX;ekJpUGnl`e&iv%sM?>f@%s(!Dfc1pw zTj|o7f%Ko(ucu2MWIgTHO_J}o%x}O$vWr`2Pv34qVs-;1FcvJ!VLo3bd=JJHClL46 zk}R74T{f5LrIM-+=~HvtcJhg+qN@5%+QR&K;J_ZPyOkoD?_?IXP;7bw^P;`v6ICU9 zK_%}ed-wG5zO{$Ppp@D>dHwG0mHlC3cqJ89bnqV5N4>qh{LDSPkJZp4?_6K^n-7Sl(}$5AT3yt&-*j}x6?j)Z)q;26|{02_KHDiEYMEhqk)wR1`h70 z*oMzdHiyy?7L$WBpqLX!)RyowsvvZHgDX)B$O{Y%@kn&G{oX%rg$?{h*T?Coyo?trX zt|0m=-vigqiKJQYrP7=a(`o1KedH6}z%oOR95gB8E1aWndM4*V$2rXL1-xhBEf;^f z^r3K?HaCgxelwYVbDN(yCjaAlA9{6hF2TtOquof*&d4J{I~GR++F8#s@vDpThRM)Q zSsHH{zs`E%@HD&?(I$>1ym{&u-z0CS;!6QAB*RVs9TfqGyKtaueWY<{A;9sq434jS z*Bf8M9AAgUh`&+4Za+;;j!jjZmj`eBrA+&@tgSx$(Jp zvBz9GJ6a9^!Ul)mXWbJj_2A1DFsQ>u1e=dNq;B8(fjHEE8!a5gSmRh5SZ7#|;{0OV;c$Fi;icp2XZh4&Z*SkQjcyXZY*@$Fo;`bq zrHvJM-4drq1|46mhTiableEH4xd#(&Za4Ax49kL*aC~*jsqn`ZWC{~O8XRBt^LS_8 z9U^ZkoN-q${b{;6xnWO+;5X8+D3R|>$!AW1{C=uAMS^?e!)Y&g?D5L*Z>I#(>8`Q> z-Xy{au-Wft(9HKT=qDB|bpIwY@W~Rqd4M;jN@{itONWyILKWVcuyGyTduc^r9Yu)a zYX@CA3vYaNG6PGL@xWUN1ywz?l^N;0?+fYURiz@-VT>`>5Dp-OK&Sy^e(u8jxC91H zyM{MuoPIieT9DKSCj0qMW#kWM1*hROAWod%c{e|wNLS2`l2>N`HYb`HZw~!#s;Lie zDdE#7_)OpF&UwEpR0I&+UkLVas_OV70j~`&IEr%kL}SenHEGjKTkur6@b z=@7o7d1J=w*bt%-SIzyvCjoJfw<^#!IKHsa!D#`9B#i$^w~jA-mhy>@;P^6a!6rMM z1$lhxX#Ub7UIUThATW+Ed@iI@98K6{@ktvvu`mwS@dX73pPK*;8OPUdS;x=v1_wa` z&p=sxrU!50;L|rq$ERH2;KOFTDyUB89D;3+A zHb3MI{_Bm#@%23mZQZJr$HX_CG)hhyqFV}HBuiYD10JlN`UMYw3ZVvO5H+hWi zeItb~=e3VdWrB9`St!sB_|<1?%Xls96K4`m!(l9(^MbjB&>6$Zi;cruDG)IA9D0IQ zPBji6GADEKnIiB&tTlW(=s6a8Axz*6ue0t65r^eX&&C-C$`diXexhZ~UNa|(?tVR) z{&HWaIA^YzgLs_Y3Z*M}uFt=p`#e2}mH#k0=N_z0yd@IAb;Z)n&&6?{6S)6<;#`5i zaU^KR%1F>olp{eKMYMCJrDG@E_CgG$7q*EraSpw8~yv^uCl3`5qyvv1#CS{G7Di`Ny??%hW4`f5E3g+68U`r<6gENY{N z-btkU-;AgAp_Me{Wt>nCqYLf}q!&I*V;)vOXHEB$^qZcIpmXo`r|X{#r;k_U(UOft z^!b{6n)_8I-T6|C#N9nNmacs)RPtQ>Kp|e!;SA zN-t;{(V2iU@BWZU$+->m>56RH5K<-vbs#vtin5CusiLNh)(4kR-)_jG#-O}|+frHE zPOJQjB!2t$?ezJoT=I!5r+`=%>=Ub`%%-qX3gLQCcS2?@J^ntA`I8}>H=8zdT47l; z{S;Wt_pF4h&l`*{C~KnXx;9#|wSfBf?4A8>MX_-$x z-NtceOxZxczhxc$_U3hT8sm3-k7w=Z>7t(PUG&+?OiIeCp&xy6=~p+bp?}Q@poXS4 z>fE-CIyyRN8((YcTIg1;>*05z=)Ai((BqC_iUjnXZq0TT8gi(>Abr((VwSp zk}{V);KTWlcN3@i(4{lBaGmSviU)nU%qAYAH0s^eMKP%r6cSfTJ9|3GC%lN#b8EOi z%=eSaDJ-Fkx;i_ky}h0L&`$M@E%elfF|>GXrnEOcy^{OaPHk;%6CT^R{h4_+^zSDF zsG_=&=_r>z{xO90gie@tAJpvEIV-vWZUn z_fK@rog3(^DeIXI*3j>7{fYiEeFOdDQC~{Qs-*8WW-?9WQE5dz{je#E)&%6z=FkF) zORJ#H&UX54Ll(XGX)L|^MI1jnmF|5tlrFw+6Rq&gp}?48TE=Y%Pb{Tx*Jsh(FXDNu zqv!?Bf5YSc^wpXSrf~z`h$3!p4$YnyK`$?iqqPBf^uhPZboIkNbo0{z^!k_a6wULu zt)q=vTU#eQ>>O?H?w}>>Gi7WRFwobzUnEd`hMUgX+uNAeG|Bumx46q%ARp*CCZ&Q3 zO6zD{U_Nc{>KIH%`R_MoQDak!q`muX5|48uFvLYl`t-^IZ#akf z$(c;I|9I4odBSG8jQPM{?%GV3bK0+NTqD21XU?9={N~p6l0PMlx3oqUfTtvkcjl9nkGT-T@wA@8!9;;&BQ?jV2yotKIyM#}Fw<(v$lh^OIPM(_rS{qbE*@gAIhjsCq>Xdf) zM3nNL5K9?(^;~8<&p|%5wQr;7lq%lm{iMELuK)YZsH>gnrh~p%olV!x38X(w*~Buz zM!EixufOBI&wM+YE}Q8m`_&}2#iVx z^uv~7`ea3}k-6lMh99Q|%5NM3d3b&TJ@$SQb#(5ajbW8kP~JxKzRi+rU|cnAjjpD+ zj0To3_tCuXvgoRZgIP}Vq046l)AUzkS+9tr-?Qv?`V=4fuNybgD~r-;-`+jq{M3Mf zZ!EB~Dh{jz+DWMa!{Pq@`>2v-;#U@B4s%Fa;Emg|+HRV=IA@qM63)u7fPd*T7KRCAyVgYy;~BrX2Fkem`A4kAu@MKzSup! z+TeD4X+07st=q}ENV4eZ_>HpAMRC}*tbQl!&ZTnj6mLcba=(ygYkZ5?P;l@OZ)#0@ zEkzDU!`_89z=?&=-9gX9Z`fF1Z-MO$|Gr^5>y~(X3w6Sw0*3*dbg-r2lghW?Ut>%U z7Na@ds)T(AzyIR;Y&MMQ|9B!+Y(%j6z##^h&oc*`!_sheO*iFWM>_i+V`H+mKiK?` z53u$;#A6(t_CeGq4lp>phS&$qzwS80l^5}_U*Stc$cJ)xtIgJ9>ooa-WDMY}!l$4y zrf|G!yVH9=mdGJ;`x~?aN5=chOT-3(Pj`PTj>-LECxLwe4pZdCK}*?SXKnJdm(*iHQ7 zn#IuuM>Wdf8E}Nb9)fv=edm%{kuqQRavpqQ8K2h0zm3o|=q=!Y+UQynV+Cwha60LD zC-8|utV`G>Utdxn2fGoE^-$Zgd(hGD1ng_D4dNcoW0Zl7O6SWh-%c6DUCjH;r~BUd zp_utno7gJ<@nkIB`C2mJ)AygQDU;91!^zgW2XB9yXQEDg3i;))jN=P&;_wv5SC5P@ z4iv!F1Unb_1e~C7S|P9f1T*U0+kcQYL>k8zJ|74hAna$bvB5_Aqd&edQ^B;6BW=Ko z3OMwO{CI0OuX4M5a=ehA3;OucuST5ztC?N)(`Hdy#~kEZl>8Bu|Yk(FpD+?=Tc4833Pnb)HG04Rh>L| z5|gl~sHmax@@jF6>a?*Uzo?qhvn!~yyp~GKYN@iy)LC9pM`h)P_DUS_6_s_AS6D4+ zxX;e3qJrWYNkbh;8Re9cX^^auG4!|1HzY6eIP#@rvQUv#L7BOg9?Dlp+q1Y0Kv9Wl z$H@ZBUwTfZ^bcdH(=i|UMKx6AnCB{7ITro^gE+@ZgaGuaMq~WUPz0^fp zPE=ObOIvmPkxchHlZD6t=7wL z%#Upw%AkKGd<88c9rT6xYK}*}(pD~uH7?KRJjJCoQcn7geifTOR`OnzS5PJS%A~)b znHm{i)Lr7bN4-E=7HF-UpHao-v3^0XRorjTQ6*&vjg?VSx@q4UuHxS{fD#n8BF;j^Al;ku?~mIyMkODQ#@ zgwpsvDZQ8yQdz-DE~5C9B1%j%zYPjGu28PY>Bz(P_|1st``Dxcjw_IBjPpLRfT9^T zZ$e4|6&00hTVxR9NT@N(PaaUre!yxs;t#L>XCyRK#()dBv3Jy3fq! zaY)VMyv3B2jWNjOd<9&th|)6hMeqXh`8SZ7o-g_H^33n#v^+{-ByhQ`Y#yt`9EwfI z;e2@%6Q50yaaj}*n?>o#p#JDNXwCHd{Q<=BQ80IqB&1w zY^ESQI)lO(h>wWLpqPX#3W-XmP)0caj*iRZ=Vi)$ET?5;=2A{h{vfimJt1#Yq^9Li zRBQ%iWx3ybU?g%}45uZfWKu+QDuqU-P)uAJr>ArMnG~OpF88tV zX(pb3Cv)CZPK)EXj0{!;I1axPIA44M@})`{AUZac!lP1njLa3kfvA`it}~TVQh3fH zlLrwJktBH$k9q*a+x&P2o*NyTB6(vuJ(AOslhe7+8S;GFhNNUXKZV*qGjzYuZDS+F8 zdhxudX!Bg;m3*8h2zf&>cQMj7i%3^qv=RC69Mp$)_yw6b)RVx^Oh`zYz!*A5pfU8t z%u9mIMF{sRhR4%MPbi1^8$QQooIjQSlsv&<3Gy3lK>Pf8-9>R*BBRmQ6amVFb6Q9^ z<}5+-ATQbs;Q9ECG62>XuHY+)iK+4otT)8rKA!7DT3kGMskw$mpuS`&8yMo^GcH~t zJSN!i3(Gfr0^R(?@Hle?KeN0G^#b5?cpmtkleYmWDZH;T&y0vddOEi^joXkSd^d)# ziAmtY;J>{0B%)66Z*xs!o($d%zMhnX{Ubwwy#aOM9y}a8Khcp69uNMHJptv?(lR9< z(y%uKg)wjA^6BaBc&V4~gTt90gd`3F>Owi}Z2_T4ve%_@y@-pBP2;+<1lX67c<;2o zJpp^|(P8NRZR52(Fu4Dxuxu2K{dag;7Qixvl^3jh5f#g_P-L1Qgb~K&gTi@SB6yFD z;&=w+qqOuKme(L-7}+E~F-v3>DZ{c9@+5Lvd{UPDhI|F!9uQe8-pE>zhmu)t%gD^* zda~u3nwBf|r={nzj94J@9OSjos0>%xECGH4A(0u92YGXIS?`E;aNy46`33Ic2-WY$ogrSMWQ<( zA7pyO<>i%7dX_1VGTD61FDT{TC4=R%vWq3ZjYqx|)?W%auC%m5;?gsV`FVI&3FYLW zo)Q<(juI|gMrCD{l2%k)F6DCh9(qt-KI&oJIM3wCFDz&Mv0U8AE}8YN?A$WZ#WHissGzWdvh$FyLhiFTP2!7Km*(Hl-|`D8Mc>QHE$8yk z<9JL;sw58jVnz<^70^3NSdS~A1R%9U@}NvA>z=t>M{#Mj=&w#4)S|51=(^C8tE=lo z4@TbXyh_oNq5s0po632NZe1_>aT4pcu$92Zm5~EoxLoWh)_#Nh0DfEh4(vDSImSi< zTMP0*A4h$NhaCa=VE3@;u<=;i5bniplMUONvFTV_kAd4(G}KOK>~Vwj*nA2eZGM2N z&zV=(q4PQOJAcD|0$YZ)Pr@#y?M|>;z&2rj!`@+SXRv|5b^;rY*hgG-A>P(!ZKT$g z1A8cJEGQ3T@f^dhgE>Sz+Jt9dK2R^7ZEPp?VzWUWYrC;+M;QRmz;B@gt`B)ISJpmi zo@eYY&OVL>#*TG#c2wIB9q$C%k7uCo7zbPHZ~rMt!i4S(_Z;w^NVr#yrCO#ASh;bq-vfNk%| z1ltFUw@wo#GOSOIPcR&C&z-Hu)@93D3>)LY{2s>Mrn&Cz9P8(J!r(j&d4{da4bOAs zslP{t=W+55Xq%h1J;6=u!`tkb3m1l#G3(2Yon31fqrr8CG)F#LxBQlT$aBC&-$UDm z|5c#dYv0_#|A86$CXYMbys6VKn-#A1_u$p>NfVRD_1m5@+>6h#I6srJ*A=%%uJ(TK z-r*-4-KQY?B;DlUP1K&4lnvDd*F8Q}1GwXRsjs(J_+KCH`*4rXfH|>?dV9@hb9U~; zTQv4<8YkRu(YS$At@<}U^W+&g{VIT9U}1j`#`g?fcgVfD;zY4ro%eF`cDwoAgOj@o zIL#~Qrmn6oL1*V^=oH|T^XS0aB%`Cf9dB%S1DvQgXlHEeFmX2C-nX{8PtH5?T#j^EDv=B6h3-8>|{ ziGMdq8m>)zZES3$h6Yb)5Y*Qn8OYbzi2M2>sH^jYxN(lUw^4s1!A#oKId|$!&Jp<|Gll&DG?zGZUH!8}J*IW%uUb))e4%oc73h*oo{d){3 zEiI+u;u0z;F&ORMiPBONU&iG)yp|j1u9*NV>aHS7vuk^+J%!vro{^9cj3>jzF zy(hZy6z5DEjpodl52XP&41HMo2fYh$Z{uvd{_R>DcKz5j;#?=T9;`p0tjt`UKsg-; zl$4nKxQTo-&lz-~f^<&c(MZEdO%C@FZs%?%i;gHTi7*;n$&jS@U81 zwtRY!cZ2Ur9_$Csc(W(q>f9$}FR3EkJ%51#{SX0lo zJ`6||fkb+TfD|bKq*sB^n;u8@ znu^8b$7;81jLC|bw&UIVMxHwN%#2Eti%87ldv)|KDP!t?nqbVbSIac{CNV7gt4>;l zP)SS7cCFS@)3T1{SG$K4-*=rGJ)0`RT%Z{a&r)T`PiM|mj&re437V?Ip0rRstz76co zxZ;jWQpMzYslnhe7yHsl<=~9Nk*WW0tv9BVa^>EnZalH&b8SN(-JAYoPc66{d9H5` z`dMd*TcF!-cxs>BvZe(|?wYmVy!5bIWZR}JY@ZHEHv93oS&ix7MUphborWF3usWKk zxbEraS#A^Y>--N4e2U}V2vzJ9f6b~H8Wc6DXbMydeAXNxV3%v@l)E~wUtic`b+ouN5vwE-!T>EzIF?y0)8rF?y~}8_93Mlw^h918H11*LOP{Bqf^Abb^Kr!n6Bg zRH}QmxnCK{IfP^siiN3E*C%o@m2^KhY?&-N6)2YQlS4-q4S8f0gT!@}gCK*HN5y8M zM{AbA2fjU&S#Qzmm4js?dDKtzhI8+V2<7V34ENS>8x{Zh5sarW&D|Hv(JPs!8rsLZ zf|q5~iFTE3T#8NXAW2o${BF-wMMz6V_c&I+NAhbHA(K;o$OsW3v-SA?oy|cZDjJmgt6pyu8H*4h4mv4mK!-3$SCNY(5GLQyyE`4>ngr-EJ97pvl;c zrOw#(Asw&-cG2CMf4Iv!ueAFNc#SFi{-?_KM(*y>bs*>ShkX=IMAD)6*8NwynkeIE8piS3NKpSKuG*FW^*pGw)vy%A)9ApAUQuz@g+{lN; z`2xNxDrC1G%*j9dlT*kJ`f)U%#J}S%is+d1U-Kd2wPu zkmz3>Y^zoyP6;mGS7^XP( z+grIJ$vVWmP3_izTF>nPRCB~?=KRA$JD0rE5DvxN2Zf5e7C?ysNY2DhF_<1!_NjOky+~`#`8z`0`p_crVOB5-x zRCY^~PZ6`?J=!~C#H4HN!vs}Jdg?E>DaSLU3tur&=G(J&v;y#bbR!#gRan1qxw2z7zZi->Dst;zQ}XNOn(7c;T-4 zaO5f=#rZ--#?6HAal^|By4}S0p*IBL;+)*vtkmz{zwe3df?Zk^6sjrVUE7WBtNHt1 zk|>*+&T{tP$OS5Ji_S4Y*FA)uu6wML9r>QpWVCtT8bHP44{#^o4@9Yjt?mM%W8_fW zRR)R!P?CVsl#_S5o0zwP3y3cQ5MLJ{zQX(!PQBrSl*p7xS;f8p24m;(;n&5$mrs=Y z+8B%jCx#E|BU2VoOKD@iVUZI+r~Bg&1kF`ca{%!=cfZ+(5d zc-h(w6S=t4(B4GjryXt#+Gxx(XW#XUz2CUE4sixY^nouKvyi|D+2X$ zrL+A1E^o6=1NJeE#4sOmZH3QgCH%7gNPvi5uN>`|ihlFdUh&w7`H6Ab$%Mv70xV`H zZ&b6_Te29bv{GiR$>%mN{SgpX0*&#;_^{^J`azyD9>1;d(c9-nS$fkC7t6*p7`d{B zBgx2|=;)zDN#fgZIObiXxR5W7QkJNaJ3=8m2sI{gQub3%qTLOaXd~e}4W7 zC7Hki9Db&*-1m&ZSRTl1Gv_S@KyhF4SDw2tyBY@qJK5>B3swWtaY|hF=)dv-@Sd$&-QW)jCL7y+_6y{Ix}**$&@-XAzZ+=zB6N7*>*{m zvSA;i_uc(vfVD%}Zt@(T;D_9!r1xv?5f!EU)rTgO}I7Cpa|& z{lQ&>kwOfKJ4)izH6Dx$Sk1yOk>bOz;MNt~@LSc9;+lS;;@%E$j&neU{(8gl5s4rM zhD~ZEB_&myQ3#zS6%&&UXn8l3^@iLgTqKkTOhvUUhIi{0uMkff*a}3qjW^1_$7rue z1p+M+;L-nJgk9g}8pyr8O1Pt-&{o(3_i&v1cAOmkXDzYkJL9?(eLT*CdViPIiQ!-! zR$azI<#o3^!$-0VSq?U$cPzp>^Ctm&aQT80e!WDJp%;fga4IUga1_pdDZv#|u{eUT z>*^xIECBMyC2L85uLr0r?^-kXvNFYl>F&181GaJZW?p6qnT0Mp)aeuZD`BP;K*D`a z^U_n1z0v&2G*rU32?z+fZ#$no?)x||{R_#oU8bzlH;0-@|8)E|dL1@)o*pr=7GHd* zy;Xu5bNh6>OSvLCc7pilFFiNX3`OEZkpU-v;G}oh^V|~&oN&Lf63NQ=K3KtxN;Px$ zB$HycNO>v1Y<#YWRhXNTf%69+lEzoe&$n8JOGI0UZ*FCV3!QHNmf_z?t8yn`c5cie zub)?ql35!?w@AIg4!7_5CrPpoGbU5|K9(_$|AIl6j^cHDOMb->zyjkkmrK@DVE~>ut=_m+{M$c*xN=%}h^?>5qDzi)D0b@_j{Z0#%Md zY9EMfFV*AkmmM|f!w94-4d2y<+&;Lwd|D-0f&9J%15hCFnzwWrGNhNpY4)`eV|S$F z*Y}Tkc-fQRLuelv98D%uu|CvrGvN_Fpwzq*n0EJ8@nft0*P7be$I}`F?duYEk^93Y zL?6VB<5M4NGMcq`!0ZbvBE^|x^5cdWrV(ZNWN2Nu^|wgzeh0X68}}Wz``9w>8y%O# zo9U<@F2_2tj6Z_Tx?^MLXy4s@33|WqiQ?xF^aY|*Nk&EX`{ZOQTDjHaY0~Z|7kaHb z;y3Um{yjg;IbFTa*fWW=4Wo>?=XCgzwP%&XYV4B^ zihxnVS@vH3g%r!My9kpB{7}!`{;+xr-Zw4beH~!-Mo2_-E*99*PA&42gWfQ?oF3hu zamL#%x^jvYUStDMQPDOxr9)n_fi+RlT-j<2nSa_CoCzHPZQTUDj>WK4o z+1a;xUGaak^oL1t#{6ls$ES^e2}Oz8xH-!vMIx%Vz*x6-_;neju9Q!aH<^GJD^x9Gf+PM{^rz!2YmWkZ zxosn6xUt?FrLEMQH;>ASSz;)iM1d+z+!=tm5@>eIi(7?_d490=$g^)6@|GMfwFY|F zMT$hcsFRw^Q|{?~*CKnrBFgD_DSV=)^}Y5#==~(@bIY|ab#;Mhn7(q3CPDtVgyqZX zqrIRbt~IjC5E`o3=0=^RR1)InLi2H4SSGOp2)!j?qoQGQuDS{eBb6k1AO8K+5T$-Q zc=baup6cWGZ>CFdc0SYbUK<8`HcRSdZDJqeP6rFY)gKyrLRM^44#hsaR{iNGqR9AI zSJURBfu}E^Y|Q`}E@ zPgmw!{>*a+2I8J|w&P#Grd)Q!?QUSq3tS4Qp}AlM&V-9rJq8+NEGH)3q{^F)HT5`s z>c%Xg_AtqZZlUc;IB?Fn&Ge$_vUAe%$En4=Yfr7qoeb>EgZ<3xdbaAa2W>aCjF&+- zw}b*M>Q=OqrH(GTt~P_a7D9gdO|V|5Oi5HZxh^%7d9gNCU2N>coypO5=YFjtT0Nd+ z3^Jc*Nz2va_(Smha-=ZQrNNA!=3s{MW*}~8D{jED7yS`o za6<7WsWDs9uuJLV!ckW}?d_;5qEg|uw z4UI~cXQHe$Z2a3{|NS*p_mL>tVR{-3HHeh&+5i3T--|Q|xE*gkv7APxTYe8! zvHbPr3F9^`@N}fPZC58IJYo*d|6X9xt)}Br<+=jZ`F|cy#f`Bj912ug$`7x&`?kM-!!r8s`~PUkSuS3*7I5$D zKX=Q3I=4wI{{5JLPmDyni0VsXQ^Cdmcb)&P({SAQ>euGG54E!VX5aWN1Wi(Gm)$)* zYMQnj1nI}@d60b%ZM-y(NT7m{}7rs6tQ z-@pEza$>DN>h#jFE^;o|2egnwDv$5a{T*rX{Vkk|QoxXK#k zI;z1PF;S*ad5E!<{vXYth%v+(e|uvOAI?*3*r}V@&g~*%^#|RCSOpyadWQb;wY+)= z$R%GpMEpYk4!sCxGSoue@be>f!!lzr!xDpT`Oetb*yH#)nn!dUwBAP}s1CrEGoLI3 z-B4|C&%cyfey6N7Q@67%8DA{ORI)F9#cf#Y=b-FVH&GJ9^`SO;_7?bxR zE_y&>YsP=?e8MfeMxx^2%I^*9@gh=wul?45)SacZ51bI==9|F2@D`#7js)5Jmum_) zB(xU(8fmgVZwF5M`%CgIJ0mTxFA26(Dwlt8{|wklpog^M5;1|Kz5X=MH@JA4k7`(y z&mLM1$()vLX=EsfG$GD@qC2gC@q31*bt!*ciBBWvn2U9_)gQL0 zW-KQ_a#YuGeYFT6Uw2<1X-!Sdj&H-s^w!+ppP+@aZv)(fQhdWwzNP7=r{e_RCgcY z1L;_`;J?0n2rzGvKGP=N`{n(K+!e*_6=|0nFkx;azn10xPnS%2M3cT(>-by59kTQW z1-<}sj%RXQ0jbnU8yg$5!+SXf;Cs-Db%5T{3 zp0K1hU|FWH8YW@#nM#N2%QG~PtMg9K`B?5<3Hq|fE01Yb9_7??p9O^c-c5Wo6=gOR zawQpD!br|2-p@3EoN*T`uKC)aVS;F#aUW{wFmY*d+F87yx61OXb3>>WTh1(XYZ)9jIgL~u^Q86(Hu1~ucTq-P z!L}Wz6uMO$mGi8efyUw&^TKhj1NxDt`<>=EWV9zY2j=}G)aj!h9pCiLPww1EN?pnC z;m}?id3SQuYZjCc(WOk#mGO}eOB@XbdI44^SGXi*=wg8M_S3Z; zp3@z(t3kLIJ#iPmr}5zw0d#Snmme%YxPV`fNz8)2Jo)-^&6UY%jp1M`Mn3kG8lyDF zL>srHIe4~m0wHYkqvm^h`=eH_W1CL1)Q?(CDN|_Rz$0>)>#DEKYy(X?^m@2kd7`8^ zC*y7_PnhT9`ULytq5soH4{dqqLWXyp|APEvxtWZq$FE=cc0(@P^nJOR-*@U4z&Spv z^oPnzl!<%yfU)rav0M5hyaLGL8ZqZNBXD^%rdQ(B8AU;Hb3WOoRAJt-Ed#b>4LH*O z{JiFqiqHNKE}JKslQ7r+AUYtP)IxvO8xx2A@XTBzAXm=M6ANf`Cy=R=_SLU1)Gr6K z>}UK=|2TQ*bCGKh#g9Q-v5;mU**6;KRCfrXHTfKY(bQwBtUluh9l;IKZBI1<%CuaZ z?P)Bzo)@O=xG&6pd#!^MdWS-GFIYtvtfORH<~}4hSwQut^}^Xb`2mZh&29sRoKiwh z?pU#I=Y zU9_tdPKx|Zs~HX*fF*o&0jv*zYmpIxT0w$90!OfWFlA$S(rotJy@+6*24I}s-7@1S zQo2A#eNTD%&hzSQD2P0Y#rHD_cfVE^8o3pGiyY~bo0w=XVH4}J()+=QAHk#yVvAUM zOD)Ca&u$b9q*XW4-LxP3^wD|QROm%4A_xDEjoKrh#SkJL9Q%oFIm&$SQwr&I1$6&| zA?>O2uw#LXT)*-xl_TNe-jsm$RiX~D^4yl3nfRn*(l?_kGo)~Xx8-tP5pa>v`7zb})e_qz@dlAg>)Gm~GGh;! z1Y}MuDQazbnKQ63r)J?{5Atc*Ty z-(-%)Bq$`_edO|OgDWOkbqeTgFTwo}4Y7DYYO-#k}sXaC0`T~1nwg;9JZ4Irp`wkPno zCSL@-zPWtai?F6GZVU4;MohJXqa}lraz$fck?3W>4@Ei+^=e9!JB)v}98_V7Ly$>s z#u#7T=SRo17R3?Yy+wGZGN`|`hqmKc6nSnMUax}lYkG`!_n9x?AwJIpcOg>Re_xI1 z!7j6G>^nhs!LLAk({)Y`9_a0s#Zc1D^^wAXXziQVu`+9OpT1VKPH*0FWZcmuw?{?F9OmPX5=&U9+~Kd$SnYN(vJ76Czjq?ch~O^FJ(^x zs|)$7RT^Hj=6h^JClg8n!cowp1#Pjhmvi&K70%!D)+hQ!pDB>typ(x}4m}PvJ5TN+ z`<>(|3xQz5+e6txy${D9>zT+Ahgby7b;f^J9L&%g5mxGA`x0Aad+DFWM*b$AU?8)e zIOwd;Lh0(9tkfl1^E!Jg0ROO^UjPs}lC9?SMDaLSW zmX9GVS*eNA6E5dvkH;T#WqgCrS1bg(c7cD2M!^Vh@u^qW5nvAGp&YqRh3kC^%E9kI z@3?Jn!0brV*X~Nn~ROZv_I$S>(y%Wk%5W{(H%E}#K?dazaWRhOLP>g zu<#wX(ID1#zg;m2_Q)-n`)`EI{q0P#a%|n^_0Dsh@7NVG+LuONcx+9G>677Z7bZ-n zy%2r9%)^(^*yVk)H96$SB!9`x7scq` z+}A^{mqi*$r3#?q^+Fl}W22jc(XJnfg*zG6jvK5!l=-m^pvt(j_TBrF99QcU>T9_{ zt#O@exn3HVp0=NM7#|hu&V>)y6}JW*KY7+f4w^7JeRu8jyZYmfGIq%q)@8Hp)1c(P ze6t9+vhWVt<2`Z6ns6Ja4_ufI?aq!j_*Vp0#YqW^fL^9H!*uJIT`Q9@8VfN*NS>$iYe=7iAGqRR!o8|U9pm?Y6{=Ba{qFJA3zl{buXCQ8ZUth3 zidFvY`Ie@Sc@Toa8)ZhZSIJVq!er*nZfX+1`{_u$8Vb4|D{#=2TA`x6AtYyl>!RUZ zv>V^l2~?Y+UE32gwLeUB!$=bbA<{%^rI@7{sBWx-Qw_D9%daoLO54DU;3r1bWH6_1 zN27fP`fy21v}QYvdUC3or0kiEL6=*|ZARCw$Kn?a`-_B2fbkRoTm7!K8E4TKh4F#& z6*K8fvf6PhBlho|Q*>RFN@~bNmn+)H?lvE4Gf4xfpmmDy0KYx#*j`ojWv^#EN`Jo0 zM1m3zoQ6Ai3Lod#VUc=WbWC=Yr5)|%{l%idGdN#4$|QNL(y~)xD#4qOLBiQ_L{LhK zZ%5`Tj&9MO)`#QvNZh&(#Ma1#e6cbJ9dU5-yU+}jW8uHAa>2Bly2QD|RuKP#J^TYO z?oK+wiu?c@&woF8fid_|s8Hlo)cSXqI|Xcc713R8mBfOR)K?v1o1wdaXX-r3XGJ&( zR)Q`;mN=~-i5}OVo*&iYzJpMjL}$gR_BZBjPyPZtMPQerd@4((cdB6ac!aRI_@Y)0M1$V9$Cz$19#+as{ucIT&! zYdJMrAypiqBMN>p8;m{K3!v5D`VR~Gum^!qULyT>TL}lyhQ-kF+SF%7)PPAXC8f<) zfY}^ethVJ6dwVThDO}uXCRt5hA{Z3&jz!3bc!CD6D57=HBvk1zpACLk=@5kBoll3sH64hevIFQ(W zTV`V#)ZzNbfObTsh~E#WIzVXPjfUB>j^Gwqc7jclc>wck7?mJ@{#mN?d41319+1Sb zsGq|ZG4Ae<64q^N*o)w}UN3a}1g$wxcTklUB^oq`bgPp_7G@b*&f-7X1yst4d;^Ze zWxK~7ZyFrNq6kFY-?=!2^jukz=0RYxMOm(;7}1!1(%v5WxUke~MN_tFT(w$KVkWgY zmga@%j&eENc+ffor(Q4d~ z`y)aA&cCale2F86Y=Mf?ZZ7^bB?$Cvq9iv^v9a1Q4IE{*Zxk@yrZ2P^#e64~Zp?_4 z7nO*W^NNBRSLwL%@v?S{h;U&3V4t-WD`=vaTOyM4-5D$h%&rp7xbySJk2_IJuRoJ) z6zw#xW)$sp?^0pH==#MTc0KEibonj6%GPE^Ud~q#)9|u^=CVHevp~;k4`Wivj(Xt_ zivj^~PQ1(??-;rm()&2z%+G9HezY-HryNo*HQ0+^uiWRA$qBlS7(dpAv4jfSgQ{{7 zP{;&G2UH7Qz`?{V_IfN^88t7weIM+Qee@NkBVrDdfAYof=XtzgJ+T+eze;}5W75p! zM%nu_+KYpCel_xwPUiDhPax#>90A%vJJo#)9lA0*ju6g8hJu~Tr~5+5mt{?2;c0>#E8o!1d)->UvA28%yP$`MLl`HScYnR_2B*p&;ZO*tt4I$4 z-^39_he6k-i+iGgJ^LvTQ}O%e3q?RJP!6*VZmZ*KPGnv9SvSN`N%iP2Y$~jYb(B#uk2X5yxEg2Bw3yUFauLj{PNaS=GF)!v=T6nM=K+My#>Qg70B{vRr286j z#*=WIg)npjxha0DgCCKwiNGh#ymX=k*(4++vfKw7SBnTFufH1lNY%0yKfs7s_uRQK zZ#rRIt^5>(AfE+7K!e0bm-k)~7R#29(_dHwroXsAr0_;}(6R}z$G@Sa$JKvw#FuEi z5OCTO7ei_R#51C9X`=bT_3vaED1=jwYnYlp!tTpwVM@ow0ckvH^(SzDZ)amup1KJV zc?qj}r#YfRKm18YhLXC`(qS4|Vm2ly4tX!c)&D_ex494Yh{(2Tkl`WLcc(VvB>mgx z%<&IJl=vnhYowWi{^0KsIN^DSN3b@w*ge0M4qD#F&r?Z+ayRTOYRJ)BfR9%<6fBwm z3rTc-hf!B1LF6gf90>Oij?S~|Ql5DMG2QcIJyYzGjDrb#UE_;wZ{QPw4+W%Brk&)Y zp^&`?^Dr?n_&aU2pB~kx^L{2>K$F9m@Mv2>FA?tv{u)BO@QRutd6fJ!0NKIUEWZF8 z(;kR#mB(n&Pnz298~V{=yl{>4g$O9jM!spU+lBe?{eoEd!-6WDB^rkYDNr6lbw$>I z``aRYz1S;vL#$FV47u8UtKG|y*Fv72{^u>)(EB$OxK^}V=iruAu*LX*|FYM(u$`_5 zBza5TIqY9*SCIDCGsQM~3sVJA2I8Kk`o@ zMI*CCY~Ox_bScJk{*Hi-%HxJZ<=43V(VWXpK|~$2x-Wr;ysdtvmcf^G4pV>UOtBw_ zz=`h`pJ-5$CgNXtEZV|2Xv6KU?TIW9~j6>q210ZvobqN9G&EY~cjd|v^=p#A|-LjxuR zQ!V1Z91v_jYwr@O7v4V!=tE`SnU~ zgqhde06y37*Jn_f6rX|Ghn$aEd|ix#Mp#W<5D!YDVaa{XKCp0d+oeZ1IKem?Fh#>s zN*)d2w~HOkY+n-KKY-YaOPH|9)j;ZYaeL3=7drZtU{M zIU?CQVE+bS1<$#rKGhU|CRRTLYJV(aHr+b9(%{a{T3=v_$+Eew$v$2MRf2fF0;mRN zsbf-RSG4*@KpzNE+;DSRmAqA#m55iS7f%se2@8U&f_ZwI6J>U}m^xLbmjczJAN6zt z>;%E~TKEF)(Lbyq2RN9XK9bUBJ-e(PO*}ne>1t(((;pl_XnM;ykL89xp9_mjAOCBh ze|s+u$0DZq!E%JnC1fVGM!hasbWu~xw6<0Ax~YN|rL{6XVTv;9xIO3QQ$}B(3e+%v4<@DvXM=F5m!OPQB)^}<2(4M+ zTE~LI55GxfA8)A7aBHW%ka$!YrpigK&x7DV<_0R`3qD?*@A5vbCT-A2`sC@Z${^ZB zWl%aI*e)DK`$$h!^;6j)Y}vY5p&gi1H$aj=r*_1q#ETl)4LCnC z;{#PxJ0)v;tGDf|jwQ|jV9%HgDILiDwtnuDq zt57s^KWhPdl^1{yrc{4GaJN2C%M^2XnH$}uRJ42=dWgVioD{pP0Beq-*Ty3SAg&YI4Pf}8@@H| zbS83$v*g44JO)-m*v3A2ofE4*m25kC2k-2gu!(gd>~zkn?TQdKmQEv9MZnGoOlqKn zv0l3jyAWexI5mUJ0l^Qo&UGfTI1b|1^#ds`j<;N_IC5K<3K+mAa8_&O@T`g$b$k}6 z$~)b}8>gRC(O>yr#d~x&SBxeGAQGqgy*42_7yw*;g^Bm>-Q~CbS=BEbX!199Z*wz! zj7XSL;p74KFMmk2_K~~?42~Nw+*pnI4^#c(Cz*Nas;!SdgKWj2l31%S_r;$3qE|;k zWKi{!uM?hu&D#IVUs)#Jwg{HsA|~pgZ}Q$>IzA~gZ4?0@YUnEdT1}hTcZHNUxrZGc z5u^eI{fahyvyJWpBn$+$U`yEBN=bDARnBRo{n%2|Tl@%ie?>L_rrpEZ;L(t8zbOvx zL(k_H0;3`3#SeT-W73?pnrE?flCJ09gf?A^ACOxt##>ZKC=|bO8@*t>U>cEHR6qF@ zc}2E_l5S>N4A?n_UC3*;*%V+_RSg3Q`(F(+6Btvb`ZGVotjctDp{y|JJV#8+a&DX z0E=*t^M(iDto=(?K}=i^ICOI5J14>ifBfiADjJLO=VI*d$!V!i*;|47tR6$$Er%^Y z0zdk)&qgek1xTPT&$Wv8Q~m0bM^e9IF2o}u=AOl4PZaKBph#No`Uz8h$DT0Fx{p%# z&|kxs5+MH6gpGT3s|vpVqNM&}O>P(Wt|gd;Gdn^hhw3BuRHTZ`FSl!koXXbT^u=~< zAYAm+g3>*wD$)5WF{e)H$)V$k@mZ*hnbL?IEl^hi?|l(-{;1*BD-@S0O^QH2K}y(* zE4LKksadxkdDxQ_+ZR7mLi;N&5ovvQnXWDUI7wrQKA^+tkXSS#PeH6L*!`Co=q9eelR*cfoq?UWiJ;IB^z4XXKc zmE-rv*5P3o)@=myobvo@#kW=7;I19KxuziI>#JS4PWg*%hpjXx&GBbQ+4kwBkD4ED z1H9Qx`-JzLo3rCh4_$r4c=FWJmKQ2vfeCM-Y{53Q?Ki{z$ard|mBRY$SxqG-Ugo#- za^@=wq@cW=-CB6fptOvs*W58<>_X4>^wjlbL*ttBc5~cL0wp@ZCl2FxItV|=d3 zT^>MUO8@8S|Dz<%P5eNN=pItsktyZ=bP5=|9q0TOFTmXe_NS8ad=5U6`&~&L0GRe+ zS~oqXcUT2?=o5bL2S54krrkixj_~NGk*TcIgOc+1KTFQ4D!7akbkm4MGW#dgbtiaa z7VIEWsdtm!bjdp&)SaEps}@YTE>S+AUSKVgG0xdN6*us_Y?g9g%~=1Cl+^Uy$64!} zxGL&4sQYxX?GL~XA?l%;gZbOu7W>%TE<=k+6&S-#HMkM%{+p1h-2b=}yO|E+T z6)s58y4|lh%-Z+abo%L{TOD@*z7}lWgUH11lG5-a8yo!3-jX@;uA9bAq_RrM&zT;* zCb~lhcL(cF_Z?%Ft`>LjM%)grW_`Fj z216-O-@yC--;j6EF^3>x<2py`>(e$r?PSQg)y)-!I*A2>$!Qu%cDi68J_|qt`Oj-k z`y1Q5_uoG%F<7bGZN2!*kc&6(DV+(~k1jtn99{gb%x@6A9tW^55q~2VU6(<*gCB+3 zMt;kQY644PNXH?$XU*_)MvA#xD((_EIkt;aXSiEq7cK9 zgV%q_(tu(e)`0Pz>DNH?RP1QGQ2M+^x%=&h(tskhuJ6~=4q`e+(`k#rX0SKm)w+NMb^PbbsaJPe(Pvz1LH>N*nhP!*|BkqsLLNKZL-JmCbO$2|L5Xwu zkI*6{#a=bjd;{pL=2LTb2R@7xKy{Z$x&3Kei(qtaa4R#a=JG$CI~9+gdeU;vXSvj+ zF(C+zIbR5v6=>$(Ji4BtU!|kB6l`odtQ*L$H$5=S&N|WoaZ^uO9F6soD zqRRl4Oh+zvRJpgDr1!jyO;=>np7tu_(NB+VZxP!0ptg@rh5XYe0B#q-BIj%T{D|Z* zqxJp|)1r!!m@0mY*3e%gNc#tjK_J>zo>Zv_W(tK%gvyQ2bEYsdIln+k0LypFl!$Fg zAP7*OF*S`3BAWW%I8Q#U#`;LUjgpK(6zPy!E#tA7b$1hRS9y!GUb&MAk#nfVYRB$A zK&O~ElL2#C>K!B*j*YUC_ns*IgXI5*A+9&0iQJ2zqz8XAEMexKUaIaBqyUB&Z|uy1 zCm9gMh$%oIJpiJ>1n?l=Tvt9ki}>q#&qUs0vXig}ORWK{_XFJ9!fy>DVWH@Eb~gZ# zN3%MZZD$LismvAik(^OWV6h%_=@L^*7ded#BR@}wekyr(b6gP=1yGG$StnV317ee$ z)0defHbK{i7X2d>u8T>7yYICG#Y}!jz3B8+vhInk^#pi*q9_1;=Qj;Jqn>camrS7R zjlofjJ|LIJf?|%?mq?nxW9w=FEqb#XlyK*nsoTfjeSzn%eplO~KNrT=IkB`TwcRY- zT>XS8g(Ln3Md6&&gLW{1;B}P?Ftxk8d)bgZ2ACM(ApU}~1Ftp$EC{c(HX33Au=?!@S&+?&g1k{8?V+MG)G ziktW@0dlhA z^6b#bS@;HIL|4e{IrY6u4}T8Dy09k;6M=mYw(bcB;LDEbpMhQG@?%4PVc9rl%u)kD z0y^D!-bYqt577O!o^FC$XnhGx!b43~NReBatB&Gy?4lZD)Sa`DR@T=)@+bai2{!hx zLaa;ztugLZZWylU!yCu72gPv46NnHH`K#mh`de$$DZn%!zba!KhzvjP6$B^%gM^cI zyyQ}wW&NC|)&M1bEwd-K?Jp>E5nsu86`%G)EFK&8uv=CV6Z=5tvkIGl$w|m~mW}SP z8u2Kb0y<`apyz}9nwL#Q%SoL>M(rPQE^-=zzauQ0zkqr3IAJs^?tkbdFx1vFLoOYo z_aY9x=bH)8>xBsh=R*g3leykvi^+g7=NGh zyl5gJA*PW$Wd+1sLpBp#4M;fKQ30&J&3)f?MRRnqOhl1rKWzQWIJ(viT1>A02;1(cR!H#l_vIn{$lP@S2Vrl!Ls&$X_R6- zs{${-X<8dRiPmjCX_~CKKIRsVC1HN_n}A?Zdsx6?5sEi%EMpxM&R5{*`ji8QJ@N%n zyuU$%9MG*(jCV?F2CJpNd2BhaB#MCbN!js=C|g0Hl7xwtKi;uF_Du#LxduE!X(2a!MhyXY>+rlAXOqL$+d^FQOUv} zRjmW(Ja#<(h&pufFBP7s_~2zfl1H$M85;FD{pw_9;r#tKQ_k0bKL&c%+|oWaq917R zBgnfr^Ct*dL;!4`U4BHc00nX}HMK1tJ%te^l2g1Z5T5gi zCAxx*vxGwP28)c+9YWXh6c?P>{MpmK?L|CNx!|2achDwA#BwLv0cfx8&RR|CPhr?j z02ZxQas@lERB-Br7LW{!uZn~M>+N;Z5uT34f_fflzzl-`m?0M)z2|EI+yeYY!ZeFu zWELU^@hbSN-3WV(UcgI}>nflPgK5S5Chw1Tg^sscUz^4v2er zAh(W{h*C|m%4mpp8`VAfAQFpvRN5?pR{y00I=Y$S4mBLDuI7H{3KKVOz$3f^?MHxI ze?56NlVJ*bAqpREhmnnDs6;>u)Mn}95vpCpC3iGxqLlwGsqc?~4iadfzmEHM&^q9< zJ7R?a&W(XxhsBmd8uU@+NX-=c_ANCrU1?JU2xyUbFCyM8b=vyI4%(7EQ)~gWgjIM@ zk?L*-jy=2F7a}lWA|9Hr@snXSX8rV;MVV!y{ov3Rn7sT61IxYt4J(YUu_j}0cZ zD2-G3-5vqmb;;F3$Q}{#@;$GJ`DTQQIa@mH1VB-^$l%B!NTP5p;LkV%kV?k89i-l z!)n@0a?GNd#XINZDi1Vi2x)$d!yTa^Y&;vGtXT2P@&dgCKCG8ZiPBqPRH5GiHZ$jS z6Q$BF1uAcRxp9m3(-?_G(f+bT@B&v){1z}*l2BZH9DT?&4RypkxR}fFzM&@Meb$l6 zMjZMdN5|9lSo#_X6Ksc6KZ6D^Ma{HfuOmjFnzUG92?uBs5`&SetKFu|bsY64J?C-& zoL(DI!%#)Yh~sd!Xr#7{3k!JnS`dnk`OFHVoKV1%yw=c&dI1Z_I=#LS7U-T(_YXt+LcA%O&K$ z7xFu~OAgGi2X&C!cOz7C&HQ$1R~L)@yQ)9$sf|;Cong<}ANqb#F6dF0P`5xYqX@%! zV3gtb=T!zDd!G_oD`#{MP`82<=(r6AgrT6PAZI$#ctxz@Ivxz;ZNvD&>;eAw&wCM{ z*8og>swcy+=%!F%-QbOq z-^%Ac0X*F1yR$wIRBa^j1~0zcFLp>~Md8YbttrvPZqSPmKr>#P{+Zi34GD}3RLeRV zkD#YdKaoq{+{4pi->?$x;8ddXe9w^Rd3R*pA@QS@hkEa$-ean_N2CSx0ctlwUgt*} z4okAudpq?3rA|{nYL({3>!sS_2=c?BiXi-d9h$PPKNfkYRrL8ww4(&Tt!!-pMc2w~ z?eqcjK5OY9WGDHJ9Uk45QB^#6i8d6k1Pw|>bUjRlY{u?LRz!;{39&1M9UvkV>DYlu z^f{hnC~+lnD^LLuxobf$;)SB9@*{T5@O22&beVB&-Fo_8FAm0?g(en2@WXcB4U!W{ zvt}$nKWy6A8^%hly z^KXwvcv$fHVXV`dX9#y<3nsN*<@UxR+@d%bGEtm7Bs|cHHPPVaa;S!o;}v%+xnVrwzNqLOBIbvA*Bqx8jRCmQ5dRDiU7SClv+!B z*FCFP)BwzTD&t?hEiOz%r#x6F7*2AYQad24!u%pD?J+U+Sg~IH$@}T}Z{WyF83>_+ zAy^Cn*Z57$O?>}Z{4e`|>R92WTUTG$g7w%-(i?PIbneADZd03!hLEnw>pC!D ziS>x@|5f$ub(;8lRzxV-4zdwW$$GbPN`lUPVr3@1rQe*&=%@07}9Nk*6d#PJ1-_8YPHGxOP+zb9(aCNHC-gPhj`^$o-h!<8)f;}NW zgf>5#LZ01!R&JFj>tCz~*#2|yPsB0=FY@}KddG&Ig1$sRG+jp^#FmaIp51_dUX^=g zj0Q&6b#Ik0;AbO33CB-E0VUbw9i4~J>Cc{T@iT!@19Ko}5cmgymb#$yNQ7!YQI}1B zOR2}gGsc?g>i?Ni3n@q-mYRkoGbb7pAz_b}OwTikIab-PSmc12crxYLr$2v`xM2e8 zJgai$x#lVy;8Lpin%FAzz z5!06>2B!;2}uEAq;u#Y|C@8~x%ZrV ze*f=zcpTJczS#SF_q*4-)_Ua^ftrXe$^nT)+QEA1vE=3!JQ|Ojp!M0A$k8MlFS!%? zLQ76P%%JB=`?tfkjnJ)#2%b{4a zaE5dOCL+yHn_=|*p!@y7iF~VpuRoW}A^AfYW!&_h2>*F|+3p)Ge@QTkKGB6~>8~vn zrNlh>%3YFwO(BsEX9^I)b;&`YzhXuJ7aq{!YN8|(ITY#WkcA0JYvi84zoa`v;-2;N zG(N+qMlnlB=ysa81rBN{L?`Y>N=PVS9ufE~*K!RP&Mwg@W1NtXfO#)?7Z4ALM4Z>N zh_U`Mm5A{G^#|tTiCKtC2bz?0^-TG6o9UW#K(;eP&$q2nleGcFpoJw3aR=-RDM2A5 zdLHbXUYU}>d@cnX@x11$h+VU*#hZW+i3h-WMQz zsJ=YOzt?jA4RS_>@UpgH$@3#)Mg{@~ML;Q(1zh8e_`=5X$=18ycDGi4P${hPWy~}DcYrP(2(V4MhU#ywfcBPd)ba*35eLC6uj$&*P}v-L$e0vlHumTYFdHaPJ5xaeY&cnmCF18*a zVM!b_eDm!gl)^IxyU9O)=7K5T_Y}vc=`D6p<*b)^eeM@Ds$+^$9cET+K-EFzxyOBA zC*|HQRFtWf)^I8LF?-us-ri?=nU3Zc0pvBCq1@Fq_vwP^2|$W3N^=W^s;*|$XEAMy zSWE%lHyOY;wRN@-td6`%Tludpkd{T;q?6cX%mGZzEmU3fkH{K zxt(`IikMr8oWAG&i?Q$NEgh6FdwMInK)M3@+gDptWC_LDW7stN5!ZZ{&Ox=_-(}W1 zS>*P1-87(aglMf!Rl;8bF5hhG{mOFB)Q5(C`PAW$N5)D_zrcjoR;m@*Fg0~)}dsNzt-cB99WH>}Q>(w|G?<_}?>AN;{|Ds5yL8nI^LmROd+TooG0l#+^dV=Eg-MbhTuVg$hHIOvDgISz#B)2@i zqqFVd3ZsTPy!fKQKct_rC`3kC>%8%LfhTq*XgpYNu$a_;1A3Ny!NmPiq-S#Nh;Vf;p*i znMizi7G>7iijVizCf4bl>o`702^{k}#PLc_+TG&8o)ZSN?gikzCv?ibJQ8wUR0y5_ zGzxeq6Qz-F0;un{dc%zeI%I=xmv4v#?W#N$H@PQ#af+|P zH}94h_k=|?`BjI#7*s*bc$dA8=NgV9lJSaf&@n_OV;^1jv)Gu0sA(QdiaJ2Ycz=7G zJi_r!)%TWCNvMrjgseZ`Db^Kr{NnBa-qb;8PtjG^-$Vwwt`;PNp-6#q2IoPI^#vx4 zL+`BXtQ51ZD1wtvwSYJSR5O_ubRC_`X{9jE=>ARwJ!U|gL2N_?wK#ujFY;agFzI{5 zh|<*k5T0NaMp?p*oYYtFr`Qb1IgYK&ts?N@6k(rfq@GD_6DdRIGFfY(>#Cge_{^V+ zcV|E$;QKTK0=Ky>o-bG_ruujUXW!oRG#C64(^LVPuU_xF7f%24fACC*iVjh zJR!iEr=VtawM?)oOIyn1kxTSI7_%vny;C=94wFqMSLMdfuhuF@^YSU!_!;%CYr&MR z+pR=u@yHvmB=Hx91Qb1hawEMpZ2k=qx{fP@j^G>M36lf0>4n&=5z&8R09OphiH|8O zy&rPqJgf1#8!jA3M2eQ#d|Vwn>4PSFGR06kUmrauHEIV!2J%ht-kUIS--BPXHc8NM zbzWNNNlvyRQg^+1=y(QWLWd#%BoGJ+K76MqvmSwJk{hoEb^ux4d2+-LsD;$#C!BNa z_68M_wy&OkdZzEUAY3o~Hjt78Hs4G(p$pUK1eXv_P;kexRc9{1sSz;zc`E4 za2Bzn>7ShS(y?IOye+!E{XJb4+hY?D4SlY|@aQ$@yXlIOz0WZR>|xPy4v@@m$-)lS zxK~nFY-=~1iE?Q`wMsb($$=w0(oyPICQ;<>^wjDdNRF?jmMal;KE*z|1xbSVXcfNO zZy#z3D6BFdm6P`8N}axXj?Iu&L@>mBN*%M+9Crv8A%1?u(EX0@ozJo7W5ENoAD6mK z@<0{jY+=xH#b`fIeNQb5212(F1_*KH+9lkJc;Wi9Zy&+2q-L1*^h``fp&Rh4!F`G%R-a>XE*^da%ps`Xg|(#99RX& zFN?@Y8e9zpIT1jvQ9k*KFoVofy0L^%S_4v{Y4sRzT{QEx>zfXi&yOndrLFGw*4z(? zqnV$=Pa5AiQ}DcDA*)}pP+tG>XY%fF%CqmzR(OH}0~ze=)mJLzljcJw>sj~PP8y36 z3QiqS4WF{=`Z%I@@kAuwMORm|;WHzkiTwm5f^zohY^veG`0%czPl3+Y+rl2O5(ts1 zBneEn(l+`oWEZ^t@tXOga8;LVjFBGu==A(e$ll8E*iNuQlVse^0$WztG59OPu)W)W zI3@WZ+WmK@I4E3rG}&GL*Ct#!?!*~Z|D^5_0mL1G z&^?kFnZu;w@cPv632rD{na8A*ApK>Q(=VXtqBthVya3F1plNKsJ;Lu>+Z{aiEvYhc zi90^Dz9JMZ-AvsCwb1n3Xs5CwGM!?k>Gya5gk$I*lwUt|sx3M5j-wuu}8B)ZO0DA`}tZ? z)+mY%+y0jg2)TF>r?ZVDneOSBUC&cY*#6cYnU|B*gqA4YqwbewuZqLa?cJdVEA8PN zIIM5I68J2hrdP14cBk-%SiV<0d_m91=wLnKalTJt=FLcDpF}bpTYxx!<5}cs>=3Yn z_dqdS(yq_?wWtt{4&cI6b}a#tZlP2&*_yFEQE!B$7c5fd=jupa>8C5i|5^C4q{p9H zecrwSxq)iH#iG>rlN%2P$*RYQ?7f7y*6WPXx1=Bp8No;{>}JC7dljOmVz8<@DLtgM z&l_PH%FMK@-fn~AH<+c)vYv-d&Mj5vyZL$r9}q14`uXWL*{M&Cl;igt8?KKuTY|sN z)=Z0Np%EpSo)2)Q*eYVT+n7c(71W68-RRSS2>ICp)YsQQb##qwp7d2M>&C8lF(cU9+9s71k#fcl52a!5-ffG5%9WjS zHHI|C(P|LCE8UJknA3J_Pp533nhM8hm+KSmF7~PYXyHyCaOqzVt4unBl~jF`rJx!R zkW3qBJQraxy*@P&n4(`92i+hl?1;$unHxPLSxRl6ieqv*MHR6vg`py|7ERL=l5(WG z79Z0fgsGj62chhtQ|tLkS2V?vgJf{9X8aOO~7a!UODK*zP( zi6C(pQS^(j;|vtZ#`TDd9?RZLltO*i8*jzl!pdi;Xq*0`Fkv5`_yl2oCOY{g8OKnB z7&5S(nRe_*_m}SEva?66fGOvbpu@L^rezY7o!W6(1dF^z(6L8ljXyah(@>p@qgD3N zBk_Yl5@u9u#?)aAd%MwNMkOhc_YudO6x0nv-7I%falFzjCA_o2V!4*UfKaE&(%&M| zfB3RNY$~JJ`_%r`>r>C4LZedya4&`LE_GWwr72tEo5qhQmSLRD7;bcRlCJm2$$iUu zPq{7t%KE7O?s#Kt^6f7#CIk;3Bs*mwx06x2F(?T5zxFgMOUPp>6R9Ja?d=^qMHCfF z`GfFHkdnOYev!QJby~=EJF?vG+au+|Zf6E%h!BKbQB&bqJHr4eBjYM%@a0)f_=heY zUZF;4gxy!h8g-hyc`dT{#)>JAh_#%ZZC=gPxsi)HCsPl&UE&l@#P+|VD@f)-kHxzK z6}ghpup!r0sJg~@*BgWs)P7Q3t_Nn(O9< zA<&KfW%XI2CVAA8st*jpGtXd8tEGG)z9~Kxyf<-3ks3Gzq4-Wwe(~pii0q9^<{?0% zO%}GP2lJKVxYl{0FUq3t)2_1k70QQ=eU*Umw+EDUjO#UeT~~GpsmBr6yLw_FiBh7C z%qQ?b#=agaNZA)P!ZLm|F)rtS*CEIu@9pynY7xZyS1yk!pFN$ux_Cp~6RVug@8QkR z8)t$hoe`t@t5kxU=4^52Ff3yP`StF*;_ji;mzdT=tMP}Hyz>enq+2>BccNNgTyMk{ zhR9-+gsEDRe~a!GUAiz>!zk3554Z~u*}7Pai=t^kgcn_l3=0feJPIq`SdaYGj)V9* zLm8oPpN*f!cI>;6C0fcKDjp{bSy+q1g3Bw)29S-q-k~>?AJ{+MN!W{|iUYiqg@>~V zv@sFly%>PJPuXYjn2D>lUzsB?ATvsi@Eu^8>~`v{GgGM!ROD7k7?+jw1OlYk=J+>C za1}VO4OL5>s=h+w1i~=vNo)dxYp|vN+6Mtc(MHBBd$XTsHNV|WRsPsrKQvd==`|m6 zDls8G3%IX1)fCTdY^`rBDxhx0CsLBDWiY4wmjIG3^boMB_lHh#{pey~k&{I7#h(Dp z!~=%girrAE3fFHYo7YH_@O*s#TV5D>Kqgg?1E5nph5_pbV5h}=?N`~-3CLDy8;<6c zm3`l}BspCO#qB47;sGTO56@Q)fOuE(->`SDv^kT01q{(XDauhbH-$y|KM6d#rbvQB zr6}eC%Zi^c+-{aWN~iCf$(vMf>&InNI;fyYjvyNFbf%Y0M(F6?nh6dRXwXLuTiyo zt3fEc-^MIPSk}_WZ=~U-;sNdI5Hptro%QyPYpE!iqcNw2yOzBs<*o!m(`1K2Gab&LBkI%FydetT*pq zImr&(XWM7E%Y8^Ly58N{$)}_5U0*Q~^8FA12h7K>{5;LiOAGFgkvZFOI;0&ib(q@Z zYUC&OkuBac_Sd=*AEC?p^^?+x7H>4>dORb*qBlVqMjLJ?v3tev9VYN&s9gX@D6d0c z#USbGg1c-hR;a~}b}s=?7g(eGAzf53tNFyX8I#2Bwmc*&S6T(nCM;l3-pW8lI8Y*; z18JJ65K(dP=J7+q1WKG{ET%yPoXk+(vv=R+CMG7>WHRNq67`S~F>g1@a39*Pm|M*w zT7?#|jP5R7DS$Q%OTd&{^}JY?1t(k4$fVP3IxXYtXHDe_@OJOn3T7q*n0$ZYQcp6Z zKPLkxu|x+=;97U@z2jR{dsu1byZaSD?CFnLOwwQUzloy-lzWDFOFU6`aWsTlp*}~U zZan6Zzma!+_mPZ3~S(!JtnCqk)e}Vq@jkI$R>=L~;(Q2hC;M%NYwvB&+jnza}fAx#1IWfPE>#V!?RscXplNX}$`c_e_j)8ZMh_#fKJ!jXTJ%6qxpBHuCc3OvKw;+oe3?G$J*S;8_A;U_%tuGj7Mh zV_VfvuH2r;o$hrUSS>yAkkR<_hXMYFufEgO#v)XCH#tAP>Eq?1FlWGlc0%G<-<}&5 z*wb_v5<1*Lmt2YuEqxozL_hY8Z#I*eGCg~^I7ub*ZGCc|?F$iiXi4z3wb{l-+}!bn zvD?l3XYUgdEY6fpPTl7OeO~ou5I0nv-}~Dv3&#M6QkbG}RA8&EmvOJ(2u2(6 zq$vTIk%*r>-Yau~p{lma-TA}!ZYf6u5!JfwE-seIf1{(Om?vZ%&@#tk;h!hGZPkj~ z-SRSwh%!jH19h4$mi!xr`n9Zv$5hU7E#TM?)vly|Bd9dQ;8YIhqZf>lYW_AuyWI@q z*{KpbWdnyeBOF}1bh;? z>9V+8s2W7(N;5ty^r{h7Mz=^h!|*!n=y*9(fH~4YRA9I;h5$kcX|HiyF+$Bvy%Hq{ zOW%a@#S>0HLSt!SDD(|Xo||UfVGjR0nUekQ!}7I)A3Qp&l!FUvg&tX>o-%g!bnbKy z7_=W2gfec(KcCae7+JRUf3XUqGn%>A@m6^F2kg$`JACFWoGdSiXQWHe0J+vXv(P8d zJcu5J_i>s4%djEyhLKosO$Z)*kI_>)X<2huIR)(bEu87Y z4cUz6_vw?Z?<4y7YvUuX%=HAwV@Vuei`_afgdwVrYo7!!3l?_HNZy8F;T>a%x(Xj!ynxB!FDP}<%XI9Iar zJD9`?;$T)l&0BFI>4Y+Y>~Ep!GGZVFokP9o)a7H|KD#@3Bi+|sI2=jsPn-NpXssBR;oV#> zP}|qOC_I1kI^{l$H+dfiiWIrxi*$62Nzh%&tr1?fh)@614micVP-YN0=k z+=chD0ujD%Ml+cov#~thqgErSi@`zw1$eoe+}$wOC4Wo8_GbOAsnu17Sm!r1`)-p8 z-D(A6qn_57XsZHx86&Qhd(kU0r5ksR#ICHpFIFWVquG61-Q-m1zW=&*R9prc6+L?; z0#Jmjw*xqOy%|>DI5<51_{kcX-RnvA_@!-lL%JBStY{aCGZr3Oid3+9s`>oB{E^I+ z#hQ7S(Befw|6*g#A%UmT3>h~4TPFJKZAm!)kxu^WgVEfZ9ITNbV+w4(aJvH(Rt^=_5Qk0$kAh&h|hQgb4;Io z7X5Fb?r($jfP<@?f~=ZTr+k*9Dv0*2xlgzxv{XuH@4x?EF(Sw~Z37ku;g_vOvS)c4 zi&`8a>KPjtsfr{g0&AGZi&EYc`PRI*nAOm6OrT98p1hl2ryxmZU_g8JmMB#Mwk%Cd zul;U66q22rv~!;}>YAa?mX(74b}lhP4dn2Ckn?xJ@lc4o34wFM3RuS!hN1Ye1 z&XcQ~NaW-SR(m~vOk}8Nj{~tR3KA)fd;VU+Tv2r88o6KMz>V$}fi!#^wlZplO|TP) zK4dy;AwSYJNDC>*QZmsM3KyPtxJr+Gb!h3Dr)q6cM`1{+c%xF^jmds=ZvX|aS-j_r zLd;SIv**LtBd?TCIuX*2*Y^ak(Pe}d=G-I~wL-5!YgcSrA275-j>u_Y(hS@ygyBH3 z06q3`ny*?M?-yHeg)vXcCG26N8s zp1qV<7K_|bU$WRfZ9zGro6Y7v$+Gjiy)lFQRaFW;N?@^=hhrnPSAY&$D$e8f-XE6P-ElE$P0Y zJfV>7k2v^JX+N#27^|FA)H1v;x3OC-6+@b*B-~-0X}z|3-nX8~6&!EWE4CB5F(`~W zC`xPCJ2!5`?++yyR8c=+rPP5sv(6U2uoYauBp>XIt4+srU}940+TS}r3B(`oP5GUy zO{&QFPH#s4m_c%?exRZ@j=-=L}Uf)eL&Wn39O$yUE zo*9^U`qYb6MBz{OuzyPp*u~~M!SJ`6DIk17wJT{E8sYy@jB0I@Jz~q)L)Qms6OkG{ ziy85KCF6g)AN?x9OZOBpOE#ODrZ>2?V%7VlH;j5~DsxFbBE(z1m)?^e7;iW#YpT^1 zoLc#W(yshc)KjRAbPR!QW>G3gMtH&~py~W}zoe9#6rOAAqkg49$&jD}T8Nj}LR27x zp6DtPunNQCHelaB>S86mx`0_DD+QGo9OedcUO5z4Y28II#U%cjPR|GFebJnY3BwXe!@>vf~Nojqnbid``|z zGE4VgVfnwr!xS&SSWNO|hzqD4?cm_U%8nW`?p@R8^E39yQFz=|Y0HXQ;3){TJ*^T7 z{zxwpDBQYaa&J7aLr%#bVww|@VMuwBp{{&mAvU>|<2|~&d(ze0mNR;Rg%R4;{ONyu zvx(be7Gv??4{5~Jb=h$Rh)F6n(sI}Y&&9d2L*7r2?}~CZdQzgmH|5(uJN9s>D(|^q zeD*(L=XXZ*rNCGk45R&UC$QiCj}HG|Z)=+jFnalydH4V4Uou!Mzb~*Mihbrk&%XCY zom7x|!yoJS5LA-g^xka@(2_7cYjAJb7(5%%iFf+@!_q4Gx9@j?`DfWyPORLv9*scZ zkTZSWfl@xEwUyIM&j!|;hJ z!js=yo3E&G;q3y2E;d5iBFaA))Z%JRs4!XGK+VrKg zc}&tYfFFwPVMs6$>#lU@^UPI^={uS5jV12!!?|M7wSC!%pEm^KJD~A^tQLrO8y|$aag`udrw!_(%H3S zp>Deud)Hd0uJP|n!hdf!bvxil_V=AA{l^|0xb$BI)bX3L<(C5HN}GD_Olbtr6KVQ? zfu~W2BMjxMY7W-7QCTDv^>XvcYip->3skp%EoBD@O2COz&kPzo8tPNRKg4MK;s9!I9wGv2F=gM3||@! zbYuri)1=h?R}d9%f~3D6zDqsEdugpf|R$5z3tWy8Wmlfc~pKiE6GGZ}K@9Nq24M z*%8xHqzHXIH&4w+4MlZ>+k5czV;UP?w~;Qb``srxP2xU9%cC zL8{L0z0&=p`pnO+$+9#$O2=~e_8lexdkxj))0U@1pBpHmRHB89y=;V5+bGf>H;;Zz zevuBEB0gCy2z^rC;uo1vgc&5=ZBF=M{_^?DF~gqb88sDraRsvgD8&TxWcg<8y<16D z45#^4X0OJb)Z-&_QS;#G8gncrZp-OWG%ot<2RkfssmfQkVN3mO-D=y2_=9a1lDaHI zi34w8$;ZK#6c-Qe>aDoXKHHJ_0xA5~OP?mZ;34B_&BL*V0sTnIa860aQ$AC<+>^Fu zJIlAiq^ttQHsNv(VK6CWMB%uNwCh)l7*T`jS03In>Z+0cAed$SsGFfy!@M9-L!gQl zR2xxykLqZB?@8kc1B!{?JgCQVprS;}d;Or&tK5O5uNU{i&CK2P0E|;#tNp@cFX4%p z1jnfuhbS;8D6mfTiWC@By{o3(jJmV$DbCZ=y|9(OVCL@@@@i`Jp__)P&~X3-X;Z-X z!L|pE_LuxyPgyv18iq!XL<;=UN+0l@nb}w6k~%Z5mAH@HV{D(l{`VDZ{y^yum$9~% zeSa=k;>%r!mUA^VLTYctYOcjNIgboqTZliEpMrPe#rrfz@Sa~oTXpF`}&YyI3GZicz%D}GNZdos4y{B)J4 z$!&|GouMRV&11Rc&0}gW=W=&=n=?LjRXk3KCT@USO|uY}6#MZe^@r_RSiVXD^{9)A zhEVyNdD`*$tnGdM!nV1LvFK*aI@P~RB`?Th{&3Y6(^Wh2L+TQi$CI7wc)63OBv6-> zXigq$ok;FqtWTR(>HjE~IQ4RCJR7i3+bCN~7WqW{uGWKfD5I_X$EOj{yx zdnFw;6j4c=YU6%-#<%!3Vu;EkT!oO}lx|wm!0LFSpK-{cMY0z*jB#mY2_rdU6o}w z({O*N2VKktwhT?UlODlJLA{`u211@)?jw8t9W?rutoJGUc;-*DK#K#}y zYbQMYjaabV08A#i$dz{4FG8X{F}>rT|QcaO$~2`i~ny$%oK zhSi*@q7LfkH~7w5Ry$>};@9dj&1}!SQO0#G%q9_qjma9Gv+pWLGEVB>r_yLc;U{`% z)k(~0B@de4d+qp5P(z~GY$={c(C$pH{cf4I>xCxI3V$)lfUjnKirJ(oNa>Tz3!^YK zM`x15s*F5tfE(435HBmyH~J_f6OXWQGbK+%8`4#IXlk27Yr`*=;x?5#QaL-mH%u-6 zyX?$tEUvVYOeXwrtV(0X%~NTB%f(Ij?7T>Gm~AUmjj(K}Wc+HOqbnlCbIik{E%S>p zc@jFn{~rf}_a3q^5*#aB$5X1UWYS)qeCMcjwA0ouZC2du-Lyu{7h<{06`PH`$C-TF z3-%aq@7kqWP20-zgIl(r1?9sA{G%G(4C<%u{GM3%2vez)5N8fLnYW}}bm=-8n6bx7 zZ~SCzvPPk=k*Q;-mi#0jVB}5f{I;NE@0QBxdXi4+E<)8Z3BmtKYYd9GC2e+7AtHX4Kb_w-jQUVrwbaqoW^X)VC}}Hfi&#_f zA4jL4g5MDy7$R{?S6>NJg6k6-UvIBHd6aSVS}BbGFLI#Iuq zU+C`IcL@5#{qNE6@=ojJRdvQO>fM14Kb`T~%G_(J>gKhQzhkjOP`bE(`Pl#W7+{n} zTn-aLc;^!Y{_$gS*dIgqzuy&^s{H31$3m0b`R6gz z^Y2UCi$kL%@#fj?{qxAGWcqW0U~&P=l`8FjZ}gAf-PM4>Oy=H@G8gtAr{YOj(BZ0k z$o_v!Ie-7Xf?MPd@73@2=l=T^G7*-|{k*E6;Olx0{T>12`nTOdRuI*g09r$wRr= z22kF)v-9IUUBlnM2K;7l_oE<^YpdB~D@+x*Xt*E$dTUSpTUC?C>r^2R-ncZJWZUqe zQSjiW!`V=-`j%(9BcGhs&<;>&E|l%xv-&*(T!V(T=NPpcYrSS2zUxuVcmr>SVZbv^ zyrAKpU!Zb!*}5Fuv)`zQw@1hFzlKK@wUqWnw=oZuyg%&Sbm#s0*BiU$`z>k+~& z0`zNPwE#rZN4x{4NoBx@It%EHm_4+Aq_NEy0ONVW;YV!w$>*4XBwp6?)1Afv|0mH8 z*#=w|lr%OgYdODa>bvoC)ULl-0le!?oZ7nK&HCe$fZj>7B`{_!FtBOxe^_7RRD?3O(B(hh^nmIIy8QQh}S8}UQ;y?8OD;!J-UkLx$*X7K)xdxgMfq!~C$ zIx|*^T5b=_Va!O9y^bkXfcws4A{JE@^AsoNgExszqZ2%XZ+~Kd2TMtuCVfM!_Un}{ zB-b41O<`&AmO!9aU)`aP-z5>qm523Q_WKOq9e#X!TNR(Z$l&$~AYu0TM-zpDCKst? zy*xeH!i({|(HcV?D14jx{aIms!;QM#S)Oza$!5sQ1)*k+%AXI;HeE_{_D6LjgPee$ z)xzNkps$VUd!!_%N?y2a0Dp#Gr>A&qr}Sq_-d>^{HOodTpf5_VTg=#m*@<2va4Z=D zeOIDq;}o`91{G+dQNEx%0}pUf`Lbw!=?t_7=)fI^jWRL~?I6UE9et*j9n*6MQy%eZ zl?Pe)_en0Lwk?O`Y7O0b-Y0QFwE{ff`sO}(eDiNZt=M``F%fCh6#{zb`K2MylE*WK z27NC#lSGSbcn)YlB3{2Yv`rPhBt_or4n!n79;LlIQ;xuAVyQ@166m?!OxEpY(w+$s zX`IUk3J0P=qZYL<`4#RWzkIHXewq>ux1{1RQLLE@q#QnMI6PDNS{;!*3Dl3nwOb8I z7cRPt^IvUeYk7;8bWBs%rVdl7`Y-!D?Jmi-g1$UmyZyHV5Gf-K*Ilg&|Jid|TAUf@oj~a+wF2hD=s%8jBg=>)c z3$%d_0?o8@Sc4b&RIGstg+xvnM~L$Z2SlE?K)vMhJD3iRqWoq-)r=|_s{`FLm-sJl4`$7-&=zZ zBntqQdf5*Ro${L@(7q1QA}~`vf!JN`R!u~zWsvST{8*5m%iOMMq5Z@Z+&+yWxN}9z zU#^XAmhXx(-|E-{LPug{1GJSaCBNbL?gYO(P&jqJ{oL3Qns;-qz~Eagct|^2Ew@G4 zy+a+o`CN?_Cu-IF%bEn9_D7Z@hE}qtAp~7$1kgB`-BuI&UO2m(uuQ@8I{@+Z7yP`$ zjX7z&HB$7MGQv$?=$C{oE4rYhn^trlD`}rP80jO6!040GEoKp-1A1ki(`QD)JHG<6 z*ha>!W>Yv(jw13b$MM6-16>$=?(};65?NV^ctjP#XPq z%X?5sbY3}iVT?O;=bLEgS><_sDM%O!<9Z`oHK|3aL9jO|u|x((tBSCC_}7EbfSCks zv{LkJ#(=nbq|$a4(MM6Tl}lkPJ;+%A8cH=_u?qJJ6dVHr&csyD zgNbB8nnwZ4?#t;xJ3-7nvp}ILFLJV8It=ul3HolPca#`XfHu;?X{ICVvN;9n*m=r2 zt^Pye5iN++@A5s90GPrF`((g2%qEf_wd8@*FcZrF$8AoV?tp6UhHPYFArttK}-txbaQpL`pS8EKtJ3g@!%B9T22rOOrt%O@gp5O3tVQYKZtjFouV$fW>GTP!q=b^ZJcYOx;T7WN+>RG z%$L(T56gmTjf#L{lVDdb(2mOq0(ZOuE^gjD$Vr#sdqm&v&cuid$T`Pk&0Oc#fC5d8 z(z?vPWA<>?+k4aJvPDycNEN>W%L2QmdB>#s_Q`BvBLkgMD5DqHG@6r>zAs&S(H-91xfhP>eO2iyC z9SNAhdL*UoeMV4HImzXP4mq8ZGl_TkiAvArT{>W^NAhk7TOO2;>GvGgHG+t{$j8_y zc90j0SX+N?8ZZztYDI=`Ibvqf z@%`BP#$o*0rp0dBc1<@#!v()03q5!#^7J{1Ti+)<|H}A(Eiu7`-O8_3Dz2A!_Je*v z7%fSC^>u&HhC_9xGYm(^FhJ5ohq)O$4^<_TbPR*2J0M+z8DDW}SliUE|5$pQw$$a%fts1H$LT&A9sjA5K zw^ieJdQkXGIZ=ksA-P{L*&y(NOwk8gXax13fBZbAEr1+z^QKRJ#iYoEK%`Ix6!*IK zDJo5SMT~{;c>x^!M~nzVsK>@-507B(>j8G4P1Z;mFgh{a;@aw*0^W!3C2y^qtrgTt zG)jWrh2dMNX!q#KMZx9<+*UH+n(CB)%9V=ya6zjd@3JoxuXLvo2uP;yO zAUU_{0ZV#eqVIQyg;qc=8zzQkpjLn0uoNZEOt3G8)8W)8t|loqtQ%r{ zTUMrFX6FM#L(z%WDR_?cC<)uIU>V`KkS7*CheYZ*3$hot-oc3J&}xW*WMZej09go@ zW;UYdH|mfYI@bGj9tI$D>+B$E2BwF@cn+94k8hhd^vysXD7q+gDVyvP#aXZM=@U3n zmN-PQgblMBsvN7ocaF%6nmjXt}s(D5UN z@u}b)XJdjzu1~CsngT6=Q9##W4MPJ6K%Z4KNG|)ZamY3QZ?>z$Kk#q&`&T*>4EvQ2%L=gnu}q_LkWzD972ifeGe4r z#dvy?kS19k#Fe&4P}_e%n~gY9h+{D{VqZ*;h_VBzMzt%5SZAzTeTupvS5+h zzs$s6vS7OBDW{`j-3>3>H zHo33mr{RheRN+wghis{1B2iD3IZ`6{_O2>z(N$6z3IY9Lj`+biuLpbeCZ$Oym)lS8 zi!kc;?fOnv0p@-NA<-tFjwaAOa0Lz< zywy%XxxLV63$lwc(3H|_r&oati0mp5XP_U0GH6e;>{{p}Nc!|Q$TqvZcQrB^kOLdZ57|lNS6<$kq(((-T!C&q7Zhvu>xbDnLba;xxy=x}V%~ z>0sheV|cVZhoSD^i*==1r~)aCIU)bdqwVdMh_ab8opa_FT|`z@bMlxegU1LWB$pjA zQwsP!5R_;p~TwE07`Ys3SKZtz%@e|CpPpbbo?1(g@G%56yb9Eq-@u4r9}X zS`t)9S0ME1Zo}Oc%2lX_jBURGwLaTF@rHljLm}eS9&C7Vjs)T}DSIw_2ZAip8)t6) zV~5M|-xL1N76YIcd{TeTi+{v=5cw^9{(N4S@K11_U>4PlvE#Ul!r3ncY9gEkUX5Qo z_A?g}2h@^<+>Url96vN>cRq6~;7nFH;m=S@E4 z=XB&qnf)?jz5i)ivDT$_%{LtlIP1 z--fT()OJ(y24!tKN7`SAn@jLIjV1B$i+omQMJ<-Iwhim>@HIAPHYVe)j-cgkj*E5u56DT-`O37qa#in{dNX++;PY z^8a!5-tkobZ~T9yP!v)|vWx5)*+tnS*?W_$Y{$%u?Cf3HgtF&36^`BE*o1877{@p# z9OHbS-k;y^cf0+*{;ONJ9L{+?pV#%c9*_Hlo19&Yj0n_u5mHVuCeWA>T6OOIw)7C& zr$F2UPydN%D6*K`|5fDkh8(=!%Pk@Q0+6`Hmc(icY6nl=t=uT?LG;|e!<;1ug2pk* zY_8yr1H}A34#*vA$I?5Rs-BO1#ySNsUTN^^YG2N6(BK<-X(SF|#XHex|GxDfp#ol} zZnEoyigWU>W|-V`AAgLhD!sbO`=Ph}3*+sj6;gnkF$8%HAIXbWn@e$n#XnwSgXjHr zd|lAEwt(B*9;jLIHgw`gx_wyA@i|#ZLHPJ<=6TJX-F6kp-wz(qPJ^vW6{`eS9*_ME zTsRoN@1*=uQ?g>lW8Gi3za_ZibS};TiL<9m!M;A%FKBC^hSaPz3?nz`j$bM{co8s@M#<8?vLEhX^qu5p?Um;do?q zR~_Db^P_*325P>@p~9zpuzT8h-OF&s?|NNU&qqw(KwVJqI6IyXF|ptTT>vlJ#CSGs zbOj?C;;X4+Fo_CQg}A|=6;)zc&0wQv-cFms%x`23&h=|}f+l87ClQkek1Wv7A<%R4 z5ha=2>R(^cN_t9dJc-rCZxrmzNAq^j$C;=8hpk(I9k+4c(32I-sFC}f3)@TF{*KJ% zlapsPylIXn4jg?&o7 zl~v@!Ya8@y-|c2Ise&0Nv%ay$_MfimbRRT)Q znq~Y6CQ$|K0XCJ~0ozj721zS1X5J*(~?g9yk$M<+#rms?fCdF}$ZhGs`WGdq%f zdTVeoX!mFHgcNgeyx7bWGmpOFyrlxF7y0R+F!KVdNLS@$0E`!j34zJuK_4q6698J) z#+6+x;OmwG{D{l|(16C=383uN;Hj$j$PPKbR?9zt1FbkekOJ8papRN91AI+^n`cdu zks9*>ZeBnU%8)Xn$daUg{Ko5^Y@luFrc9c4;r!d2jn_4#Za5j+u5PBpC zY!9P4KpFico~iy#_Ab(tFRP}Gyg(qH{)q+t?clcr-#7&Z=WkY$0Gnb0lk1(dJU!a0 zdNJ6>7;-`Ys(~s{vD|weFNZ=yz_PHa?{4bQbCd&9d!b@{zg33sfYd4mkGT;0d*l&h zqlRc-h?X%tz7kI7e!TF6H{o{FVY;DpAGRjsjHeU@R%HDSdLbfiZ@hlFYX zX7UeHL(E4pJKdR3X)c~mJT71AXZ`0l?KZ&!h5qb+a0tUB1JFgYrhg_jz`9a^A$33{ zcgRB+Gv~LgA&I@(FF!MI@=zL5xm1M~V039k*&8{e9scafP5d(H!xpl=^oa~#^~+D- zkZ-)B@8A{E5R3+Z=nOrkc|8;mwGL~b@H_#1gMF=Zc{omN4i-W%ZA6+Zy%bPe(1lp* zO^)AOeR*Czb2`^ScJ7)AvGL`%wB8 z8|~FZcI*rL25|A+xYLOf=FNQ2-m*1Wxm0yo%m{Mr-O!SG1UnOuUVKxx6bSe*pmRGi zrB%DvpDD?8>dv^WbA+_me}zQ*>xrcxRRJ^lgV$yiVMiEK-B%zTt<-8-s(|_^aF!F8 zinqYBLecmYt29Y_X$hb#NbSG!(#EpFW|D*};o)C=S9gCp?CC+WieMBT=4VyyBscHn zcIsQ8$_8D=OwJ;GEBwud#7{BVClA%}EkGIY@CUdfloz1h*vjZ(0;l#&4feqiwdRc* z5J((^+y^>hJ8@0_8;`m116G-0z7e|)ErIRvlN2xQZUdCExm@q^7AU1BwA~s`fUYOe zV^rh$aO+j+=62JDX*Sw>&P}r1YflAeY@SH)5RY&X^o;a9pDt#6L&4(6hI}uki;07z zOcvwDURw@lSmbq;Fv=;qTpqjL9b3op@TQpzC)2?f;mI6ihblKiGB(M6N+!+x z1U{)}V3x)de6;8V9>-F-Q>*TTO@Q)ozL+@F?p@}!J{#J784LrRV#=I|RR<7}kf7Dh zqZI`@26M{wFV~rDl42GNE_0NSyFP58$tL4(e%Vu0m*CPkEe5z?kgA_nFYifs*dF*u zf{o!wjvVKY|3L3t4-Rc_(^Ajaa6tMKz8H5vej;z(Bl_}iSR{@~hC@r&pDd22gWjBF zR|76bI7WB@p3u6+9dE0nC!k}R`PWhqEGZOE-)^ef88ZDbOKKoGYqa^9NNHkE$JpMj z&OTq*NeA|zfFS+;CU_MN#z1W<0(vAT!_xbkmHBdDA05)zsVXf*WTzU6M_2V7Y*q>F zf2;?e@byi&`6}-LDv1x=YY$NX{H_IramYl> zu8XGC#@R*BO2>AG>ee@4EK_=b<7=E^0}Ifdn%3@wDAx`B zYx4q0&Xvul%*t1+7~lT53`w|jhL>5$7CGg5-vF-SHLTEYwsB|5WY*`VDv2F9?TMm!K5QM!mtW9wycW{%YeoV# zdY3MgunB|a01?w-P4q*}hD~+LT0bt-oz}T%8fe<1U~KtumYj$oTx|ahfDraxy(2gy zv{qgTy4`@vgtNjKVJdF@(wi_=URkXr9*y#>A~7YZUFHOwEl6@Sg0U7BQ=Z7d?SdI*g1c>V)u0+pGY%FbynzWt3v~*~@DJg75 zE~ugayf4NaOV4CZ1}wx7i*M9;s+&DH?*A>xInSD99KIO_75;szZ?!0JT#lg>OltE_ z@88x54xI=8(<%S5TR>qsi-Qxt`Vfxjf$W17Mpurh-+N?|X! z$404oK%0_Xv#AE4%W~@;Vg-3%&5mdACpPi~HL<&bGyOH#caW-99c4bd=6>#wt=CQSk=lg+maEVgl$jf3%ENs_K!djGGjw z+1G{f`xsE_NpJS~+s(Q(IgOui2X8+OZdeQhqdcjmS(Oz`ndreF47H(FLcD)5!dL@% zK;0FB62y798k>cGFnp`tN>XQH=7VgUIYPQlOQ~I%+*_$bzd$u_>jt{vd>iOzL&um~ za4rODbftizRr zb7_J2I4`g+#&8B86g~f-=Prg}V04jw3jJ<)o+z<4rOqb1-^*SlEKxVkbm)YeHCU4* zTiTVqqH^Ec`hpEKh?}9~`pS(_@{T*q7bEMdX(%Yh=rs(M;N%7DwD3s?EY$s6+UsFPz=|1IOMYKh3swz6zJEP{^aMD` zb?it%OIUE9cM$EIN`Z%vd23DX&=6oI{ruzOBKy*9Biwxz03Cn(U$Qdw8hLRn9yFW! zQn?xIelcNEDn^0FaN{-4Hj985)5ZMMC65T0ArkUE4drz8DYlR{!cY(7TMP4t4_xzPmlWL9htfW_UoE}PRJ=_YxbzRPYJoeu=ev zTEK+IP{4e~Z?Mg0ZK5?eMp_)_E5av}Y|ak7@!+?0+q+~qTP@uxFWaMq6=czfOv+EZ z4B7P*w|Q-vZ{P%VxF_wW%+Hbnfl)bF-0fWl#bai?yd=WM5}N}sSfgMtU2xM52+CUT zwZp8s+B)CEQaMMxmnz@Mh97170D+1!)7@&zGEg{14iLK}i|Ax#KC?m7wH%Ms`Lc@Z z;xZq`Gl&k@cF`_hUjDb&W6<`KUYEX+ej|xYOi(br^Fp_!r1#-BST=s~$m4|< zIjTDTNlq&dIN=Ro*<1|2=@Ytidiibs<_Da!6U5w~kqdIGQV`Kwm&MVNKO}wfl%G{| znS=~RUG&$GEb6i^;^Lua73qLReny|!K zuL8rHwQQ#yERbnePnFyDNxJ$L&KuQ&6Gm?QJOqDfA1=TiBorpXk!^G7HVM@wFr(2E zix{8{KVXaH&8GWB=Mb_R&b%hn?)Q@gPPh1DiR$~+{i6Ykb8ZZ^_5G*8j+g%<1G+YE31@K&4YVsgjN6Y)h@tlYas>XF5Pp!ZA3p`8Dwgv0`#JY8n2 zm&TqQkmWd}UJL&$>Yu=wrc$M$g6&Is#28(+0DKYaRX=5I z?>k8$VtYTp5F&qII``r;FgTeS)7mK&6RLj>+K?^hiFxS&dnM+1pr(h|E9YcKBB>_M}C( zkP{PrqSna-aM58L!o~fXk}^2{@qB%mcXf9uR>8BKl+oofRrt$Mb2{hb@*kOTnSc!Y z(jV+JsXeDJ;?o^W8FyGMWCbW3m!vCf7|62E<^s!s zsaCl(CmU`)N4{s)H`i4jvK|b=j`VoQ@IOyGIp^=H-4{L7gU>Yil+Hh5AY;dmXblMu zxQSd|YHj@$&F6}{a=a+vXuajYt6u_0jYICz*x^n{J~{aUUfZrpz+t0A2k|BrP5p&w zh5567Z`0udbKdC4@4tWUj*SMN;a6)*140H0_AVtyj?Dw@`GBy)8hbiHf}&W_mN2%g zV3^6i#$(1Rm&CF!_*k&|V#n6d0HYwW9up6<7h6-mOzSwt_eJ2{*OfEMc}EqbAHzN9 zz>vUgCnGhdRbMP*IR57Sam~HGmI7Vhw6@dH#Qm!-O<$|P-mtMht{X}gKbqhF;CZ>D zd;?WG{frRbTSVP;V{W!ARPd!z1D6%;7s~Q3%TMIR zKLgOE(x|35hW+Y95qRi#MwIneWXQ1iS&Tv;dKoOx&O1pFKA(ET67Th9aQI(x3pfIG zcPJv+rBSsK#9*Ps=rd`CWjfZe5R?IJF_~4-@4k$Bo`DbQ1kqBemRHiusJmQ5=$nkj zDWC>1t9rbvDZzKH|Ie!sq5EE~;8Pq<>QhZJ^XeWLMy(%*d2uCrB4h**m91e8*bs2r zTl{YFXR+`qg(VX&>(OUWYKh$|H^|qkj&?((*xP;)tp)udTnl%0kX`ZS4k-?tK%Kj8QL46ud^R?+>29y=)6bHz-gIj4|H8 zT(>5nw-+Ynb2qW+jjKTkw^WpYGvvYyHw$fP9#_UdZvJXF@5qk57utPI}dCwuQ_Yv5}vH~&v+T%>-8N)Y2 zHXVA9_f9;R^@a^Z$a$U`Ax|?LxtWq%!?r|Z{8DQWZ2I>Tp3tn+T{1S?V062C36>&z1EeX(h_N(Wr?D* zi}iN;)`Y`PKH* zqas@Nroz*H_y-PuFta?*6uKR!)&4Z%TDvb@FBJv39N5T(KvIMp72aCDE!dn{q)hwj z^Hwu))ah-XY2GXOe8-l4++8$+xi!GNFl|W`r;+1a>v%r&!%Ui%PMR)#`#Fg%H4%g0 zCl;12)>GhZd0^qNv4rbBx<>w5%zcBIe%5!&>b-MwgC+j_FeC{G+F7TjGTXR?-b-*V z_eTPjl*V8hE){3-G-PSYHbtXW`^Kq;1yVqZe9x<|x6kH1XA*nZYI5j}6(h;j#Ly3) zJ18y4=k2P)9$A)soS3%H;Y+#QogxpVybX6?&_$Q6Yv5Jsr_J1WFDtrGcYWkApIy7m zLQyPDcO`z&dx`9ifqrC}C6;^Tjqk2(p@3k~2pQugg=$G6(m~en-2wjZiN3SMm98_Zc-5FEmk zlI|#p7krPgVqj2UnPjjue4IZyk^nZkAEI>V-^k%#2To9Z#4Qk@RpEE89j`2Q4uqR; zq$fOW*_E|vSBg+gX;kJElexRwZ2k4Uzgg%P_U9yS5h~a zuM5IfsGc2yDY(;nPNGzsQ_4K_=SKZ@t=6!0h=PzHEz9-P*dx#b?c74-dc0CNE9(v0 zH(^xNb)sbnj;#hRy28$sXUBs7Xv(yrX~`-9*+gLTj@8SVyMiwd0=?a0`OdRdJXUq0 zUqPB)b9;}ph8wVyIiUT20w2)f^hwWp@HLg+e<_eZiKrKWk|d0$edYJ&Gb7uBq6{Yb zd|qiXtWbA=CbJ{aeYV!8f0QW?41;_-Zk0*R6n}%n(uLgTte|-9ALgwTI3mS(=M}~G z&`(*&j_Oah#A5_(!~Rha5n~aX%%~9(nnW%#zkH!|6~pE*ikzp849P* z5vJt%)L#6(T>ukDx^E7DtHhP@9n?$aMcNG92ngKolc{Mb`(76H7T7v}0CZqh>SqPI zy?}LZ94?na?XnD4` zGM>S*KX%{ zS!Juma^hcdW^cqMJ;NL12z+fzlCP#ujAIvL+>pa*PiVoV5vhAUYgCPc{W@pie+1-! zYz0yOF7eK{Vn}+e-ure8G%f(I8uWQj10P|f5b7HPNp*1fA)Ppt>UftEUA@PyMgeP_F8=6)v73S4`Z$8 zA8&(&Q}N7@x@$(XvyX_xG$h@LMx11vQkW6yoBBm`qRZT0pv#YQ!(Lf#>x${7tv?Oa zB;oqD2yeZv;w)nXbNG$Pha@z1{u2p2if*UM$sDV_bm`qCmFLfNr~S#Y9Z@mro=XGT z)a18UpAe@gwsA9t>(KMiYzb~;^5WZC6fD+)c&^_3QUZD z7?6>RlM8d?N7G5w-dSGg)+(MRYwnO!n%2_v>5ORqp4pz&-YQ`T>-Z^2b|_ zc(}dglf?%1>V?xa@#(*=d(Zn5P+XT$VY`+Q4r6xpLOw>#cLXU>;SdfSIEhfEBA;#Q zt;(#G9i;Vs$63e4(HQO)(5|1|m2oXq2$n>zqYyZf*Yb)LI#`}6 z9xm~SNJ+W#!a*4Ca1Gtm%qO{;=ZeZbq&=G!Qe2O|Or|mK)f?e4R&>hdc4K<@Zt9$U z&e_9^0eLdM)PYV@?P-bpQ8r!Ku@rfJ!OVlJZ*E((N3;)6la!<%EhQVp6_BTTf(L|y zvglf$c3k)x@xE=c00#TwJ)So9)`MiHgq%-utxtF7J+!}!xUvi-pf2%>P+!F5$rT;B;T`j!ywT^tH+h14OupY2miaUKd|W7sBZrZUt`~jv)}zBE8U1L3 zB5fi2P}y&qnL_$^mttMjOh$bu-}hg0pSio&-K_U(Qk;RkC_|g#_%oDSM^R?}ZJZap zL|#)_>uiOBScEJ%DEzxcu5<)C$F4}_ezZ<2@5-k=lZ7Jl*9_Q)vOQ9g6^ewecZP|G zGvShZAp`fHX6~y~fLrpqjWiGB*^^`YG>;d;uwD7_#-iE#Kh{W}1`2V6HPi@mqwHCP z)`!>8Z67!v2BpXIdoRVlN3K7Fj*&$DwhnknaF%{#_n}?b;;JIM4XJWr?J$hS?Dkoz zKRa{#0sY(JJSgSo{;%(X!q2;khj>I*OBYfoFz>`n6WWW(5PM9O0d!aLgh|==>4D@K zB}?~tGpF>%Ml&kDkMwJZm7Mm!7qlRyg11&=fev~)+dtjiNEwWYqkWf)#9VI6lGT>8 zE$jp%&X4Of(12n&p5@v-ioUM4*h!&%wGeyr*fL}g!=cN$h-spIDY!3UHD+NA`fgMfSmb8c zB#M+^U*T{YbnlobroAyyBgGUA}Z2L(N0 zf!<#vgH}b{5z$0r=qEMWk-$X7YGSvtHE z1#D9CHsIEZWxzGX-(v!abNN{COm&xqE9zbplezgfAV#iTi~BwpB(<^e!__uQZ&Cla zS#~of`5J3XyP#2B?0crTvxFI&M+dUu#~0`@G-q`eTiCS$YN-;tDWe$hw?< za=QI^3(q9>M%;N$8CE%HmE=rg{jSSu>oLVHY(H5uM`Y0Zho)x_DT^{{G~osH_|8+< z+MU$^p8aPQ{nT`65YAAKNBPQ6eU+IdUkNmDMoHtQwd1_BrBmp(>GG*=##PT#O*FyI z@}!f~&58HrZwc1YxLqgSS!#(UJq3%=VYfP1SU;35fg$eE^vD;RS}W$`5@%k~SSH>u zRIjaB?hNb4;#(n`wCJ}*L_uK^_dXiT-%sf&tfaJ1X7biz(Al9`rRnYmCGc7~_N&ZE zI`h#^zE8g)7eWp{lISV>V}!E%Z`_z=W`^tb*~tPSn4ie=s-&Jn@-Glfy}PGMp?q&O zX7_0VM4zs`hK@P)n!=^qCNqAy*n$bp_;z`%l@J6>s z`}w+>Gr5iOB&0*~$m`UfpE679M_%*N1%fkTLn^Y8e;3XTf7{g(a1Gtg{-hxmK?G6w zHd-!L!Q^Owe`-S;_RK8Z_3V}wz2vuzQ5w__H-s>$p3>A!M4Q;S&6y=DNh{G(xvOE* z&dc{_0$b3a6Z=AG_B&tVL)Fb=ON|cl16AtSuaG6{vJAmfh0`IwrHaq#DEbex>Ba*? zG=s*K?k+J=Dxr(^ytbx!IMG4Xl+yK)n2=VJ`3Sp+Iy8d)PjqLAhA^h`KCbSM%2=@o)(B=oJe zcw}+SOMx4qMZBqPqW1=hbprQ`C`S3|7E@CiScy12u1S!>m9ECLT>h2tS^NIXhwt;= zXP+fG)vb7V;Hmw~n^U(y3L;q^@A%FAbCM4JQjmv=cwvRuL-MQ1h2-7ES^_zOSe@0A zk26xU&m{c!|9G~^p(#_Fgt4wDlkraZf4foKIV4}dtC^4lRYAfWy1DpKI?N|nRLX$1 zU;hAMCN91l9lwrkTYZg|3|-In&`ba1O0%y~z#jDL73~JFrdIP5?#Bz_NcK2*aSKfw z{iX&41LghJ#d}kv{Zo~RBey;@xv)@XLNlqN1(rX=`%Sp6{e^2sTrky3!=Fr+ZZ=_0 z;6w;AqOUa-}5X-p*@;m$4V{Prf5Uy1cT{qzmoPMtq(}U*A zis~uJ)_)&Ma9^bgPqcxn>-rG{{YP2~L;X;GlXSlyS@ep#-&@xRQ}^lDw2RVpEF_s{ zTy1-rz?>p$DJmk1WL>6}haa?SsgHdixpAAKoKqv3d)e72Zu;FprNtSy*6pB^URyG{ zry5K@+kY7v??>`}ruFU*EPCfYEMlkLbjdoJgsh~t?gOjZeE;5aa%}!JVj*HL$LByT z>*PeH>@kg9%MT#cPcee;FI`?>q$3I0PbsifC>1TE?SAAkAK83$A;o${z;Z*!ZDagT zw{PW&9^*^bYA#>u$`>sy^{mBIC0jsgLe-es}ks ziBd3M*rky7pY>Y4;Ws3;?7BRs7Vhtu@-pi$?a^#l%dPDX$+zh)(;C?(s}Bp(At7f^ z9+83FZ)+f6X82y5QlSIys`F99qS%Fprl4!+MLD;UM7o)w4>2{&4pw_ZQ~8!?~}udp&|!mSfL#=lJs*$e++OJfGv9 z_tXg+BE@0MxfrfKmmqGu<I-ZBa4~HLYs|np&GVx^Fgcej?s;xl{t~_VPywWD(|^w z+qe;Ev3AEJ+^}hBk^;H{8#_u@n3%zVKrbg1VF%jE{XEd4#fh5Z%#Lq0Oj3OD*KAt7 z$YzO|Z0Kbm4jaQ($Mnm!-^ebPFF-e|p~p@B4|hJgDt>(B7KbC{j?#7^vLYgn?bgxq z_sdXW4l@hx=SZn$3CgE#lzk2z%G`7k+l>Nl3dL? zzK5RJz)5Lp}VX&c&2**DPci5Vo z(SmSvXR*?~D#T!D{491luSZ?Em-eoU=B%|z1#{L;*Au5JOxaLA7a2G9Tk?Z5@LcU6 zK9{-V5@()?S9VOU!L~sq=iNjvW3xHxf<9fiE3FEap6*h001nc~qeG$*=pi=cezSO8+f!#z_feia34$X^ej39B z$dCVNwqQbRJVM>;WY_%6XUuq(ePjX`{Y#yH{WW^Y98#1gR91}Y^mp9<10xY{fgZ3kGev=bUHLSX(}nwi{>_IH(42ZNFXWx_;czs+oO!&2Fr^ zh8lXfx&3XLmc^XTAjg}Os(<-=(RlJ5+!IwZ?la|}9U}x;=6dRSSI;PU%wGxr#-|EL zW4Q95iA}S%ma1=qDLDsbC|y}xp#J2-?=#&F(;QsQQ}E9s_<&IW1b}^3lErTdS4}%; z-CQZ1XC_V2F3F|ufF(bkC8tTHt9u8}nDIq^;y+Kn;8(`!%U(Xkg;*Vav z)DmHr`O=YZZgq$4+b68I#;^i?K(T?LH^$ZRbxBi%0OKz{N33t3=ZMSCA`iS*ZT;B6 z2Eku$d=R$VWdi4fi<(<`%QOuHgdS~rf6Y?FOn5(V4GM09sy!*bC9zZdd(ZYxx?HScN7UQL*{LA@ZKzrvpV66&g0!&I;3PCvh+>yx*CKPA|Z zk?q-q_X9siMvuy3np>0+J9}Hy&C2K+Q0WZurir_}2%NUGOo?tN9*XM?V7Ln@n06kQ zM!%F~%qVk97b~LaxxC0WFU_7OR&;lXj7l$B;5Vu7yb+Z@Kv$5NWGYty6N*?F%jIN) zmkDHI95YYePMQUI!UQZ5UWn)K9|<#|z$|SoZhvenKv~%>#^Z@b zp8T8pXvs4t!H9HrY}0(}jN#ml`E;D&(v{NnR1BZ^D3TiGy~HEv{f*&w!Z%bT3lvKC zzeLE7xGOz0X%t+v-xZRTo=SB(unb=%QfQOxdN<-4K296i+83Vso{J^vz8wS@ku=W# zl3&dir->D-46T>?`8hE5(eClzsV7~}-QHa$0JBTBGj)F)xwmp2p*8e-qWibNSa<(W zD;+cIKwfzmd1AqP(tQ1=etZ3IZodqD`Zp(@KJ-Zb3Tp*jQ%s>pe5vxmXN5|eOOV`b z3gAJsas`1?ytXNegR%lnB54?~jYgJajsffl#%45j;Y9^#=ua~$SJ@xku5fMi8!QH6`k`u9E05ms}f*lHA)-><=5@6wq0!IL=Rc|{FG zk(*>H4r{tjawW8e80x$f`;Ba(y)#LTv>u-v`ocn5ol-?Y1%Wr=y9D1(UDGkFi*qPA zc2cI3h8+vUr1<6jPqSZ7%Os|?X1RJ;e0*#8 zqh0nM*Bt6X?Y`U-J}dqN2{Kk;g=SAGlxW`A8*iW2;p#Ft@c(}pM7@(5=+}pbagT;X z#R#{K1Ccqcn?v79-Up5&ParFsxlQ@18vtihly@-)X=~n`JhjjL7joc#=6l>mZ+_6S zc7TI*FOxoFGLbIzbK7Iu>_0M(I~n>0(hdU)RZG43olvEL-Ff~$YYRgTt!+%Vfwh?(n2OfHKapH_rDMr>!?0F=*Us+Rnx0&y3Ny_)UK(&Cv-!bP&4EX zK7$L{uh$NdDZ0Hn?)qDPqpI{Q$iUmyIQ@OSn9Rq4j%uVx12QdtrFafkp5|{HP_fyH z)o0*s%|%Lo6E6x(sep9w&!t)T9+oJ!a?jG6wIZ`NE$RnlwSu;GT6T6cP!f}^{We;* z#uKYZeE=Qe?#MfIbaeZT=#aW}%k^#KPLkw0IW=_ty^LT-kc$x+3&!2YgpICC#hahG`y!zs zuJMlS0?LJoxnmzw+LCmTQ->B#GE#jnS3B3XJ98f3y=)eM?MEalc>ohiM6xFG8@45D zz0?)M2wwm|p@(z8r~%mw&)fLu6`uWz9g=_TG9_bkUw20Yhudry1&HnQWj!-XlR75LRX zng3`~N+!zuGvw@C0(lmHGdDIm35Rm@ecMa^99nSLX=~mD13;hYnyTG$Mfcrx4 zaAp+c;+YA2?-CgnW}I=U;G?F*5vv-s!Ox77E~BNj2vjk4X&A55t}PlRsRch;eLaX{ ztTdPEYN01{4ztv1v$=?WNv>&|MH*-U{!RN9CS-vf zWELz+Dh6lfL`Z;nnvg|f5xAGmACd+<*T1n~yCpKav}2@^cjj!4yw1#Y)om|J9I))D zT7t($4WC0VJi_(AZiMtkm^$|CK-NL+kR@POqBy~VbL&q4Na)PW^RQ{`A0tBMb8msrlc;TPQZC7% zH0BHhcD!q4ffCo+Iv=+VFeL*kj;vqNc;}MJ<2I%eQ8sI4$jV5rm}3X%H%!htjZ$PL z$1xxoyZH;4xlfGG`cY}w^8nOqQ9UCBO25WfStD_(#yd)(5I zet1?>AbozcJPK0t5=O8w{&{=qz-^xJFkyXQ3wodngwIbZIfRhfdMbqw~@Vy?u|N@uT^HD zowFVIN4YLS{L^$G!^7jN4hLT2ZQuvz)EK|DC7@H3_TA}oB#kPd%h+-h{hP8u)K6K* zdh-P5(Lrx2e-{9URv?IT%|Rx)sF-?KMmnDC z_&`X8rKdX|$mIY34*6Evpx1U&5>J4U``Iy}WD7<(hj>2ow7Ih6g#Bcvb5B-B%^tg;;Wrt%X#vN7ea8tf9E2`t{C7b z0CyyomulRMLhR=+#Mu@Vg(eRkNNeRHeB|7(OGg>|{{zgiMS7asX`yw+ckc=ZUa^$>${jb!X+k`fZ{;K){(nu?Vuu8bdz_nH#lH}|7 zT$P9|x?6DA4_*Tp9%#)!*V|W5)oQE7p5A!Zox#F5TzIziNH6i@- zV#Ray)PPyjNH{04l>o+^R`rnb3=$^4mos)5W~0F1J)`ZqIu9K9Qz&yP$B}hI21cK$ zMaRQALcTavdh2Ir{`TE37u>dnewe>O{z8D8ZjVT}ntI-&<*;T{=+XAQ&!vD6CBErB zn5io6#ZBH?;l{+Z_|e1o+S#!gmH^^}Yp3O-tI$S6;Gz^wZG3nOk=lL&!~X29Y&6X{ z{13YZ;D$zSV6oM@YCam1@m6g*9XsmQ@yARKzhQ%PX_}khiu)L~pX;lZ{l~Y;pRkc| zs3ucHf*Q80_P{ zQ}raF#izWXAO*JCKE`=QP6Z6;c4AKqQ>U!>6Ibb5>lwTYDV_Ph`04vWC~ zD2jPw1_Bv%(0=*G{B#y5hlzF#O`T-duV>Y6Xo0f_uI>TymsBY+jQbaC zkWy$?pgA}yyg+`Y7!!m!iQyDUy!@B1VXJnaSYkc5@^L~{N(|kTy)Npy&-DCY1s=9X z?wpHZ7R0yyY%%_#b5TD9&i9WOhWh#oq9cgx@E#J#RhESR-E)I1fsjZyB=E_>psZt> zo_-&(YmOS&ES*pftbpsjf7T`1L9k5O<{d z^gM8E*0HJ|MN>HvhTQD?7^E9#T~DwVV7QK45V}kqeXS%_Lw{^;6?ym70@Qb1h~m2K zdx`Ko(Tk`l3h>|tkCPUL#AbPignI5fCcmI@84}9ru*1Dzll_9r`0`s0Zy{4K+ah@UKcnqBK47@mR){q$5+$TR@c zws29k9}CdmA2{;C9rEu8yIT5P%U2doc$+!ljoG%EJAA8cPBnrO}<>8xGD5xal(3slD;!71{LwkYD}nJ`-05+ytxs*W3NZEEbP1ZrU|pI*nvo zEZ41hzL}eOY=h1b*`NsV=unhZ+XSUkhD@w<9e_Wr`j_0=9ll^Dddvh90&qU1=?QE7 zRtr9}+sBZ@u{uD8l*veL?WQKKr-~w6xk^FtP5vDDI8UH%P#EHwR^Lm6x$CXiazP-P z4mWQZAt90r+WmA9tMio6F9osTR#T>a-T8JAfVEl^vpu-&41h5$LF&0jjtOM<3(B-8 z1qAN6ybPdRU<%kuK4IMx3LGsJ#L>3%chrG5tq3%|Pc|bAq9p;#g`ojG>Ffw%-u?Ne zGi<{`NaH_f**TVvgKD(KuNUh?6(yGft zUYA(cHYqg18)wlD&yx-MjrN#hgW&_tTHhwGd;h9!^@WrM;F{~njzi@WSy3A3gmShRzfz!6TuthElqkr!Xjc++h-vNc`^}# zWm~`&5wvMhYJNy=xgMqfXAUTqVSdCu&awZw8Q)53V~uHauwE;K1>6}{af!D{ z`ufZ8pv=vd@3=7?t$|~yGpvZDGU{14@mi~`hW~}l|CEl<-&z-U-Al16KB8eQgW0?9 zB%Ju>j_BlnwCL7Y+5OGufkBM>lz?P4lw{5djaCuJd_uc4Q1r(-M(Ah6f(T6-&DP-p z;i>G$!a12(juhKib0%||-*Fmox-?WA_??WHe{tNz;>6@ib5oH6(GDYHSpB-c$IHxw zss5Ap>2F<6;Zsiqc7&Aw4^?j&7j@LNZ4ce0q_jv0I)wC4l7ckSDJ3NWA`CeQ79E3h zDBUHX#DH`oNOwypFarn-;j_8#>w4bj{h+_$3;Y@O?7j9{=Xo9`r{*cYfu|2H!F%bS zyaHjA+Xnfl{E=NozE4+-Lr`PZ`=DpSY6UX=kok;6Ba&-TFM)DhOEvH;9bJ%Vi#fbu zy=Z%9tWfl|@D(ScAFFT?`u#(`gJ+YibNQ`67gPYsbIu`&rHgIU`QAtncI6#=RxAsi zJ!zQN4DIU-5grk&a3j~-D6wu0J0frfjr_at;30=O{Z`~Tr6AV@R{+JUq4b_iydz@}Dg`&&jN^?i0 zJ9zwf;+1sWzG`F9EN2?xRb1QmLG%HvUbt80N-sG7o!3v)|Jo>4l10o!hDd)a^P=Etx4`vC-2-t5#)99T|o2;JI zaCS9!3Hy_OnSeQ*x!sZ2yIp&oQE^dDyKSI-WlR*cyLmsRIt{+LWWV|*{>C=qKrcAz z+sk$tVrMSdsD!a9a3ne6ZR3&&#zimvDAdWec-1WFmZiAJ(K#rEY~h*(cW&$J*x6q~ z>+#P+fHBxHGu2!WM3cdve_AZDi&@0Cy1VMsDHfubmO6sdA+lO3eFNQTAs3bYUX$&$ z_!*yvkD9C@FLDSNb+ZS;dlJ8W#=H!-LkBA5aBOe=Rg=eh&Vp@Rlw^<(HKrl(3uBX_ zM%-7(r4c`1?q27k&9F)q-`!hgbvtTnC?INbHZf4tQM1r5j$ntzPDb__QOuIjhvUWIV@AUb+kD(FJ zmekFL%_W7J2OqWO!-g(tg4A(VK;a|b`_|h>f&F2_1dDtBA2n>x(suNAC(q7J1&v-X=s8hMbhjGo-DJNjcBi)#+}KoSf1?% z&wqq)HZ(R(kr#&N2gdUPW#_kW7W%hx^FcvXxoe;i6_KHR$@cmUWtl9n!_Jw(m*h`8 zv0TbbK}O23{p!-g04zK_)&CAfx3RNf{n+Q2N0ARW{I9^0_F^8V`uX9(Sn&&3?!suY zIFI~w;P1z==WlF(C*I!(?e(Ox&6gMbxYb0qQu=$tG9=v3cz7obqg+rtwa7q!LpQ*n z;ywF5m+#*N-+zy7b_~i|sb68<5{t+5HTac0fPMU#v%p1}loU-F^XkdJap)Sajx#>E z#{Q97M+W{BZudyGeATD?4eJm5tQ#jug(n~*;=}ya@uzN5hf=}g*q0h3J402r9bTtf4N}zENYvqZc_ja%S%=g=xAOOdM<5MZCJ(wx z{au{rlKuQ=-0yawwm1vTJ&1N)BAx|$1^VHb*EQ2mm+ef$-)9UaB-<%&aB~ zv!nff-E9ul`qnH1czBUr@dR;Q+#QBeuOew)3*-N8uU81epj1F|ngM;JAR(>Hd!~Xb zO!ANn$y)Rq%gbl6|4)zDfER1fE!;iQ*uW5$aQ<*Jnfsxn!;s;cPPjTBC2hTsojoHV1l?RKY* z9MI0>g)5i}!dT;xfE_fU`?O@_3saW~{@A-C8eUeBS-jlB+zZAD1QGQJ)4u4(Lw!YD zk+;H**J0H&MSK~aYb9Tj*J20pI`>l~&kP(lA+O?P8e`Bm*GWQqi~4@_?vnT6HRJj% z_LAw)!iB`gSSwlcQY?SjgkuI#%0DEmQ% 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�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¥+*fynaN$9Mv9ASZ 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�#@Z62Q>yf^LM5_h>KNVL8D3KY5 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 E11kJmVgLXD literal 0 HcmV?d00001 diff --git a/docs/source/library-user-guide/upgrading.md b/docs/source/library-user-guide/upgrading.md index 11fd495665225..db5078d603f2f 100644 --- a/docs/source/library-user-guide/upgrading.md +++ b/docs/source/library-user-guide/upgrading.md @@ -19,6 +19,122 @@ # Upgrade Guides +## DataFusion `47.0.0` + +This section calls out some of the major changes in the `47.0.0` release of DataFusion. + +Here are some example upgrade PRs that demonstrate changes required when upgrading from DataFusion 46.0.0: + +- [delta-rs Upgrade to `47.0.0`](https://github.com/delta-io/delta-rs/pull/3378) +- [DataFusion Comet Upgrade to `47.0.0`](https://github.com/apache/datafusion-comet/pull/1563) +- [Sail Upgrade to `47.0.0`](https://github.com/lakehq/sail/pull/434) + +### Upgrades to `arrow-rs` and `arrow-parquet` 55.0.0 and `object_store` 0.12.0 + +Several APIs are changed in the underlying arrow and parquet libraries to use a +`u64` instead of `usize` to better support WASM (See [#7371] and [#6961]) + +Additionally `ObjectStore::list` and `ObjectStore::list_with_offset` have been changed to return `static` lifetimes (See [#6619]) + +[#6619]: https://github.com/apache/arrow-rs/pull/6619 +[#7371]: https://github.com/apache/arrow-rs/pull/7371 +[#7328]: https://github.com/apache/arrow-rs/pull/6961 + +This requires converting from `usize` to `u64` occasionally as well as changes to `ObjectStore` implementations such as + +```rust +# /* comment to avoid running +impl Objectstore { + ... + // The range is now a u64 instead of usize + async fn get_range(&self, location: &Path, range: Range) -> ObjectStoreResult { + self.inner.get_range(location, range).await + } + ... + // the lifetime is now 'static instead of `_ (meaning the captured closure can't contain references) + // (this also applies to list_with_offset) + fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, ObjectStoreResult> { + self.inner.list(prefix) + } +} +# */ +``` + +The `ParquetObjectReader` has been updated to no longer require the object size +(it can be fetched using a single suffix request). See [#7334] for details + +[#7334]: https://github.com/apache/arrow-rs/pull/7334 + +Pattern in DataFusion `46.0.0`: + +```rust +# /* comment to avoid running +let meta: ObjectMeta = ...; +let reader = ParquetObjectReader::new(store, meta); +# */ +``` + +Pattern in DataFusion `47.0.0`: + +```rust +# /* comment to avoid running +let meta: ObjectMeta = ...; +let reader = ParquetObjectReader::new(store, location) + .with_file_size(meta.size); +# */ +``` + +### `DisplayFormatType::TreeRender` + +DataFusion now supports [`tree` style explain plans]. Implementations of +`Executionplan` must also provide a description in the +`DisplayFormatType::TreeRender` format. This can be the same as the existing +`DisplayFormatType::Default`. + +[`tree` style explain plans]: https://datafusion.apache.org/user-guide/sql/explain.html#tree-format-default + +### Removed Deprecated APIs + +Several APIs have been removed in this release. These were either deprecated +previously or were hard to use correctly such as the multiple different +`ScalarUDFImpl::invoke*` APIs. See [#15130], [#15123], and [#15027] for more +details. + +[#15130]: https://github.com/apache/datafusion/pull/15130 +[#15123]: https://github.com/apache/datafusion/pull/15123 +[#15027]: https://github.com/apache/datafusion/pull/15027 + +## `FileScanConfig` --> `FileScanConfigBuilder` + +Previously, `FileScanConfig::build()` directly created ExecutionPlans. In +DataFusion 47.0.0 this has been changed to use `FileScanConfigBuilder`. See +[#15352] for details. + +[#15352]: https://github.com/apache/datafusion/pull/15352 + +Pattern in DataFusion `46.0.0`: + +```rust +# /* comment to avoid running +let plan = FileScanConfig::new(url, schema, Arc::new(file_source)) + .with_statistics(stats) + ... + .build() +# */ +``` + +Pattern in DataFusion `47.0.0`: + +```rust +# /* comment to avoid running +let config = FileScanConfigBuilder::new(url, schema, Arc::new(file_source)) + .with_statistics(stats) + ... + .build(); +let scan = DataSourceExec::from_data_source(config); +# */ +``` + ## DataFusion `46.0.0` ### Use `invoke_with_args` instead of `invoke()` and `invoke_batch()` @@ -39,7 +155,7 @@ below. See [PR 14876] for an example. Given existing code like this: ```rust -# /* +# /* comment to avoid running impl ScalarUDFImpl for SparkConcat { ... fn invoke_batch(&self, args: &[ColumnarValue], number_rows: usize) -> Result { @@ -59,7 +175,7 @@ impl ScalarUDFImpl for SparkConcat { To ```rust -# /* comment out so they don't run +# /* comment to avoid running impl ScalarUDFImpl for SparkConcat { ... fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { diff --git a/docs/source/user-guide/cli/datasources.md b/docs/source/user-guide/cli/datasources.md index 39172e94e5f80..2e14f1f54c6c1 100644 --- a/docs/source/user-guide/cli/datasources.md +++ b/docs/source/user-guide/cli/datasources.md @@ -95,8 +95,7 @@ additional configuration options. # `CREATE EXTERNAL TABLE` It is also possible to create a table backed by files or remote locations via -`CREATE EXTERNAL TABLE` as shown below. Note that wildcards (e.g. `*`) are also -supported +`CREATE EXTERNAL TABLE` as shown below. Note that DataFusion does not support wildcards (e.g. `*`) in file paths; instead, specify the directory path directly to read all compatible files in that directory. For example, to create a table `hits` backed by a local parquet file, use: @@ -126,6 +125,32 @@ select count(*) from hits; 1 row in set. Query took 0.344 seconds. ``` +**Why Wildcards Are Not Supported** + +Although wildcards (e.g., _.parquet or \*\*/_.parquet) may work for local filesystems in some cases, they are not officially supported by DataFusion. This is because wildcards are not universally applicable across all storage backends (e.g., S3, GCS). Instead, DataFusion expects the user to specify the directory path, and it will automatically read all compatible files within that directory. + +For example, the following usage is not supported: + +```sql +CREATE EXTERNAL TABLE test ( + message TEXT, + day DATE +) +STORED AS PARQUET +LOCATION 'gs://bucket/*.parquet'; +``` + +Instead, you should use: + +```sql +CREATE EXTERNAL TABLE test ( + message TEXT, + day DATE +) +STORED AS PARQUET +LOCATION 'gs://bucket/my_table'; +``` + # Formats ## Parquet @@ -149,14 +174,6 @@ STORED AS PARQUET LOCATION '/mnt/nyctaxi/'; ``` -Register a single folder parquet datasource by specifying a wildcard for files to read - -```sql -CREATE EXTERNAL TABLE taxi -STORED AS PARQUET -LOCATION '/mnt/nyctaxi/*.parquet'; -``` - ## CSV DataFusion will infer the CSV schema automatically or you can provide it explicitly. diff --git a/docs/source/user-guide/cli/usage.md b/docs/source/user-guide/cli/usage.md index fb238dad10bb1..68b09d3199840 100644 --- a/docs/source/user-guide/cli/usage.md +++ b/docs/source/user-guide/cli/usage.md @@ -57,6 +57,9 @@ OPTIONS: --mem-pool-type Specify the memory pool type 'greedy' or 'fair', default to 'greedy' + -d, --disk-limit + Available disk space for spilling queries (e.g. '10g'), default to None (uses DataFusion's default value of '100g') + -p, --data-path Path to your data, default to current directory diff --git a/docs/source/user-guide/concepts-readings-events.md b/docs/source/user-guide/concepts-readings-events.md index fef677dd3a621..ad444ef91c474 100644 --- a/docs/source/user-guide/concepts-readings-events.md +++ b/docs/source/user-guide/concepts-readings-events.md @@ -37,6 +37,10 @@ This is a list of DataFusion related blog posts, articles, and other resources. Please open a PR to add any new resources you create or find +- **2025-03-21** [Blog: Efficient Filter Pushdown in Parquet](https://datafusion.apache.org/blog/2025/03/21/parquet-pushdown/) + +- **2025-03-20** [Blog: Parquet Pruning in DataFusion: Read Only What Matters](https://datafusion.apache.org/blog/2025/03/20/parquet-pruning/) + - **2025-02-12** [Video: Alex Kesling on Apache Arrow DataFusion - Papers We Love NYC ](https://www.youtube.com/watch?v=6A4vFRpSq3k) - **2025-01-30** [Video: Data & Drinks: Building Next-Gen Data Systems with Apache DataFusion](https://www.youtube.com/watch?v=GruBeVDoWq4) @@ -134,6 +138,8 @@ This is a list of DataFusion related blog posts, articles, and other resources. ## 📅 Release Notes & Updates +- **2025-03-24** [Apache DataFusion 46.0.0 Released](https://datafusion.apache.org/blog/2025/03/24/datafusion-46.0.0/) + - **2024-09-14** [Apache DataFusion Python 43.1.0 Released](https://datafusion.apache.org/blog/2024/12/14/datafusion-python-43.1.0/) - **2024-08-24** [Apache DataFusion Python 40.1.0 Released, Significant usability updates](https://datafusion.apache.org/blog/2024/08/20/python-datafusion-40.0.0/) diff --git a/docs/source/user-guide/configs.md b/docs/source/user-guide/configs.md index 68e21183938b1..7a46d59d893e6 100644 --- a/docs/source/user-guide/configs.md +++ b/docs/source/user-guide/configs.md @@ -58,6 +58,7 @@ Environment variables are read during `SessionConfig` initialisation so they mus | datafusion.execution.parquet.reorder_filters | false | (reading) If true, filter expressions evaluated during the parquet decoding operation will be reordered heuristically to minimize the cost of evaluation. If false, the filters are applied in the same order as written in the query | | datafusion.execution.parquet.schema_force_view_types | true | (reading) If true, parquet reader will read columns of `Utf8/Utf8Large` with `Utf8View`, and `Binary/BinaryLarge` with `BinaryView`. | | datafusion.execution.parquet.binary_as_string | false | (reading) If true, parquet reader will read columns of `Binary/LargeBinary` with `Utf8`, and `BinaryView` with `Utf8View`. Parquet files generated by some legacy writers do not correctly set the UTF8 flag for strings, causing string columns to be loaded as BLOB instead. | +| datafusion.execution.parquet.coerce_int96 | NULL | (reading) If true, parquet reader will read columns of physical type int96 as originating from a different resolution than nanosecond. This is useful for reading data from systems like Spark which stores microsecond resolution timestamps in an int96 allowing it to write values with a larger date range than 64-bit timestamps with nanosecond resolution. | | datafusion.execution.parquet.data_pagesize_limit | 1048576 | (writing) Sets best effort maximum size of data page in bytes | | datafusion.execution.parquet.write_batch_size | 1024 | (writing) Sets write_batch_size in bytes | | datafusion.execution.parquet.writer_version | 1.0 | (writing) Sets parquet writer version valid values are "1.0" and "2.0" | @@ -68,7 +69,7 @@ Environment variables are read during `SessionConfig` initialisation so they mus | datafusion.execution.parquet.statistics_enabled | page | (writing) Sets if statistics are enabled for any column Valid values are: "none", "chunk", and "page" These values are not case sensitive. If NULL, uses default parquet writer setting | | datafusion.execution.parquet.max_statistics_size | 4096 | (writing) Sets max statistics size for any column. If NULL, uses default parquet writer setting max_statistics_size is deprecated, currently it is not being used | | datafusion.execution.parquet.max_row_group_size | 1048576 | (writing) Target maximum number of rows in each row group (defaults to 1M rows). Writing larger row groups requires more memory to write, but can get better compression and be faster to read. | -| datafusion.execution.parquet.created_by | datafusion version 46.0.1 | (writing) Sets "created by" property | +| datafusion.execution.parquet.created_by | datafusion version 47.0.0 | (writing) Sets "created by" property | | datafusion.execution.parquet.column_index_truncate_length | 64 | (writing) Sets column index truncate length | | datafusion.execution.parquet.statistics_truncate_length | NULL | (writing) Sets statictics truncate length. If NULL, uses default parquet writer setting | | datafusion.execution.parquet.data_page_row_count_limit | 20000 | (writing) Sets best effort maximum number of rows in data page | diff --git a/docs/source/user-guide/introduction.md b/docs/source/user-guide/introduction.md index 14d6ab177dc34..1879221aa4d11 100644 --- a/docs/source/user-guide/introduction.md +++ b/docs/source/user-guide/introduction.md @@ -95,6 +95,7 @@ Here are some active projects using DataFusion: - [Arroyo](https://github.com/ArroyoSystems/arroyo) Distributed stream processing engine in Rust +- [ArkFlow](https://github.com/arkflow-rs/arkflow) High-performance Rust stream processing engine - [Ballista](https://github.com/apache/datafusion-ballista) Distributed SQL Query Engine - [Blaze](https://github.com/kwai/blaze) The Blaze accelerator for Apache Spark leverages native vectorized execution to accelerate query processing - [CnosDB](https://github.com/cnosdb/cnosdb) Open Source Distributed Time Series Database @@ -104,6 +105,7 @@ Here are some active projects using DataFusion: - [datafusion-dft](https://github.com/datafusion-contrib/datafusion-dft) Batteries included CLI, TUI, and server implementations for DataFusion. - [delta-rs](https://github.com/delta-io/delta-rs) Native Rust implementation of Delta Lake - [Exon](https://github.com/wheretrue/exon) Analysis toolkit for life-science applications +- [Feldera](https://github.com/feldera/feldera) Fast query engine for incremental computation - [Funnel](https://funnel.io/) Data Platform powering Marketing Intelligence applications. - [GlareDB](https://github.com/GlareDB/glaredb) Fast SQL database for querying and analyzing distributed data. - [GreptimeDB](https://github.com/GreptimeTeam/greptimedb) Open Source & Cloud Native Distributed Time Series Database diff --git a/docs/source/user-guide/runtime_configs.md b/docs/source/user-guide/runtime_configs.md new file mode 100644 index 0000000000000..feef709db9929 --- /dev/null +++ b/docs/source/user-guide/runtime_configs.md @@ -0,0 +1,40 @@ + + + + +# Runtime Environment Configurations + +DataFusion runtime configurations can be set via SQL using the `SET` command. + +For example, to configure `datafusion.runtime.memory_limit`: + +```sql +SET datafusion.runtime.memory_limit = '2G'; +``` + +The following runtime configuration settings are available: + +| key | default | description | +| ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| datafusion.runtime.memory_limit | NULL | Maximum memory limit for query execution. Supports suffixes K (kilobytes), M (megabytes), and G (gigabytes). Example: '2G' for 2 gigabytes. | diff --git a/docs/source/user-guide/sql/aggregate_functions.md b/docs/source/user-guide/sql/aggregate_functions.md index c7f5c5f674424..774a4fae6bf32 100644 --- a/docs/source/user-guide/sql/aggregate_functions.md +++ b/docs/source/user-guide/sql/aggregate_functions.md @@ -371,10 +371,10 @@ min(expression) ### `string_agg` -Concatenates the values of string expressions and places separator values between them. +Concatenates the values of string expressions and places separator values between them. If ordering is required, strings are concatenated in the specified order. This aggregation function can only mix DISTINCT and ORDER BY if the ordering expression is exactly the same as the first argument expression. ```sql -string_agg(expression, delimiter) +string_agg([DISTINCT] expression, delimiter [ORDER BY expression]) ``` #### Arguments @@ -390,7 +390,21 @@ string_agg(expression, delimiter) +--------------------------+ | names_list | +--------------------------+ -| Alice, Bob, Charlie | +| Alice, Bob, Bob, Charlie | ++--------------------------+ +> SELECT string_agg(name, ', ' ORDER BY name DESC) AS names_list + FROM employee; ++--------------------------+ +| names_list | ++--------------------------+ +| Charlie, Bob, Bob, Alice | ++--------------------------+ +> SELECT string_agg(DISTINCT name, ', ' ORDER BY name DESC) AS names_list + FROM employee; ++--------------------------+ +| names_list | ++--------------------------+ +| Charlie, Bob, Alice | +--------------------------+ ``` @@ -794,7 +808,7 @@ approx_distinct(expression) ### `approx_median` -Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(x, 0.5)`. +Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY x)`. ```sql approx_median(expression) @@ -820,7 +834,7 @@ approx_median(expression) Returns the approximate percentile of input values using the t-digest algorithm. ```sql -approx_percentile_cont(expression, percentile, centroids) +approx_percentile_cont(percentile, centroids) WITHIN GROUP (ORDER BY expression) ``` #### Arguments @@ -832,12 +846,12 @@ approx_percentile_cont(expression, percentile, centroids) #### Example ```sql -> SELECT approx_percentile_cont(column_name, 0.75, 100) FROM table_name; -+-------------------------------------------------+ -| approx_percentile_cont(column_name, 0.75, 100) | -+-------------------------------------------------+ -| 65.0 | -+-------------------------------------------------+ +> SELECT approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) FROM table_name; ++-----------------------------------------------------------------------+ +| approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) | ++-----------------------------------------------------------------------+ +| 65.0 | ++-----------------------------------------------------------------------+ ``` ### `approx_percentile_cont_with_weight` @@ -845,7 +859,7 @@ approx_percentile_cont(expression, percentile, centroids) Returns the weighted approximate percentile of input values using the t-digest algorithm. ```sql -approx_percentile_cont_with_weight(expression, weight, percentile) +approx_percentile_cont_with_weight(weight, percentile) WITHIN GROUP (ORDER BY expression) ``` #### Arguments @@ -857,10 +871,10 @@ approx_percentile_cont_with_weight(expression, weight, percentile) #### Example ```sql -> SELECT approx_percentile_cont_with_weight(column_name, weight_column, 0.90) FROM table_name; -+----------------------------------------------------------------------+ -| approx_percentile_cont_with_weight(column_name, weight_column, 0.90) | -+----------------------------------------------------------------------+ -| 78.5 | -+----------------------------------------------------------------------+ +> SELECT approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) FROM table_name; ++---------------------------------------------------------------------------------------------+ +| approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) | ++---------------------------------------------------------------------------------------------+ +| 78.5 | ++---------------------------------------------------------------------------------------------+ ``` diff --git a/docs/source/user-guide/sql/data_types.md b/docs/source/user-guide/sql/data_types.md index 18c95cdea70ed..d977a4396e40d 100644 --- a/docs/source/user-guide/sql/data_types.md +++ b/docs/source/user-guide/sql/data_types.md @@ -60,20 +60,20 @@ select arrow_cast(now(), 'Timestamp(Second, None)'); ## Numeric Types -| SQL DataType | Arrow DataType | Notes | -| ------------------------------------ | :----------------------------- | ----------------------------------------------------------------------------------------------------- | -| `TINYINT` | `Int8` | | -| `SMALLINT` | `Int16` | | -| `INT` or `INTEGER` | `Int32` | | -| `BIGINT` | `Int64` | | -| `TINYINT UNSIGNED` | `UInt8` | | -| `SMALLINT UNSIGNED` | `UInt16` | | -| `INT UNSIGNED` or `INTEGER UNSIGNED` | `UInt32` | | -| `BIGINT UNSIGNED` | `UInt64` | | -| `FLOAT` | `Float32` | | -| `REAL` | `Float32` | | -| `DOUBLE` | `Float64` | | -| `DECIMAL(precision, scale)` | `Decimal128(precision, scale)` | Decimal support is currently experimental ([#3523](https://github.com/apache/datafusion/issues/3523)) | +| SQL DataType | Arrow DataType | +| ------------------------------------ | :----------------------------- | +| `TINYINT` | `Int8` | +| `SMALLINT` | `Int16` | +| `INT` or `INTEGER` | `Int32` | +| `BIGINT` | `Int64` | +| `TINYINT UNSIGNED` | `UInt8` | +| `SMALLINT UNSIGNED` | `UInt16` | +| `INT UNSIGNED` or `INTEGER UNSIGNED` | `UInt32` | +| `BIGINT UNSIGNED` | `UInt64` | +| `FLOAT` | `Float32` | +| `REAL` | `Float32` | +| `DOUBLE` | `Float64` | +| `DECIMAL(precision, scale)` | `Decimal128(precision, scale)` | ## Date/Time Types diff --git a/docs/source/user-guide/sql/ddl.md b/docs/source/user-guide/sql/ddl.md index 71475cff9a39b..fc18154becda6 100644 --- a/docs/source/user-guide/sql/ddl.md +++ b/docs/source/user-guide/sql/ddl.md @@ -74,7 +74,7 @@ LOCATION := ( , ...) ``` -For a detailed list of write related options which can be passed in the OPTIONS key_value_list, see [Write Options](write_options). +For a comprehensive list of format-specific options that can be specified in the `OPTIONS` clause, see [Format Options](format_options.md). `file_type` is one of `CSV`, `ARROW`, `PARQUET`, `AVRO` or `JSON` diff --git a/docs/source/user-guide/sql/dml.md b/docs/source/user-guide/sql/dml.md index 4eda59d6dea10..c29447f23cd9c 100644 --- a/docs/source/user-guide/sql/dml.md +++ b/docs/source/user-guide/sql/dml.md @@ -49,7 +49,7 @@ The output format is determined by the first match of the following rules: 1. Value of `STORED AS` 2. Filename extension (e.g. `foo.parquet` implies `PARQUET` format) -For a detailed list of valid OPTIONS, see [Write Options](write_options). +For a detailed list of valid OPTIONS, see [Format Options](format_options.md). ### Examples diff --git a/docs/source/user-guide/sql/explain.md b/docs/source/user-guide/sql/explain.md index f89e854ebffd5..9984de147ecc5 100644 --- a/docs/source/user-guide/sql/explain.md +++ b/docs/source/user-guide/sql/explain.md @@ -39,39 +39,7 @@ the format from the [configuration value] `datafusion.explain.format`. [configuration value]: ../configs.md -### `indent` format (default) - -The `indent` format shows both the logical and physical plan, with one line for -each operator in the plan. Child plans are indented to show the hierarchy. - -See [Reading Explain Plans](../explain-usage.md) for more information on how to interpret these plans. - -```sql -> CREATE TABLE t(x int, b int) AS VALUES (1, 2), (2, 3); -0 row(s) fetched. -Elapsed 0.004 seconds. - -> EXPLAIN SELECT SUM(x) FROM t GROUP BY b; -+---------------+-------------------------------------------------------------------------------+ -| plan_type | plan | -+---------------+-------------------------------------------------------------------------------+ -| logical_plan | Projection: sum(t.x) | -| | Aggregate: groupBy=[[t.b]], aggr=[[sum(CAST(t.x AS Int64))]] | -| | TableScan: t projection=[x, b] | -| physical_plan | ProjectionExec: expr=[sum(t.x)@1 as sum(t.x)] | -| | AggregateExec: mode=FinalPartitioned, gby=[b@0 as b], aggr=[sum(t.x)] | -| | CoalesceBatchesExec: target_batch_size=8192 | -| | RepartitionExec: partitioning=Hash([b@0], 16), input_partitions=16 | -| | RepartitionExec: partitioning=RoundRobinBatch(16), input_partitions=1 | -| | AggregateExec: mode=Partial, gby=[b@1 as b], aggr=[sum(t.x)] | -| | DataSourceExec: partitions=1, partition_sizes=[1] | -| | | -+---------------+-------------------------------------------------------------------------------+ -2 row(s) fetched. -Elapsed 0.004 seconds. -``` - -### `tree` format +### `tree` format (default) The `tree` format is modeled after [DuckDB plans] and is designed to be easier to see the high level structure of the plan @@ -103,7 +71,7 @@ to see the high level structure of the plan | | ┌─────────────┴─────────────┐ | | | │ RepartitionExec │ | | | │ -------------------- │ | -| | │ output_partition_count: │ | +| | │ input_partition_count: │ | | | │ 16 │ | | | │ │ | | | │ partitioning_scheme: │ | @@ -112,7 +80,7 @@ to see the high level structure of the plan | | ┌─────────────┴─────────────┐ | | | │ RepartitionExec │ | | | │ -------------------- │ | -| | │ output_partition_count: │ | +| | │ input_partition_count: │ | | | │ 1 │ | | | │ │ | | | │ partitioning_scheme: │ | @@ -138,6 +106,38 @@ to see the high level structure of the plan Elapsed 0.016 seconds. ``` +### `indent` format + +The `indent` format shows both the logical and physical plan, with one line for +each operator in the plan. Child plans are indented to show the hierarchy. + +See [Reading Explain Plans](../explain-usage.md) for more information on how to interpret these plans. + +```sql +> CREATE TABLE t(x int, b int) AS VALUES (1, 2), (2, 3); +0 row(s) fetched. +Elapsed 0.004 seconds. + +> EXPLAIN SELECT SUM(x) FROM t GROUP BY b; ++---------------+-------------------------------------------------------------------------------+ +| plan_type | plan | ++---------------+-------------------------------------------------------------------------------+ +| logical_plan | Projection: sum(t.x) | +| | Aggregate: groupBy=[[t.b]], aggr=[[sum(CAST(t.x AS Int64))]] | +| | TableScan: t projection=[x, b] | +| physical_plan | ProjectionExec: expr=[sum(t.x)@1 as sum(t.x)] | +| | AggregateExec: mode=FinalPartitioned, gby=[b@0 as b], aggr=[sum(t.x)] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([b@0], 16), input_partitions=16 | +| | RepartitionExec: partitioning=RoundRobinBatch(16), input_partitions=1 | +| | AggregateExec: mode=Partial, gby=[b@1 as b], aggr=[sum(t.x)] | +| | DataSourceExec: partitions=1, partition_sizes=[1] | +| | | ++---------------+-------------------------------------------------------------------------------+ +2 row(s) fetched. +Elapsed 0.004 seconds. +``` + ### `pgjson` format The `pgjson` format is modeled after [Postgres JSON] format. diff --git a/docs/source/user-guide/sql/format_options.md b/docs/source/user-guide/sql/format_options.md new file mode 100644 index 0000000000000..e8008eafb166c --- /dev/null +++ b/docs/source/user-guide/sql/format_options.md @@ -0,0 +1,180 @@ + + +# Format Options + +DataFusion supports customizing how data is read from or written to disk as a result of a `COPY`, `INSERT INTO`, or `CREATE EXTERNAL TABLE` statements. There are a few special options, file format (e.g., CSV or Parquet) specific options, and Parquet column-specific options. In some cases, Options can be specified in multiple ways with a set order of precedence. + +## Specifying Options and Order of Precedence + +Format-related options can be specified in three ways, in decreasing order of precedence: + +- `CREATE EXTERNAL TABLE` syntax +- `COPY` option tuples +- Session-level config defaults + +For a list of supported session-level config defaults, see [Configuration Settings](../configs). These defaults apply to all operations but have the lowest level of precedence. + +If creating an external table, table-specific format options can be specified when the table is created using the `OPTIONS` clause: + +```sql +CREATE EXTERNAL TABLE + my_table(a bigint, b bigint) + STORED AS csv + LOCATION '/tmp/my_csv_table/' + OPTIONS( + NULL_VALUE 'NAN', + 'has_header' 'true', + 'format.delimiter' ';' + ); +``` + +When running `INSERT INTO my_table ...`, the options from the `CREATE TABLE` will be respected (e.g., gzip compression, special delimiter, and header row included). Note that compression, header, and delimiter settings can also be specified within the `OPTIONS` tuple list. Dedicated syntax within the SQL statement always takes precedence over arbitrary option tuples, so if both are specified, the `OPTIONS` setting will be ignored. + +For example, with the table defined above, running the following command: + +```sql +INSERT INTO my_table VALUES(1,2); +``` + +Results in a new CSV file with the specified options: + +```shell +$ cat /tmp/my_csv_table/bmC8zWFvLMtWX68R_0.csv +a;b +1;2 +``` + +Finally, options can be passed when running a `COPY` command. + +```sql +COPY source_table + TO 'test/table_with_options' + PARTITIONED BY (column3, column4) + OPTIONS ( + format parquet, + compression snappy, + 'compression::column1' 'zstd(5)', + ) +``` + +In this example, we write the entire `source_table` out to a folder of Parquet files. One Parquet file will be written in parallel to the folder for each partition in the query. The next option `compression` set to `snappy` indicates that unless otherwise specified, all columns should use the snappy compression codec. The option `compression::col1` sets an override, so that the column `col1` in the Parquet file will use the ZSTD compression codec with compression level `5`. In general, Parquet options that support column-specific settings can be specified with the syntax `OPTION::COLUMN.NESTED.PATH`. + +# Available Options + +## JSON Format Options + +The following options are available when reading or writing JSON files. Note: If any unsupported option is specified, an error will be raised and the query will fail. + +| Option | Description | Default Value | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| COMPRESSION | Sets the compression that should be applied to the entire JSON file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | + +**Example:** + +```sql +CREATE EXTERNAL TABLE t(a int) +STORED AS JSON +LOCATION '/tmp/foo/' +OPTIONS('COMPRESSION' 'gzip'); +``` + +## CSV Format Options + +The following options are available when reading or writing CSV files. Note: If any unsupported option is specified, an error will be raised and the query will fail. + +| Option | Description | Default Value | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| COMPRESSION | Sets the compression that should be applied to the entire CSV file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | +| HAS_HEADER | Sets if the CSV file should include column headers. If not set, uses session or system default. | None | +| DELIMITER | Sets the character which should be used as the column delimiter within the CSV file. | `,` (comma) | +| QUOTE | Sets the character which should be used for quoting values within the CSV file. | `"` (double quote) | +| TERMINATOR | Sets the character which should be used as the line terminator within the CSV file. | None | +| ESCAPE | Sets the character which should be used for escaping special characters within the CSV file. | None | +| DOUBLE_QUOTE | Sets if quotes within quoted fields should be escaped by doubling them (e.g., `"aaa""bbb"`). | None | +| NEWLINES_IN_VALUES | Sets if newlines in quoted values are supported. If not set, uses session or system default. | None | +| DATE_FORMAT | Sets the format that dates should be encoded in within the CSV file. | None | +| DATETIME_FORMAT | Sets the format that datetimes should be encoded in within the CSV file. | None | +| TIMESTAMP_FORMAT | Sets the format that timestamps should be encoded in within the CSV file. | None | +| TIMESTAMP_TZ_FORMAT | Sets the format that timestamps with timezone should be encoded in within the CSV file. | None | +| TIME_FORMAT | Sets the format that times should be encoded in within the CSV file. | None | +| NULL_VALUE | Sets the string which should be used to indicate null values within the CSV file. | None | +| NULL_REGEX | Sets the regex pattern to match null values when loading CSVs. | None | +| SCHEMA_INFER_MAX_REC | Sets the maximum number of records to scan to infer the schema. | None | +| COMMENT | Sets the character which should be used to indicate comment lines in the CSV file. | None | + +**Example:** + +```sql +CREATE EXTERNAL TABLE t (col1 varchar, col2 int, col3 boolean) +STORED AS CSV +LOCATION '/tmp/foo/' +OPTIONS('DELIMITER' '|', 'HAS_HEADER' 'true', 'NEWLINES_IN_VALUES' 'true'); +``` + +## Parquet Format Options + +The following options are available when reading or writing Parquet files. If any unsupported option is specified, an error will be raised and the query will fail. If a column-specific option is specified for a column that does not exist, the option will be ignored without error. + +| Option | Can be Column Specific? | Description | OPTIONS Key | Default Value | +| ------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------ | +| COMPRESSION | Yes | Sets the internal Parquet **compression codec** for data pages, optionally including the compression level. Applies globally if set without `::col`, or specifically to a column if set using `'compression::column_name'`. Valid values: `uncompressed`, `snappy`, `gzip(level)`, `lzo`, `brotli(level)`, `lz4`, `zstd(level)`, `lz4_raw`. | `'compression'` or `'compression::col'` | zstd(3) | +| ENCODING | Yes | Sets the **encoding** scheme for data pages. Valid values: `plain`, `plain_dictionary`, `rle`, `bit_packed`, `delta_binary_packed`, `delta_length_byte_array`, `delta_byte_array`, `rle_dictionary`, `byte_stream_split`. Use key `'encoding'` or `'encoding::col'` in OPTIONS. | `'encoding'` or `'encoding::col'` | None | +| DICTIONARY_ENABLED | Yes | Sets whether dictionary encoding should be enabled globally or for a specific column. | `'dictionary_enabled'` or `'dictionary_enabled::col'` | true | +| STATISTICS_ENABLED | Yes | Sets the level of statistics to write (`none`, `chunk`, `page`). | `'statistics_enabled'` or `'statistics_enabled::col'` | page | +| BLOOM_FILTER_ENABLED | Yes | Sets whether a bloom filter should be written for a specific column. | `'bloom_filter_enabled::column_name'` | None | +| BLOOM_FILTER_FPP | Yes | Sets bloom filter false positive probability (global or per column). | `'bloom_filter_fpp'` or `'bloom_filter_fpp::col'` | None | +| BLOOM_FILTER_NDV | Yes | Sets bloom filter number of distinct values (global or per column). | `'bloom_filter_ndv'` or `'bloom_filter_ndv::col'` | None | +| MAX_ROW_GROUP_SIZE | No | Sets the maximum number of rows per row group. Larger groups require more memory but can improve compression and scan efficiency. | `'max_row_group_size'` | 1048576 | +| ENABLE_PAGE_INDEX | No | If true, reads the Parquet data page level metadata (the Page Index), if present, to reduce I/O and decoding. | `'enable_page_index'` | true | +| PRUNING | No | If true, enables row group pruning based on min/max statistics. | `'pruning'` | true | +| SKIP_METADATA | No | If true, skips optional embedded metadata in the file schema. | `'skip_metadata'` | true | +| METADATA_SIZE_HINT | No | Sets the size hint (in bytes) for fetching Parquet file metadata. | `'metadata_size_hint'` | None | +| PUSHDOWN_FILTERS | No | If true, enables filter pushdown during Parquet decoding. | `'pushdown_filters'` | false | +| REORDER_FILTERS | No | If true, enables heuristic reordering of filters during Parquet decoding. | `'reorder_filters'` | false | +| SCHEMA_FORCE_VIEW_TYPES | No | If true, reads Utf8/Binary columns as view types. | `'schema_force_view_types'` | true | +| BINARY_AS_STRING | No | If true, reads Binary columns as strings. | `'binary_as_string'` | false | +| DATA_PAGESIZE_LIMIT | No | Sets best effort maximum size of data page in bytes. | `'data_pagesize_limit'` | 1048576 | +| DATA_PAGE_ROW_COUNT_LIMIT | No | Sets best effort maximum number of rows in data page. | `'data_page_row_count_limit'` | 20000 | +| DICTIONARY_PAGE_SIZE_LIMIT | No | Sets best effort maximum dictionary page size, in bytes. | `'dictionary_page_size_limit'` | 1048576 | +| WRITE_BATCH_SIZE | No | Sets write_batch_size in bytes. | `'write_batch_size'` | 1024 | +| WRITER_VERSION | No | Sets the Parquet writer version (`1.0` or `2.0`). | `'writer_version'` | 1.0 | +| SKIP_ARROW_METADATA | No | If true, skips writing Arrow schema information into the Parquet file metadata. | `'skip_arrow_metadata'` | false | +| CREATED_BY | No | Sets the "created by" string in the Parquet file metadata. | `'created_by'` | datafusion version X.Y.Z | +| COLUMN_INDEX_TRUNCATE_LENGTH | No | Sets the length (in bytes) to truncate min/max values in column indexes. | `'column_index_truncate_length'` | 64 | +| STATISTICS_TRUNCATE_LENGTH | No | Sets statistics truncate length. | `'statistics_truncate_length'` | None | +| BLOOM_FILTER_ON_WRITE | No | Sets whether bloom filters should be written for all columns by default (can be overridden per column). | `'bloom_filter_on_write'` | false | +| ALLOW_SINGLE_FILE_PARALLELISM | No | Enables parallel serialization of columns in a single file. | `'allow_single_file_parallelism'` | true | +| MAXIMUM_PARALLEL_ROW_GROUP_WRITERS | No | Maximum number of parallel row group writers. | `'maximum_parallel_row_group_writers'` | 1 | +| MAXIMUM_BUFFERED_RECORD_BATCHES_PER_STREAM | No | Maximum number of buffered record batches per stream. | `'maximum_buffered_record_batches_per_stream'` | 2 | +| KEY_VALUE_METADATA | No (Key is specific) | Adds custom key-value pairs to the file metadata. Use the format `'metadata::your_key_name' 'your_value'`. Multiple entries allowed. | `'metadata::key_name'` | None | + +**Example:** + +```sql +CREATE EXTERNAL TABLE t (id bigint, value double, category varchar) +STORED AS PARQUET +LOCATION '/tmp/parquet_data/' +OPTIONS( + 'COMPRESSION::user_id' 'snappy', + 'ENCODING::col_a' 'delta_binary_packed', + 'MAX_ROW_GROUP_SIZE' '1000000', + 'BLOOM_FILTER_ENABLED::id' 'true' +); +``` diff --git a/docs/source/user-guide/sql/index.rst b/docs/source/user-guide/sql/index.rst index 8e3f51bf8b0bc..a13d40334b639 100644 --- a/docs/source/user-guide/sql/index.rst +++ b/docs/source/user-guide/sql/index.rst @@ -33,5 +33,5 @@ SQL Reference window_functions scalar_functions special_functions - write_options + format_options prepared_statements diff --git a/docs/source/user-guide/sql/window_functions.md b/docs/source/user-guide/sql/window_functions.md index 1c02804f0deed..68a7003803123 100644 --- a/docs/source/user-guide/sql/window_functions.md +++ b/docs/source/user-guide/sql/window_functions.md @@ -160,12 +160,31 @@ All [aggregate functions](aggregate_functions.md) can be used as window function ### `cume_dist` -Relative rank of the current row: (number of rows preceding or peer with current row) / (total rows). +Relative rank of the current row: (number of rows preceding or peer with the current row) / (total rows). ```sql cume_dist() ``` +#### Example + +```sql + --Example usage of the cume_dist window function: + SELECT salary, + cume_dist() OVER (ORDER BY salary) AS cume_dist + FROM employees; +``` + +```sql ++--------+-----------+ +| salary | cume_dist | ++--------+-----------+ +| 30000 | 0.33 | +| 50000 | 0.67 | +| 70000 | 1.00 | ++--------+-----------+ +``` + ### `dense_rank` Returns the rank of the current row without gaps. This function ranks rows in a dense manner, meaning consecutive ranks are assigned even for identical values. @@ -272,7 +291,7 @@ lead(expression, offset, default) ### `nth_value` -Returns value evaluated at the row that is the nth row of the window frame (counting from 1); null if no such row. +Returns the value evaluated at the nth row of the window frame (counting from 1). Returns NULL if no such row exists. ```sql nth_value(expression, n) @@ -280,5 +299,37 @@ nth_value(expression, n) #### Arguments -- **expression**: The name the column of which nth value to retrieve -- **n**: Integer. Specifies the n in nth +- **expression**: The column from which to retrieve the nth value. +- **n**: Integer. Specifies the row number (starting from 1) in the window frame. + +#### Example + +```sql +-- Sample employees table: +CREATE TABLE employees (id INT, salary INT); +INSERT INTO employees (id, salary) VALUES +(1, 30000), +(2, 40000), +(3, 50000), +(4, 60000), +(5, 70000); + +-- Example usage of nth_value: +SELECT nth_value(salary, 2) OVER ( + ORDER BY salary + ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW +) AS nth_value +FROM employees; +``` + +```text ++-----------+ +| nth_value | ++-----------+ +| 40000 | +| 40000 | +| 40000 | +| 40000 | +| 40000 | ++-----------+ +``` diff --git a/docs/source/user-guide/sql/write_options.md b/docs/source/user-guide/sql/write_options.md deleted file mode 100644 index 521e29436212d..0000000000000 --- a/docs/source/user-guide/sql/write_options.md +++ /dev/null @@ -1,127 +0,0 @@ - - -# Write Options - -DataFusion supports customizing how data is written out to disk as a result of a `COPY` or `INSERT INTO` query. There are a few special options, file format (e.g. CSV or parquet) specific options, and parquet column specific options. Options can also in some cases be specified in multiple ways with a set order of precedence. - -## Specifying Options and Order of Precedence - -Write related options can be specified in the following ways: - -- Session level config defaults -- `CREATE EXTERNAL TABLE` options -- `COPY` option tuples - -For a list of supported session level config defaults see [Configuration Settings](../configs). These defaults apply to all write operations but have the lowest level of precedence. - -If inserting to an external table, table specific write options can be specified when the table is created using the `OPTIONS` clause: - -```sql -CREATE EXTERNAL TABLE - my_table(a bigint, b bigint) - STORED AS csv - COMPRESSION TYPE gzip - LOCATION '/test/location/my_csv_table/' - OPTIONS( - NULL_VALUE 'NAN', - 'has_header' 'true', - 'format.delimiter' ';' - ) -``` - -When running `INSERT INTO my_table ...`, the options from the `CREATE TABLE` will be respected (gzip compression, special delimiter, and header row included). There will be a single output file if the output path doesn't have folder format, i.e. ending with a `\`. Note that compression, header, and delimiter settings can also be specified within the `OPTIONS` tuple list. Dedicated syntax within the SQL statement always takes precedence over arbitrary option tuples, so if both are specified the `OPTIONS` setting will be ignored. NULL_VALUE is a CSV format specific option that determines how null values should be encoded within the CSV file. - -Finally, options can be passed when running a `COPY` command. - - - -```sql -COPY source_table - TO 'test/table_with_options' - PARTITIONED BY (column3, column4) - OPTIONS ( - format parquet, - compression snappy, - 'compression::column1' 'zstd(5)', - ) -``` - -In this example, we write the entirety of `source_table` out to a folder of parquet files. One parquet file will be written in parallel to the folder for each partition in the query. The next option `compression` set to `snappy` indicates that unless otherwise specified all columns should use the snappy compression codec. The option `compression::col1` sets an override, so that the column `col1` in the parquet file will use `ZSTD` compression codec with compression level `5`. In general, parquet options which support column specific settings can be specified with the syntax `OPTION::COLUMN.NESTED.PATH`. - -## Available Options - -### Execution Specific Options - -The following options are available when executing a `COPY` query. - -| Option | Description | Default Value | -| ----------------------------------- | ---------------------------------------------------------------------------------- | ------------- | -| execution.keep_partition_by_columns | Flag to retain the columns in the output data when using `PARTITIONED BY` queries. | false | - -Note: `execution.keep_partition_by_columns` flag can also be enabled through `ExecutionOptions` within `SessionConfig`. - -### JSON Format Specific Options - -The following options are available when writing JSON files. Note: If any unsupported option is specified, an error will be raised and the query will fail. - -| Option | Description | Default Value | -| ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| COMPRESSION | Sets the compression that should be applied to the entire JSON file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | - -### CSV Format Specific Options - -The following options are available when writing CSV files. Note: if any unsupported options is specified an error will be raised and the query will fail. - -| Option | Description | Default Value | -| --------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -| COMPRESSION | Sets the compression that should be applied to the entire CSV file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | -| HEADER | Sets if the CSV file should include column headers | false | -| DATE_FORMAT | Sets the format that dates should be encoded in within the CSV file | arrow-rs default | -| DATETIME_FORMAT | Sets the format that datetimes should be encoded in within the CSV file | arrow-rs default | -| TIME_FORMAT | Sets the format that times should be encoded in within the CSV file | arrow-rs default | -| RFC3339 | If true, uses RFC339 format for date and time encodings | arrow-rs default | -| NULL_VALUE | Sets the string which should be used to indicate null values within the CSV file. | arrow-rs default | -| DELIMITER | Sets the character which should be used as the column delimiter within the CSV file. | arrow-rs default | - -### Parquet Format Specific Options - -The following options are available when writing parquet files. If any unsupported option is specified an error will be raised and the query will fail. If a column specific option is specified for a column which does not exist, the option will be ignored without error. For default values, see: [Configuration Settings](https://datafusion.apache.org/user-guide/configs.html). - -| Option | Can be Column Specific? | Description | -| ---------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -| COMPRESSION | Yes | Sets the compression codec and if applicable compression level to use | -| MAX_ROW_GROUP_SIZE | No | Sets the maximum number of rows that can be encoded in a single row group. Larger row groups require more memory to write and read. | -| DATA_PAGESIZE_LIMIT | No | Sets the best effort maximum page size in bytes | -| WRITE_BATCH_SIZE | No | Maximum number of rows written for each column in a single batch | -| WRITER_VERSION | No | Parquet writer version (1.0 or 2.0) | -| DICTIONARY_PAGE_SIZE_LIMIT | No | Sets best effort maximum dictionary page size in bytes | -| CREATED_BY | No | Sets the "created by" property in the parquet file | -| COLUMN_INDEX_TRUNCATE_LENGTH | No | Sets the max length of min/max value fields in the column index. | -| DATA_PAGE_ROW_COUNT_LIMIT | No | Sets best effort maximum number of rows in a data page. | -| BLOOM_FILTER_ENABLED | Yes | Sets whether a bloom filter should be written into the file. | -| ENCODING | Yes | Sets the encoding that should be used (e.g. PLAIN or RLE) | -| DICTIONARY_ENABLED | Yes | Sets if dictionary encoding is enabled. Use this instead of ENCODING to set dictionary encoding. | -| STATISTICS_ENABLED | Yes | Sets if statistics are enabled at PAGE or ROW_GROUP level. | -| MAX_STATISTICS_SIZE | Yes | Sets the maximum size in bytes that statistics can take up. | -| BLOOM_FILTER_FPP | Yes | Sets the false positive probability (fpp) for the bloom filter. Implicitly sets BLOOM_FILTER_ENABLED to true. | -| BLOOM_FILTER_NDV | Yes | Sets the number of distinct values (ndv) for the bloom filter. Implicitly sets bloom_filter_enabled to true. | diff --git a/parquet-testing b/parquet-testing index f4d7ed772a62a..6e851ddd768d6 160000 --- a/parquet-testing +++ b/parquet-testing @@ -1 +1 @@ -Subproject commit f4d7ed772a62a95111db50fbcad2460833e8c882 +Subproject commit 6e851ddd768d6af741c7b15dc594874399fc3cff diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 11f4fb798c376..a85e6fa54299d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -19,5 +19,5 @@ # to compile this workspace and run CI jobs. [toolchain] -channel = "1.85.0" +channel = "1.86.0" components = ["rustfmt", "clippy"] diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 9db8920833ae5..47f23de4951e3 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -67,10 +67,9 @@ pub fn add_empty_batches( .flat_map(|batch| { // insert 0, or 1 empty batches before and after the current batch let empty_batch = RecordBatch::new_empty(schema.clone()); - std::iter::repeat(empty_batch.clone()) - .take(rng.gen_range(0..2)) + std::iter::repeat_n(empty_batch.clone(), rng.gen_range(0..2)) .chain(std::iter::once(batch)) - .chain(std::iter::repeat(empty_batch).take(rng.gen_range(0..2))) + .chain(std::iter::repeat_n(empty_batch, rng.gen_range(0..2))) }) .collect() } From b0b4e43e5b1b0c60675db505b23f615cba7a1c79 Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Mon, 18 Aug 2025 16:32:15 +0900 Subject: [PATCH 06/15] Revert "update" This reverts commit 1fa2db8a8051796f68f5dc747565b93e4d671007. --- .../actions/setup-rust-runtime/action.yaml | 12 +- .github/workflows/audit.yml | 6 +- .github/workflows/extended.yml | 10 +- .github/workflows/pr_comment_commands.yml | 10 +- .github/workflows/rust.yml | 33 +- Cargo.lock | 360 +- Cargo.toml | 97 +- benchmarks/README.md | 25 +- benchmarks/bench.sh | 7 - benchmarks/queries/clickbench/README.md | 33 +- benchmarks/queries/clickbench/extended.sql | 3 +- benchmarks/queries/clickbench/queries.sql | 22 +- benchmarks/src/sort_tpch.rs | 34 +- benchmarks/src/tpch/convert.rs | 22 +- benchmarks/src/tpch/run.rs | 31 +- datafusion-cli/Cargo.toml | 2 +- datafusion-cli/src/main.rs | 82 +- datafusion-cli/tests/cli_integration.rs | 25 - ...es@explain_plan_environment_overrides.snap | 44 - .../cli_quick_test@can_see_indent_format.snap | 27 - .../cli_quick_test@default_explain_plan.snap | 31 - datafusion-examples/Cargo.toml | 1 - .../examples/advanced_parquet_index.rs | 9 +- datafusion-examples/examples/parquet_index.rs | 2 +- datafusion-examples/examples/sql_dialect.rs | 6 +- datafusion-testing | 2 +- datafusion/catalog/src/lib.rs | 2 +- datafusion/catalog/src/memory/mod.rs | 6 - datafusion/catalog/src/memory/table.rs | 296 -- datafusion/common-runtime/src/common.rs | 86 +- datafusion/common/Cargo.toml | 4 +- datafusion/common/src/config.rs | 72 +- datafusion/common/src/dfschema.rs | 18 +- .../common/src/file_options/parquet_writer.rs | 3 - .../common/src/functional_dependencies.rs | 10 +- datafusion/common/src/scalar/mod.rs | 87 +- datafusion/common/src/stats.rs | 356 +- datafusion/common/src/utils/memory.rs | 2 +- datafusion/core/Cargo.toml | 4 +- .../core/benches/aggregate_query_sql.rs | 23 +- datafusion/core/benches/csv_load.rs | 11 +- datafusion/core/benches/data_utils/mod.rs | 60 +- datafusion/core/benches/dataframe.rs | 8 +- datafusion/core/benches/distinct_query_sql.rs | 68 +- datafusion/core/benches/filter_query_sql.rs | 9 +- datafusion/core/benches/math_query_sql.rs | 14 +- datafusion/core/benches/physical_plan.rs | 10 +- .../core/benches/sort_limit_query_sql.rs | 15 +- datafusion/core/benches/sql_planner.rs | 52 +- datafusion/core/benches/struct_query_sql.rs | 9 +- datafusion/core/benches/topk_aggregate.rs | 111 +- datafusion/core/benches/window_query_sql.rs | 16 +- .../core/src/bin/print_runtime_config_docs.rs | 23 - datafusion/core/src/dataframe/mod.rs | 79 - .../core/src/datasource/file_format/arrow.rs | 5 +- .../core/src/datasource/file_format/avro.rs | 11 +- .../core/src/datasource/file_format/csv.rs | 21 +- .../core/src/datasource/file_format/json.rs | 2 +- .../core/src/datasource/file_format/mod.rs | 17 +- .../src/datasource/file_format/parquet.rs | 23 +- .../core/src/datasource/listing/table.rs | 141 +- .../datasource/{memory_test.rs => memory.rs} | 371 +- datafusion/core/src/datasource/mod.rs | 7 +- .../datasource/physical_plan/arrow_file.rs | 21 +- .../core/src/datasource/physical_plan/csv.rs | 2 +- .../core/src/datasource/physical_plan/json.rs | 2 +- .../src/datasource/physical_plan/parquet.rs | 375 +- datafusion/core/src/datasource/statistics.rs | 219 + datafusion/core/src/execution/context/mod.rs | 73 +- .../core/src/execution/session_state.rs | 20 +- datafusion/core/src/lib.rs | 23 +- datafusion/core/src/physical_planner.rs | 87 +- datafusion/core/src/test/object_store.rs | 4 +- datafusion/core/src/test_util/parquet.rs | 2 +- datafusion/core/tests/core_integration.rs | 3 - .../tests/dataframe/dataframe_functions.rs | 87 +- datafusion/core/tests/dataframe/mod.rs | 92 - .../core/tests/execution/logical_plan.rs | 44 +- .../core/tests/expr_api/simplification.rs | 4 +- .../core/tests/fuzz_cases/aggregate_fuzz.rs | 256 +- .../aggregation_fuzzer/context_generator.rs | 2 +- .../aggregation_fuzzer/data_generator.rs | 590 ++- .../fuzz_cases/aggregation_fuzzer/fuzzer.rs | 10 +- .../fuzz_cases/aggregation_fuzzer/mod.rs | 3 +- datafusion/core/tests/fuzz_cases/mod.rs | 4 - .../fuzz_cases/record_batch_generator.rs | 644 --- .../core/tests/fuzz_cases/sort_query_fuzz.rs | 625 --- .../memory_limit_validation/utils.rs | 12 +- datafusion/core/tests/memory_limit/mod.rs | 133 +- .../core/tests/parquet/custom_reader.rs | 9 +- datafusion/core/tests/parquet/mod.rs | 12 +- datafusion/core/tests/parquet/page_pruning.rs | 2 +- .../enforce_distribution.rs | 45 - .../physical_optimizer/enforce_sorting.rs | 37 +- .../core/tests/physical_optimizer/mod.rs | 1 - .../physical_optimizer/push_down_filter.rs | 542 -- .../replace_with_order_preserving_variants.rs | 57 +- datafusion/core/tests/sql/mod.rs | 1 - datafusion/core/tests/sql/path_partition.rs | 18 +- datafusion/core/tests/sql/runtime_config.rs | 166 - datafusion/core/tests/sql/sql_api.rs | 17 - .../core/tests/tracing/asserting_tracer.rs | 142 - datafusion/core/tests/tracing/mod.rs | 108 - .../tests/tracing/traceable_object_store.rs | 125 - .../user_defined_window_functions.rs | 2 +- datafusion/datasource-csv/src/source.rs | 1 - datafusion/datasource-json/src/file_format.rs | 1 - datafusion/datasource-json/src/source.rs | 1 - .../datasource-parquet/src/file_format.rs | 90 +- datafusion/datasource-parquet/src/opener.rs | 231 +- .../datasource-parquet/src/page_filter.rs | 22 +- datafusion/datasource-parquet/src/reader.rs | 27 +- .../datasource-parquet/src/row_filter.rs | 4 +- .../src/row_group_filter.rs | 7 +- datafusion/datasource-parquet/src/source.rs | 81 +- datafusion/datasource/Cargo.toml | 7 +- .../benches/split_groups_by_statistics.rs | 108 - datafusion/datasource/src/file.rs | 22 +- datafusion/datasource/src/file_groups.rs | 23 +- datafusion/datasource/src/file_scan_config.rs | 395 +- datafusion/datasource/src/file_sink_config.rs | 1 - datafusion/datasource/src/file_stream.rs | 2 +- datafusion/datasource/src/memory.rs | 92 +- datafusion/datasource/src/mod.rs | 152 +- datafusion/datasource/src/schema_adapter.rs | 2 +- datafusion/datasource/src/source.rs | 79 +- datafusion/datasource/src/statistics.rs | 214 - datafusion/datasource/src/url.rs | 6 +- datafusion/datasource/src/write/demux.rs | 10 +- datafusion/execution/Cargo.toml | 2 +- datafusion/execution/src/config.rs | 8 +- datafusion/execution/src/disk_manager.rs | 102 +- datafusion/execution/src/memory_pool/mod.rs | 87 +- datafusion/execution/src/memory_pool/pool.rs | 172 +- datafusion/execution/src/runtime_env.rs | 54 +- .../expr-common/src/interval_arithmetic.rs | 2 +- datafusion/expr-common/src/signature.rs | 12 +- .../src/type_coercion/aggregates.rs | 3 - .../expr-common/src/type_coercion/binary.rs | 69 - datafusion/expr/src/logical_plan/builder.rs | 121 +- .../expr/src/logical_plan/invariants.rs | 6 +- datafusion/expr/src/logical_plan/plan.rs | 416 -- .../expr/src/type_coercion/functions.rs | 8 +- datafusion/expr/src/udaf.rs | 38 +- datafusion/ffi/Cargo.toml | 1 - datafusion/ffi/src/lib.rs | 1 - datafusion/ffi/src/table_provider.rs | 55 +- datafusion/ffi/src/tests/mod.rs | 11 +- datafusion/ffi/src/tests/udf_udaf_udwf.rs | 21 +- datafusion/ffi/src/tests/utils.rs | 87 - datafusion/ffi/src/{udf/mod.rs => udf.rs} | 47 +- datafusion/ffi/src/udf/return_info.rs | 53 - datafusion/ffi/src/udf/return_type_args.rs | 142 - datafusion/ffi/src/udtf.rs | 321 -- datafusion/ffi/tests/ffi_integration.rs | 116 +- datafusion/ffi/tests/ffi_udf.rs | 104 - datafusion/ffi/tests/ffi_udtf.rs | 64 - .../functions-aggregate/benches/array_agg.rs | 28 +- .../functions-aggregate/src/approx_median.rs | 2 +- .../src/approx_percentile_cont.rs | 54 +- .../src/approx_percentile_cont_with_weight.rs | 22 +- .../functions-aggregate/src/array_agg.rs | 4 +- datafusion/functions-aggregate/src/average.rs | 114 +- .../functions-aggregate/src/first_last.rs | 253 +- .../functions-aggregate/src/string_agg.rs | 397 +- datafusion/functions-nested/src/array_has.rs | 4 +- datafusion/functions-nested/src/flatten.rs | 153 +- datafusion/functions-nested/src/sort.rs | 20 +- .../functions-table/src/generate_series.rs | 9 +- .../functions-window-common/src/expr.rs | 4 +- .../functions-window-common/src/field.rs | 4 +- .../functions-window-common/src/partition.rs | 8 +- datafusion/functions-window/src/cume_dist.rs | 21 +- datafusion/functions-window/src/macros.rs | 24 +- datafusion/functions-window/src/nth_value.rs | 43 +- datafusion/functions-window/src/rank.rs | 6 +- datafusion/functions/Cargo.toml | 2 +- datafusion/functions/benches/chr.rs | 10 +- datafusion/functions/benches/regx.rs | 8 +- .../functions/src/datetime/to_timestamp.rs | 31 +- datafusion/optimizer/Cargo.toml | 6 - .../benches/projection_unnecessary.rs | 79 - datafusion/optimizer/src/decorrelate.rs | 5 +- .../optimizer/src/optimize_projections/mod.rs | 35 +- datafusion/optimizer/src/optimizer.rs | 7 +- .../optimizer/src/scalar_subquery_to_join.rs | 216 +- .../simplify_expressions/expr_simplifier.rs | 251 +- .../simplify_expressions/simplify_exprs.rs | 9 +- .../src/simplify_expressions/unwrap_cast.rs | 29 - datafusion/optimizer/src/utils.rs | 72 +- .../optimizer/tests/optimizer_integration.rs | 380 +- .../physical-expr-common/src/physical_expr.rs | 77 - datafusion/physical-expr/Cargo.toml | 5 - datafusion/physical-expr/benches/binary_op.rs | 312 -- datafusion/physical-expr/src/aggregate.rs | 88 - .../src/equivalence/projection.rs | 4 +- .../src/equivalence/properties/mod.rs | 61 +- .../physical-expr/src/expressions/binary.rs | 506 +- .../src/expressions/dynamic_filters.rs | 474 -- .../physical-expr/src/expressions/mod.rs | 1 - datafusion/physical-expr/src/lib.rs | 2 +- datafusion/physical-expr/src/planner.rs | 2 +- datafusion/physical-expr/src/utils/mod.rs | 25 - .../src/aggregate_statistics.rs | 1 - .../src/enforce_distribution.rs | 11 +- .../src/enforce_sorting/mod.rs | 6 +- .../replace_with_order_preserving_variants.rs | 17 +- datafusion/physical-optimizer/src/lib.rs | 1 - .../physical-optimizer/src/limit_pushdown.rs | 11 +- .../physical-optimizer/src/optimizer.rs | 5 - datafusion/physical-optimizer/src/pruning.rs | 14 +- .../src/push_down_filter.rs | 535 -- datafusion/physical-plan/Cargo.toml | 5 - datafusion/physical-plan/benches/spill_io.rs | 123 - .../group_values/multi_group_by/primitive.rs | 2 +- .../src/aggregates/group_values/row.rs | 1 - .../src/aggregates/order/full.rs | 6 +- .../src/aggregates/order/partial.rs | 2 +- .../physical-plan/src/aggregates/row_hash.rs | 14 +- datafusion/physical-plan/src/coalesce/mod.rs | 12 +- .../physical-plan/src/coalesce_batches.rs | 15 - datafusion/physical-plan/src/display.rs | 2 +- .../physical-plan/src/execution_plan.rs | 42 +- datafusion/physical-plan/src/filter.rs | 56 +- .../physical-plan/src/filter_pushdown.rs | 95 - .../physical-plan/src/joins/cross_join.rs | 33 +- .../physical-plan/src/joins/hash_join.rs | 56 +- datafusion/physical-plan/src/joins/mod.rs | 6 - .../src/joins/nested_loop_join.rs | 32 +- .../src/joins/sort_merge_join.rs | 74 +- .../physical-plan/src/joins/test_utils.rs | 8 +- datafusion/physical-plan/src/joins/utils.rs | 23 +- datafusion/physical-plan/src/lib.rs | 2 - datafusion/physical-plan/src/projection.rs | 7 +- .../physical-plan/src/repartition/mod.rs | 26 +- datafusion/physical-plan/src/sorts/cursor.rs | 2 +- datafusion/physical-plan/src/sorts/merge.rs | 16 +- datafusion/physical-plan/src/sorts/sort.rs | 407 +- .../src/spill/in_progress_spill_file.rs | 12 +- datafusion/physical-plan/src/spill/mod.rs | 195 +- .../physical-plan/src/spill/spill_manager.rs | 26 +- datafusion/physical-plan/src/topk/mod.rs | 325 +- .../proto/datafusion_common.proto | 4 - datafusion/proto-common/src/common.rs | 1 - datafusion/proto-common/src/from_proto/mod.rs | 3 - .../proto-common/src/generated/pbjson.rs | 22 - .../proto-common/src/generated/prost.rs | 7 - datafusion/proto-common/src/to_proto/mod.rs | 1 - datafusion/proto/Cargo.toml | 1 + datafusion/proto/proto/datafusion.proto | 4 +- .../src/generated/datafusion_proto_common.rs | 7 - datafusion/proto/src/generated/prost.rs | 4 +- .../proto/src/logical_plan/file_formats.rs | 6 - datafusion/proto/src/logical_plan/mod.rs | 81 +- .../proto/src/physical_plan/from_proto.rs | 16 +- datafusion/proto/src/physical_plan/mod.rs | 4187 +++++++-------- .../proto/src/physical_plan/to_proto.rs | 12 +- .../tests/cases/roundtrip_logical_plan.rs | 39 +- .../tests/cases/roundtrip_physical_plan.rs | 46 +- datafusion/proto/tests/cases/serialize.rs | 2 +- datafusion/sql/Cargo.toml | 1 - datafusion/sql/src/expr/function.rs | 73 +- datafusion/sql/src/expr/value.rs | 2 +- datafusion/sql/src/parser.rs | 183 +- datafusion/sql/src/planner.rs | 2 +- datafusion/sql/src/select.rs | 106 +- datafusion/sql/src/statement.rs | 12 +- datafusion/sql/src/unparser/ast.rs | 15 - datafusion/sql/src/unparser/dialect.rs | 7 +- datafusion/sql/src/unparser/expr.rs | 126 +- datafusion/sql/src/unparser/mod.rs | 4 +- datafusion/sql/src/unparser/plan.rs | 68 +- datafusion/sql/src/unparser/utils.rs | 71 +- datafusion/sql/tests/cases/diagnostic.rs | 115 +- datafusion/sql/tests/cases/plan_to_sql.rs | 1672 +++--- datafusion/sql/tests/sql_integration.rs | 4615 ++++++----------- datafusion/sqllogictest/Cargo.toml | 4 +- datafusion/sqllogictest/bin/sqllogictests.rs | 7 +- .../sqllogictest/test_files/aggregate.slt | 269 +- datafusion/sqllogictest/test_files/array.slt | 71 +- datafusion/sqllogictest/test_files/binary.slt | 30 +- .../sqllogictest/test_files/clickbench.slt | 28 +- datafusion/sqllogictest/test_files/copy.slt | 2 +- .../test_files/create_external_table.slt | 2 +- datafusion/sqllogictest/test_files/cte.slt | 2 +- datafusion/sqllogictest/test_files/dates.slt | 2 +- .../sqllogictest/test_files/dictionary.slt | 7 - .../sqllogictest/test_files/explain.slt | 3 - .../sqllogictest/test_files/explain_tree.slt | 154 +- .../test_files/expr/date_part.slt | 6 +- .../sqllogictest/test_files/functions.slt | 4 +- .../sqllogictest/test_files/group_by.slt | 27 +- .../test_files/information_schema.slt | 46 +- datafusion/sqllogictest/test_files/joins.slt | 48 - .../sqllogictest/test_files/parquet.slt | 18 - .../test_files/parquet_sorted_statistics.slt | 17 +- datafusion/sqllogictest/test_files/regexp.slt | 898 ++++ .../sqllogictest/test_files/regexp/README.md | 59 - .../test_files/regexp/init_data.slt.part | 31 - .../test_files/regexp/regexp_count.slt | 344 -- .../test_files/regexp/regexp_like.slt | 280 - .../test_files/regexp/regexp_match.slt | 201 - .../test_files/regexp/regexp_replace.slt | 129 - .../sqllogictest/test_files/simplify_expr.slt | 42 - .../sqllogictest/test_files/subquery.slt | 44 +- .../sqllogictest/test_files/timestamps.slt | 42 +- datafusion/sqllogictest/test_files/topk.slt | 162 - datafusion/sqllogictest/test_files/window.slt | 49 +- .../substrait/src/logical_plan/consumer.rs | 3 +- .../substrait/src/physical_plan/producer.rs | 2 +- .../tests/cases/consumer_integration.rs | 27 - .../tests/cases/roundtrip_logical_plan.rs | 375 +- .../testdata/test_plans/multiple_joins.json | 536 -- datafusion/wasmtest/README.md | 2 + .../datafusion-wasm-app/package-lock.json | 13 +- datafusion/wasmtest/src/lib.rs | 28 +- datafusion/wasmtest/webdriver.json | 15 - dev/changelog/47.0.0.md | 506 -- dev/update_runtime_config_docs.sh | 76 - docs/source/index.rst | 1 - docs/source/library-user-guide/profiling.md | 43 +- .../library-user-guide/samply_profiler.png | Bin 605887 -> 0 bytes docs/source/library-user-guide/upgrading.md | 120 +- docs/source/user-guide/cli/datasources.md | 37 +- docs/source/user-guide/cli/usage.md | 3 - .../user-guide/concepts-readings-events.md | 6 - docs/source/user-guide/configs.md | 3 +- docs/source/user-guide/introduction.md | 2 - docs/source/user-guide/runtime_configs.md | 40 - .../user-guide/sql/aggregate_functions.md | 50 +- docs/source/user-guide/sql/data_types.md | 28 +- docs/source/user-guide/sql/ddl.md | 2 +- docs/source/user-guide/sql/dml.md | 2 +- docs/source/user-guide/sql/explain.md | 70 +- docs/source/user-guide/sql/format_options.md | 180 - docs/source/user-guide/sql/index.rst | 2 +- .../source/user-guide/sql/window_functions.md | 59 +- docs/source/user-guide/sql/write_options.md | 127 + parquet-testing | 2 +- rust-toolchain.toml | 2 +- test-utils/src/lib.rs | 5 +- 341 files changed, 9256 insertions(+), 25057 deletions(-) delete mode 100644 datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap delete mode 100644 datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap delete mode 100644 datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap delete mode 100644 datafusion/catalog/src/memory/table.rs delete mode 100644 datafusion/core/src/bin/print_runtime_config_docs.rs rename datafusion/core/src/datasource/{memory_test.rs => memory.rs} (58%) create mode 100644 datafusion/core/src/datasource/statistics.rs delete mode 100644 datafusion/core/tests/fuzz_cases/record_batch_generator.rs delete mode 100644 datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs delete mode 100644 datafusion/core/tests/physical_optimizer/push_down_filter.rs delete mode 100644 datafusion/core/tests/sql/runtime_config.rs delete mode 100644 datafusion/core/tests/tracing/asserting_tracer.rs delete mode 100644 datafusion/core/tests/tracing/mod.rs delete mode 100644 datafusion/core/tests/tracing/traceable_object_store.rs delete mode 100644 datafusion/datasource/benches/split_groups_by_statistics.rs delete mode 100644 datafusion/ffi/src/tests/utils.rs rename datafusion/ffi/src/{udf/mod.rs => udf.rs} (87%) delete mode 100644 datafusion/ffi/src/udf/return_info.rs delete mode 100644 datafusion/ffi/src/udf/return_type_args.rs delete mode 100644 datafusion/ffi/src/udtf.rs delete mode 100644 datafusion/ffi/tests/ffi_udf.rs delete mode 100644 datafusion/ffi/tests/ffi_udtf.rs delete mode 100644 datafusion/optimizer/benches/projection_unnecessary.rs delete mode 100644 datafusion/physical-expr/benches/binary_op.rs delete mode 100644 datafusion/physical-expr/src/expressions/dynamic_filters.rs delete mode 100644 datafusion/physical-optimizer/src/push_down_filter.rs delete mode 100644 datafusion/physical-plan/benches/spill_io.rs delete mode 100644 datafusion/physical-plan/src/filter_pushdown.rs create mode 100644 datafusion/sqllogictest/test_files/regexp.slt delete mode 100644 datafusion/sqllogictest/test_files/regexp/README.md delete mode 100644 datafusion/sqllogictest/test_files/regexp/init_data.slt.part delete mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_count.slt delete mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_like.slt delete mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_match.slt delete mode 100644 datafusion/sqllogictest/test_files/regexp/regexp_replace.slt delete mode 100644 datafusion/substrait/tests/testdata/test_plans/multiple_joins.json delete mode 100644 datafusion/wasmtest/webdriver.json delete mode 100644 dev/changelog/47.0.0.md delete mode 100755 dev/update_runtime_config_docs.sh delete mode 100644 docs/source/library-user-guide/samply_profiler.png delete mode 100644 docs/source/user-guide/runtime_configs.md delete mode 100644 docs/source/user-guide/sql/format_options.md create mode 100644 docs/source/user-guide/sql/write_options.md diff --git a/.github/actions/setup-rust-runtime/action.yaml b/.github/actions/setup-rust-runtime/action.yaml index b6fb2c898bf2f..cd18be9890315 100644 --- a/.github/actions/setup-rust-runtime/action.yaml +++ b/.github/actions/setup-rust-runtime/action.yaml @@ -20,10 +20,8 @@ description: 'Setup Rust Runtime Environment' runs: using: "composite" steps: - # https://github.com/apache/datafusion/issues/15535 - # disabled because neither version nor git hash works with apache github policy - #- name: Run sccache-cache - # uses: mozilla-actions/sccache-action@65101d47ea8028ed0c98a1cdea8dd9182e9b5133 # v0.0.8 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.4 - name: Configure runtime env shell: bash # do not produce debug symbols to keep memory usage down @@ -32,11 +30,9 @@ runs: # # Set debuginfo=line-tables-only as debuginfo=0 causes immensely slow build # See for more details: https://github.com/rust-lang/rust/issues/119560 - # - # readd the following to the run below once sccache-cache is re-enabled - # echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV - # echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV run: | + echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV + echo "SCCACHE_GHA_ENABLED=true" >> $GITHUB_ENV echo "RUST_BACKTRACE=1" >> $GITHUB_ENV echo "RUSTFLAGS=-C debuginfo=line-tables-only -C incremental=false" >> $GITHUB_ENV diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 491fa27c2a56a..0d65b1aa809ff 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -26,8 +26,6 @@ on: paths: - "**/Cargo.toml" - "**/Cargo.lock" - branches: - - main pull_request: paths: @@ -42,6 +40,4 @@ jobs: - name: Install cargo-audit run: cargo install cargo-audit - name: Run audit check - # Ignored until https://github.com/apache/datafusion/issues/15571 - # ignored py03 warning until arrow 55 upgrade - run: cargo audit --ignore RUSTSEC-2024-0370 --ignore RUSTSEC-2025-0020 + run: cargo audit diff --git a/.github/workflows/extended.yml b/.github/workflows/extended.yml index d80fdb75d932d..a5d68ff079b56 100644 --- a/.github/workflows/extended.yml +++ b/.github/workflows/extended.yml @@ -47,7 +47,7 @@ on: permissions: contents: read checks: write - + jobs: # Check crate compiles and base cargo check passes @@ -58,7 +58,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Install Rust @@ -82,7 +81,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Free Disk Space (Ubuntu) @@ -116,7 +114,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Setup Rust toolchain @@ -137,7 +134,6 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.pr_head_sha }} # will be empty if triggered by push submodules: true fetch-depth: 1 - name: Setup Rust toolchain @@ -165,14 +161,14 @@ jobs: echo "workflow_status=completed" >> $GITHUB_OUTPUT echo "conclusion=success" >> $GITHUB_OUTPUT fi - + - name: Update check run uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const workflowRunUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; - + await github.rest.checks.update({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/.github/workflows/pr_comment_commands.yml b/.github/workflows/pr_comment_commands.yml index 6aa6caaf34d02..a20a5b15965dd 100644 --- a/.github/workflows/pr_comment_commands.yml +++ b/.github/workflows/pr_comment_commands.yml @@ -44,12 +44,12 @@ jobs: repo: context.repo.repo, pull_number: context.payload.issue.number }); - + // Extract the branch name const branchName = pullRequest.head.ref; const headSha = pullRequest.head.sha; const workflowRunsUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions?query=workflow%3A%22Datafusion+extended+tests%22+branch%3A${branchName}`; - + // Create a check run that links to the Actions tab so the run will be visible in GitHub UI const check = await github.rest.checks.create({ owner: context.repo.owner, @@ -69,7 +69,7 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, workflow_id: 'extended.yml', - ref: 'main', + ref: branchName, inputs: { pr_number: context.payload.issue.number.toString(), check_run_id: check.data.id.toString(), @@ -77,7 +77,7 @@ jobs: } }); - - name: Add reaction to comment + - name: Add reaction to comment uses: actions/github-script@v7 with: script: | @@ -86,4 +86,4 @@ jobs: repo: context.repo.repo, comment_id: context.payload.comment.id, content: 'rocket' - }); + }); \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f3b7e19a4970b..1e6cd97acea33 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -384,25 +384,25 @@ jobs: run: ci/scripts/rust_docs.sh linux-wasm-pack: - name: build and run with wasm-pack - runs-on: ubuntu-24.04 + name: build with wasm-pack + runs-on: ubuntu-latest + container: + image: amd64/rust steps: - uses: actions/checkout@v4 - - name: Setup for wasm32 - run: | - rustup target add wasm32-unknown-unknown + - name: Setup Rust toolchain + uses: ./.github/actions/setup-builder + with: + rust-version: stable - name: Install dependencies run: | - sudo apt-get update -qq - sudo apt-get install -y -qq clang - - name: Setup wasm-pack - run: | - cargo install wasm-pack - - name: Run tests with headless mode + apt-get update -qq + apt-get install -y -qq clang + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: Build with wasm-pack working-directory: ./datafusion/wasmtest - run: | - wasm-pack test --headless --firefox - wasm-pack test --headless --chrome --chromedriver $CHROMEWEBDRIVER/chromedriver + run: wasm-pack build --dev # verify that the benchmark queries return the correct results verify-benchmark-results: @@ -693,11 +693,6 @@ jobs: # If you encounter an error, run './dev/update_function_docs.sh' and commit ./dev/update_function_docs.sh git diff --exit-code - - name: Check if runtime_configs.md has been modified - run: | - # If you encounter an error, run './dev/update_runtime_config_docs.sh' and commit - ./dev/update_runtime_config_docs.sh - git diff --exit-code # Verify MSRV for the crates which are directly used by other projects: # - datafusion diff --git a/Cargo.lock b/Cargo.lock index 299ea0dc4c6fd..8aba95bdcca4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,9 +246,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "arrow" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3095aaf545942ff5abd46654534f15b03a90fba78299d661e045e5d587222f0d" +checksum = "dc208515aa0151028e464cc94a692156e945ce5126abd3537bb7fd6ba2143ed1" dependencies = [ "arrow-arith", "arrow-array", @@ -265,14 +265,14 @@ dependencies = [ "arrow-string", "half", "pyo3", - "rand 0.9.0", + "rand 0.8.5", ] [[package]] name = "arrow-arith" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00752064ff47cee746e816ddb8450520c3a52cbad1e256f6fa861a35f86c45e7" +checksum = "e07e726e2b3f7816a85c6a45b6ec118eeeabf0b2a8c208122ad949437181f49a" dependencies = [ "arrow-array", "arrow-buffer", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "arrow-array" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cebfe926794fbc1f49ddd0cdaf898956ca9f6e79541efce62dabccfd81380472" +checksum = "a2262eba4f16c78496adfd559a29fe4b24df6088efc9985a873d58e92be022d5" dependencies = [ "ahash 0.8.11", "arrow-buffer", @@ -301,9 +301,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0303c7ec4cf1a2c60310fc4d6bbc3350cd051a17bf9e9c0a8e47b4db79277824" +checksum = "4e899dade2c3b7f5642eb8366cfd898958bcca099cde6dfea543c7e8d3ad88d4" dependencies = [ "bytes", "half", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335f769c5a218ea823d3760a743feba1ef7857cba114c01399a891c2fff34285" +checksum = "4103d88c5b441525ed4ac23153be7458494c2b0c9a11115848fdb9b81f6f886a" dependencies = [ "arrow-array", "arrow-buffer", @@ -333,9 +333,9 @@ dependencies = [ [[package]] name = "arrow-csv" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "510db7dfbb4d5761826516cc611d97b3a68835d0ece95b034a052601109c0b1b" +checksum = "43d3cb0914486a3cae19a5cad2598e44e225d53157926d0ada03c20521191a65" dependencies = [ "arrow-array", "arrow-cast", @@ -349,9 +349,9 @@ dependencies = [ [[package]] name = "arrow-data" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8affacf3351a24039ea24adab06f316ded523b6f8c3dbe28fbac5f18743451b" +checksum = "0a329fb064477c9ec5f0870d2f5130966f91055c7c5bce2b3a084f116bc28c3b" dependencies = [ "arrow-buffer", "arrow-schema", @@ -361,9 +361,9 @@ dependencies = [ [[package]] name = "arrow-flight" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e0fad280f41a918d53ba48288a246ff04202d463b3b380fbc0edecdcb52cfd" +checksum = "c7408f2bf3b978eddda272c7699f439760ebc4ac70feca25fefa82c5b8ce808d" dependencies = [ "arrow-arith", "arrow-array", @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69880a9e6934d9cba2b8630dd08a3463a91db8693b16b499d54026b6137af284" +checksum = "ddecdeab02491b1ce88885986e25002a3da34dd349f682c7cfe67bab7cc17b86" dependencies = [ "arrow-array", "arrow-buffer", @@ -402,9 +402,9 @@ dependencies = [ [[package]] name = "arrow-json" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8dafd17a05449e31e0114d740530e0ada7379d7cb9c338fd65b09a8130960b0" +checksum = "d03b9340013413eb84868682ace00a1098c81a5ebc96d279f7ebf9a4cac3c0fd" dependencies = [ "arrow-array", "arrow-buffer", @@ -413,20 +413,18 @@ dependencies = [ "arrow-schema", "chrono", "half", - "indexmap 2.9.0", + "indexmap 2.8.0", "lexical-core", - "memchr", "num", "serde", "serde_json", - "simdutf8", ] [[package]] name = "arrow-ord" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "895644523af4e17502d42c3cb6b27cb820f0cb77954c22d75c23a85247c849e1" +checksum = "f841bfcc1997ef6ac48ee0305c4dfceb1f7c786fe31e67c1186edf775e1f1160" dependencies = [ "arrow-array", "arrow-buffer", @@ -437,9 +435,9 @@ dependencies = [ [[package]] name = "arrow-row" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be8a2a4e5e7d9c822b2b8095ecd77010576d824f654d347817640acfc97d229" +checksum = "1eeb55b0a0a83851aa01f2ca5ee5648f607e8506ba6802577afdda9d75cdedcd" dependencies = [ "arrow-array", "arrow-buffer", @@ -450,9 +448,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7450c76ab7c5a6805be3440dc2e2096010da58f7cab301fdc996a4ee3ee74e49" +checksum = "85934a9d0261e0fa5d4e2a5295107d743b543a6e0484a835d4b8db2da15306f9" dependencies = [ "bitflags 2.8.0", "serde", @@ -460,9 +458,9 @@ dependencies = [ [[package]] name = "arrow-select" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa5f5a93c75f46ef48e4001535e7b6c922eeb0aa20b73cf58d09e13d057490d8" +checksum = "7e2932aece2d0c869dd2125feb9bd1709ef5c445daa3838ac4112dcfa0fda52c" dependencies = [ "ahash 0.8.11", "arrow-array", @@ -474,9 +472,9 @@ dependencies = [ [[package]] name = "arrow-string" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e7005d858d84b56428ba2a98a107fe88c0132c61793cf6b8232a1f9bfc0452b" +checksum = "912e38bd6a7a7714c1d9b61df80315685553b7455e8a6045c27531d8ecd5b458" dependencies = [ "arrow-array", "arrow-buffer", @@ -1049,9 +1047,9 @@ dependencies = [ [[package]] name = "bigdecimal" -version = "0.4.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" +checksum = "7f31f3af01c5c65a07985c804d3366560e6fa7883d640a122819b14ec327482c" dependencies = [ "autocfg", "libm", @@ -1119,9 +1117,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389a099b34312839e16420d499a9cad9650541715937ffbdd40d36f49e77eeb3" +checksum = "b17679a8d69b6d7fd9cd9801a536cec9fa5e5970b69f9d4747f70b39b031f5e7" dependencies = [ "arrayref", "arrayvec", @@ -1363,9 +1361,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.40" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -1373,7 +1371,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-targets 0.52.6", ] [[package]] @@ -1448,9 +1446,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.36" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df961d8c8a0d08aa9945718ccf584145eee3f3aa06cddbeac12933781102e04" +checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff" dependencies = [ "clap_builder", "clap_derive", @@ -1458,9 +1456,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.36" +version = "4.5.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dbda40fb6753878316a489d5a1242a8ef2f0d9e47ba01c951ea8aa7d013a5" +checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489" dependencies = [ "anstream", "anstyle", @@ -1642,7 +1640,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.36", + "clap 4.5.34", "criterion-plot", "futures", "is-terminal", @@ -1673,9 +1671,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" dependencies = [ "crossbeam-utils", ] @@ -1809,7 +1807,7 @@ dependencies = [ [[package]] name = "datafusion" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "arrow-ipc", @@ -1879,7 +1877,7 @@ dependencies = [ [[package]] name = "datafusion-benchmarks" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "datafusion", @@ -1903,7 +1901,7 @@ dependencies = [ [[package]] name = "datafusion-catalog" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", @@ -1927,7 +1925,7 @@ dependencies = [ [[package]] name = "datafusion-catalog-listing" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", @@ -1949,14 +1947,14 @@ dependencies = [ [[package]] name = "datafusion-cli" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "assert_cmd", "async-trait", "aws-config", "aws-credential-types", - "clap 4.5.36", + "clap 4.5.34", "ctor", "datafusion", "dirs", @@ -1978,7 +1976,7 @@ dependencies = [ [[package]] name = "datafusion-common" -version = "47.0.0" +version = "46.0.1" dependencies = [ "ahash 0.8.11", "apache-avro", @@ -1988,7 +1986,7 @@ dependencies = [ "chrono", "half", "hashbrown 0.14.5", - "indexmap 2.9.0", + "indexmap 2.8.0", "insta", "libc", "log", @@ -2005,7 +2003,7 @@ dependencies = [ [[package]] name = "datafusion-common-runtime" -version = "47.0.0" +version = "46.0.1" dependencies = [ "futures", "log", @@ -2014,7 +2012,7 @@ dependencies = [ [[package]] name = "datafusion-datasource" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-compression", @@ -2022,7 +2020,6 @@ dependencies = [ "bytes", "bzip2 0.5.2", "chrono", - "criterion", "datafusion-common", "datafusion-common-runtime", "datafusion-execution", @@ -2049,7 +2046,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-avro" -version = "47.0.0" +version = "46.0.1" dependencies = [ "apache-avro", "arrow", @@ -2074,7 +2071,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-csv" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", @@ -2097,7 +2094,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-json" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", @@ -2120,7 +2117,7 @@ dependencies = [ [[package]] name = "datafusion-datasource-parquet" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", @@ -2150,11 +2147,11 @@ dependencies = [ [[package]] name = "datafusion-doc" -version = "47.0.0" +version = "46.0.1" [[package]] name = "datafusion-examples" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "arrow-flight", @@ -2163,7 +2160,6 @@ dependencies = [ "bytes", "dashmap", "datafusion", - "datafusion-ffi", "datafusion-proto", "env_logger", "futures", @@ -2184,7 +2180,7 @@ dependencies = [ [[package]] name = "datafusion-execution" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "chrono", @@ -2202,7 +2198,7 @@ dependencies = [ [[package]] name = "datafusion-expr" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "chrono", @@ -2214,7 +2210,7 @@ dependencies = [ "datafusion-functions-window-common", "datafusion-physical-expr-common", "env_logger", - "indexmap 2.9.0", + "indexmap 2.8.0", "paste", "recursive", "serde_json", @@ -2223,22 +2219,21 @@ dependencies = [ [[package]] name = "datafusion-expr-common" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "datafusion-common", - "indexmap 2.9.0", + "indexmap 2.8.0", "itertools 0.14.0", "paste", ] [[package]] name = "datafusion-ffi" -version = "47.0.0" +version = "46.0.1" dependencies = [ "abi_stable", "arrow", - "arrow-schema", "async-ffi", "async-trait", "datafusion", @@ -2253,7 +2248,7 @@ dependencies = [ [[package]] name = "datafusion-functions" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "arrow-buffer", @@ -2282,7 +2277,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate" -version = "47.0.0" +version = "46.0.1" dependencies = [ "ahash 0.8.11", "arrow", @@ -2303,7 +2298,7 @@ dependencies = [ [[package]] name = "datafusion-functions-aggregate-common" -version = "47.0.0" +version = "46.0.1" dependencies = [ "ahash 0.8.11", "arrow", @@ -2316,7 +2311,7 @@ dependencies = [ [[package]] name = "datafusion-functions-nested" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "arrow-ord", @@ -2337,7 +2332,7 @@ dependencies = [ [[package]] name = "datafusion-functions-table" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", @@ -2351,7 +2346,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "datafusion-common", @@ -2367,7 +2362,7 @@ dependencies = [ [[package]] name = "datafusion-functions-window-common" -version = "47.0.0" +version = "46.0.1" dependencies = [ "datafusion-common", "datafusion-physical-expr-common", @@ -2375,7 +2370,7 @@ dependencies = [ [[package]] name = "datafusion-macros" -version = "47.0.0" +version = "46.0.1" dependencies = [ "datafusion-expr", "quote", @@ -2384,12 +2379,11 @@ dependencies = [ [[package]] name = "datafusion-optimizer" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", "chrono", - "criterion", "ctor", "datafusion-common", "datafusion-expr", @@ -2399,8 +2393,7 @@ dependencies = [ "datafusion-physical-expr", "datafusion-sql", "env_logger", - "indexmap 2.9.0", - "insta", + "indexmap 2.8.0", "itertools 0.14.0", "log", "recursive", @@ -2410,7 +2403,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr" -version = "47.0.0" +version = "46.0.1" dependencies = [ "ahash 0.8.11", "arrow", @@ -2423,8 +2416,7 @@ dependencies = [ "datafusion-physical-expr-common", "half", "hashbrown 0.14.5", - "indexmap 2.9.0", - "insta", + "indexmap 2.8.0", "itertools 0.14.0", "log", "paste", @@ -2435,7 +2427,7 @@ dependencies = [ [[package]] name = "datafusion-physical-expr-common" -version = "47.0.0" +version = "46.0.1" dependencies = [ "ahash 0.8.11", "arrow", @@ -2447,7 +2439,7 @@ dependencies = [ [[package]] name = "datafusion-physical-optimizer" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "datafusion-common", @@ -2466,7 +2458,7 @@ dependencies = [ [[package]] name = "datafusion-physical-plan" -version = "47.0.0" +version = "46.0.1" dependencies = [ "ahash 0.8.11", "arrow", @@ -2487,7 +2479,7 @@ dependencies = [ "futures", "half", "hashbrown 0.14.5", - "indexmap 2.9.0", + "indexmap 2.8.0", "insta", "itertools 0.14.0", "log", @@ -2496,13 +2488,12 @@ dependencies = [ "rand 0.8.5", "rstest", "rstest_reuse", - "tempfile", "tokio", ] [[package]] name = "datafusion-proto" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "chrono", @@ -2525,7 +2516,7 @@ dependencies = [ [[package]] name = "datafusion-proto-common" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "datafusion-common", @@ -2538,7 +2529,7 @@ dependencies = [ [[package]] name = "datafusion-session" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", @@ -2560,7 +2551,7 @@ dependencies = [ [[package]] name = "datafusion-sql" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "bigdecimal", @@ -2572,8 +2563,7 @@ dependencies = [ "datafusion-functions-nested", "datafusion-functions-window", "env_logger", - "indexmap 2.9.0", - "insta", + "indexmap 2.8.0", "log", "paste", "recursive", @@ -2584,14 +2574,14 @@ dependencies = [ [[package]] name = "datafusion-sqllogictest" -version = "47.0.0" +version = "46.0.1" dependencies = [ "arrow", "async-trait", "bigdecimal", "bytes", "chrono", - "clap 4.5.36", + "clap 4.5.34", "datafusion", "env_logger", "futures", @@ -2615,7 +2605,7 @@ dependencies = [ [[package]] name = "datafusion-substrait" -version = "47.0.0" +version = "46.0.1" dependencies = [ "async-recursion", "async-trait", @@ -2635,7 +2625,7 @@ dependencies = [ [[package]] name = "datafusion-wasmtest" -version = "47.0.0" +version = "46.0.1" dependencies = [ "chrono", "console_error_panic_hook", @@ -2805,9 +2795,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697" dependencies = [ "anstream", "anstyle", @@ -2928,22 +2918,21 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flatbuffers" -version = "25.2.10" +version = "24.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1" +checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096" dependencies = [ - "bitflags 2.8.0", + "bitflags 1.3.2", "rustc_version", ] [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" dependencies = [ "crc32fast", - "libz-rs-sys", "miniz_oxide", ] @@ -3190,7 +3179,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.2.0", - "indexmap 2.9.0", + "indexmap 2.8.0", "slab", "tokio", "tokio-util", @@ -3199,9 +3188,9 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" dependencies = [ "cfg-if", "crunchy", @@ -3639,9 +3628,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -3878,9 +3867,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libflate" @@ -3934,9 +3923,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmimalloc-sys" -version = "0.1.42" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9d6fac27761dabcd4ee73571cdb06b7022dc99089acbe5435691edffaac0f4" +checksum = "07d0e07885d6a754b9c7993f2625187ad694ee985d60f23355ff0e7077261502" dependencies = [ "cc", "libc", @@ -3961,19 +3950,10 @@ checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" dependencies = [ "anstream", "anstyle", - "clap 4.5.36", + "clap 4.5.34", "escape8259", ] -[[package]] -name = "libz-rs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" -dependencies = [ - "zlib-rs", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -4020,7 +4000,7 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5" dependencies = [ - "twox-hash 1.6.3", + "twox-hash", ] [[package]] @@ -4067,9 +4047,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.46" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "995942f432bbb4822a7e9c3faa87a695185b0d09273ba85f097b54f4e458f2af" +checksum = "99585191385958383e13f6b822e6b6d8d9cf928e7d286ceb092da92b43c87bc1" dependencies = [ "libmimalloc-sys", ] @@ -4098,9 +4078,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", ] @@ -4265,15 +4245,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" -[[package]] -name = "objc2-core-foundation" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" -dependencies = [ - "bitflags 2.8.0", -] - [[package]] name = "object" version = "0.36.7" @@ -4285,21 +4256,18 @@ dependencies = [ [[package]] name = "object_store" -version = "0.12.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9ce831b09395f933addbc56d894d889e4b226eba304d4e7adbab591e26daf1e" +checksum = "3cfccb68961a56facde1163f9319e0d15743352344e7808a11795fb99698dcaf" dependencies = [ "async-trait", "base64 0.22.1", "bytes", "chrono", - "form_urlencoded", "futures", - "http 1.2.0", - "http-body-util", "humantime", "hyper", - "itertools 0.14.0", + "itertools 0.13.0", "md-5", "parking_lot", "percent-encoding", @@ -4310,8 +4278,7 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "serde_urlencoded", - "thiserror 2.0.12", + "snafu", "tokio", "tracing", "url", @@ -4394,9 +4361,9 @@ dependencies = [ [[package]] name = "parquet" -version = "55.0.0" +version = "54.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd31a8290ac5b19f09ad77ee7a1e6a541f1be7674ad410547d5f1eef6eef4a9c" +checksum = "f88838dca3b84d41444a0341b19f347e8098a3898b0f21536654b8b799e11abd" dependencies = [ "ahash 0.8.11", "arrow-array", @@ -4424,8 +4391,9 @@ dependencies = [ "snap", "thrift", "tokio", - "twox-hash 2.1.0", + "twox-hash", "zstd", + "zstd-sys", ] [[package]] @@ -4518,7 +4486,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.9.0", + "indexmap 2.8.0", ] [[package]] @@ -4872,9 +4840,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.2" +version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "7778bffd85cf38175ac1f545509665d0b9b92a198ca7941f131f85f7a4f9a872" dependencies = [ "cfg-if", "indoc", @@ -4890,9 +4858,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "94f6cbe86ef3bf18998d9df6e0f3fc1050a8c5efa409bf712e661a4366e010fb" dependencies = [ "once_cell", "target-lexicon", @@ -4900,9 +4868,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "e9f1b4c431c0bb1c8fb0a338709859eed0d030ff6daa34368d3b152a63dfdd8d" dependencies = [ "libc", "pyo3-build-config", @@ -4910,9 +4878,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "fbc2201328f63c4710f68abdf653c89d8dbc2858b88c5d88b0ff38a75288a9da" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -4922,9 +4890,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.23.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "fca6726ad0f3da9c9de093d6f116a93c1a38e417ed73bf138472cf4064f72028" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5721,7 +5689,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.9.0", + "indexmap 2.8.0", "serde", "serde_derive", "serde_json", @@ -5747,7 +5715,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.8.0", "itoa", "ryu", "serde", @@ -5822,6 +5790,27 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.100", +] + [[package]] name = "snap" version = "1.1.1" @@ -5858,9 +5847,9 @@ dependencies = [ [[package]] name = "sqllogictest" -version = "0.28.1" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6199c1e008acc669b1e5873c138bf3ad4f8709ccd5c5d88913e664ae4f75de" +checksum = "17b2f0b80fc250ed3fdd82fc88c0ada5ad62ee1ed5314ac5474acfa52082f518" dependencies = [ "async-trait", "educe", @@ -6119,14 +6108,15 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.34.2" +version = "0.33.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4b93974b3d3aeaa036504b8eefd4c039dced109171c1ae973f1dc63b2c7e4b2" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ + "core-foundation-sys", "libc", "memchr", "ntapi", - "objc2-core-foundation", + "rayon", "windows", ] @@ -6138,9 +6128,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-lexicon" -version = "0.13.2" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" @@ -6471,7 +6461,7 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.9.0", + "indexmap 2.8.0", "toml_datetime", "winnow", ] @@ -6641,12 +6631,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "twox-hash" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7b17f197b3050ba473acf9181f7b1d3b66d1cf7356c6cc57886662276e65908" - [[package]] name = "typed-arena" version = "2.0.2" @@ -7139,12 +7123,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - [[package]] name = "windows-registry" version = "0.2.0" @@ -7511,12 +7489,6 @@ dependencies = [ "syn 2.0.100", ] -[[package]] -name = "zlib-rs" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" - [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 5a735666f8e7e..b6164f89d31e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ repository = "https://github.com/apache/datafusion" # Define Minimum Supported Rust Version (MSRV) rust-version = "1.82.0" # Define DataFusion version -version = "47.0.0" +version = "46.0.1" [workspace.dependencies] # We turn off default-features for some dependencies here so the workspaces which inherit them can @@ -87,69 +87,69 @@ ahash = { version = "0.8", default-features = false, features = [ "runtime-rng", ] } apache-avro = { version = "0.17", default-features = false } -arrow = { version = "55.0.0", features = [ +arrow = { version = "54.2.1", features = [ "prettyprint", "chrono-tz", ] } -arrow-buffer = { version = "55.0.0", default-features = false } -arrow-flight = { version = "55.0.0", features = [ +arrow-buffer = { version = "54.1.0", default-features = false } +arrow-flight = { version = "54.2.1", features = [ "flight-sql-experimental", ] } -arrow-ipc = { version = "55.0.0", default-features = false, features = [ +arrow-ipc = { version = "54.2.0", default-features = false, features = [ "lz4", ] } -arrow-ord = { version = "55.0.0", default-features = false } -arrow-schema = { version = "55.0.0", default-features = false } +arrow-ord = { version = "54.1.0", default-features = false } +arrow-schema = { version = "54.1.0", default-features = false } async-trait = "0.1.88" -bigdecimal = "0.4.8" +bigdecimal = "0.4.7" bytes = "1.10" chrono = { version = "0.4.38", default-features = false } criterion = "0.5.1" ctor = "0.2.9" dashmap = "6.0.1" -datafusion = { path = "datafusion/core", version = "47.0.0", default-features = false } -datafusion-catalog = { path = "datafusion/catalog", version = "47.0.0" } -datafusion-catalog-listing = { path = "datafusion/catalog-listing", version = "47.0.0" } -datafusion-common = { path = "datafusion/common", version = "47.0.0", default-features = false } -datafusion-common-runtime = { path = "datafusion/common-runtime", version = "47.0.0" } -datafusion-datasource = { path = "datafusion/datasource", version = "47.0.0", default-features = false } -datafusion-datasource-avro = { path = "datafusion/datasource-avro", version = "47.0.0", default-features = false } -datafusion-datasource-csv = { path = "datafusion/datasource-csv", version = "47.0.0", default-features = false } -datafusion-datasource-json = { path = "datafusion/datasource-json", version = "47.0.0", default-features = false } -datafusion-datasource-parquet = { path = "datafusion/datasource-parquet", version = "47.0.0", default-features = false } -datafusion-doc = { path = "datafusion/doc", version = "47.0.0" } -datafusion-execution = { path = "datafusion/execution", version = "47.0.0" } -datafusion-expr = { path = "datafusion/expr", version = "47.0.0" } -datafusion-expr-common = { path = "datafusion/expr-common", version = "47.0.0" } -datafusion-ffi = { path = "datafusion/ffi", version = "47.0.0" } -datafusion-functions = { path = "datafusion/functions", version = "47.0.0" } -datafusion-functions-aggregate = { path = "datafusion/functions-aggregate", version = "47.0.0" } -datafusion-functions-aggregate-common = { path = "datafusion/functions-aggregate-common", version = "47.0.0" } -datafusion-functions-nested = { path = "datafusion/functions-nested", version = "47.0.0" } -datafusion-functions-table = { path = "datafusion/functions-table", version = "47.0.0" } -datafusion-functions-window = { path = "datafusion/functions-window", version = "47.0.0" } -datafusion-functions-window-common = { path = "datafusion/functions-window-common", version = "47.0.0" } -datafusion-macros = { path = "datafusion/macros", version = "47.0.0" } -datafusion-optimizer = { path = "datafusion/optimizer", version = "47.0.0", default-features = false } -datafusion-physical-expr = { path = "datafusion/physical-expr", version = "47.0.0", default-features = false } -datafusion-physical-expr-common = { path = "datafusion/physical-expr-common", version = "47.0.0", default-features = false } -datafusion-physical-optimizer = { path = "datafusion/physical-optimizer", version = "47.0.0" } -datafusion-physical-plan = { path = "datafusion/physical-plan", version = "47.0.0" } -datafusion-proto = { path = "datafusion/proto", version = "47.0.0" } -datafusion-proto-common = { path = "datafusion/proto-common", version = "47.0.0" } -datafusion-session = { path = "datafusion/session", version = "47.0.0" } -datafusion-sql = { path = "datafusion/sql", version = "47.0.0" } +datafusion = { path = "datafusion/core", version = "46.0.1", default-features = false } +datafusion-catalog = { path = "datafusion/catalog", version = "46.0.1" } +datafusion-catalog-listing = { path = "datafusion/catalog-listing", version = "46.0.1" } +datafusion-common = { path = "datafusion/common", version = "46.0.1", default-features = false } +datafusion-common-runtime = { path = "datafusion/common-runtime", version = "46.0.1" } +datafusion-datasource = { path = "datafusion/datasource", version = "46.0.1", default-features = false } +datafusion-datasource-avro = { path = "datafusion/datasource-avro", version = "46.0.1", default-features = false } +datafusion-datasource-csv = { path = "datafusion/datasource-csv", version = "46.0.1", default-features = false } +datafusion-datasource-json = { path = "datafusion/datasource-json", version = "46.0.1", default-features = false } +datafusion-datasource-parquet = { path = "datafusion/datasource-parquet", version = "46.0.1", default-features = false } +datafusion-doc = { path = "datafusion/doc", version = "46.0.1" } +datafusion-execution = { path = "datafusion/execution", version = "46.0.1" } +datafusion-expr = { path = "datafusion/expr", version = "46.0.1" } +datafusion-expr-common = { path = "datafusion/expr-common", version = "46.0.1" } +datafusion-ffi = { path = "datafusion/ffi", version = "46.0.1" } +datafusion-functions = { path = "datafusion/functions", version = "46.0.1" } +datafusion-functions-aggregate = { path = "datafusion/functions-aggregate", version = "46.0.1" } +datafusion-functions-aggregate-common = { path = "datafusion/functions-aggregate-common", version = "46.0.1" } +datafusion-functions-nested = { path = "datafusion/functions-nested", version = "46.0.1" } +datafusion-functions-table = { path = "datafusion/functions-table", version = "46.0.1" } +datafusion-functions-window = { path = "datafusion/functions-window", version = "46.0.1" } +datafusion-functions-window-common = { path = "datafusion/functions-window-common", version = "46.0.1" } +datafusion-macros = { path = "datafusion/macros", version = "46.0.1" } +datafusion-optimizer = { path = "datafusion/optimizer", version = "46.0.1", default-features = false } +datafusion-physical-expr = { path = "datafusion/physical-expr", version = "46.0.1", default-features = false } +datafusion-physical-expr-common = { path = "datafusion/physical-expr-common", version = "46.0.1", default-features = false } +datafusion-physical-optimizer = { path = "datafusion/physical-optimizer", version = "46.0.1" } +datafusion-physical-plan = { path = "datafusion/physical-plan", version = "46.0.1" } +datafusion-proto = { path = "datafusion/proto", version = "46.0.1" } +datafusion-proto-common = { path = "datafusion/proto-common", version = "46.0.1" } +datafusion-session = { path = "datafusion/session", version = "46.0.1" } +datafusion-sql = { path = "datafusion/sql", version = "46.0.1" } doc-comment = "0.3" env_logger = "0.11" futures = "0.3" -half = { version = "2.6.0", default-features = false } +half = { version = "2.5.0", default-features = false } hashbrown = { version = "0.14.5", features = ["raw"] } -indexmap = "2.9.0" +indexmap = "2.8.0" itertools = "0.14" log = "^0.4" -object_store = { version = "0.12.0", default-features = false } +object_store = { version = "0.11.0", default-features = false } parking_lot = "0.12" -parquet = { version = "55.0.0", default-features = false, features = [ +parquet = { version = "54.2.1", default-features = false, features = [ "arrow", "async", "object_store", @@ -191,20 +191,13 @@ strip = false # Retain debug info for flamegraphs inherits = "dev" incremental = false -# ci turns off debug info, etc. for dependencies to allow for smaller binaries making caching more effective +# ci turns off debug info, etc for dependencies to allow for smaller binaries making caching more effective [profile.ci.package."*"] debug = false debug-assertions = false strip = "debuginfo" incremental = false -# release inherited profile keeping debug information and symbols -# for mem/cpu profiling -[profile.profiling] -inherits = "release" -debug = true -strip = false - [workspace.lints.clippy] # Detects large stack-allocated futures that may cause stack overflow crashes (see threshold in clippy.toml) large_futures = "warn" diff --git a/benchmarks/README.md b/benchmarks/README.md index 86b2e1b3b958f..8acaa298bd3ad 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -200,16 +200,6 @@ cargo run --release --bin tpch -- convert --input ./data --output /mnt/tpch-parq Or if you want to verify and run all the queries in the benchmark, you can just run `cargo test`. -#### Sorted Conversion - -The TPCH tables generated by the dbgen utility are sorted by their first column (their primary key for most tables, the `l_orderkey` column for the `lineitem` table.) - -To preserve this sorted order information during conversion (useful for benchmarking execution on pre-sorted data) include the `--sort` flag: - -```bash -cargo run --release --bin tpch -- convert --input ./data --output /mnt/tpch-sorted-parquet --format parquet --sort -``` - ### Comparing results between runs Any `dfbench` execution with `-o

` argument will produce a @@ -455,29 +445,20 @@ Test performance of end-to-end sort SQL queries. (While the `Sort` benchmark foc Sort integration benchmark runs whole table sort queries on TPCH `lineitem` table, with different characteristics. For example, different number of sort keys, different sort key cardinality, different number of payload columns, etc. -If the TPCH tables have been converted as sorted on their first column (see [Sorted Conversion](#sorted-conversion)), you can use the `--sorted` flag to indicate that the input data is pre-sorted, allowing DataFusion to leverage that order during query execution. - -Additionally, an optional `--limit` flag is available for the sort benchmark. When specified, this flag appends a `LIMIT n` clause to the SQL query, effectively converting the query into a TopK query. Combining the `--sorted` and `--limit` options enables benchmarking of TopK queries on pre-sorted inputs. - See [`sort_tpch.rs`](src/sort_tpch.rs) for more details. ### Sort TPCH Benchmark Example Runs 1. Run all queries with default setting: ```bash - cargo run --release --bin dfbench -- sort-tpch -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' + cargo run --release --bin dfbench -- sort-tpch -p '....../datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' ``` 2. Run a specific query: ```bash - cargo run --release --bin dfbench -- sort-tpch -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' --query 2 -``` - -3. Run all queries as TopK queries on presorted data: -```bash - cargo run --release --bin dfbench -- sort-tpch --sorted --limit 10 -p './datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' + cargo run --release --bin dfbench -- sort-tpch -p '....../datafusion/benchmarks/data/tpch_sf1' -o '/tmp/sort_tpch.json' --query 2 ``` -4. Run all queries with `bench.sh` script: +3. Run all queries with `bench.sh` script: ```bash ./bench.sh run sort_tpch ``` diff --git a/benchmarks/bench.sh b/benchmarks/bench.sh index 5d3ad3446ddb9..5be825eb0dafd 100755 --- a/benchmarks/bench.sh +++ b/benchmarks/bench.sh @@ -412,10 +412,7 @@ run_tpch() { echo "Running tpch benchmark..." # Optional query filter to run specific query QUERY=$([ -n "$ARG3" ] && echo "--query $ARG3" || echo "") - # debug the target command - set -x $CARGO_COMMAND --bin tpch -- benchmark datafusion --iterations 5 --path "${TPCH_DIR}" --prefer_hash_join "${PREFER_HASH_JOIN}" --format parquet -o "${RESULTS_FILE}" $QUERY - set +x } # Runs the tpch in memory @@ -430,13 +427,9 @@ run_tpch_mem() { RESULTS_FILE="${RESULTS_DIR}/tpch_mem_sf${SCALE_FACTOR}.json" echo "RESULTS_FILE: ${RESULTS_FILE}" echo "Running tpch_mem benchmark..." - # Optional query filter to run specific query QUERY=$([ -n "$ARG3" ] && echo "--query $ARG3" || echo "") - # debug the target command - set -x # -m means in memory $CARGO_COMMAND --bin tpch -- benchmark datafusion --iterations 5 --path "${TPCH_DIR}" --prefer_hash_join "${PREFER_HASH_JOIN}" -m --format parquet -o "${RESULTS_FILE}" $QUERY - set +x } # Runs the cancellation benchmark diff --git a/benchmarks/queries/clickbench/README.md b/benchmarks/queries/clickbench/README.md index fdb7d1676be0f..6797797409c1a 100644 --- a/benchmarks/queries/clickbench/README.md +++ b/benchmarks/queries/clickbench/README.md @@ -93,14 +93,12 @@ LIMIT 10; Results look like -``` +-------------+---------------------+---+------+------+------+ | ClientIP | WatchID | c | tmin | tmed | tmax | +-------------+---------------------+---+------+------+------+ | 1611957945 | 6655575552203051303 | 2 | 0 | 0 | 0 | | -1402644643 | 8566928176839891583 | 2 | 0 | 0 | 0 | +-------------+---------------------+---+------+------+------+ -``` ### Q5: Response start time distribution analysis (p95) @@ -122,42 +120,13 @@ LIMIT 10; ``` Results look like -``` + +-------------+---------------------+---+------+------+------+ | ClientIP | WatchID | c | tmin | tp95 | tmax | +-------------+---------------------+---+------+------+------+ | 1611957945 | 6655575552203051303 | 2 | 0 | 0 | 0 | | -1402644643 | 8566928176839891583 | 2 | 0 | 0 | 0 | +-------------+---------------------+---+------+------+------+ -``` - -### Q6: How many social shares meet complex multi-stage filtering criteria? -**Question**: What is the count of sharing actions from iPhone mobile users on specific social networks, within common timezones, participating in seasonal campaigns, with high screen resolutions and closely matched UTM parameters? -**Important Query Properties**: Simple filter with high-selectivity, Costly string matching, A large number of filters with high overhead are positioned relatively later in the process - -```sql -SELECT COUNT(*) AS ShareCount -FROM hits -WHERE - -- Stage 1: High-selectivity filters (fast) - "IsMobile" = 1 -- Filter mobile users - AND "MobilePhoneModel" LIKE 'iPhone%' -- Match iPhone models - AND "SocialAction" = 'share' -- Identify social sharing actions - - -- Stage 2: Moderate filters (cheap) - AND "SocialSourceNetworkID" IN (5, 12) -- Filter specific social networks - AND "ClientTimeZone" BETWEEN -5 AND 5 -- Restrict to common timezones - - -- Stage 3: Heavy computations (expensive) - AND regexp_match("Referer", '\/campaign\/(spring|summer)_promo') IS NOT NULL -- Find campaign-specific referrers - AND CASE - WHEN split_part(split_part("URL", 'resolution=', 2), '&', 1) ~ '^\d+$' - THEN split_part(split_part("URL", 'resolution=', 2), '&', 1)::INT - ELSE 0 - END > 1920 -- Extract and validate resolution parameter - AND levenshtein(CAST("UTMSource" AS STRING), CAST("UTMCampaign" AS STRING)) < 3 -- Verify UTM parameter similarity -``` -Result is empty,Since it has already been filtered by `"SocialAction" = 'share'`. ## Data Notes diff --git a/benchmarks/queries/clickbench/extended.sql b/benchmarks/queries/clickbench/extended.sql index e967583fd6442..fbabaf2a70218 100644 --- a/benchmarks/queries/clickbench/extended.sql +++ b/benchmarks/queries/clickbench/extended.sql @@ -3,5 +3,4 @@ SELECT COUNT(DISTINCT "HitColor"), COUNT(DISTINCT "BrowserCountry"), COUNT(DISTI SELECT "BrowserCountry", COUNT(DISTINCT "SocialNetwork"), COUNT(DISTINCT "HitColor"), COUNT(DISTINCT "BrowserLanguage"), COUNT(DISTINCT "SocialAction") FROM hits GROUP BY 1 ORDER BY 2 DESC LIMIT 10; SELECT "SocialSourceNetworkID", "RegionID", COUNT(*), AVG("Age"), AVG("ParamPrice"), STDDEV("ParamPrice") as s, VAR("ParamPrice") FROM hits GROUP BY "SocialSourceNetworkID", "RegionID" HAVING s IS NOT NULL ORDER BY s DESC LIMIT 10; SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, MEDIAN("ResponseStartTiming") tmed, MAX("ResponseStartTiming") tmax FROM hits WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tmed DESC LIMIT 10; -SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, APPROX_PERCENTILE_CONT("ResponseStartTiming", 0.95) tp95, MAX("ResponseStartTiming") tmax FROM 'hits' WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tp95 DESC LIMIT 10; -SELECT COUNT(*) AS ShareCount FROM hits WHERE "IsMobile" = 1 AND "MobilePhoneModel" LIKE 'iPhone%' AND "SocialAction" = 'share' AND "SocialSourceNetworkID" IN (5, 12) AND "ClientTimeZone" BETWEEN -5 AND 5 AND regexp_match("Referer", '\/campaign\/(spring|summer)_promo') IS NOT NULL AND CASE WHEN split_part(split_part("URL", 'resolution=', 2), '&', 1) ~ '^\d+$' THEN split_part(split_part("URL", 'resolution=', 2), '&', 1)::INT ELSE 0 END > 1920 AND levenshtein(CAST("UTMSource" AS STRING), CAST("UTMCampaign" AS STRING)) < 3; +SELECT "ClientIP", "WatchID", COUNT(*) c, MIN("ResponseStartTiming") tmin, APPROX_PERCENTILE_CONT("ResponseStartTiming", 0.95) tp95, MAX("ResponseStartTiming") tmax FROM 'hits' WHERE "JavaEnable" = 0 GROUP BY "ClientIP", "WatchID" HAVING c > 1 ORDER BY tp95 DESC LIMIT 10; \ No newline at end of file diff --git a/benchmarks/queries/clickbench/queries.sql b/benchmarks/queries/clickbench/queries.sql index 9a183cd6e259c..52e72e02e1e0d 100644 --- a/benchmarks/queries/clickbench/queries.sql +++ b/benchmarks/queries/clickbench/queries.sql @@ -4,7 +4,7 @@ SELECT SUM("AdvEngineID"), COUNT(*), AVG("ResolutionWidth") FROM hits; SELECT AVG("UserID") FROM hits; SELECT COUNT(DISTINCT "UserID") FROM hits; SELECT COUNT(DISTINCT "SearchPhrase") FROM hits; -SELECT MIN("EventDate"), MAX("EventDate") FROM hits; +SELECT MIN("EventDate"::INT::DATE), MAX("EventDate"::INT::DATE) FROM hits; SELECT "AdvEngineID", COUNT(*) FROM hits WHERE "AdvEngineID" <> 0 GROUP BY "AdvEngineID" ORDER BY COUNT(*) DESC; SELECT "RegionID", COUNT(DISTINCT "UserID") AS u FROM hits GROUP BY "RegionID" ORDER BY u DESC LIMIT 10; SELECT "RegionID", SUM("AdvEngineID"), COUNT(*) AS c, AVG("ResolutionWidth"), COUNT(DISTINCT "UserID") FROM hits GROUP BY "RegionID" ORDER BY c DESC LIMIT 10; @@ -21,10 +21,10 @@ SELECT "UserID" FROM hits WHERE "UserID" = 435090932899640449; SELECT COUNT(*) FROM hits WHERE "URL" LIKE '%google%'; SELECT "SearchPhrase", MIN("URL"), COUNT(*) AS c FROM hits WHERE "URL" LIKE '%google%' AND "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; SELECT "SearchPhrase", MIN("URL"), MIN("Title"), COUNT(*) AS c, COUNT(DISTINCT "UserID") FROM hits WHERE "Title" LIKE '%Google%' AND "URL" NOT LIKE '%.google.%' AND "SearchPhrase" <> '' GROUP BY "SearchPhrase" ORDER BY c DESC LIMIT 10; -SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY "EventTime" LIMIT 10; -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime" LIMIT 10; +SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhrase" LIMIT 10; -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime", "SearchPhrase" LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime"), "SearchPhrase" LIMIT 10; SELECT "CounterID", AVG(length("URL")) AS l, COUNT(*) AS c FROM hits WHERE "URL" <> '' GROUP BY "CounterID" HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; SELECT REGEXP_REPLACE("Referer", '^https?://(?:www\.)?([^/]+)/.*$', '\1') AS k, AVG(length("Referer")) AS l, COUNT(*) AS c, MIN("Referer") FROM hits WHERE "Referer" <> '' GROUP BY k HAVING COUNT(*) > 100000 ORDER BY l DESC LIMIT 25; SELECT SUM("ResolutionWidth"), SUM("ResolutionWidth" + 1), SUM("ResolutionWidth" + 2), SUM("ResolutionWidth" + 3), SUM("ResolutionWidth" + 4), SUM("ResolutionWidth" + 5), SUM("ResolutionWidth" + 6), SUM("ResolutionWidth" + 7), SUM("ResolutionWidth" + 8), SUM("ResolutionWidth" + 9), SUM("ResolutionWidth" + 10), SUM("ResolutionWidth" + 11), SUM("ResolutionWidth" + 12), SUM("ResolutionWidth" + 13), SUM("ResolutionWidth" + 14), SUM("ResolutionWidth" + 15), SUM("ResolutionWidth" + 16), SUM("ResolutionWidth" + 17), SUM("ResolutionWidth" + 18), SUM("ResolutionWidth" + 19), SUM("ResolutionWidth" + 20), SUM("ResolutionWidth" + 21), SUM("ResolutionWidth" + 22), SUM("ResolutionWidth" + 23), SUM("ResolutionWidth" + 24), SUM("ResolutionWidth" + 25), SUM("ResolutionWidth" + 26), SUM("ResolutionWidth" + 27), SUM("ResolutionWidth" + 28), SUM("ResolutionWidth" + 29), SUM("ResolutionWidth" + 30), SUM("ResolutionWidth" + 31), SUM("ResolutionWidth" + 32), SUM("ResolutionWidth" + 33), SUM("ResolutionWidth" + 34), SUM("ResolutionWidth" + 35), SUM("ResolutionWidth" + 36), SUM("ResolutionWidth" + 37), SUM("ResolutionWidth" + 38), SUM("ResolutionWidth" + 39), SUM("ResolutionWidth" + 40), SUM("ResolutionWidth" + 41), SUM("ResolutionWidth" + 42), SUM("ResolutionWidth" + 43), SUM("ResolutionWidth" + 44), SUM("ResolutionWidth" + 45), SUM("ResolutionWidth" + 46), SUM("ResolutionWidth" + 47), SUM("ResolutionWidth" + 48), SUM("ResolutionWidth" + 49), SUM("ResolutionWidth" + 50), SUM("ResolutionWidth" + 51), SUM("ResolutionWidth" + 52), SUM("ResolutionWidth" + 53), SUM("ResolutionWidth" + 54), SUM("ResolutionWidth" + 55), SUM("ResolutionWidth" + 56), SUM("ResolutionWidth" + 57), SUM("ResolutionWidth" + 58), SUM("ResolutionWidth" + 59), SUM("ResolutionWidth" + 60), SUM("ResolutionWidth" + 61), SUM("ResolutionWidth" + 62), SUM("ResolutionWidth" + 63), SUM("ResolutionWidth" + 64), SUM("ResolutionWidth" + 65), SUM("ResolutionWidth" + 66), SUM("ResolutionWidth" + 67), SUM("ResolutionWidth" + 68), SUM("ResolutionWidth" + 69), SUM("ResolutionWidth" + 70), SUM("ResolutionWidth" + 71), SUM("ResolutionWidth" + 72), SUM("ResolutionWidth" + 73), SUM("ResolutionWidth" + 74), SUM("ResolutionWidth" + 75), SUM("ResolutionWidth" + 76), SUM("ResolutionWidth" + 77), SUM("ResolutionWidth" + 78), SUM("ResolutionWidth" + 79), SUM("ResolutionWidth" + 80), SUM("ResolutionWidth" + 81), SUM("ResolutionWidth" + 82), SUM("ResolutionWidth" + 83), SUM("ResolutionWidth" + 84), SUM("ResolutionWidth" + 85), SUM("ResolutionWidth" + 86), SUM("ResolutionWidth" + 87), SUM("ResolutionWidth" + 88), SUM("ResolutionWidth" + 89) FROM hits; @@ -34,10 +34,10 @@ SELECT "WatchID", "ClientIP", COUNT(*) AS c, SUM("IsRefresh"), AVG("ResolutionWi SELECT "URL", COUNT(*) AS c FROM hits GROUP BY "URL" ORDER BY c DESC LIMIT 10; SELECT 1, "URL", COUNT(*) AS c FROM hits GROUP BY 1, "URL" ORDER BY c DESC LIMIT 10; SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c FROM hits GROUP BY "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3 ORDER BY c DESC LIMIT 10; -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; -SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; -SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; -SELECT "URLHash", "EventDate", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate" ORDER BY PageViews DESC LIMIT 10 OFFSET 100; -SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; -SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-14' AND "EventDate" <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; +SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "URLHash", "EventDate"::INT::DATE, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate"::INT::DATE ORDER BY PageViews DESC LIMIT 10 OFFSET 100; +SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; +SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-14' AND "EventDate"::INT::DATE <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; diff --git a/benchmarks/src/sort_tpch.rs b/benchmarks/src/sort_tpch.rs index 176234eca541c..956bb92b6c78d 100644 --- a/benchmarks/src/sort_tpch.rs +++ b/benchmarks/src/sort_tpch.rs @@ -63,15 +63,6 @@ pub struct RunOpt { /// Load the data into a MemTable before executing the query #[structopt(short = "m", long = "mem-table")] mem_table: bool, - - /// Mark the first column of each table as sorted in ascending order. - /// The tables should have been created with the `--sort` option for this to have any effect. - #[structopt(short = "t", long = "sorted")] - sorted: bool, - - /// Append a `LIMIT n` clause to the query - #[structopt(short = "l", long = "limit")] - limit: Option, } struct QueryResult { @@ -172,7 +163,7 @@ impl RunOpt { r#" SELECT l_shipmode, l_comment, l_partkey FROM lineitem - ORDER BY l_shipmode + ORDER BY l_shipmode; "#, ]; @@ -221,14 +212,9 @@ impl RunOpt { let start = Instant::now(); let query_idx = query_id - 1; // 1-indexed -> 0-indexed - let base_sql = Self::SORT_QUERIES[query_idx].to_string(); - let sql = if let Some(limit) = self.limit { - format!("{base_sql} LIMIT {limit}") - } else { - base_sql - }; + let sql = Self::SORT_QUERIES[query_idx]; - let row_count = self.execute_query(&ctx, sql.as_str()).await?; + let row_count = self.execute_query(&ctx, sql).await?; let elapsed = start.elapsed(); //.as_secs_f64() * 1000.0; let ms = elapsed.as_secs_f64() * 1000.0; @@ -329,18 +315,8 @@ impl RunOpt { .with_collect_stat(state.config().collect_statistics()); let table_path = ListingTableUrl::parse(path)?; - let schema = options.infer_schema(&state, &table_path).await?; - let options = if self.sorted { - let key_column_name = schema.fields()[0].name(); - options - .with_file_sort_order(vec![vec![col(key_column_name).sort(true, false)]]) - } else { - options - }; - - let config = ListingTableConfig::new(table_path) - .with_listing_options(options) - .with_schema(schema); + let config = ListingTableConfig::new(table_path).with_listing_options(options); + let config = config.infer_schema(&state).await?; Ok(Arc::new(ListingTable::try_new(config)?)) } diff --git a/benchmarks/src/tpch/convert.rs b/benchmarks/src/tpch/convert.rs index 5219e09cd3052..7f391d930045a 100644 --- a/benchmarks/src/tpch/convert.rs +++ b/benchmarks/src/tpch/convert.rs @@ -22,14 +22,15 @@ use std::path::{Path, PathBuf}; use datafusion::common::not_impl_err; -use super::get_tbl_tpch_table_schema; -use super::TPCH_TABLES; use datafusion::error::Result; use datafusion::prelude::*; use parquet::basic::Compression; use parquet::file::properties::WriterProperties; use structopt::StructOpt; +use super::get_tbl_tpch_table_schema; +use super::TPCH_TABLES; + /// Convert tpch .slt files to .parquet or .csv files #[derive(Debug, StructOpt)] pub struct ConvertOpt { @@ -56,10 +57,6 @@ pub struct ConvertOpt { /// Batch size when reading CSV or Parquet files #[structopt(short = "s", long = "batch-size", default_value = "8192")] batch_size: usize, - - /// Sort each table by its first column in ascending order. - #[structopt(short = "t", long = "sort")] - sort: bool, } impl ConvertOpt { @@ -73,7 +70,6 @@ impl ConvertOpt { for table in TPCH_TABLES { let start = Instant::now(); let schema = get_tbl_tpch_table_schema(table); - let key_column_name = schema.fields()[0].name(); let input_path = format!("{input_path}/{table}.tbl"); let options = CsvReadOptions::new() @@ -81,13 +77,6 @@ impl ConvertOpt { .has_header(false) .delimiter(b'|') .file_extension(".tbl"); - let options = if self.sort { - // indicated that the file is already sorted by its first column to speed up the conversion - options - .file_sort_order(vec![vec![col(key_column_name).sort(true, false)]]) - } else { - options - }; let config = SessionConfig::new().with_batch_size(self.batch_size); let ctx = SessionContext::new_with_config(config); @@ -110,11 +99,6 @@ impl ConvertOpt { if partitions > 1 { csv = csv.repartition(Partitioning::RoundRobinBatch(partitions))? } - let csv = if self.sort { - csv.sort_by(vec![col(key_column_name)])? - } else { - csv - }; // create the physical plan let csv = csv.create_physical_plan().await?; diff --git a/benchmarks/src/tpch/run.rs b/benchmarks/src/tpch/run.rs index 752a5a1a6ba01..eb9db821db02f 100644 --- a/benchmarks/src/tpch/run.rs +++ b/benchmarks/src/tpch/run.rs @@ -90,11 +90,6 @@ pub struct RunOpt { /// True by default. #[structopt(short = "j", long = "prefer_hash_join", default_value = "true")] prefer_hash_join: BoolDefaultTrue, - - /// Mark the first column of each table as sorted in ascending order. - /// The tables should have been created with the `--sort` option for this to have any effect. - #[structopt(short = "t", long = "sorted")] - sorted: bool, } const TPCH_QUERY_START_ID: usize = 1; @@ -280,28 +275,20 @@ impl RunOpt { } }; - let table_path = ListingTableUrl::parse(path)?; let options = ListingOptions::new(format) .with_file_extension(extension) .with_target_partitions(target_partitions) .with_collect_stat(state.config().collect_statistics()); - let schema = match table_format { - "parquet" => options.infer_schema(&state, &table_path).await?, - "tbl" => Arc::new(get_tbl_tpch_table_schema(table)), - "csv" => Arc::new(get_tpch_table_schema(table)), + + let table_path = ListingTableUrl::parse(path)?; + let config = ListingTableConfig::new(table_path).with_listing_options(options); + + let config = match table_format { + "parquet" => config.infer_schema(&state).await?, + "tbl" => config.with_schema(Arc::new(get_tbl_tpch_table_schema(table))), + "csv" => config.with_schema(Arc::new(get_tpch_table_schema(table))), _ => unreachable!(), }; - let options = if self.sorted { - let key_column_name = schema.fields()[0].name(); - options - .with_file_sort_order(vec![vec![col(key_column_name).sort(true, false)]]) - } else { - options - }; - - let config = ListingTableConfig::new(table_path) - .with_listing_options(options) - .with_schema(schema); Ok(Arc::new(ListingTable::try_new(config)?)) } @@ -370,7 +357,6 @@ mod tests { output_path: None, disable_statistics: false, prefer_hash_join: true, - sorted: false, }; opt.register_tables(&ctx).await?; let queries = get_query_sql(query)?; @@ -407,7 +393,6 @@ mod tests { output_path: None, disable_statistics: false, prefer_hash_join: true, - sorted: false, }; opt.register_tables(&ctx).await?; let queries = get_query_sql(query)?; diff --git a/datafusion-cli/Cargo.toml b/datafusion-cli/Cargo.toml index e21c005cee5bf..c70e3fc1caec5 100644 --- a/datafusion-cli/Cargo.toml +++ b/datafusion-cli/Cargo.toml @@ -39,7 +39,7 @@ arrow = { workspace = true } async-trait = { workspace = true } aws-config = "1.6.1" aws-credential-types = "1.2.0" -clap = { version = "4.5.36", features = ["derive", "cargo"] } +clap = { version = "4.5.34", features = ["derive", "cargo"] } datafusion = { workspace = true, features = [ "avro", "crypto_expressions", diff --git a/datafusion-cli/src/main.rs b/datafusion-cli/src/main.rs index dad2d15f01a11..e21006312d85a 100644 --- a/datafusion-cli/src/main.rs +++ b/datafusion-cli/src/main.rs @@ -25,7 +25,6 @@ use datafusion::error::{DataFusionError, Result}; use datafusion::execution::context::SessionConfig; use datafusion::execution::memory_pool::{FairSpillPool, GreedyMemoryPool, MemoryPool}; use datafusion::execution::runtime_env::RuntimeEnvBuilder; -use datafusion::execution::DiskManager; use datafusion::prelude::SessionContext; use datafusion_cli::catalog::DynamicObjectStoreCatalog; use datafusion_cli::functions::ParquetMetadataFunc; @@ -38,9 +37,6 @@ use datafusion_cli::{ }; use clap::Parser; -use datafusion::common::config_err; -use datafusion::config::ConfigOptions; -use datafusion::execution::disk_manager::DiskManagerConfig; use mimalloc::MiMalloc; #[global_allocator] @@ -127,14 +123,6 @@ struct Args { #[clap(long, help = "Enables console syntax highlighting")] color: bool, - - #[clap( - short = 'd', - long, - help = "Available disk space for spilling queries (e.g. '10g'), default to None (uses DataFusion's default value of '100g')", - value_parser(extract_disk_limit) - )] - disk_limit: Option, } #[tokio::main] @@ -162,7 +150,11 @@ async fn main_inner() -> Result<()> { env::set_current_dir(p).unwrap(); }; - let session_config = get_session_config(&args)?; + let mut session_config = SessionConfig::from_env()?.with_information_schema(true); + + if let Some(batch_size) = args.batch_size { + session_config = session_config.with_batch_size(batch_size); + }; let mut rt_builder = RuntimeEnvBuilder::new(); // set memory pool size @@ -175,18 +167,6 @@ async fn main_inner() -> Result<()> { rt_builder = rt_builder.with_memory_pool(pool) } - // set disk limit - if let Some(disk_limit) = args.disk_limit { - let disk_manager = DiskManager::try_new(DiskManagerConfig::NewOs)?; - - let disk_manager = Arc::try_unwrap(disk_manager) - .expect("DiskManager should be a single instance") - .with_max_temp_directory_size(disk_limit.try_into().unwrap())?; - - let disk_config = DiskManagerConfig::new_existing(Arc::new(disk_manager)); - rt_builder = rt_builder.with_disk_manager(disk_config); - } - let runtime_env = rt_builder.build_arc()?; // enable dynamic file query @@ -246,30 +226,6 @@ async fn main_inner() -> Result<()> { Ok(()) } -/// Get the session configuration based on the provided arguments -/// and environment settings. -fn get_session_config(args: &Args) -> Result { - // Read options from environment variables and merge with command line options - let mut config_options = ConfigOptions::from_env()?; - - if let Some(batch_size) = args.batch_size { - if batch_size == 0 { - return config_err!("batch_size must be greater than 0"); - } - config_options.execution.batch_size = batch_size; - }; - - // use easier to understand "tree" mode by default - // if the user hasn't specified an explain format in the environment - if env::var_os("DATAFUSION_EXPLAIN_FORMAT").is_none() { - config_options.explain.format = String::from("tree"); - } - - let session_config = - SessionConfig::from(config_options).with_information_schema(true); - Ok(session_config) -} - fn parse_valid_file(dir: &str) -> Result { if Path::new(dir).is_file() { Ok(dir.to_string()) @@ -322,7 +278,7 @@ impl ByteUnit { } } -fn parse_size_string(size: &str, label: &str) -> Result { +fn extract_memory_pool_size(size: &str) -> Result { static BYTE_SUFFIXES: LazyLock> = LazyLock::new(|| { let mut m = HashMap::new(); @@ -344,33 +300,25 @@ fn parse_size_string(size: &str, label: &str) -> Result { let lower = size.to_lowercase(); if let Some(caps) = SUFFIX_REGEX.captures(&lower) { let num_str = caps.get(1).unwrap().as_str(); - let num = num_str - .parse::() - .map_err(|_| format!("Invalid numeric value in {} '{}'", label, size))?; + let num = num_str.parse::().map_err(|_| { + format!("Invalid numeric value in memory pool size '{}'", size) + })?; let suffix = caps.get(2).map(|m| m.as_str()).unwrap_or("b"); - let unit = BYTE_SUFFIXES + let unit = &BYTE_SUFFIXES .get(suffix) - .ok_or_else(|| format!("Invalid {} '{}'", label, size))?; - let total_bytes = usize::try_from(unit.multiplier()) + .ok_or_else(|| format!("Invalid memory pool size '{}'", size))?; + let memory_pool_size = usize::try_from(unit.multiplier()) .ok() .and_then(|multiplier| num.checked_mul(multiplier)) - .ok_or_else(|| format!("{} '{}' is too large", label, size))?; + .ok_or_else(|| format!("Memory pool size '{}' is too large", size))?; - Ok(total_bytes) + Ok(memory_pool_size) } else { - Err(format!("Invalid {} '{}'", label, size)) + Err(format!("Invalid memory pool size '{}'", size)) } } -pub fn extract_memory_pool_size(size: &str) -> Result { - parse_size_string(size, "memory pool size") -} - -pub fn extract_disk_limit(size: &str) -> Result { - parse_size_string(size, "disk limit") -} - #[cfg(test)] mod tests { use super::*; diff --git a/datafusion-cli/tests/cli_integration.rs b/datafusion-cli/tests/cli_integration.rs index 9ac09955512b8..a54a920e97bbf 100644 --- a/datafusion-cli/tests/cli_integration.rs +++ b/datafusion-cli/tests/cli_integration.rs @@ -59,16 +59,6 @@ fn init() { "batch_size", ["--command", "show datafusion.execution.batch_size", "-q", "-b", "1"], )] -#[case::default_explain_plan( - "default_explain_plan", - // default explain format should be tree - ["--command", "EXPLAIN SELECT 123"], -)] -#[case::can_see_indent_format( - "can_see_indent_format", - // can choose the old explain format too - ["--command", "EXPLAIN FORMAT indent SELECT 123"], -)] #[test] fn cli_quick_test<'a>( #[case] snapshot_name: &'a str, @@ -84,21 +74,6 @@ fn cli_quick_test<'a>( assert_cmd_snapshot!(cmd); } -#[test] -fn cli_explain_environment_overrides() { - let mut settings = make_settings(); - settings.set_snapshot_suffix("explain_plan_environment_overrides"); - let _bound = settings.bind_to_scope(); - - let mut cmd = cli(); - - // should use the environment variable to override the default explain plan - cmd.env("DATAFUSION_EXPLAIN_FORMAT", "pgjson") - .args(["--command", "EXPLAIN SELECT 123"]); - - assert_cmd_snapshot!(cmd); -} - #[rstest] #[case("csv")] #[case("tsv")] diff --git a/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap b/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap deleted file mode 100644 index 6b3a247dd7b82..0000000000000 --- a/datafusion-cli/tests/snapshots/cli_explain_environment_overrides@explain_plan_environment_overrides.snap +++ /dev/null @@ -1,44 +0,0 @@ ---- -source: datafusion-cli/tests/cli_integration.rs -info: - program: datafusion-cli - args: - - "--command" - - EXPLAIN SELECT 123 - env: - DATAFUSION_EXPLAIN_FORMAT: pgjson -snapshot_kind: text ---- -success: true -exit_code: 0 ------ stdout ----- -[CLI_VERSION] -+--------------+-----------------------------------------+ -| plan_type | plan | -+--------------+-----------------------------------------+ -| logical_plan | [ | -| | { | -| | "Plan": { | -| | "Expressions": [ | -| | "Int64(123)" | -| | ], | -| | "Node Type": "Projection", | -| | "Output": [ | -| | "Int64(123)" | -| | ], | -| | "Plans": [ | -| | { | -| | "Node Type": "EmptyRelation", | -| | "Output": [], | -| | "Plans": [] | -| | } | -| | ] | -| | } | -| | } | -| | ] | -+--------------+-----------------------------------------+ -1 row(s) fetched. -[ELAPSED] - - ------ stderr ----- diff --git a/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap b/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap deleted file mode 100644 index b2fb64709974e..0000000000000 --- a/datafusion-cli/tests/snapshots/cli_quick_test@can_see_indent_format.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: datafusion-cli/tests/cli_integration.rs -info: - program: datafusion-cli - args: - - "--command" - - EXPLAIN FORMAT indent SELECT 123 -snapshot_kind: text ---- -success: true -exit_code: 0 ------ stdout ----- -[CLI_VERSION] -+---------------+------------------------------------------+ -| plan_type | plan | -+---------------+------------------------------------------+ -| logical_plan | Projection: Int64(123) | -| | EmptyRelation | -| physical_plan | ProjectionExec: expr=[123 as Int64(123)] | -| | PlaceholderRowExec | -| | | -+---------------+------------------------------------------+ -2 row(s) fetched. -[ELAPSED] - - ------ stderr ----- diff --git a/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap b/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap deleted file mode 100644 index 46ee6be64f624..0000000000000 --- a/datafusion-cli/tests/snapshots/cli_quick_test@default_explain_plan.snap +++ /dev/null @@ -1,31 +0,0 @@ ---- -source: datafusion-cli/tests/cli_integration.rs -info: - program: datafusion-cli - args: - - "--command" - - EXPLAIN SELECT 123 -snapshot_kind: text ---- -success: true -exit_code: 0 ------ stdout ----- -[CLI_VERSION] -+---------------+-------------------------------+ -| plan_type | plan | -+---------------+-------------------------------+ -| physical_plan | ┌───────────────────────────┐ | -| | │ ProjectionExec │ | -| | │ -------------------- │ | -| | │ Int64(123): 123 │ | -| | └─────────────┬─────────────┘ | -| | ┌─────────────┴─────────────┐ | -| | │ PlaceholderRowExec │ | -| | └───────────────────────────┘ | -| | | -+---------------+-------------------------------+ -1 row(s) fetched. -[ELAPSED] - - ------ stderr ----- diff --git a/datafusion-examples/Cargo.toml b/datafusion-examples/Cargo.toml index 2ba1673d97b99..f6b7d641d1264 100644 --- a/datafusion-examples/Cargo.toml +++ b/datafusion-examples/Cargo.toml @@ -62,7 +62,6 @@ bytes = { workspace = true } dashmap = { workspace = true } # note only use main datafusion crate for examples datafusion = { workspace = true, default-features = true } -datafusion-ffi = { workspace = true } datafusion-proto = { workspace = true } env_logger = { workspace = true } futures = { workspace = true } diff --git a/datafusion-examples/examples/advanced_parquet_index.rs b/datafusion-examples/examples/advanced_parquet_index.rs index 03ef3d66f9d71..b8c303e221618 100644 --- a/datafusion-examples/examples/advanced_parquet_index.rs +++ b/datafusion-examples/examples/advanced_parquet_index.rs @@ -571,9 +571,7 @@ impl ParquetFileReaderFactory for CachedParquetFileReaderFactory { .to_string(); let object_store = Arc::clone(&self.object_store); - let mut inner = - ParquetObjectReader::new(object_store, file_meta.object_meta.location) - .with_file_size(file_meta.object_meta.size); + let mut inner = ParquetObjectReader::new(object_store, file_meta.object_meta); if let Some(hint) = metadata_size_hint { inner = inner.with_footer_size_hint(hint) @@ -601,7 +599,7 @@ struct ParquetReaderWithCache { impl AsyncFileReader for ParquetReaderWithCache { fn get_bytes( &mut self, - range: Range, + range: Range, ) -> BoxFuture<'_, datafusion::parquet::errors::Result> { println!("get_bytes: {} Reading range {:?}", self.filename, range); self.inner.get_bytes(range) @@ -609,7 +607,7 @@ impl AsyncFileReader for ParquetReaderWithCache { fn get_byte_ranges( &mut self, - ranges: Vec>, + ranges: Vec>, ) -> BoxFuture<'_, datafusion::parquet::errors::Result>> { println!( "get_byte_ranges: {} Reading ranges {:?}", @@ -620,7 +618,6 @@ impl AsyncFileReader for ParquetReaderWithCache { fn get_metadata( &mut self, - _options: Option<&ArrowReaderOptions>, ) -> BoxFuture<'_, datafusion::parquet::errors::Result>> { println!("get_metadata: {} returning cached metadata", self.filename); diff --git a/datafusion-examples/examples/parquet_index.rs b/datafusion-examples/examples/parquet_index.rs index 7d6ce4d86af1a..0b6bccc27b1d1 100644 --- a/datafusion-examples/examples/parquet_index.rs +++ b/datafusion-examples/examples/parquet_index.rs @@ -685,7 +685,7 @@ fn make_demo_file(path: impl AsRef, value_range: Range) -> Result<()> let num_values = value_range.len(); let file_names = - StringArray::from_iter_values(std::iter::repeat_n(&filename, num_values)); + StringArray::from_iter_values(std::iter::repeat(&filename).take(num_values)); let values = Int32Array::from_iter_values(value_range); let batch = RecordBatch::try_from_iter(vec![ ("file_name", Arc::new(file_names) as ArrayRef), diff --git a/datafusion-examples/examples/sql_dialect.rs b/datafusion-examples/examples/sql_dialect.rs index 840faa63b1a48..12141847ca361 100644 --- a/datafusion-examples/examples/sql_dialect.rs +++ b/datafusion-examples/examples/sql_dialect.rs @@ -17,10 +17,10 @@ use std::fmt::Display; -use datafusion::error::{DataFusionError, Result}; +use datafusion::error::Result; use datafusion::sql::{ parser::{CopyToSource, CopyToStatement, DFParser, DFParserBuilder, Statement}, - sqlparser::{keywords::Keyword, tokenizer::Token}, + sqlparser::{keywords::Keyword, parser::ParserError, tokenizer::Token}, }; /// This example demonstrates how to use the DFParser to parse a statement in a custom way @@ -62,7 +62,7 @@ impl<'a> MyParser<'a> { /// This is the entry point to our parser -- it handles `COPY` statements specially /// but otherwise delegates to the existing DataFusion parser. - pub fn parse_statement(&mut self) -> Result { + pub fn parse_statement(&mut self) -> Result { if self.is_copy() { self.df_parser.parser.next_token(); // COPY let df_statement = self.df_parser.parse_copy()?; diff --git a/datafusion-testing b/datafusion-testing index e9f9e22ccf091..243047b9dd682 160000 --- a/datafusion-testing +++ b/datafusion-testing @@ -1 +1 @@ -Subproject commit e9f9e22ccf09145a7368f80fd6a871f11e2b4481 +Subproject commit 243047b9dd682be688628539c604daaddfe640f9 diff --git a/datafusion/catalog/src/lib.rs b/datafusion/catalog/src/lib.rs index 0394b05277dac..f160bddd2b9c1 100644 --- a/datafusion/catalog/src/lib.rs +++ b/datafusion/catalog/src/lib.rs @@ -50,7 +50,7 @@ pub use catalog::*; pub use datafusion_session::Session; pub use dynamic_file::catalog::*; pub use memory::{ - MemTable, MemoryCatalogProvider, MemoryCatalogProviderList, MemorySchemaProvider, + MemoryCatalogProvider, MemoryCatalogProviderList, MemorySchemaProvider, }; pub use r#async::*; pub use schema::*; diff --git a/datafusion/catalog/src/memory/mod.rs b/datafusion/catalog/src/memory/mod.rs index 541d25b3345b4..4c5cf1a9ae9de 100644 --- a/datafusion/catalog/src/memory/mod.rs +++ b/datafusion/catalog/src/memory/mod.rs @@ -17,12 +17,6 @@ pub(crate) mod catalog; pub(crate) mod schema; -pub(crate) mod table; pub use catalog::*; pub use schema::*; -pub use table::*; - -// backward compatibility -pub use datafusion_datasource::memory::MemorySourceConfig; -pub use datafusion_datasource::source::DataSourceExec; diff --git a/datafusion/catalog/src/memory/table.rs b/datafusion/catalog/src/memory/table.rs deleted file mode 100644 index 81243e2c4889e..0000000000000 --- a/datafusion/catalog/src/memory/table.rs +++ /dev/null @@ -1,296 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//! [`MemTable`] for querying `Vec` by DataFusion. - -use std::any::Any; -use std::collections::HashMap; -use std::fmt::Debug; -use std::sync::Arc; - -use crate::TableProvider; -use datafusion_common::error::Result; -use datafusion_expr::Expr; -use datafusion_expr::TableType; -use datafusion_physical_expr::create_physical_sort_exprs; -use datafusion_physical_plan::repartition::RepartitionExec; -use datafusion_physical_plan::{ - common, ExecutionPlan, ExecutionPlanProperties, Partitioning, -}; - -use arrow::datatypes::SchemaRef; -use arrow::record_batch::RecordBatch; -use datafusion_common::{not_impl_err, plan_err, Constraints, DFSchema, SchemaExt}; -use datafusion_common_runtime::JoinSet; -use datafusion_datasource::memory::MemSink; -use datafusion_datasource::memory::MemorySourceConfig; -use datafusion_datasource::sink::DataSinkExec; -use datafusion_datasource::source::DataSourceExec; -use datafusion_expr::dml::InsertOp; -use datafusion_expr::SortExpr; -use datafusion_session::Session; - -use async_trait::async_trait; -use futures::StreamExt; -use log::debug; -use parking_lot::Mutex; -use tokio::sync::RwLock; - -// backward compatibility -pub use datafusion_datasource::memory::PartitionData; - -/// In-memory data source for presenting a `Vec` as a -/// data source that can be queried by DataFusion. This allows data to -/// be pre-loaded into memory and then repeatedly queried without -/// incurring additional file I/O overhead. -#[derive(Debug)] -pub struct MemTable { - schema: SchemaRef, - // batches used to be pub(crate), but it's needed to be public for the tests - pub batches: Vec, - constraints: Constraints, - column_defaults: HashMap, - /// Optional pre-known sort order(s). Must be `SortExpr`s. - /// inserting data into this table removes the order - pub sort_order: Arc>>>, -} - -impl MemTable { - /// Create a new in-memory table from the provided schema and record batches - pub fn try_new(schema: SchemaRef, partitions: Vec>) -> Result { - for batches in partitions.iter().flatten() { - let batches_schema = batches.schema(); - if !schema.contains(&batches_schema) { - debug!( - "mem table schema does not contain batches schema. \ - Target_schema: {schema:?}. Batches Schema: {batches_schema:?}" - ); - return plan_err!("Mismatch between schema and batches"); - } - } - - Ok(Self { - schema, - batches: partitions - .into_iter() - .map(|e| Arc::new(RwLock::new(e))) - .collect::>(), - constraints: Constraints::empty(), - column_defaults: HashMap::new(), - sort_order: Arc::new(Mutex::new(vec![])), - }) - } - - /// Assign constraints - pub fn with_constraints(mut self, constraints: Constraints) -> Self { - self.constraints = constraints; - self - } - - /// Assign column defaults - pub fn with_column_defaults( - mut self, - column_defaults: HashMap, - ) -> Self { - self.column_defaults = column_defaults; - self - } - - /// Specify an optional pre-known sort order(s). Must be `SortExpr`s. - /// - /// If the data is not sorted by this order, DataFusion may produce - /// incorrect results. - /// - /// DataFusion may take advantage of this ordering to omit sorts - /// or use more efficient algorithms. - /// - /// Note that multiple sort orders are supported, if some are known to be - /// equivalent, - pub fn with_sort_order(self, mut sort_order: Vec>) -> Self { - std::mem::swap(self.sort_order.lock().as_mut(), &mut sort_order); - self - } - - /// Create a mem table by reading from another data source - pub async fn load( - t: Arc, - output_partitions: Option, - state: &dyn Session, - ) -> Result { - let schema = t.schema(); - let constraints = t.constraints(); - let exec = t.scan(state, None, &[], None).await?; - let partition_count = exec.output_partitioning().partition_count(); - - let mut join_set = JoinSet::new(); - - for part_idx in 0..partition_count { - let task = state.task_ctx(); - let exec = Arc::clone(&exec); - join_set.spawn(async move { - let stream = exec.execute(part_idx, task)?; - common::collect(stream).await - }); - } - - let mut data: Vec> = - Vec::with_capacity(exec.output_partitioning().partition_count()); - - while let Some(result) = join_set.join_next().await { - match result { - Ok(res) => data.push(res?), - Err(e) => { - if e.is_panic() { - std::panic::resume_unwind(e.into_panic()); - } else { - unreachable!(); - } - } - } - } - - let mut exec = DataSourceExec::new(Arc::new(MemorySourceConfig::try_new( - &data, - Arc::clone(&schema), - None, - )?)); - if let Some(cons) = constraints { - exec = exec.with_constraints(cons.clone()); - } - - if let Some(num_partitions) = output_partitions { - let exec = RepartitionExec::try_new( - Arc::new(exec), - Partitioning::RoundRobinBatch(num_partitions), - )?; - - // execute and collect results - let mut output_partitions = vec![]; - for i in 0..exec.properties().output_partitioning().partition_count() { - // execute this *output* partition and collect all batches - let task_ctx = state.task_ctx(); - let mut stream = exec.execute(i, task_ctx)?; - let mut batches = vec![]; - while let Some(result) = stream.next().await { - batches.push(result?); - } - output_partitions.push(batches); - } - - return MemTable::try_new(Arc::clone(&schema), output_partitions); - } - MemTable::try_new(Arc::clone(&schema), data) - } -} - -#[async_trait] -impl TableProvider for MemTable { - fn as_any(&self) -> &dyn Any { - self - } - - fn schema(&self) -> SchemaRef { - Arc::clone(&self.schema) - } - - fn constraints(&self) -> Option<&Constraints> { - Some(&self.constraints) - } - - fn table_type(&self) -> TableType { - TableType::Base - } - - async fn scan( - &self, - state: &dyn Session, - projection: Option<&Vec>, - _filters: &[Expr], - _limit: Option, - ) -> Result> { - let mut partitions = vec![]; - for arc_inner_vec in self.batches.iter() { - let inner_vec = arc_inner_vec.read().await; - partitions.push(inner_vec.clone()) - } - - let mut source = - MemorySourceConfig::try_new(&partitions, self.schema(), projection.cloned())?; - - let show_sizes = state.config_options().explain.show_sizes; - source = source.with_show_sizes(show_sizes); - - // add sort information if present - let sort_order = self.sort_order.lock(); - if !sort_order.is_empty() { - let df_schema = DFSchema::try_from(self.schema.as_ref().clone())?; - - let file_sort_order = sort_order - .iter() - .map(|sort_exprs| { - create_physical_sort_exprs( - sort_exprs, - &df_schema, - state.execution_props(), - ) - }) - .collect::>>()?; - source = source.try_with_sort_information(file_sort_order)?; - } - - Ok(DataSourceExec::from_data_source(source)) - } - - /// Returns an ExecutionPlan that inserts the execution results of a given [`ExecutionPlan`] into this [`MemTable`]. - /// - /// The [`ExecutionPlan`] must have the same schema as this [`MemTable`]. - /// - /// # Arguments - /// - /// * `state` - The [`SessionState`] containing the context for executing the plan. - /// * `input` - The [`ExecutionPlan`] to execute and insert. - /// - /// # Returns - /// - /// * A plan that returns the number of rows written. - /// - /// [`SessionState`]: https://docs.rs/datafusion/latest/datafusion/execution/session_state/struct.SessionState.html - async fn insert_into( - &self, - _state: &dyn Session, - input: Arc, - insert_op: InsertOp, - ) -> Result> { - // If we are inserting into the table, any sort order may be messed up so reset it here - *self.sort_order.lock() = vec![]; - - // Create a physical plan from the logical plan. - // Check that the schema of the plan matches the schema of this table. - self.schema() - .logically_equivalent_names_and_types(&input.schema())?; - - if insert_op != InsertOp::Append { - return not_impl_err!("{insert_op} not implemented for MemoryTable yet"); - } - let sink = MemSink::try_new(self.batches.clone(), Arc::clone(&self.schema))?; - Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None))) - } - - fn get_column_default(&self, column: &str) -> Option<&Expr> { - self.column_defaults.get(column) - } -} diff --git a/datafusion/common-runtime/src/common.rs b/datafusion/common-runtime/src/common.rs index e7aba1d455ee6..361f6af95cf13 100644 --- a/datafusion/common-runtime/src/common.rs +++ b/datafusion/common-runtime/src/common.rs @@ -15,25 +15,18 @@ // specific language governing permissions and limitations // under the License. -use std::{ - future::Future, - pin::Pin, - task::{Context, Poll}, -}; +use std::future::Future; -use tokio::task::{JoinError, JoinHandle}; - -use crate::trace_utils::{trace_block, trace_future}; +use crate::JoinSet; +use tokio::task::JoinError; /// Helper that provides a simple API to spawn a single task and join it. /// Provides guarantees of aborting on `Drop` to keep it cancel-safe. -/// Note that if the task was spawned with `spawn_blocking`, it will only be -/// aborted if it hasn't started yet. /// -/// Technically, it's just a wrapper of a `JoinHandle` overriding drop. +/// Technically, it's just a wrapper of `JoinSet` (with size=1). #[derive(Debug)] pub struct SpawnedTask { - inner: JoinHandle, + inner: JoinSet, } impl SpawnedTask { @@ -43,9 +36,8 @@ impl SpawnedTask { T: Send + 'static, R: Send, { - // Ok to use spawn here as SpawnedTask handles aborting/cancelling the task on Drop - #[allow(clippy::disallowed_methods)] - let inner = tokio::task::spawn(trace_future(task)); + let mut inner = JoinSet::new(); + inner.spawn(task); Self { inner } } @@ -55,21 +47,22 @@ impl SpawnedTask { T: Send + 'static, R: Send, { - // Ok to use spawn_blocking here as SpawnedTask handles aborting/cancelling the task on Drop - #[allow(clippy::disallowed_methods)] - let inner = tokio::task::spawn_blocking(trace_block(task)); + let mut inner = JoinSet::new(); + inner.spawn_blocking(task); Self { inner } } /// Joins the task, returning the result of join (`Result`). - /// Same as awaiting the spawned task, but left for backwards compatibility. - pub async fn join(self) -> Result { - self.await + pub async fn join(mut self) -> Result { + self.inner + .join_next() + .await + .expect("`SpawnedTask` instance always contains exactly 1 task") } /// Joins the task and unwinds the panic if it happens. pub async fn join_unwind(self) -> Result { - self.await.map_err(|e| { + self.join().await.map_err(|e| { // `JoinError` can be caused either by panic or cancellation. We have to handle panics: if e.is_panic() { std::panic::resume_unwind(e.into_panic()); @@ -84,32 +77,17 @@ impl SpawnedTask { } } -impl Future for SpawnedTask { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Pin::new(&mut self.inner).poll(cx) - } -} - -impl Drop for SpawnedTask { - fn drop(&mut self) { - self.inner.abort(); - } -} - #[cfg(test)] mod tests { use super::*; use std::future::{pending, Pending}; - use tokio::{runtime::Runtime, sync::oneshot}; + use tokio::runtime::Runtime; #[tokio::test] async fn runtime_shutdown() { let rt = Runtime::new().unwrap(); - #[allow(clippy::async_yields_async)] let task = rt .spawn(async { SpawnedTask::spawn(async { @@ -141,36 +119,4 @@ mod tests { .await .ok(); } - - #[tokio::test] - async fn cancel_not_started_task() { - let (sender, receiver) = oneshot::channel::(); - let task = SpawnedTask::spawn(async { - // Shouldn't be reached. - sender.send(42).unwrap(); - }); - - drop(task); - - // If the task was cancelled, the sender was also dropped, - // and awaiting the receiver should result in an error. - assert!(receiver.await.is_err()); - } - - #[tokio::test] - async fn cancel_ongoing_task() { - let (sender, mut receiver) = tokio::sync::mpsc::channel(1); - let task = SpawnedTask::spawn(async move { - sender.send(1).await.unwrap(); - // This line will never be reached because the channel has a buffer - // of 1. - sender.send(2).await.unwrap(); - }); - // Let the task start. - assert_eq!(receiver.recv().await.unwrap(), 1); - drop(task); - - // The sender was dropped so we receive `None`. - assert!(receiver.recv().await.is_none()); - } } diff --git a/datafusion/common/Cargo.toml b/datafusion/common/Cargo.toml index d471e48be4e75..39b47a96bccf3 100644 --- a/datafusion/common/Cargo.toml +++ b/datafusion/common/Cargo.toml @@ -58,12 +58,12 @@ base64 = "0.22.1" half = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } -libc = "0.2.172" +libc = "0.2.171" log = { workspace = true } object_store = { workspace = true, optional = true } parquet = { workspace = true, optional = true, default-features = true } paste = "1.0.15" -pyo3 = { version = "0.24.2", optional = true } +pyo3 = { version = "0.23.5", optional = true } recursive = { workspace = true, optional = true } sqlparser = { workspace = true } tokio = { workspace = true } diff --git a/datafusion/common/src/config.rs b/datafusion/common/src/config.rs index 1e0f63d6d81ca..b0f17630c910c 100644 --- a/datafusion/common/src/config.rs +++ b/datafusion/common/src/config.rs @@ -149,17 +149,9 @@ macro_rules! config_namespace { // $(#[allow(deprecated)])? { $(let value = $transform(value);)? // Apply transformation if specified + $(log::warn!($warn);)? // Log warning if specified #[allow(deprecated)] - let ret = self.$field_name.set(rem, value.as_ref()); - - $(if !$warn.is_empty() { - let default: $field_type = $default; - #[allow(deprecated)] - if default != self.$field_name { - log::warn!($warn); - } - })? // Log warning if specified, and the value is not the default - ret + self.$field_name.set(rem, value.as_ref()) } }, )* @@ -300,7 +292,7 @@ config_namespace! { /// concurrency. /// /// Defaults to the number of CPU cores on the system - pub target_partitions: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism() + pub target_partitions: usize, default = get_available_parallelism() /// The default time zone /// @@ -316,7 +308,7 @@ config_namespace! { /// This is mostly use to plan `UNION` children in parallel. /// /// Defaults to the number of CPU cores on the system - pub planning_concurrency: usize, transform = ExecutionOptions::normalized_parallelism, default = get_available_parallelism() + pub planning_concurrency: usize, default = get_available_parallelism() /// When set to true, skips verifying that the schema produced by /// planning the input of `LogicalPlan::Aggregate` exactly matches the @@ -459,14 +451,6 @@ config_namespace! { /// BLOB instead. pub binary_as_string: bool, default = false - /// (reading) If true, parquet reader will read columns of - /// physical type int96 as originating from a different resolution - /// than nanosecond. This is useful for reading data from systems like Spark - /// which stores microsecond resolution timestamps in an int96 allowing it - /// to write values with a larger date range than 64-bit timestamps with - /// nanosecond resolution. - pub coerce_int96: Option, transform = str::to_lowercase, default = None - // The following options affect writing to parquet files // and map to parquet::file::properties::WriterProperties @@ -739,19 +723,6 @@ config_namespace! { } } -impl ExecutionOptions { - /// Returns the correct parallelism based on the provided `value`. - /// If `value` is `"0"`, returns the default available parallelism, computed with - /// `get_available_parallelism`. Otherwise, returns `value`. - fn normalized_parallelism(value: &str) -> String { - if value.parse::() == Ok(0) { - get_available_parallelism().to_string() - } else { - value.to_owned() - } - } -} - /// A key value pair, with a corresponding description #[derive(Debug)] pub struct ConfigEntry { @@ -2028,8 +1999,8 @@ mod tests { use std::collections::HashMap; use crate::config::{ - ConfigEntry, ConfigExtension, ConfigField, ConfigFileType, ExtensionOptions, - Extensions, TableOptions, + ConfigEntry, ConfigExtension, ConfigFileType, ExtensionOptions, Extensions, + TableOptions, }; #[derive(Default, Debug, Clone)] @@ -2114,37 +2085,6 @@ mod tests { assert_eq!(table_config.csv.escape.unwrap() as char, '\''); } - #[test] - fn warning_only_not_default() { - use std::sync::atomic::AtomicUsize; - static COUNT: AtomicUsize = AtomicUsize::new(0); - use log::{Level, LevelFilter, Metadata, Record}; - struct SimpleLogger; - impl log::Log for SimpleLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= Level::Info - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - } - } - fn flush(&self) {} - } - log::set_logger(&SimpleLogger).unwrap(); - log::set_max_level(LevelFilter::Info); - let mut sql_parser_options = crate::config::SqlParserOptions::default(); - sql_parser_options - .set("enable_options_value_normalization", "false") - .unwrap(); - assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 0); - sql_parser_options - .set("enable_options_value_normalization", "true") - .unwrap(); - assert_eq!(COUNT.load(std::sync::atomic::Ordering::Relaxed), 1); - } - #[cfg(feature = "parquet")] #[test] fn parquet_table_options() { diff --git a/datafusion/common/src/dfschema.rs b/datafusion/common/src/dfschema.rs index 66a26a18c0dc8..43d082f9dc936 100644 --- a/datafusion/common/src/dfschema.rs +++ b/datafusion/common/src/dfschema.rs @@ -641,7 +641,7 @@ impl DFSchema { || (!DFSchema::datatype_is_semantically_equal( f1.data_type(), f2.data_type(), - )) + ) && !can_cast_types(f2.data_type(), f1.data_type())) { _plan_err!( "Schema mismatch: Expected field '{}' with type {:?}, \ @@ -659,12 +659,9 @@ impl DFSchema { } /// Checks if two [`DataType`]s are logically equal. This is a notably weaker constraint - /// than datatype_is_semantically_equal in that different representations of same data can be - /// logically but not semantically equivalent. Semantically equivalent types are always also - /// logically equivalent. For example: - /// - a Dictionary type is logically equal to a plain V type - /// - a Dictionary is also logically equal to Dictionary - /// - Utf8 and Utf8View are logically equal + /// than datatype_is_semantically_equal in that a Dictionary type is logically + /// equal to a plain V type, but not semantically equal. Dictionary is also + /// logically equal to Dictionary. pub fn datatype_is_logically_equal(dt1: &DataType, dt2: &DataType) -> bool { // check nested fields match (dt1, dt2) { @@ -714,15 +711,12 @@ impl DFSchema { .zip(iter2) .all(|((t1, f1), (t2, f2))| t1 == t2 && Self::field_is_logically_equal(f1, f2)) } - // Utf8 and Utf8View are logically equivalent - (DataType::Utf8, DataType::Utf8View) => true, - (DataType::Utf8View, DataType::Utf8) => true, - _ => Self::datatype_is_semantically_equal(dt1, dt2), + _ => dt1 == dt2, } } /// Returns true of two [`DataType`]s are semantically equal (same - /// name and type), ignoring both metadata and nullability, and decimal precision/scale. + /// name and type), ignoring both metadata and nullability. /// /// request to upstream: pub fn datatype_is_semantically_equal(dt1: &DataType, dt2: &DataType) -> bool { diff --git a/datafusion/common/src/file_options/parquet_writer.rs b/datafusion/common/src/file_options/parquet_writer.rs index 3e33466edf505..939cb5e1a3578 100644 --- a/datafusion/common/src/file_options/parquet_writer.rs +++ b/datafusion/common/src/file_options/parquet_writer.rs @@ -239,7 +239,6 @@ impl ParquetOptions { bloom_filter_on_read: _, // reads not used for writer props schema_force_view_types: _, binary_as_string: _, // not used for writer props - coerce_int96: _, // not used for writer props skip_arrow_metadata: _, } = self; @@ -517,7 +516,6 @@ mod tests { schema_force_view_types: defaults.schema_force_view_types, binary_as_string: defaults.binary_as_string, skip_arrow_metadata: defaults.skip_arrow_metadata, - coerce_int96: None, } } @@ -624,7 +622,6 @@ mod tests { schema_force_view_types: global_options_defaults.schema_force_view_types, binary_as_string: global_options_defaults.binary_as_string, skip_arrow_metadata: global_options_defaults.skip_arrow_metadata, - coerce_int96: None, }, column_specific_options, key_value_metadata, diff --git a/datafusion/common/src/functional_dependencies.rs b/datafusion/common/src/functional_dependencies.rs index c4f2805f82856..5f262d634af37 100644 --- a/datafusion/common/src/functional_dependencies.rs +++ b/datafusion/common/src/functional_dependencies.rs @@ -47,13 +47,11 @@ impl Constraints { Constraints::new_unverified(vec![]) } - /// Create a new [`Constraints`] object from the given `constraints`. - /// Users should use the [`Constraints::empty`] or [`SqlToRel::new_constraint_from_table_constraints`] functions - /// for constructing [`Constraints`]. This constructor is for internal + /// Create a new `Constraints` object from the given `constraints`. + /// Users should use the `empty` or `new_from_table_constraints` functions + /// for constructing `Constraints`. This constructor is for internal /// purposes only and does not check whether the argument is valid. The user - /// is responsible for supplying a valid vector of [`Constraint`] objects. - /// - /// [`SqlToRel::new_constraint_from_table_constraints`]: https://docs.rs/datafusion/latest/datafusion/sql/planner/struct.SqlToRel.html#method.new_constraint_from_table_constraints + /// is responsible for supplying a valid vector of `Constraint` objects. pub fn new_unverified(constraints: Vec) -> Self { Self { inner: constraints } } diff --git a/datafusion/common/src/scalar/mod.rs b/datafusion/common/src/scalar/mod.rs index b8d9aea810f03..2b758f4568760 100644 --- a/datafusion/common/src/scalar/mod.rs +++ b/datafusion/common/src/scalar/mod.rs @@ -27,7 +27,7 @@ use std::convert::Infallible; use std::fmt; use std::hash::Hash; use std::hash::Hasher; -use std::iter::repeat_n; +use std::iter::repeat; use std::mem::{size_of, size_of_val}; use std::str::FromStr; use std::sync::Arc; @@ -802,14 +802,12 @@ fn dict_from_scalar( let values_array = value.to_array_of_size(1)?; // Create a key array with `size` elements, each of 0 - let key_array: PrimitiveArray = repeat_n( - if value.is_null() { - None - } else { - Some(K::default_value()) - }, - size, - ) + let key_array: PrimitiveArray = repeat(if value.is_null() { + None + } else { + Some(K::default_value()) + }) + .take(size) .collect(); // create a new DictionaryArray @@ -2191,7 +2189,8 @@ impl ScalarValue { scale: i8, size: usize, ) -> Result { - Ok(repeat_n(value, size) + Ok(repeat(value) + .take(size) .collect::() .with_precision_and_scale(precision, scale)?) } @@ -2417,47 +2416,53 @@ impl ScalarValue { } ScalarValue::Utf8(e) => match e { Some(value) => { - Arc::new(StringArray::from_iter_values(repeat_n(value, size))) + Arc::new(StringArray::from_iter_values(repeat(value).take(size))) } None => new_null_array(&DataType::Utf8, size), }, ScalarValue::Utf8View(e) => match e { Some(value) => { - Arc::new(StringViewArray::from_iter_values(repeat_n(value, size))) + Arc::new(StringViewArray::from_iter_values(repeat(value).take(size))) } None => new_null_array(&DataType::Utf8View, size), }, ScalarValue::LargeUtf8(e) => match e { Some(value) => { - Arc::new(LargeStringArray::from_iter_values(repeat_n(value, size))) + Arc::new(LargeStringArray::from_iter_values(repeat(value).take(size))) } None => new_null_array(&DataType::LargeUtf8, size), }, ScalarValue::Binary(e) => match e { Some(value) => Arc::new( - repeat_n(Some(value.as_slice()), size).collect::(), + repeat(Some(value.as_slice())) + .take(size) + .collect::(), ), - None => Arc::new(repeat_n(None::<&str>, size).collect::()), + None => { + Arc::new(repeat(None::<&str>).take(size).collect::()) + } }, ScalarValue::BinaryView(e) => match e { Some(value) => Arc::new( - repeat_n(Some(value.as_slice()), size).collect::(), + repeat(Some(value.as_slice())) + .take(size) + .collect::(), ), None => { - Arc::new(repeat_n(None::<&str>, size).collect::()) + Arc::new(repeat(None::<&str>).take(size).collect::()) } }, ScalarValue::FixedSizeBinary(s, e) => match e { Some(value) => Arc::new( FixedSizeBinaryArray::try_from_sparse_iter_with_size( - repeat_n(Some(value.as_slice()), size), + repeat(Some(value.as_slice())).take(size), *s, ) .unwrap(), ), None => Arc::new( FixedSizeBinaryArray::try_from_sparse_iter_with_size( - repeat_n(None::<&[u8]>, size), + repeat(None::<&[u8]>).take(size), *s, ) .unwrap(), @@ -2465,11 +2470,15 @@ impl ScalarValue { }, ScalarValue::LargeBinary(e) => match e { Some(value) => Arc::new( - repeat_n(Some(value.as_slice()), size).collect::(), + repeat(Some(value.as_slice())) + .take(size) + .collect::(), + ), + None => Arc::new( + repeat(None::<&str>) + .take(size) + .collect::(), ), - None => { - Arc::new(repeat_n(None::<&str>, size).collect::()) - } }, ScalarValue::List(arr) => { Self::list_to_array_of_size(arr.as_ref() as &dyn Array, size)? @@ -2597,7 +2606,7 @@ impl ScalarValue { child_arrays.push(ar); new_fields.push(field.clone()); } - let type_ids = repeat_n(*v_id, size); + let type_ids = repeat(*v_id).take(size); let type_ids = ScalarBuffer::::from_iter(type_ids); let value_offsets = match mode { UnionMode::Sparse => None, @@ -2665,7 +2674,7 @@ impl ScalarValue { } fn list_to_array_of_size(arr: &dyn Array, size: usize) -> Result { - let arrays = repeat_n(arr, size).collect::>(); + let arrays = repeat(arr).take(size).collect::>(); let ret = match !arrays.is_empty() { true => arrow::compute::concat(arrays.as_slice())?, false => arr.slice(0, 0), @@ -3027,34 +3036,6 @@ impl ScalarValue { DataType::Timestamp(TimeUnit::Nanosecond, None), ) => ScalarValue::Int64(Some((float_ts * 1_000_000_000_f64).trunc() as i64)) .to_array()?, - ( - ScalarValue::Decimal128(Some(decimal_value), _, scale), - DataType::Timestamp(time_unit, None), - ) => { - let scale_factor = 10_i128.pow(*scale as u32); - let seconds = decimal_value / scale_factor; - let fraction = decimal_value % scale_factor; - - let timestamp_value = match time_unit { - TimeUnit::Second => ScalarValue::Int64(Some(seconds as i64)), - TimeUnit::Millisecond => { - let millis = seconds * 1_000 + (fraction * 1_000) / scale_factor; - ScalarValue::Int64(Some(millis as i64)) - } - TimeUnit::Microsecond => { - let micros = - seconds * 1_000_000 + (fraction * 1_000_000) / scale_factor; - ScalarValue::Int64(Some(micros as i64)) - } - TimeUnit::Nanosecond => { - let nanos = seconds * 1_000_000_000 - + (fraction * 1_000_000_000) / scale_factor; - ScalarValue::Int64(Some(nanos as i64)) - } - }; - - timestamp_value.to_array()? - } _ => self.to_array()?, }; diff --git a/datafusion/common/src/stats.rs b/datafusion/common/src/stats.rs index 807d885b3a4de..5b841db53c5ee 100644 --- a/datafusion/common/src/stats.rs +++ b/datafusion/common/src/stats.rs @@ -21,7 +21,6 @@ use std::fmt::{self, Debug, Display}; use crate::{Result, ScalarValue}; -use crate::error::_plan_err; use arrow::datatypes::{DataType, Schema, SchemaRef}; /// Represents a value with a degree of certainty. `Precision` is used to @@ -272,25 +271,11 @@ pub struct Statistics { pub num_rows: Precision, /// Total bytes of the table rows. pub total_byte_size: Precision, - /// Statistics on a column level. - /// - /// It must contains a [`ColumnStatistics`] for each field in the schema of - /// the table to which the [`Statistics`] refer. + /// Statistics on a column level. It contains a [`ColumnStatistics`] for + /// each field in the schema of the table to which the [`Statistics`] refer. pub column_statistics: Vec, } -impl Default for Statistics { - /// Returns a new [`Statistics`] instance with all fields set to unknown - /// and no columns. - fn default() -> Self { - Self { - num_rows: Precision::Absent, - total_byte_size: Precision::Absent, - column_statistics: vec![], - } - } -} - impl Statistics { /// Returns a [`Statistics`] instance for the given schema by assigning /// unknown statistics to each column in the schema. @@ -311,24 +296,6 @@ impl Statistics { .collect() } - /// Set the number of rows - pub fn with_num_rows(mut self, num_rows: Precision) -> Self { - self.num_rows = num_rows; - self - } - - /// Set the total size, in bytes - pub fn with_total_byte_size(mut self, total_byte_size: Precision) -> Self { - self.total_byte_size = total_byte_size; - self - } - - /// Add a column to the column statistics - pub fn add_column_statistics(mut self, column_stats: ColumnStatistics) -> Self { - self.column_statistics.push(column_stats); - self - } - /// If the exactness of a [`Statistics`] instance is lost, this function relaxes /// the exactness of all information by converting them [`Precision::Inexact`]. pub fn to_inexact(mut self) -> Self { @@ -384,8 +351,7 @@ impl Statistics { self } - /// Calculates the statistics after applying `fetch` and `skip` operations. - /// + /// Calculates the statistics after `fetch` and `skip` operations apply. /// Here, `self` denotes per-partition statistics. Use the `n_partitions` /// parameter to compute global statistics in a multi-partition setting. pub fn with_fetch( @@ -448,100 +414,6 @@ impl Statistics { self.total_byte_size = Precision::Absent; Ok(self) } - - /// Summarize zero or more statistics into a single `Statistics` instance. - /// - /// Returns an error if the statistics do not match the specified schemas. - pub fn try_merge_iter<'a, I>(items: I, schema: &Schema) -> Result - where - I: IntoIterator, - { - let mut items = items.into_iter(); - - let Some(init) = items.next() else { - return Ok(Statistics::new_unknown(schema)); - }; - items.try_fold(init.clone(), |acc: Statistics, item_stats: &Statistics| { - acc.try_merge(item_stats) - }) - } - - /// Merge this Statistics value with another Statistics value. - /// - /// Returns an error if the statistics do not match (different schemas). - /// - /// # Example - /// ``` - /// # use datafusion_common::{ColumnStatistics, ScalarValue, Statistics}; - /// # use arrow::datatypes::{Field, Schema, DataType}; - /// # use datafusion_common::stats::Precision; - /// let stats1 = Statistics::default() - /// .with_num_rows(Precision::Exact(1)) - /// .with_total_byte_size(Precision::Exact(2)) - /// .add_column_statistics(ColumnStatistics::new_unknown() - /// .with_null_count(Precision::Exact(3)) - /// .with_min_value(Precision::Exact(ScalarValue::from(4))) - /// .with_max_value(Precision::Exact(ScalarValue::from(5))) - /// ); - /// - /// let stats2 = Statistics::default() - /// .with_num_rows(Precision::Exact(10)) - /// .with_total_byte_size(Precision::Inexact(20)) - /// .add_column_statistics(ColumnStatistics::new_unknown() - /// // absent null count - /// .with_min_value(Precision::Exact(ScalarValue::from(40))) - /// .with_max_value(Precision::Exact(ScalarValue::from(50))) - /// ); - /// - /// let merged_stats = stats1.try_merge(&stats2).unwrap(); - /// let expected_stats = Statistics::default() - /// .with_num_rows(Precision::Exact(11)) - /// .with_total_byte_size(Precision::Inexact(22)) // inexact in stats2 --> inexact - /// .add_column_statistics( - /// ColumnStatistics::new_unknown() - /// .with_null_count(Precision::Absent) // missing from stats2 --> absent - /// .with_min_value(Precision::Exact(ScalarValue::from(4))) - /// .with_max_value(Precision::Exact(ScalarValue::from(50))) - /// ); - /// - /// assert_eq!(merged_stats, expected_stats) - /// ``` - pub fn try_merge(self, other: &Statistics) -> Result { - let Self { - mut num_rows, - mut total_byte_size, - mut column_statistics, - } = self; - - // Accumulate statistics for subsequent items - num_rows = num_rows.add(&other.num_rows); - total_byte_size = total_byte_size.add(&other.total_byte_size); - - if column_statistics.len() != other.column_statistics.len() { - return _plan_err!( - "Cannot merge statistics with different number of columns: {} vs {}", - column_statistics.len(), - other.column_statistics.len() - ); - } - - for (item_col_stats, col_stats) in other - .column_statistics - .iter() - .zip(column_statistics.iter_mut()) - { - col_stats.null_count = col_stats.null_count.add(&item_col_stats.null_count); - col_stats.max_value = col_stats.max_value.max(&item_col_stats.max_value); - col_stats.min_value = col_stats.min_value.min(&item_col_stats.min_value); - col_stats.sum_value = col_stats.sum_value.add(&item_col_stats.sum_value); - } - - Ok(Statistics { - num_rows, - total_byte_size, - column_statistics, - }) - } } /// Creates an estimate of the number of rows in the output using the given @@ -649,36 +521,6 @@ impl ColumnStatistics { } } - /// Set the null count - pub fn with_null_count(mut self, null_count: Precision) -> Self { - self.null_count = null_count; - self - } - - /// Set the max value - pub fn with_max_value(mut self, max_value: Precision) -> Self { - self.max_value = max_value; - self - } - - /// Set the min value - pub fn with_min_value(mut self, min_value: Precision) -> Self { - self.min_value = min_value; - self - } - - /// Set the sum value - pub fn with_sum_value(mut self, sum_value: Precision) -> Self { - self.sum_value = sum_value; - self - } - - /// Set the distinct count - pub fn with_distinct_count(mut self, distinct_count: Precision) -> Self { - self.distinct_count = distinct_count; - self - } - /// If the exactness of a [`ColumnStatistics`] instance is lost, this /// function relaxes the exactness of all information by converting them /// [`Precision::Inexact`]. @@ -695,9 +537,6 @@ impl ColumnStatistics { #[cfg(test)] mod tests { use super::*; - use crate::assert_contains; - use arrow::datatypes::Field; - use std::sync::Arc; #[test] fn test_get_value() { @@ -959,193 +798,4 @@ mod tests { distinct_count: Precision::Exact(100), } } - - #[test] - fn test_try_merge_basic() { - // Create a schema with two columns - let schema = Arc::new(Schema::new(vec![ - Field::new("col1", DataType::Int32, false), - Field::new("col2", DataType::Int32, false), - ])); - - // Create items with statistics - let stats1 = Statistics { - num_rows: Precision::Exact(10), - total_byte_size: Precision::Exact(100), - column_statistics: vec![ - ColumnStatistics { - null_count: Precision::Exact(1), - max_value: Precision::Exact(ScalarValue::Int32(Some(100))), - min_value: Precision::Exact(ScalarValue::Int32(Some(1))), - sum_value: Precision::Exact(ScalarValue::Int32(Some(500))), - distinct_count: Precision::Absent, - }, - ColumnStatistics { - null_count: Precision::Exact(2), - max_value: Precision::Exact(ScalarValue::Int32(Some(200))), - min_value: Precision::Exact(ScalarValue::Int32(Some(10))), - sum_value: Precision::Exact(ScalarValue::Int32(Some(1000))), - distinct_count: Precision::Absent, - }, - ], - }; - - let stats2 = Statistics { - num_rows: Precision::Exact(15), - total_byte_size: Precision::Exact(150), - column_statistics: vec![ - ColumnStatistics { - null_count: Precision::Exact(2), - max_value: Precision::Exact(ScalarValue::Int32(Some(120))), - min_value: Precision::Exact(ScalarValue::Int32(Some(-10))), - sum_value: Precision::Exact(ScalarValue::Int32(Some(600))), - distinct_count: Precision::Absent, - }, - ColumnStatistics { - null_count: Precision::Exact(3), - max_value: Precision::Exact(ScalarValue::Int32(Some(180))), - min_value: Precision::Exact(ScalarValue::Int32(Some(5))), - sum_value: Precision::Exact(ScalarValue::Int32(Some(1200))), - distinct_count: Precision::Absent, - }, - ], - }; - - let items = vec![stats1, stats2]; - - let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap(); - - // Verify the results - assert_eq!(summary_stats.num_rows, Precision::Exact(25)); // 10 + 15 - assert_eq!(summary_stats.total_byte_size, Precision::Exact(250)); // 100 + 150 - - // Verify column statistics - let col1_stats = &summary_stats.column_statistics[0]; - assert_eq!(col1_stats.null_count, Precision::Exact(3)); // 1 + 2 - assert_eq!( - col1_stats.max_value, - Precision::Exact(ScalarValue::Int32(Some(120))) - ); - assert_eq!( - col1_stats.min_value, - Precision::Exact(ScalarValue::Int32(Some(-10))) - ); - assert_eq!( - col1_stats.sum_value, - Precision::Exact(ScalarValue::Int32(Some(1100))) - ); // 500 + 600 - - let col2_stats = &summary_stats.column_statistics[1]; - assert_eq!(col2_stats.null_count, Precision::Exact(5)); // 2 + 3 - assert_eq!( - col2_stats.max_value, - Precision::Exact(ScalarValue::Int32(Some(200))) - ); - assert_eq!( - col2_stats.min_value, - Precision::Exact(ScalarValue::Int32(Some(5))) - ); - assert_eq!( - col2_stats.sum_value, - Precision::Exact(ScalarValue::Int32(Some(2200))) - ); // 1000 + 1200 - } - - #[test] - fn test_try_merge_mixed_precision() { - // Create a schema with one column - let schema = Arc::new(Schema::new(vec![Field::new( - "col1", - DataType::Int32, - false, - )])); - - // Create items with different precision levels - let stats1 = Statistics { - num_rows: Precision::Exact(10), - total_byte_size: Precision::Inexact(100), - column_statistics: vec![ColumnStatistics { - null_count: Precision::Exact(1), - max_value: Precision::Exact(ScalarValue::Int32(Some(100))), - min_value: Precision::Inexact(ScalarValue::Int32(Some(1))), - sum_value: Precision::Exact(ScalarValue::Int32(Some(500))), - distinct_count: Precision::Absent, - }], - }; - - let stats2 = Statistics { - num_rows: Precision::Inexact(15), - total_byte_size: Precision::Exact(150), - column_statistics: vec![ColumnStatistics { - null_count: Precision::Inexact(2), - max_value: Precision::Inexact(ScalarValue::Int32(Some(120))), - min_value: Precision::Exact(ScalarValue::Int32(Some(-10))), - sum_value: Precision::Absent, - distinct_count: Precision::Absent, - }], - }; - - let items = vec![stats1, stats2]; - - let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap(); - - assert_eq!(summary_stats.num_rows, Precision::Inexact(25)); - assert_eq!(summary_stats.total_byte_size, Precision::Inexact(250)); - - let col_stats = &summary_stats.column_statistics[0]; - assert_eq!(col_stats.null_count, Precision::Inexact(3)); - assert_eq!( - col_stats.max_value, - Precision::Inexact(ScalarValue::Int32(Some(120))) - ); - assert_eq!( - col_stats.min_value, - Precision::Inexact(ScalarValue::Int32(Some(-10))) - ); - assert!(matches!(col_stats.sum_value, Precision::Absent)); - } - - #[test] - fn test_try_merge_empty() { - let schema = Arc::new(Schema::new(vec![Field::new( - "col1", - DataType::Int32, - false, - )])); - - // Empty collection - let items: Vec = vec![]; - - let summary_stats = Statistics::try_merge_iter(&items, &schema).unwrap(); - - // Verify default values for empty collection - assert_eq!(summary_stats.num_rows, Precision::Absent); - assert_eq!(summary_stats.total_byte_size, Precision::Absent); - assert_eq!(summary_stats.column_statistics.len(), 1); - assert_eq!( - summary_stats.column_statistics[0].null_count, - Precision::Absent - ); - } - - #[test] - fn test_try_merge_mismatched_size() { - // Create a schema with one column - let schema = Arc::new(Schema::new(vec![Field::new( - "col1", - DataType::Int32, - false, - )])); - - // No column statistics - let stats1 = Statistics::default(); - - let stats2 = - Statistics::default().add_column_statistics(ColumnStatistics::new_unknown()); - - let items = vec![stats1, stats2]; - - let e = Statistics::try_merge_iter(&items, &schema).unwrap_err(); - assert_contains!(e.to_string(), "Error during planning: Cannot merge statistics with different number of columns: 0 vs 1"); - } } diff --git a/datafusion/common/src/utils/memory.rs b/datafusion/common/src/utils/memory.rs index 7ac081e0beb84..ab73996fcd8b7 100644 --- a/datafusion/common/src/utils/memory.rs +++ b/datafusion/common/src/utils/memory.rs @@ -25,7 +25,7 @@ use std::mem::size_of; /// # Parameters /// - `num_elements`: The number of elements expected in the hash table. /// - `fixed_size`: A fixed overhead size associated with the collection -/// (e.g., HashSet or HashTable). +/// (e.g., HashSet or HashTable). /// - `T`: The type of elements stored in the hash table. /// /// # Details diff --git a/datafusion/core/Cargo.toml b/datafusion/core/Cargo.toml index edc0d34b539ac..56698e4d7e255 100644 --- a/datafusion/core/Cargo.toml +++ b/datafusion/core/Cargo.toml @@ -125,7 +125,7 @@ datafusion-physical-optimizer = { workspace = true } datafusion-physical-plan = { workspace = true } datafusion-session = { workspace = true } datafusion-sql = { workspace = true } -flate2 = { version = "1.1.1", optional = true } +flate2 = { version = "1.1.0", optional = true } futures = { workspace = true } itertools = { workspace = true } log = { workspace = true } @@ -160,7 +160,7 @@ rand_distr = "0.4.3" regex = { workspace = true } rstest = { workspace = true } serde_json = { workspace = true } -sysinfo = "0.34.2" +sysinfo = "0.33.1" test-utils = { path = "../../test-utils" } tokio = { workspace = true, features = ["rt-multi-thread", "parking_lot", "fs"] } diff --git a/datafusion/core/benches/aggregate_query_sql.rs b/datafusion/core/benches/aggregate_query_sql.rs index 057a0e1d1b54c..ebe94450c1f8d 100644 --- a/datafusion/core/benches/aggregate_query_sql.rs +++ b/datafusion/core/benches/aggregate_query_sql.rs @@ -29,7 +29,8 @@ use parking_lot::Mutex; use std::sync::Arc; use tokio::runtime::Runtime; -fn query(ctx: Arc>, rt: &Runtime, sql: &str) { +fn query(ctx: Arc>, sql: &str) { + let rt = Runtime::new().unwrap(); let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -50,13 +51,11 @@ fn criterion_benchmark(c: &mut Criterion) { let array_len = 32768 * 2; // 2^16 let batch_size = 2048; // 2^11 let ctx = create_context(partitions_len, array_len, batch_size).unwrap(); - let rt = Runtime::new().unwrap(); c.bench_function("aggregate_query_no_group_by 15 12", |b| { b.iter(|| { query( ctx.clone(), - &rt, "SELECT MIN(f64), AVG(f64), COUNT(f64) \ FROM t", ) @@ -67,7 +66,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT MIN(f64), MAX(f64) \ FROM t", ) @@ -78,7 +76,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT COUNT(DISTINCT u64_wide) \ FROM t", ) @@ -89,7 +86,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT COUNT(DISTINCT u64_narrow) \ FROM t", ) @@ -100,7 +96,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT utf8, MIN(f64), AVG(f64), COUNT(f64) \ FROM t GROUP BY utf8", ) @@ -111,7 +106,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT utf8, MIN(f64), AVG(f64), COUNT(f64) \ FROM t \ WHERE f32 > 10 AND f32 < 20 GROUP BY utf8", @@ -123,7 +117,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT u64_narrow, MIN(f64), AVG(f64), COUNT(f64) \ FROM t GROUP BY u64_narrow", ) @@ -134,7 +127,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT u64_narrow, MIN(f64), AVG(f64), COUNT(f64) \ FROM t \ WHERE f32 > 10 AND f32 < 20 GROUP BY u64_narrow", @@ -146,7 +138,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT u64_wide, utf8, MIN(f64), AVG(f64), COUNT(f64) \ FROM t GROUP BY u64_wide, utf8", ) @@ -157,8 +148,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, - "SELECT utf8, approx_percentile_cont(0.5, 2500) WITHIN GROUP (ORDER BY u64_wide) \ + "SELECT utf8, approx_percentile_cont(u64_wide, 0.5, 2500) \ FROM t GROUP BY utf8", ) }) @@ -168,8 +158,7 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, - "SELECT utf8, approx_percentile_cont(0.5, 2500) WITHIN GROUP (ORDER BY f32) \ + "SELECT utf8, approx_percentile_cont(f32, 0.5, 2500) \ FROM t GROUP BY utf8", ) }) @@ -179,7 +168,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT MEDIAN(DISTINCT u64_wide), MEDIAN(DISTINCT u64_narrow) \ FROM t", ) @@ -190,7 +178,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT first_value(u64_wide order by f64, u64_narrow, utf8),\ last_value(u64_wide order by f64, u64_narrow, utf8) \ FROM t GROUP BY u64_narrow", @@ -202,7 +189,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT first_value(u64_wide ignore nulls order by f64, u64_narrow, utf8), \ last_value(u64_wide ignore nulls order by f64, u64_narrow, utf8) \ FROM t GROUP BY u64_narrow", @@ -214,7 +200,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT first_value(u64_wide order by f64), \ last_value(u64_wide order by f64) \ FROM t GROUP BY u64_narrow", diff --git a/datafusion/core/benches/csv_load.rs b/datafusion/core/benches/csv_load.rs index 3f984757466d5..2d42121ec9b25 100644 --- a/datafusion/core/benches/csv_load.rs +++ b/datafusion/core/benches/csv_load.rs @@ -32,12 +32,8 @@ use std::time::Duration; use test_utils::AccessLogGenerator; use tokio::runtime::Runtime; -fn load_csv( - ctx: Arc>, - rt: &Runtime, - path: &str, - options: CsvReadOptions, -) { +fn load_csv(ctx: Arc>, path: &str, options: CsvReadOptions) { + let rt = Runtime::new().unwrap(); let df = rt.block_on(ctx.lock().read_csv(path, options)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -65,7 +61,6 @@ fn generate_test_file() -> TestCsvFile { fn criterion_benchmark(c: &mut Criterion) { let ctx = create_context().unwrap(); - let rt = Runtime::new().unwrap(); let test_file = generate_test_file(); let mut group = c.benchmark_group("load csv testing"); @@ -75,7 +70,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { load_csv( ctx.clone(), - &rt, test_file.path().to_str().unwrap(), CsvReadOptions::default(), ) @@ -86,7 +80,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { load_csv( ctx.clone(), - &rt, test_file.path().to_str().unwrap(), CsvReadOptions::default().null_regex(Some("^NULL$|^$".to_string())), ) diff --git a/datafusion/core/benches/data_utils/mod.rs b/datafusion/core/benches/data_utils/mod.rs index fc5f8945c4392..38f6a2c76df6d 100644 --- a/datafusion/core/benches/data_utils/mod.rs +++ b/datafusion/core/benches/data_utils/mod.rs @@ -19,8 +19,7 @@ use arrow::array::{ builder::{Int64Builder, StringBuilder}, - ArrayRef, Float32Array, Float64Array, RecordBatch, StringArray, StringViewBuilder, - UInt64Array, + Float32Array, Float64Array, RecordBatch, StringArray, UInt64Array, }; use arrow::datatypes::{DataType, Field, Schema, SchemaRef}; use datafusion::datasource::MemTable; @@ -159,31 +158,6 @@ pub fn create_record_batches( .collect::>() } -/// An enum that wraps either a regular StringBuilder or a GenericByteViewBuilder -/// so that both can be used interchangeably. -enum TraceIdBuilder { - Utf8(StringBuilder), - Utf8View(StringViewBuilder), -} - -impl TraceIdBuilder { - /// Append a value to the builder. - fn append_value(&mut self, value: &str) { - match self { - TraceIdBuilder::Utf8(builder) => builder.append_value(value), - TraceIdBuilder::Utf8View(builder) => builder.append_value(value), - } - } - - /// Finish building and return the ArrayRef. - fn finish(self) -> ArrayRef { - match self { - TraceIdBuilder::Utf8(mut builder) => Arc::new(builder.finish()), - TraceIdBuilder::Utf8View(mut builder) => Arc::new(builder.finish()), - } - } -} - /// Create time series data with `partition_cnt` partitions and `sample_cnt` rows per partition /// in ascending order, if `asc` is true, otherwise randomly sampled using a Pareto distribution #[allow(dead_code)] @@ -191,7 +165,6 @@ pub(crate) fn make_data( partition_cnt: i32, sample_cnt: i32, asc: bool, - use_view: bool, ) -> Result<(Arc, Vec>), DataFusionError> { // constants observed from trace data let simultaneous_group_cnt = 2000; @@ -204,17 +177,11 @@ pub(crate) fn make_data( let mut rng = rand::rngs::SmallRng::from_seed([0; 32]); // populate data - let schema = test_schema(use_view); + let schema = test_schema(); let mut partitions = vec![]; let mut cur_time = 16909000000000i64; for _ in 0..partition_cnt { - // Choose the appropriate builder based on use_view. - let mut id_builder = if use_view { - TraceIdBuilder::Utf8View(StringViewBuilder::new()) - } else { - TraceIdBuilder::Utf8(StringBuilder::new()) - }; - + let mut id_builder = StringBuilder::new(); let mut ts_builder = Int64Builder::new(); let gen_id = |rng: &mut rand::rngs::SmallRng| { rng.gen::<[u8; 16]>() @@ -263,19 +230,10 @@ pub(crate) fn make_data( Ok((schema, partitions)) } -/// Returns a Schema based on the use_view flag -fn test_schema(use_view: bool) -> SchemaRef { - if use_view { - // Return Utf8View schema - Arc::new(Schema::new(vec![ - Field::new("trace_id", DataType::Utf8View, false), - Field::new("timestamp_ms", DataType::Int64, false), - ])) - } else { - // Return regular Utf8 schema - Arc::new(Schema::new(vec![ - Field::new("trace_id", DataType::Utf8, false), - Field::new("timestamp_ms", DataType::Int64, false), - ])) - } +/// The Schema used by make_data +fn test_schema() -> SchemaRef { + Arc::new(Schema::new(vec![ + Field::new("trace_id", DataType::Utf8, false), + Field::new("timestamp_ms", DataType::Int64, false), + ])) } diff --git a/datafusion/core/benches/dataframe.rs b/datafusion/core/benches/dataframe.rs index 832553ebed82a..03078e05e1054 100644 --- a/datafusion/core/benches/dataframe.rs +++ b/datafusion/core/benches/dataframe.rs @@ -44,7 +44,9 @@ fn create_context(field_count: u32) -> datafusion_common::Result, rt: &Runtime) { +fn run(column_count: u32, ctx: Arc) { + let rt = Runtime::new().unwrap(); + criterion::black_box(rt.block_on(async { let mut data_frame = ctx.table("t").await.unwrap(); @@ -65,13 +67,11 @@ fn run(column_count: u32, ctx: Arc, rt: &Runtime) { } fn criterion_benchmark(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); - for column_count in [10, 100, 200, 500] { let ctx = create_context(column_count).unwrap(); c.bench_function(&format!("with_column_{column_count}"), |b| { - b.iter(|| run(column_count, ctx.clone(), &rt)) + b.iter(|| run(column_count, ctx.clone())) }); } } diff --git a/datafusion/core/benches/distinct_query_sql.rs b/datafusion/core/benches/distinct_query_sql.rs index c7056aab86897..c242798a56f00 100644 --- a/datafusion/core/benches/distinct_query_sql.rs +++ b/datafusion/core/benches/distinct_query_sql.rs @@ -33,7 +33,8 @@ use parking_lot::Mutex; use std::{sync::Arc, time::Duration}; use tokio::runtime::Runtime; -fn query(ctx: Arc>, rt: &Runtime, sql: &str) { +fn query(ctx: Arc>, sql: &str) { + let rt = Runtime::new().unwrap(); let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -54,7 +55,6 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { let array_len = 1 << 26; // 64 M let batch_size = 8192; let ctx = create_context(partitions_len, array_len, batch_size).unwrap(); - let rt = Runtime::new().unwrap(); let mut group = c.benchmark_group("custom-measurement-time"); group.measurement_time(Duration::from_secs(40)); @@ -63,7 +63,6 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10", ) }) @@ -73,7 +72,6 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 100", ) }) @@ -83,7 +81,6 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 1000", ) }) @@ -93,7 +90,6 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT DISTINCT u64_narrow FROM t GROUP BY u64_narrow LIMIT 10000", ) }) @@ -103,7 +99,6 @@ fn criterion_benchmark_limited_distinct(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT u64_narrow, u64_wide, utf8, f64 FROM t GROUP BY 1, 2, 3, 4 LIMIT 10", ) }) @@ -123,9 +118,12 @@ async fn distinct_with_limit( Ok(()) } -fn run(rt: &Runtime, plan: Arc, ctx: Arc) { - criterion::black_box(rt.block_on(distinct_with_limit(plan.clone(), ctx.clone()))) - .unwrap(); +fn run(plan: Arc, ctx: Arc) { + let rt = Runtime::new().unwrap(); + criterion::black_box( + rt.block_on(async { distinct_with_limit(plan.clone(), ctx.clone()).await }), + ) + .unwrap(); } pub async fn create_context_sampled_data( @@ -133,8 +131,7 @@ pub async fn create_context_sampled_data( partition_cnt: i32, sample_cnt: i32, ) -> Result<(Arc, Arc)> { - let (schema, parts) = - make_data(partition_cnt, sample_cnt, false /* asc */, false).unwrap(); + let (schema, parts) = make_data(partition_cnt, sample_cnt, false /* asc */).unwrap(); let mem_table = Arc::new(MemTable::try_new(schema, parts).unwrap()); // Create the DataFrame @@ -148,47 +145,58 @@ pub async fn create_context_sampled_data( fn criterion_benchmark_limited_distinct_sampled(c: &mut Criterion) { let rt = Runtime::new().unwrap(); + let limit = 10; let partitions = 100; let samples = 100_000; let sql = format!("select DISTINCT trace_id from traces group by trace_id limit {limit};"); + + let distinct_trace_id_100_partitions_100_000_samples_limit_100 = rt.block_on(async { + create_context_sampled_data(sql.as_str(), partitions, samples) + .await + .unwrap() + }); + c.bench_function( format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(), - |b| b.iter(|| { - let (plan, ctx) = rt.block_on( - create_context_sampled_data(sql.as_str(), partitions, samples) - ).unwrap(); - run(&rt, plan.clone(), ctx.clone()) - }), + |b| b.iter(|| run(distinct_trace_id_100_partitions_100_000_samples_limit_100.0.clone(), + distinct_trace_id_100_partitions_100_000_samples_limit_100.1.clone())), ); let partitions = 10; let samples = 1_000_000; let sql = format!("select DISTINCT trace_id from traces group by trace_id limit {limit};"); + + let distinct_trace_id_10_partitions_1_000_000_samples_limit_10 = rt.block_on(async { + create_context_sampled_data(sql.as_str(), partitions, samples) + .await + .unwrap() + }); + c.bench_function( format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(), - |b| b.iter(|| { - let (plan, ctx) = rt.block_on( - create_context_sampled_data(sql.as_str(), partitions, samples) - ).unwrap(); - run(&rt, plan.clone(), ctx.clone()) - }), + |b| b.iter(|| run(distinct_trace_id_10_partitions_1_000_000_samples_limit_10.0.clone(), + distinct_trace_id_10_partitions_1_000_000_samples_limit_10.1.clone())), ); let partitions = 1; let samples = 10_000_000; let sql = format!("select DISTINCT trace_id from traces group by trace_id limit {limit};"); + + let rt = Runtime::new().unwrap(); + let distinct_trace_id_1_partition_10_000_000_samples_limit_10 = rt.block_on(async { + create_context_sampled_data(sql.as_str(), partitions, samples) + .await + .unwrap() + }); + c.bench_function( format!("distinct query with {} partitions and {} samples per partition with limit {}", partitions, samples, limit).as_str(), - |b| b.iter(|| { - let (plan, ctx) = rt.block_on( - create_context_sampled_data(sql.as_str(), partitions, samples) - ).unwrap(); - run(&rt, plan.clone(), ctx.clone()) - }), + |b| b.iter(|| run(distinct_trace_id_1_partition_10_000_000_samples_limit_10.0.clone(), + distinct_trace_id_1_partition_10_000_000_samples_limit_10.1.clone())), ); } diff --git a/datafusion/core/benches/filter_query_sql.rs b/datafusion/core/benches/filter_query_sql.rs index c82a1607184dc..0e09ae09d7c2e 100644 --- a/datafusion/core/benches/filter_query_sql.rs +++ b/datafusion/core/benches/filter_query_sql.rs @@ -27,7 +27,9 @@ use futures::executor::block_on; use std::sync::Arc; use tokio::runtime::Runtime; -async fn query(ctx: &SessionContext, rt: &Runtime, sql: &str) { +async fn query(ctx: &SessionContext, sql: &str) { + let rt = Runtime::new().unwrap(); + // execute the query let df = rt.block_on(ctx.sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); @@ -66,11 +68,10 @@ fn create_context(array_len: usize, batch_size: usize) -> Result fn criterion_benchmark(c: &mut Criterion) { let array_len = 524_288; // 2^19 let batch_size = 4096; // 2^12 - let rt = Runtime::new().unwrap(); c.bench_function("filter_array", |b| { let ctx = create_context(array_len, batch_size).unwrap(); - b.iter(|| block_on(query(&ctx, &rt, "select f32, f64 from t where f32 >= f64"))) + b.iter(|| block_on(query(&ctx, "select f32, f64 from t where f32 >= f64"))) }); c.bench_function("filter_scalar", |b| { @@ -78,7 +79,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { block_on(query( &ctx, - &rt, "select f32, f64 from t where f32 >= 250 and f64 > 250", )) }) @@ -89,7 +89,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { block_on(query( &ctx, - &rt, "select f32, f64 from t where f32 in (10, 20, 30, 40)", )) }) diff --git a/datafusion/core/benches/math_query_sql.rs b/datafusion/core/benches/math_query_sql.rs index 76824850c114c..92c59d5066401 100644 --- a/datafusion/core/benches/math_query_sql.rs +++ b/datafusion/core/benches/math_query_sql.rs @@ -36,7 +36,9 @@ use datafusion::datasource::MemTable; use datafusion::error::Result; use datafusion::execution::context::SessionContext; -fn query(ctx: Arc>, rt: &Runtime, sql: &str) { +fn query(ctx: Arc>, sql: &str) { + let rt = Runtime::new().unwrap(); + // execute the query let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); rt.block_on(df.collect()).unwrap(); @@ -79,31 +81,29 @@ fn criterion_benchmark(c: &mut Criterion) { let array_len = 1048576; // 2^20 let batch_size = 512; // 2^9 let ctx = create_context(array_len, batch_size).unwrap(); - let rt = Runtime::new().unwrap(); - c.bench_function("sqrt_20_9", |b| { - b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) }); let array_len = 1048576; // 2^20 let batch_size = 4096; // 2^12 let ctx = create_context(array_len, batch_size).unwrap(); c.bench_function("sqrt_20_12", |b| { - b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) }); let array_len = 4194304; // 2^22 let batch_size = 4096; // 2^12 let ctx = create_context(array_len, batch_size).unwrap(); c.bench_function("sqrt_22_12", |b| { - b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) }); let array_len = 4194304; // 2^22 let batch_size = 16384; // 2^14 let ctx = create_context(array_len, batch_size).unwrap(); c.bench_function("sqrt_22_14", |b| { - b.iter(|| query(ctx.clone(), &rt, "SELECT sqrt(f32) FROM t")) + b.iter(|| query(ctx.clone(), "SELECT sqrt(f32) FROM t")) }); } diff --git a/datafusion/core/benches/physical_plan.rs b/datafusion/core/benches/physical_plan.rs index 0a65c52f72def..aae1457ab9e6d 100644 --- a/datafusion/core/benches/physical_plan.rs +++ b/datafusion/core/benches/physical_plan.rs @@ -42,7 +42,6 @@ use datafusion_physical_expr_common::sort_expr::LexOrdering; // as inputs. All record batches must have the same schema. fn sort_preserving_merge_operator( session_ctx: Arc, - rt: &Runtime, batches: Vec, sort: &[&str], ) { @@ -64,6 +63,7 @@ fn sort_preserving_merge_operator( .unwrap(); let merge = Arc::new(SortPreservingMergeExec::new(sort, exec)); let task_ctx = session_ctx.task_ctx(); + let rt = Runtime::new().unwrap(); rt.block_on(collect(merge, task_ctx)).unwrap(); } @@ -166,16 +166,14 @@ fn criterion_benchmark(c: &mut Criterion) { ]; let ctx = Arc::new(SessionContext::new()); - let rt = Runtime::new().unwrap(); - for (name, input) in benches { - c.bench_function(name, |b| { + let ctx_clone = ctx.clone(); + c.bench_function(name, move |b| { b.iter_batched( || input.clone(), |input| { sort_preserving_merge_operator( - ctx.clone(), - &rt, + ctx_clone.clone(), input, &["a", "b", "c", "d"], ); diff --git a/datafusion/core/benches/sort_limit_query_sql.rs b/datafusion/core/benches/sort_limit_query_sql.rs index e535a018161f1..cfd4b8bc4bba8 100644 --- a/datafusion/core/benches/sort_limit_query_sql.rs +++ b/datafusion/core/benches/sort_limit_query_sql.rs @@ -37,7 +37,9 @@ use datafusion::execution::context::SessionContext; use tokio::runtime::Runtime; -fn query(ctx: Arc>, rt: &Runtime, sql: &str) { +fn query(ctx: Arc>, sql: &str) { + let rt = Runtime::new().unwrap(); + // execute the query let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); rt.block_on(df.collect()).unwrap(); @@ -102,14 +104,11 @@ fn create_context() -> Arc> { } fn criterion_benchmark(c: &mut Criterion) { - let ctx = create_context(); - let rt = Runtime::new().unwrap(); - c.bench_function("sort_and_limit_by_int", |b| { + let ctx = create_context(); b.iter(|| { query( ctx.clone(), - &rt, "SELECT c1, c13, c6, c10 \ FROM aggregate_test_100 \ ORDER BY c6 @@ -119,10 +118,10 @@ fn criterion_benchmark(c: &mut Criterion) { }); c.bench_function("sort_and_limit_by_float", |b| { + let ctx = create_context(); b.iter(|| { query( ctx.clone(), - &rt, "SELECT c1, c13, c12 \ FROM aggregate_test_100 \ ORDER BY c13 @@ -132,10 +131,10 @@ fn criterion_benchmark(c: &mut Criterion) { }); c.bench_function("sort_and_limit_lex_by_int", |b| { + let ctx = create_context(); b.iter(|| { query( ctx.clone(), - &rt, "SELECT c1, c13, c6, c10 \ FROM aggregate_test_100 \ ORDER BY c6 DESC, c10 DESC @@ -145,10 +144,10 @@ fn criterion_benchmark(c: &mut Criterion) { }); c.bench_function("sort_and_limit_lex_by_string", |b| { + let ctx = create_context(); b.iter(|| { query( ctx.clone(), - &rt, "SELECT c1, c13, c6, c10 \ FROM aggregate_test_100 \ ORDER BY c1, c13 diff --git a/datafusion/core/benches/sql_planner.rs b/datafusion/core/benches/sql_planner.rs index 49cc830d58bc4..2d79778d4d42f 100644 --- a/datafusion/core/benches/sql_planner.rs +++ b/datafusion/core/benches/sql_planner.rs @@ -45,12 +45,14 @@ const BENCHMARKS_PATH_2: &str = "./benchmarks/"; const CLICKBENCH_DATA_PATH: &str = "data/hits_partitioned/"; /// Create a logical plan from the specified sql -fn logical_plan(ctx: &SessionContext, rt: &Runtime, sql: &str) { +fn logical_plan(ctx: &SessionContext, sql: &str) { + let rt = Runtime::new().unwrap(); criterion::black_box(rt.block_on(ctx.sql(sql)).unwrap()); } /// Create a physical ExecutionPlan (by way of logical plan) -fn physical_plan(ctx: &SessionContext, rt: &Runtime, sql: &str) { +fn physical_plan(ctx: &SessionContext, sql: &str) { + let rt = Runtime::new().unwrap(); criterion::black_box(rt.block_on(async { ctx.sql(sql) .await @@ -102,8 +104,9 @@ fn register_defs(ctx: SessionContext, defs: Vec) -> SessionContext { ctx } -fn register_clickbench_hits_table(rt: &Runtime) -> SessionContext { +fn register_clickbench_hits_table() -> SessionContext { let ctx = SessionContext::new(); + let rt = Runtime::new().unwrap(); // use an external table for clickbench benchmarks let path = @@ -125,11 +128,7 @@ fn register_clickbench_hits_table(rt: &Runtime) -> SessionContext { /// Target of this benchmark: control that placeholders replacing does not get slower, /// if the query does not contain placeholders at all. -fn benchmark_with_param_values_many_columns( - ctx: &SessionContext, - rt: &Runtime, - b: &mut Bencher, -) { +fn benchmark_with_param_values_many_columns(ctx: &SessionContext, b: &mut Bencher) { const COLUMNS_NUM: usize = 200; let mut aggregates = String::new(); for i in 0..COLUMNS_NUM { @@ -141,6 +140,7 @@ fn benchmark_with_param_values_many_columns( // SELECT max(attr0), ..., max(attrN) FROM t1. let query = format!("SELECT {} FROM t1", aggregates); let statement = ctx.state().sql_to_statement(&query, "Generic").unwrap(); + let rt = Runtime::new().unwrap(); let plan = rt.block_on(async { ctx.state().statement_to_plan(statement).await.unwrap() }); b.iter(|| { @@ -230,35 +230,33 @@ fn criterion_benchmark(c: &mut Criterion) { } let ctx = create_context(); - let rt = Runtime::new().unwrap(); // Test simplest // https://github.com/apache/datafusion/issues/5157 c.bench_function("logical_select_one_from_700", |b| { - b.iter(|| logical_plan(&ctx, &rt, "SELECT c1 FROM t700")) + b.iter(|| logical_plan(&ctx, "SELECT c1 FROM t700")) }); // Test simplest // https://github.com/apache/datafusion/issues/5157 c.bench_function("physical_select_one_from_700", |b| { - b.iter(|| physical_plan(&ctx, &rt, "SELECT c1 FROM t700")) + b.iter(|| physical_plan(&ctx, "SELECT c1 FROM t700")) }); // Test simplest c.bench_function("logical_select_all_from_1000", |b| { - b.iter(|| logical_plan(&ctx, &rt, "SELECT * FROM t1000")) + b.iter(|| logical_plan(&ctx, "SELECT * FROM t1000")) }); // Test simplest c.bench_function("physical_select_all_from_1000", |b| { - b.iter(|| physical_plan(&ctx, &rt, "SELECT * FROM t1000")) + b.iter(|| physical_plan(&ctx, "SELECT * FROM t1000")) }); c.bench_function("logical_trivial_join_low_numbered_columns", |b| { b.iter(|| { logical_plan( &ctx, - &rt, "SELECT t1.a2, t2.b2 \ FROM t1, t2 WHERE a1 = b1", ) @@ -269,7 +267,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { logical_plan( &ctx, - &rt, "SELECT t1.a99, t2.b99 \ FROM t1, t2 WHERE a199 = b199", ) @@ -280,7 +277,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { logical_plan( &ctx, - &rt, "SELECT t1.a99, MIN(t2.b1), MAX(t2.b199), AVG(t2.b123), COUNT(t2.b73) \ FROM t1 JOIN t2 ON t1.a199 = t2.b199 GROUP BY t1.a99", ) @@ -297,7 +293,7 @@ fn criterion_benchmark(c: &mut Criterion) { } let query = format!("SELECT {} FROM t1", aggregates); b.iter(|| { - physical_plan(&ctx, &rt, &query); + physical_plan(&ctx, &query); }); }); @@ -306,7 +302,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, - &rt, "SELECT t1.a7, t2.b8 \ FROM t1, t2 WHERE a7 = b7 \ ORDER BY a7", @@ -318,7 +313,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, - &rt, "SELECT t1.a7, t2.b8 \ FROM t1, t2 WHERE a7 < b7 \ ORDER BY a7", @@ -330,7 +324,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, - &rt, "SELECT ta.a9, tb.a10, tc.a11, td.a12, te.a13, tf.a14 \ FROM t1 AS ta, t1 AS tb, t1 AS tc, t1 AS td, t1 AS te, t1 AS tf \ WHERE ta.a9 = tb.a10 AND tb.a10 = tc.a11 AND tc.a11 = td.a12 AND \ @@ -343,7 +336,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, - &rt, "SELECT t1.a7 \ FROM t1 WHERE a7 = (SELECT b8 FROM t2)", ); @@ -354,7 +346,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { physical_plan( &ctx, - &rt, "SELECT t1.a7 FROM t1 \ INTERSECT SELECT t2.b8 FROM t2", ); @@ -365,7 +356,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { logical_plan( &ctx, - &rt, "SELECT DISTINCT t1.a7 \ FROM t1, t2 WHERE t1.a7 = t2.b8", ); @@ -380,7 +370,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_sorted_union_orderby", |b| { // SELECT ... UNION ALL ... let query = union_orderby_query(20); - b.iter(|| physical_plan(&ctx, &rt, &query)) + b.iter(|| physical_plan(&ctx, &query)) }); // --- TPC-H --- @@ -403,7 +393,7 @@ fn criterion_benchmark(c: &mut Criterion) { let sql = std::fs::read_to_string(format!("{benchmarks_path}queries/{q}.sql")).unwrap(); c.bench_function(&format!("physical_plan_tpch_{}", q), |b| { - b.iter(|| physical_plan(&tpch_ctx, &rt, &sql)) + b.iter(|| physical_plan(&tpch_ctx, &sql)) }); } @@ -417,7 +407,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_plan_tpch_all", |b| { b.iter(|| { for sql in &all_tpch_sql_queries { - physical_plan(&tpch_ctx, &rt, sql) + physical_plan(&tpch_ctx, sql) } }) }); @@ -452,7 +442,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_plan_tpcds_all", |b| { b.iter(|| { for sql in &all_tpcds_sql_queries { - physical_plan(&tpcds_ctx, &rt, sql) + physical_plan(&tpcds_ctx, sql) } }) }); @@ -478,7 +468,7 @@ fn criterion_benchmark(c: &mut Criterion) { .map(|l| l.expect("Could not parse line")) .collect_vec(); - let clickbench_ctx = register_clickbench_hits_table(&rt); + let clickbench_ctx = register_clickbench_hits_table(); // for (i, sql) in clickbench_queries.iter().enumerate() { // c.bench_function(&format!("logical_plan_clickbench_q{}", i + 1), |b| { @@ -488,7 +478,7 @@ fn criterion_benchmark(c: &mut Criterion) { for (i, sql) in clickbench_queries.iter().enumerate() { c.bench_function(&format!("physical_plan_clickbench_q{}", i + 1), |b| { - b.iter(|| physical_plan(&clickbench_ctx, &rt, sql)) + b.iter(|| physical_plan(&clickbench_ctx, sql)) }); } @@ -503,13 +493,13 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("physical_plan_clickbench_all", |b| { b.iter(|| { for sql in &clickbench_queries { - physical_plan(&clickbench_ctx, &rt, sql) + physical_plan(&clickbench_ctx, sql) } }) }); c.bench_function("with_param_values_many_columns", |b| { - benchmark_with_param_values_many_columns(&ctx, &rt, b); + benchmark_with_param_values_many_columns(&ctx, b); }); } diff --git a/datafusion/core/benches/struct_query_sql.rs b/datafusion/core/benches/struct_query_sql.rs index f9cc43d1ea2c5..3ef7292c66271 100644 --- a/datafusion/core/benches/struct_query_sql.rs +++ b/datafusion/core/benches/struct_query_sql.rs @@ -27,7 +27,9 @@ use futures::executor::block_on; use std::sync::Arc; use tokio::runtime::Runtime; -async fn query(ctx: &SessionContext, rt: &Runtime, sql: &str) { +async fn query(ctx: &SessionContext, sql: &str) { + let rt = Runtime::new().unwrap(); + // execute the query let df = rt.block_on(ctx.sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); @@ -66,11 +68,10 @@ fn create_context(array_len: usize, batch_size: usize) -> Result fn criterion_benchmark(c: &mut Criterion) { let array_len = 524_288; // 2^19 let batch_size = 4096; // 2^12 - let ctx = create_context(array_len, batch_size).unwrap(); - let rt = Runtime::new().unwrap(); c.bench_function("struct", |b| { - b.iter(|| block_on(query(&ctx, &rt, "select struct(f32, f64) from t"))) + let ctx = create_context(array_len, batch_size).unwrap(); + b.iter(|| block_on(query(&ctx, "select struct(f32, f64) from t"))) }); } diff --git a/datafusion/core/benches/topk_aggregate.rs b/datafusion/core/benches/topk_aggregate.rs index cf3c7fa2e26fe..922cbd2b42292 100644 --- a/datafusion/core/benches/topk_aggregate.rs +++ b/datafusion/core/benches/topk_aggregate.rs @@ -33,9 +33,8 @@ async fn create_context( sample_cnt: i32, asc: bool, use_topk: bool, - use_view: bool, ) -> Result<(Arc, Arc)> { - let (schema, parts) = make_data(partition_cnt, sample_cnt, asc, use_view).unwrap(); + let (schema, parts) = make_data(partition_cnt, sample_cnt, asc).unwrap(); let mem_table = Arc::new(MemTable::try_new(schema, parts).unwrap()); // Create the DataFrame @@ -56,7 +55,8 @@ async fn create_context( Ok((physical_plan, ctx.task_ctx())) } -fn run(rt: &Runtime, plan: Arc, ctx: Arc, asc: bool) { +fn run(plan: Arc, ctx: Arc, asc: bool) { + let rt = Runtime::new().unwrap(); criterion::black_box( rt.block_on(async { aggregate(plan.clone(), ctx.clone(), asc).await }), ) @@ -99,37 +99,40 @@ async fn aggregate( } fn criterion_benchmark(c: &mut Criterion) { - let rt = Runtime::new().unwrap(); let limit = 10; let partitions = 10; let samples = 1_000_000; + let rt = Runtime::new().unwrap(); + let topk_real = rt.block_on(async { + create_context(limit, partitions, samples, false, true) + .await + .unwrap() + }); + let topk_asc = rt.block_on(async { + create_context(limit, partitions, samples, true, true) + .await + .unwrap() + }); + let real = rt.block_on(async { + create_context(limit, partitions, samples, false, false) + .await + .unwrap() + }); + let asc = rt.block_on(async { + create_context(limit, partitions, samples, true, false) + .await + .unwrap() + }); + c.bench_function( format!("aggregate {} time-series rows", partitions * samples).as_str(), - |b| { - b.iter(|| { - let real = rt.block_on(async { - create_context(limit, partitions, samples, false, false, false) - .await - .unwrap() - }); - run(&rt, real.0.clone(), real.1.clone(), false) - }) - }, + |b| b.iter(|| run(real.0.clone(), real.1.clone(), false)), ); c.bench_function( format!("aggregate {} worst-case rows", partitions * samples).as_str(), - |b| { - b.iter(|| { - let asc = rt.block_on(async { - create_context(limit, partitions, samples, true, false, false) - .await - .unwrap() - }); - run(&rt, asc.0.clone(), asc.1.clone(), true) - }) - }, + |b| b.iter(|| run(asc.0.clone(), asc.1.clone(), true)), ); c.bench_function( @@ -138,16 +141,7 @@ fn criterion_benchmark(c: &mut Criterion) { partitions * samples ) .as_str(), - |b| { - b.iter(|| { - let topk_real = rt.block_on(async { - create_context(limit, partitions, samples, false, true, false) - .await - .unwrap() - }); - run(&rt, topk_real.0.clone(), topk_real.1.clone(), false) - }) - }, + |b| b.iter(|| run(topk_real.0.clone(), topk_real.1.clone(), false)), ); c.bench_function( @@ -156,54 +150,7 @@ fn criterion_benchmark(c: &mut Criterion) { partitions * samples ) .as_str(), - |b| { - b.iter(|| { - let topk_asc = rt.block_on(async { - create_context(limit, partitions, samples, true, true, false) - .await - .unwrap() - }); - run(&rt, topk_asc.0.clone(), topk_asc.1.clone(), true) - }) - }, - ); - - // Utf8View schema,time-series rows - c.bench_function( - format!( - "top k={limit} aggregate {} time-series rows [Utf8View]", - partitions * samples - ) - .as_str(), - |b| { - b.iter(|| { - let topk_real = rt.block_on(async { - create_context(limit, partitions, samples, false, true, true) - .await - .unwrap() - }); - run(&rt, topk_real.0.clone(), topk_real.1.clone(), false) - }) - }, - ); - - // Utf8View schema,worst-case rows - c.bench_function( - format!( - "top k={limit} aggregate {} worst-case rows [Utf8View]", - partitions * samples - ) - .as_str(), - |b| { - b.iter(|| { - let topk_asc = rt.block_on(async { - create_context(limit, partitions, samples, true, true, true) - .await - .unwrap() - }); - run(&rt, topk_asc.0.clone(), topk_asc.1.clone(), true) - }) - }, + |b| b.iter(|| run(topk_asc.0.clone(), topk_asc.1.clone(), true)), ); } diff --git a/datafusion/core/benches/window_query_sql.rs b/datafusion/core/benches/window_query_sql.rs index a55d17a7c5dcf..42a1e51be361a 100644 --- a/datafusion/core/benches/window_query_sql.rs +++ b/datafusion/core/benches/window_query_sql.rs @@ -29,7 +29,8 @@ use parking_lot::Mutex; use std::sync::Arc; use tokio::runtime::Runtime; -fn query(ctx: Arc>, rt: &Runtime, sql: &str) { +fn query(ctx: Arc>, sql: &str) { + let rt = Runtime::new().unwrap(); let df = rt.block_on(ctx.lock().sql(sql)).unwrap(); criterion::black_box(rt.block_on(df.collect()).unwrap()); } @@ -50,13 +51,11 @@ fn criterion_benchmark(c: &mut Criterion) { let array_len = 1024 * 1024; let batch_size = 8 * 1024; let ctx = create_context(partitions_len, array_len, batch_size).unwrap(); - let rt = Runtime::new().unwrap(); c.bench_function("window empty over, aggregate functions", |b| { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ MAX(f64) OVER (), \ MIN(f32) OVER (), \ @@ -70,7 +69,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ FIRST_VALUE(f64) OVER (), \ LAST_VALUE(f32) OVER (), \ @@ -84,7 +82,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ MAX(f64) OVER (ORDER BY u64_narrow), \ MIN(f32) OVER (ORDER BY u64_narrow DESC), \ @@ -98,7 +95,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ FIRST_VALUE(f64) OVER (ORDER BY u64_narrow), \ LAST_VALUE(f32) OVER (ORDER BY u64_narrow DESC), \ @@ -112,7 +108,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_wide), \ MIN(f32) OVER (PARTITION BY u64_wide), \ @@ -128,7 +123,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_narrow), \ MIN(f32) OVER (PARTITION BY u64_narrow), \ @@ -143,7 +137,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_wide), \ LAST_VALUE(f32) OVER (PARTITION BY u64_wide), \ @@ -157,7 +150,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_narrow), \ LAST_VALUE(f32) OVER (PARTITION BY u64_narrow), \ @@ -173,7 +165,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_wide ORDER by f64), \ MIN(f32) OVER (PARTITION BY u64_wide ORDER by f64), \ @@ -190,7 +181,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ MAX(f64) OVER (PARTITION BY u64_narrow ORDER by f64), \ MIN(f32) OVER (PARTITION BY u64_narrow ORDER by f64), \ @@ -207,7 +197,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_wide ORDER by f64), \ LAST_VALUE(f32) OVER (PARTITION BY u64_wide ORDER by f64), \ @@ -224,7 +213,6 @@ fn criterion_benchmark(c: &mut Criterion) { b.iter(|| { query( ctx.clone(), - &rt, "SELECT \ FIRST_VALUE(f64) OVER (PARTITION BY u64_narrow ORDER by f64), \ LAST_VALUE(f32) OVER (PARTITION BY u64_narrow ORDER by f64), \ diff --git a/datafusion/core/src/bin/print_runtime_config_docs.rs b/datafusion/core/src/bin/print_runtime_config_docs.rs deleted file mode 100644 index f374a5acb78a0..0000000000000 --- a/datafusion/core/src/bin/print_runtime_config_docs.rs +++ /dev/null @@ -1,23 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use datafusion_execution::runtime_env::RuntimeEnvBuilder; - -fn main() { - let docs = RuntimeEnvBuilder::generate_config_markdown(); - println!("{}", docs); -} diff --git a/datafusion/core/src/dataframe/mod.rs b/datafusion/core/src/dataframe/mod.rs index 9a70f8f43fb61..9c27c7c5d3076 100644 --- a/datafusion/core/src/dataframe/mod.rs +++ b/datafusion/core/src/dataframe/mod.rs @@ -685,46 +685,6 @@ impl DataFrame { }) } - /// Calculate the union of two [`DataFrame`]s using column names, preserving duplicate rows. - /// - /// The two [`DataFrame`]s are combined using column names rather than position, - /// filling missing columns with null. - /// - /// - /// # Example - /// ``` - /// # use datafusion::prelude::*; - /// # use datafusion::error::Result; - /// # use datafusion_common::assert_batches_sorted_eq; - /// # #[tokio::main] - /// # async fn main() -> Result<()> { - /// let ctx = SessionContext::new(); - /// let df = ctx.read_csv("tests/data/example.csv", CsvReadOptions::new()).await?; - /// let d2 = df.clone().select_columns(&["b", "c", "a"])?.with_column("d", lit("77"))?; - /// let df = df.union_by_name(d2)?; - /// let expected = vec![ - /// "+---+---+---+----+", - /// "| a | b | c | d |", - /// "+---+---+---+----+", - /// "| 1 | 2 | 3 | |", - /// "| 1 | 2 | 3 | 77 |", - /// "+---+---+---+----+" - /// ]; - /// # assert_batches_sorted_eq!(expected, &df.collect().await?); - /// # Ok(()) - /// # } - /// ``` - pub fn union_by_name(self, dataframe: DataFrame) -> Result { - let plan = LogicalPlanBuilder::from(self.plan) - .union_by_name(dataframe.plan)? - .build()?; - Ok(DataFrame { - session_state: self.session_state, - plan, - projection_requires_validation: true, - }) - } - /// Calculate the distinct union of two [`DataFrame`]s. /// /// The two [`DataFrame`]s must have exactly the same schema. Any duplicate @@ -764,45 +724,6 @@ impl DataFrame { }) } - /// Calculate the union of two [`DataFrame`]s using column names with all duplicated rows removed. - /// - /// The two [`DataFrame`]s are combined using column names rather than position, - /// filling missing columns with null. - /// - /// - /// # Example - /// ``` - /// # use datafusion::prelude::*; - /// # use datafusion::error::Result; - /// # use datafusion_common::assert_batches_sorted_eq; - /// # #[tokio::main] - /// # async fn main() -> Result<()> { - /// let ctx = SessionContext::new(); - /// let df = ctx.read_csv("tests/data/example.csv", CsvReadOptions::new()).await?; - /// let d2 = df.clone().select_columns(&["b", "c", "a"])?; - /// let df = df.union_by_name_distinct(d2)?; - /// let expected = vec![ - /// "+---+---+---+", - /// "| a | b | c |", - /// "+---+---+---+", - /// "| 1 | 2 | 3 |", - /// "+---+---+---+" - /// ]; - /// # assert_batches_sorted_eq!(expected, &df.collect().await?); - /// # Ok(()) - /// # } - /// ``` - pub fn union_by_name_distinct(self, dataframe: DataFrame) -> Result { - let plan = LogicalPlanBuilder::from(self.plan) - .union_by_name_distinct(dataframe.plan)? - .build()?; - Ok(DataFrame { - session_state: self.session_state, - plan, - projection_requires_validation: true, - }) - } - /// Return a new `DataFrame` with all duplicated rows removed. /// /// # Example diff --git a/datafusion/core/src/datasource/file_format/arrow.rs b/datafusion/core/src/datasource/file_format/arrow.rs index 7fc27453d1ad5..6c7c9463cf3b7 100644 --- a/datafusion/core/src/datasource/file_format/arrow.rs +++ b/datafusion/core/src/datasource/file_format/arrow.rs @@ -144,7 +144,6 @@ impl FileFormat for ArrowFormat { for object in objects { let r = store.as_ref().get(&object.location).await?; let schema = match r.payload { - #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(mut file, _) => { let reader = FileReader::try_new(&mut file, None)?; reader.schema() @@ -443,7 +442,7 @@ mod tests { let object_meta = ObjectMeta { location, last_modified: DateTime::default(), - size: u64::MAX, + size: usize::MAX, e_tag: None, version: None, }; @@ -486,7 +485,7 @@ mod tests { let object_meta = ObjectMeta { location, last_modified: DateTime::default(), - size: u64::MAX, + size: usize::MAX, e_tag: None, version: None, }; diff --git a/datafusion/core/src/datasource/file_format/avro.rs b/datafusion/core/src/datasource/file_format/avro.rs index 3428d08a6ae52..a9516aad9e22d 100644 --- a/datafusion/core/src/datasource/file_format/avro.rs +++ b/datafusion/core/src/datasource/file_format/avro.rs @@ -382,15 +382,6 @@ mod tests { let testdata = test_util::arrow_test_data(); let store_root = format!("{testdata}/avro"); let format = AvroFormat {}; - scan_format( - state, - &format, - None, - &store_root, - file_name, - projection, - limit, - ) - .await + scan_format(state, &format, &store_root, file_name, projection, limit).await } } diff --git a/datafusion/core/src/datasource/file_format/csv.rs b/datafusion/core/src/datasource/file_format/csv.rs index 323bc28057d43..309458975ab6c 100644 --- a/datafusion/core/src/datasource/file_format/csv.rs +++ b/datafusion/core/src/datasource/file_format/csv.rs @@ -72,7 +72,7 @@ mod tests { #[derive(Debug)] struct VariableStream { bytes_to_repeat: Bytes, - max_iterations: u64, + max_iterations: usize, iterations_detected: Arc>, } @@ -103,15 +103,14 @@ mod tests { async fn get(&self, location: &Path) -> object_store::Result { let bytes = self.bytes_to_repeat.clone(); - let len = bytes.len() as u64; - let range = 0..len * self.max_iterations; + let range = 0..bytes.len() * self.max_iterations; let arc = self.iterations_detected.clone(); let stream = futures::stream::repeat_with(move || { let arc_inner = arc.clone(); *arc_inner.lock().unwrap() += 1; Ok(bytes.clone()) }) - .take(self.max_iterations as usize) + .take(self.max_iterations) .boxed(); Ok(GetResult { @@ -139,7 +138,7 @@ mod tests { async fn get_ranges( &self, _location: &Path, - _ranges: &[Range], + _ranges: &[Range], ) -> object_store::Result> { unimplemented!() } @@ -155,7 +154,7 @@ mod tests { fn list( &self, _prefix: Option<&Path>, - ) -> BoxStream<'static, object_store::Result> { + ) -> BoxStream<'_, object_store::Result> { unimplemented!() } @@ -180,7 +179,7 @@ mod tests { } impl VariableStream { - pub fn new(bytes_to_repeat: Bytes, max_iterations: u64) -> Self { + pub fn new(bytes_to_repeat: Bytes, max_iterations: usize) -> Self { Self { bytes_to_repeat, max_iterations, @@ -250,7 +249,6 @@ mod tests { let exec = scan_format( &state, &format, - None, root, "aggregate_test_100_with_nulls.csv", projection, @@ -301,7 +299,6 @@ mod tests { let exec = scan_format( &state, &format, - None, root, "aggregate_test_100_with_nulls.csv", projection, @@ -374,7 +371,7 @@ mod tests { let object_meta = ObjectMeta { location: Path::parse("/")?, last_modified: DateTime::default(), - size: u64::MAX, + size: usize::MAX, e_tag: None, version: None, }; @@ -432,7 +429,7 @@ mod tests { let object_meta = ObjectMeta { location: Path::parse("/")?, last_modified: DateTime::default(), - size: u64::MAX, + size: usize::MAX, e_tag: None, version: None, }; @@ -584,7 +581,7 @@ mod tests { ) -> Result> { let root = format!("{}/csv", arrow_test_data()); let format = CsvFormat::default().with_has_header(has_header); - scan_format(state, &format, None, &root, file_name, projection, limit).await + scan_format(state, &format, &root, file_name, projection, limit).await } #[tokio::test] diff --git a/datafusion/core/src/datasource/file_format/json.rs b/datafusion/core/src/datasource/file_format/json.rs index a70a0f51d3307..d533dcf7646da 100644 --- a/datafusion/core/src/datasource/file_format/json.rs +++ b/datafusion/core/src/datasource/file_format/json.rs @@ -149,7 +149,7 @@ mod tests { ) -> Result> { let filename = "tests/data/2.json"; let format = JsonFormat::default(); - scan_format(state, &format, None, ".", filename, projection, limit).await + scan_format(state, &format, ".", filename, projection, limit).await } #[tokio::test] diff --git a/datafusion/core/src/datasource/file_format/mod.rs b/datafusion/core/src/datasource/file_format/mod.rs index 3a098301f14e3..e921f0158e540 100644 --- a/datafusion/core/src/datasource/file_format/mod.rs +++ b/datafusion/core/src/datasource/file_format/mod.rs @@ -36,20 +36,19 @@ pub use datafusion_datasource::write; #[cfg(test)] pub(crate) mod test_util { - use arrow_schema::SchemaRef; + use std::sync::Arc; + use datafusion_catalog::Session; use datafusion_common::Result; use datafusion_datasource::file_scan_config::FileScanConfigBuilder; use datafusion_datasource::{file_format::FileFormat, PartitionedFile}; use datafusion_execution::object_store::ObjectStoreUrl; - use std::sync::Arc; use crate::test::object_store::local_unpartitioned_file; pub async fn scan_format( state: &dyn Session, format: &dyn FileFormat, - schema: Option, store_root: &str, file_name: &str, projection: Option>, @@ -58,13 +57,9 @@ pub(crate) mod test_util { let store = Arc::new(object_store::local::LocalFileSystem::new()) as _; let meta = local_unpartitioned_file(format!("{store_root}/{file_name}")); - let file_schema = if let Some(file_schema) = schema { - file_schema - } else { - format - .infer_schema(state, &store, std::slice::from_ref(&meta)) - .await? - }; + let file_schema = format + .infer_schema(state, &store, std::slice::from_ref(&meta)) + .await?; let statistics = format .infer_stats(state, &store, file_schema.clone(), &meta) @@ -132,7 +127,7 @@ mod tests { .write_parquet(out_dir_url, DataFrameWriteOptions::new(), None) .await .expect_err("should fail because input file does not match inferred schema"); - assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'"); + assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4"); Ok(()) } } diff --git a/datafusion/core/src/datasource/file_format/parquet.rs b/datafusion/core/src/datasource/file_format/parquet.rs index 7b8b99273f4ea..27a7e7ae3c061 100644 --- a/datafusion/core/src/datasource/file_format/parquet.rs +++ b/datafusion/core/src/datasource/file_format/parquet.rs @@ -67,13 +67,13 @@ pub(crate) mod test_util { .into_iter() .zip(tmp_files.into_iter()) .map(|(batch, mut output)| { - let mut builder = parquet::file::properties::WriterProperties::builder(); - if multi_page { - builder = builder.set_data_page_row_count_limit(ROWS_PER_PAGE) + let builder = parquet::file::properties::WriterProperties::builder(); + let props = if multi_page { + builder.set_data_page_row_count_limit(ROWS_PER_PAGE) + } else { + builder } - builder = builder.set_bloom_filter_enabled(true); - - let props = builder.build(); + .build(); let mut writer = parquet::arrow::ArrowWriter::try_new( &mut output, @@ -331,7 +331,7 @@ mod tests { fn list( &self, _prefix: Option<&Path>, - ) -> BoxStream<'static, object_store::Result> { + ) -> BoxStream<'_, object_store::Result> { Box::pin(futures::stream::once(async { Err(object_store::Error::NotImplemented) })) @@ -408,7 +408,7 @@ mod tests { ))); // Use the file size as the hint so we can get the full metadata from the first fetch - let size_hint = meta[0].size as usize; + let size_hint = meta[0].size; fetch_parquet_metadata(store.upcast().as_ref(), &meta[0], Some(size_hint)) .await @@ -443,7 +443,7 @@ mod tests { ))); // Use the a size hint larger than the file size to make sure we don't panic - let size_hint = (meta[0].size + 100) as usize; + let size_hint = meta[0].size + 100; fetch_parquet_metadata(store.upcast().as_ref(), &meta[0], Some(size_hint)) .await @@ -1075,10 +1075,7 @@ mod tests { .map(|factory| factory.create(state, &Default::default()).unwrap()) .unwrap_or(Arc::new(ParquetFormat::new())); - scan_format( - state, &*format, None, &testdata, file_name, projection, limit, - ) - .await + scan_format(state, &*format, &testdata, file_name, projection, limit).await } /// Test that 0-byte files don't break while reading diff --git a/datafusion/core/src/datasource/listing/table.rs b/datafusion/core/src/datasource/listing/table.rs index a9834da92e5a4..61eeb419a4800 100644 --- a/datafusion/core/src/datasource/listing/table.rs +++ b/datafusion/core/src/datasource/listing/table.rs @@ -17,16 +17,18 @@ //! The table implementation. -use super::helpers::{expr_applicable_for_cols, pruned_partition_list}; -use super::{ListingTableUrl, PartitionedFile}; use std::collections::HashMap; use std::{any::Any, str::FromStr, sync::Arc}; +use super::helpers::{expr_applicable_for_cols, pruned_partition_list}; +use super::{ListingTableUrl, PartitionedFile}; + use crate::datasource::{ create_ordering, file_format::{ file_compression_type::FileCompressionType, FileFormat, FilePushdownSupport, }, + get_statistics_with_limit, physical_plan::FileSinkConfig, }; use crate::execution::context::SessionState; @@ -53,11 +55,9 @@ use datafusion_physical_expr::{ use async_trait::async_trait; use datafusion_catalog::Session; -use datafusion_common::stats::Precision; -use datafusion_datasource::compute_all_files_statistics; use datafusion_datasource::file_groups::FileGroup; use datafusion_physical_expr_common::sort_expr::LexRequirement; -use futures::{future, stream, Stream, StreamExt, TryStreamExt}; +use futures::{future, stream, StreamExt, TryStreamExt}; use itertools::Itertools; use object_store::ObjectStore; @@ -715,13 +715,9 @@ impl ListingOptions { #[derive(Debug)] pub struct ListingTable { table_paths: Vec, - /// `file_schema` contains only the columns physically stored in the data files themselves. - /// - Represents the actual fields found in files like Parquet, CSV, etc. - /// - Used when reading the raw data from files + /// File fields only file_schema: SchemaRef, - /// `table_schema` combines `file_schema` + partition columns - /// - Partition columns are derived from directory paths (not stored in files) - /// - These are columns like "year=2022/month=01" in paths like `/data/year=2022/month=01/file.parquet` + /// File fields + partition columns table_schema: SchemaRef, options: ListingOptions, definition: Option, @@ -799,7 +795,7 @@ impl ListingTable { /// If `None`, creates a new [`DefaultFileStatisticsCache`] scoped to this query. pub fn with_cache(mut self, cache: Option) -> Self { self.collected_statistics = - cache.unwrap_or_else(|| Arc::new(DefaultFileStatisticsCache::default())); + cache.unwrap_or(Arc::new(DefaultFileStatisticsCache::default())); self } @@ -878,13 +874,15 @@ impl TableProvider for ListingTable { filters.iter().cloned().partition(|filter| { can_be_evaluted_for_partition_pruning(&table_partition_col_names, filter) }); + // TODO (https://github.com/apache/datafusion/issues/11600) remove downcast_ref from here? + let session_state = state.as_any().downcast_ref::().unwrap(); // We should not limit the number of partitioned files to scan if there are filters and limit // at the same time. This is because the limit should be applied after the filters are applied. let statistic_file_limit = if filters.is_empty() { limit } else { None }; let (mut partitioned_file_lists, statistics) = self - .list_files_for_scan(state, &partition_filters, statistic_file_limit) + .list_files_for_scan(session_state, &partition_filters, statistic_file_limit) .await?; // if no files need to be read, return an `EmptyExec` @@ -900,11 +898,10 @@ impl TableProvider for ListingTable { .split_file_groups_by_statistics .then(|| { output_ordering.first().map(|output_ordering| { - FileScanConfig::split_groups_by_statistics_with_target_partitions( + FileScanConfig::split_groups_by_statistics( &self.table_schema, &partitioned_file_lists, output_ordering, - self.options.target_partitions, ) }) }) @@ -944,7 +941,7 @@ impl TableProvider for ListingTable { self.options .format .create_physical_plan( - state, + session_state, FileScanConfigBuilder::new( object_store_url, Arc::clone(&self.file_schema), @@ -1024,8 +1021,10 @@ impl TableProvider for ListingTable { // Get the object store for the table path. let store = state.runtime_env().object_store(table_path)?; + // TODO (https://github.com/apache/datafusion/issues/11600) remove downcast_ref from here? + let session_state = state.as_any().downcast_ref::().unwrap(); let file_list_stream = pruned_partition_list( - state, + session_state, store.as_ref(), table_path, &[], @@ -1073,7 +1072,7 @@ impl TableProvider for ListingTable { self.options() .format - .create_writer_physical_plan(input, state, config, order_requirements) + .create_writer_physical_plan(input, session_state, config, order_requirements) .await } @@ -1116,26 +1115,32 @@ impl ListingTable { let files = file_list .map(|part_file| async { let part_file = part_file?; - let statistics = if self.options.collect_stat { - self.do_collect_statistics(ctx, &store, &part_file).await? + if self.options.collect_stat { + let statistics = + self.do_collect_statistics(ctx, &store, &part_file).await?; + Ok((part_file, statistics)) } else { - Arc::new(Statistics::new_unknown(&self.file_schema)) - }; - Ok(part_file.with_statistics(statistics)) + Ok(( + part_file, + Arc::new(Statistics::new_unknown(&self.file_schema)), + )) + } }) .boxed() .buffer_unordered(ctx.config_options().execution.meta_fetch_concurrency); - let (file_group, inexact_stats) = - get_files_with_limit(files, limit, self.options.collect_stat).await?; - - let file_groups = file_group.split_files(self.options.target_partitions); - compute_all_files_statistics( - file_groups, + let (files, statistics) = get_statistics_with_limit( + files, self.schema(), + limit, self.options.collect_stat, - inexact_stats, ) + .await?; + + Ok(( + files.split_files(self.options.target_partitions), + statistics, + )) } /// Collects statistics for a given partitioned file. @@ -1177,82 +1182,6 @@ impl ListingTable { } } -/// Processes a stream of partitioned files and returns a `FileGroup` containing the files. -/// -/// This function collects files from the provided stream until either: -/// 1. The stream is exhausted -/// 2. The accumulated number of rows exceeds the provided `limit` (if specified) -/// -/// # Arguments -/// * `files` - A stream of `Result` items to process -/// * `limit` - An optional row count limit. If provided, the function will stop collecting files -/// once the accumulated number of rows exceeds this limit -/// * `collect_stats` - Whether to collect and accumulate statistics from the files -/// -/// # Returns -/// A `Result` containing a `FileGroup` with the collected files -/// and a boolean indicating whether the statistics are inexact. -/// -/// # Note -/// The function will continue processing files if statistics are not available or if the -/// limit is not provided. If `collect_stats` is false, statistics won't be accumulated -/// but files will still be collected. -async fn get_files_with_limit( - files: impl Stream>, - limit: Option, - collect_stats: bool, -) -> Result<(FileGroup, bool)> { - let mut file_group = FileGroup::default(); - // Fusing the stream allows us to call next safely even once it is finished. - let mut all_files = Box::pin(files.fuse()); - enum ProcessingState { - ReadingFiles, - ReachedLimit, - } - - let mut state = ProcessingState::ReadingFiles; - let mut num_rows = Precision::Absent; - - while let Some(file_result) = all_files.next().await { - // Early exit if we've already reached our limit - if matches!(state, ProcessingState::ReachedLimit) { - break; - } - - let file = file_result?; - - // Update file statistics regardless of state - if collect_stats { - if let Some(file_stats) = &file.statistics { - num_rows = if file_group.is_empty() { - // For the first file, just take its row count - file_stats.num_rows - } else { - // For subsequent files, accumulate the counts - num_rows.add(&file_stats.num_rows) - }; - } - } - - // Always add the file to our group - file_group.push(file); - - // Check if we've hit the limit (if one was specified) - if let Some(limit) = limit { - if let Precision::Exact(row_count) = num_rows { - if row_count > limit { - state = ProcessingState::ReachedLimit; - } - } - } - } - // If we still have files in the stream, it means that the limit kicked - // in, and the statistic could have been different had we processed the - // files in a different order. - let inexact_stats = all_files.next().await.is_some(); - Ok((file_group, inexact_stats)) -} - #[cfg(test)] mod tests { use super::*; diff --git a/datafusion/core/src/datasource/memory_test.rs b/datafusion/core/src/datasource/memory.rs similarity index 58% rename from datafusion/core/src/datasource/memory_test.rs rename to datafusion/core/src/datasource/memory.rs index 381000ab8ee1e..0288cd3e8bc7d 100644 --- a/datafusion/core/src/datasource/memory_test.rs +++ b/datafusion/core/src/datasource/memory.rs @@ -15,25 +15,378 @@ // specific language governing permissions and limitations // under the License. +//! [`MemTable`] for querying `Vec` by DataFusion. + +use std::any::Any; +use std::collections::HashMap; +use std::fmt::{self, Debug}; +use std::sync::Arc; + +use crate::datasource::{TableProvider, TableType}; +use crate::error::Result; +use crate::logical_expr::Expr; +use crate::physical_plan::repartition::RepartitionExec; +use crate::physical_plan::{ + common, DisplayAs, DisplayFormatType, ExecutionPlan, ExecutionPlanProperties, + Partitioning, SendableRecordBatchStream, +}; +use crate::physical_planner::create_physical_sort_exprs; + +use arrow::datatypes::SchemaRef; +use arrow::record_batch::RecordBatch; +use datafusion_catalog::Session; +use datafusion_common::{not_impl_err, plan_err, Constraints, DFSchema, SchemaExt}; +use datafusion_common_runtime::JoinSet; +pub use datafusion_datasource::memory::MemorySourceConfig; +use datafusion_datasource::sink::{DataSink, DataSinkExec}; +pub use datafusion_datasource::source::DataSourceExec; +use datafusion_execution::TaskContext; +use datafusion_expr::dml::InsertOp; +use datafusion_expr::SortExpr; + +use async_trait::async_trait; +use futures::StreamExt; +use log::debug; +use parking_lot::Mutex; +use tokio::sync::RwLock; + +/// Type alias for partition data +pub type PartitionData = Arc>>; + +/// In-memory data source for presenting a `Vec` as a +/// data source that can be queried by DataFusion. This allows data to +/// be pre-loaded into memory and then repeatedly queried without +/// incurring additional file I/O overhead. +#[derive(Debug)] +pub struct MemTable { + schema: SchemaRef, + pub(crate) batches: Vec, + constraints: Constraints, + column_defaults: HashMap, + /// Optional pre-known sort order(s). Must be `SortExpr`s. + /// inserting data into this table removes the order + pub sort_order: Arc>>>, +} + +impl MemTable { + /// Create a new in-memory table from the provided schema and record batches + pub fn try_new(schema: SchemaRef, partitions: Vec>) -> Result { + for batches in partitions.iter().flatten() { + let batches_schema = batches.schema(); + if !schema.contains(&batches_schema) { + debug!( + "mem table schema does not contain batches schema. \ + Target_schema: {schema:?}. Batches Schema: {batches_schema:?}" + ); + return plan_err!("Mismatch between schema and batches"); + } + } + + Ok(Self { + schema, + batches: partitions + .into_iter() + .map(|e| Arc::new(RwLock::new(e))) + .collect::>(), + constraints: Constraints::empty(), + column_defaults: HashMap::new(), + sort_order: Arc::new(Mutex::new(vec![])), + }) + } + + /// Assign constraints + pub fn with_constraints(mut self, constraints: Constraints) -> Self { + self.constraints = constraints; + self + } + + /// Assign column defaults + pub fn with_column_defaults( + mut self, + column_defaults: HashMap, + ) -> Self { + self.column_defaults = column_defaults; + self + } + + /// Specify an optional pre-known sort order(s). Must be `SortExpr`s. + /// + /// If the data is not sorted by this order, DataFusion may produce + /// incorrect results. + /// + /// DataFusion may take advantage of this ordering to omit sorts + /// or use more efficient algorithms. + /// + /// Note that multiple sort orders are supported, if some are known to be + /// equivalent, + pub fn with_sort_order(self, mut sort_order: Vec>) -> Self { + std::mem::swap(self.sort_order.lock().as_mut(), &mut sort_order); + self + } + + /// Create a mem table by reading from another data source + pub async fn load( + t: Arc, + output_partitions: Option, + state: &dyn Session, + ) -> Result { + let schema = t.schema(); + let constraints = t.constraints(); + let exec = t.scan(state, None, &[], None).await?; + let partition_count = exec.output_partitioning().partition_count(); + + let mut join_set = JoinSet::new(); + + for part_idx in 0..partition_count { + let task = state.task_ctx(); + let exec = Arc::clone(&exec); + join_set.spawn(async move { + let stream = exec.execute(part_idx, task)?; + common::collect(stream).await + }); + } + + let mut data: Vec> = + Vec::with_capacity(exec.output_partitioning().partition_count()); + + while let Some(result) = join_set.join_next().await { + match result { + Ok(res) => data.push(res?), + Err(e) => { + if e.is_panic() { + std::panic::resume_unwind(e.into_panic()); + } else { + unreachable!(); + } + } + } + } + + let mut exec = DataSourceExec::new(Arc::new(MemorySourceConfig::try_new( + &data, + Arc::clone(&schema), + None, + )?)); + if let Some(cons) = constraints { + exec = exec.with_constraints(cons.clone()); + } + + if let Some(num_partitions) = output_partitions { + let exec = RepartitionExec::try_new( + Arc::new(exec), + Partitioning::RoundRobinBatch(num_partitions), + )?; + + // execute and collect results + let mut output_partitions = vec![]; + for i in 0..exec.properties().output_partitioning().partition_count() { + // execute this *output* partition and collect all batches + let task_ctx = state.task_ctx(); + let mut stream = exec.execute(i, task_ctx)?; + let mut batches = vec![]; + while let Some(result) = stream.next().await { + batches.push(result?); + } + output_partitions.push(batches); + } + + return MemTable::try_new(Arc::clone(&schema), output_partitions); + } + MemTable::try_new(Arc::clone(&schema), data) + } +} + +#[async_trait] +impl TableProvider for MemTable { + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> SchemaRef { + Arc::clone(&self.schema) + } + + fn constraints(&self) -> Option<&Constraints> { + Some(&self.constraints) + } + + fn table_type(&self) -> TableType { + TableType::Base + } + + async fn scan( + &self, + state: &dyn Session, + projection: Option<&Vec>, + _filters: &[Expr], + _limit: Option, + ) -> Result> { + let mut partitions = vec![]; + for arc_inner_vec in self.batches.iter() { + let inner_vec = arc_inner_vec.read().await; + partitions.push(inner_vec.clone()) + } + + let mut source = + MemorySourceConfig::try_new(&partitions, self.schema(), projection.cloned())?; + + let show_sizes = state.config_options().explain.show_sizes; + source = source.with_show_sizes(show_sizes); + + // add sort information if present + let sort_order = self.sort_order.lock(); + if !sort_order.is_empty() { + let df_schema = DFSchema::try_from(self.schema.as_ref().clone())?; + + let file_sort_order = sort_order + .iter() + .map(|sort_exprs| { + create_physical_sort_exprs( + sort_exprs, + &df_schema, + state.execution_props(), + ) + }) + .collect::>>()?; + source = source.try_with_sort_information(file_sort_order)?; + } + + Ok(DataSourceExec::from_data_source(source)) + } + + /// Returns an ExecutionPlan that inserts the execution results of a given [`ExecutionPlan`] into this [`MemTable`]. + /// + /// The [`ExecutionPlan`] must have the same schema as this [`MemTable`]. + /// + /// # Arguments + /// + /// * `state` - The [`SessionState`] containing the context for executing the plan. + /// * `input` - The [`ExecutionPlan`] to execute and insert. + /// + /// # Returns + /// + /// * A plan that returns the number of rows written. + /// + /// [`SessionState`]: crate::execution::context::SessionState + async fn insert_into( + &self, + _state: &dyn Session, + input: Arc, + insert_op: InsertOp, + ) -> Result> { + // If we are inserting into the table, any sort order may be messed up so reset it here + *self.sort_order.lock() = vec![]; + + // Create a physical plan from the logical plan. + // Check that the schema of the plan matches the schema of this table. + self.schema() + .logically_equivalent_names_and_types(&input.schema())?; + + if insert_op != InsertOp::Append { + return not_impl_err!("{insert_op} not implemented for MemoryTable yet"); + } + let sink = MemSink::try_new(self.batches.clone(), Arc::clone(&self.schema))?; + Ok(Arc::new(DataSinkExec::new(input, Arc::new(sink), None))) + } + + fn get_column_default(&self, column: &str) -> Option<&Expr> { + self.column_defaults.get(column) + } +} + +/// Implements for writing to a [`MemTable`] +struct MemSink { + /// Target locations for writing data + batches: Vec, + schema: SchemaRef, +} + +impl Debug for MemSink { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MemSink") + .field("num_partitions", &self.batches.len()) + .finish() + } +} + +impl DisplayAs for MemSink { + fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match t { + DisplayFormatType::Default | DisplayFormatType::Verbose => { + let partition_count = self.batches.len(); + write!(f, "MemoryTable (partitions={partition_count})") + } + DisplayFormatType::TreeRender => { + // TODO: collect info + write!(f, "") + } + } + } +} + +impl MemSink { + /// Creates a new [`MemSink`]. + /// + /// The caller is responsible for ensuring that there is at least one partition to insert into. + fn try_new(batches: Vec, schema: SchemaRef) -> Result { + if batches.is_empty() { + return plan_err!("Cannot insert into MemTable with zero partitions"); + } + Ok(Self { batches, schema }) + } +} + +#[async_trait] +impl DataSink for MemSink { + fn as_any(&self) -> &dyn Any { + self + } + + fn schema(&self) -> &SchemaRef { + &self.schema + } + + async fn write_all( + &self, + mut data: SendableRecordBatchStream, + _context: &Arc, + ) -> Result { + let num_partitions = self.batches.len(); + + // buffer up the data round robin style into num_partitions + + let mut new_batches = vec![vec![]; num_partitions]; + let mut i = 0; + let mut row_count = 0; + while let Some(batch) = data.next().await.transpose()? { + row_count += batch.num_rows(); + new_batches[i].push(batch); + i = (i + 1) % num_partitions; + } + + // write the outputs into the batches + for (target, mut batches) in self.batches.iter().zip(new_batches.into_iter()) { + // Append all the new batches in one go to minimize locking overhead + target.write().await.append(&mut batches); + } + + Ok(row_count as u64) + } +} + #[cfg(test)] mod tests { - use crate::datasource::MemTable; + use super::*; use crate::datasource::{provider_as_source, DefaultTableSource}; use crate::physical_plan::collect; use crate::prelude::SessionContext; + use arrow::array::{AsArray, Int32Array}; use arrow::datatypes::{DataType, Field, Schema, UInt64Type}; use arrow::error::ArrowError; - use arrow::record_batch::RecordBatch; - use arrow_schema::SchemaRef; - use datafusion_catalog::TableProvider; - use datafusion_common::{DataFusionError, Result}; - use datafusion_expr::dml::InsertOp; + use datafusion_common::DataFusionError; use datafusion_expr::LogicalPlanBuilder; - use futures::StreamExt; - use std::collections::HashMap; - use std::sync::Arc; #[tokio::test] async fn test_with_projection() -> Result<()> { diff --git a/datafusion/core/src/datasource/mod.rs b/datafusion/core/src/datasource/mod.rs index 25a89644cd2a4..35a451cbc803a 100644 --- a/datafusion/core/src/datasource/mod.rs +++ b/datafusion/core/src/datasource/mod.rs @@ -24,9 +24,10 @@ pub mod empty; pub mod file_format; pub mod listing; pub mod listing_table_factory; -mod memory_test; +pub mod memory; pub mod physical_plan; pub mod provider; +mod statistics; mod view_test; // backwards compatibility @@ -39,7 +40,6 @@ pub use crate::catalog::TableProvider; pub use crate::logical_expr::TableType; pub use datafusion_catalog::cte_worktable; pub use datafusion_catalog::default_table_source; -pub use datafusion_catalog::memory; pub use datafusion_catalog::stream; pub use datafusion_catalog::view; pub use datafusion_datasource::schema_adapter; @@ -47,6 +47,7 @@ pub use datafusion_datasource::sink; pub use datafusion_datasource::source; pub use datafusion_execution::object_store; pub use datafusion_physical_expr::create_ordering; +pub use statistics::get_statistics_with_limit; #[cfg(all(test, feature = "parquet"))] mod tests { @@ -106,7 +107,7 @@ mod tests { let meta = ObjectMeta { location, last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len(), + size: metadata.len() as usize, e_tag: None, version: None, }; diff --git a/datafusion/core/src/datasource/physical_plan/arrow_file.rs b/datafusion/core/src/datasource/physical_plan/arrow_file.rs index f0a1f94d87e1f..5dcf4df73f57a 100644 --- a/datafusion/core/src/datasource/physical_plan/arrow_file.rs +++ b/datafusion/core/src/datasource/physical_plan/arrow_file.rs @@ -273,7 +273,6 @@ impl FileOpener for ArrowOpener { None => { let r = object_store.get(file_meta.location()).await?; match r.payload { - #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(file, _) => { let arrow_reader = arrow::ipc::reader::FileReader::try_new( file, projection, @@ -306,7 +305,7 @@ impl FileOpener for ArrowOpener { )?; // read footer according to footer_len let get_option = GetOptions { - range: Some(GetRange::Suffix(10 + (footer_len as u64))), + range: Some(GetRange::Suffix(10 + footer_len)), ..Default::default() }; let get_result = object_store @@ -333,9 +332,9 @@ impl FileOpener for ArrowOpener { .iter() .flatten() .map(|block| { - let block_len = - block.bodyLength() as u64 + block.metaDataLength() as u64; - let block_offset = block.offset() as u64; + let block_len = block.bodyLength() as usize + + block.metaDataLength() as usize; + let block_offset = block.offset() as usize; block_offset..block_offset + block_len }) .collect_vec(); @@ -355,9 +354,9 @@ impl FileOpener for ArrowOpener { .iter() .flatten() .filter(|block| { - let block_offset = block.offset() as u64; - block_offset >= range.start as u64 - && block_offset < range.end as u64 + let block_offset = block.offset() as usize; + block_offset >= range.start as usize + && block_offset < range.end as usize }) .copied() .collect_vec(); @@ -365,9 +364,9 @@ impl FileOpener for ArrowOpener { let recordbatch_ranges = recordbatches .iter() .map(|block| { - let block_len = - block.bodyLength() as u64 + block.metaDataLength() as u64; - let block_offset = block.offset() as u64; + let block_len = block.bodyLength() as usize + + block.metaDataLength() as usize; + let block_offset = block.offset() as usize; block_offset..block_offset + block_len }) .collect_vec(); diff --git a/datafusion/core/src/datasource/physical_plan/csv.rs b/datafusion/core/src/datasource/physical_plan/csv.rs index 3ef4030134520..5914924797dce 100644 --- a/datafusion/core/src/datasource/physical_plan/csv.rs +++ b/datafusion/core/src/datasource/physical_plan/csv.rs @@ -658,7 +658,7 @@ mod tests { ) .await .expect_err("should fail because input file does not match inferred schema"); - assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'"); + assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4"); Ok(()) } diff --git a/datafusion/core/src/datasource/physical_plan/json.rs b/datafusion/core/src/datasource/physical_plan/json.rs index 736248fbd95df..910c4316d9734 100644 --- a/datafusion/core/src/datasource/physical_plan/json.rs +++ b/datafusion/core/src/datasource/physical_plan/json.rs @@ -495,7 +495,7 @@ mod tests { .write_json(out_dir_url, DataFrameWriteOptions::new(), None) .await .expect_err("should fail because input file does not match inferred schema"); - assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value 'd' as type 'Int64' for column 0 at line 4. Row data: '[d,4]'"); + assert_eq!(e.strip_backtrace(), "Arrow error: Parser error: Error while parsing value d for column 0 at line 4"); Ok(()) } diff --git a/datafusion/core/src/datasource/physical_plan/parquet.rs b/datafusion/core/src/datasource/physical_plan/parquet.rs index e9bb8b0db3682..9e1b2822e8540 100644 --- a/datafusion/core/src/datasource/physical_plan/parquet.rs +++ b/datafusion/core/src/datasource/physical_plan/parquet.rs @@ -38,12 +38,11 @@ mod tests { use crate::prelude::{ParquetReadOptions, SessionConfig, SessionContext}; use crate::test::object_store::local_unpartitioned_file; use arrow::array::{ - ArrayRef, AsArray, Date64Array, Int32Array, Int64Array, Int8Array, StringArray, + ArrayRef, Date64Array, Int32Array, Int64Array, Int8Array, StringArray, StructArray, }; use arrow::datatypes::{DataType, Field, Fields, Schema, SchemaBuilder}; use arrow::record_batch::RecordBatch; - use arrow::util::pretty::pretty_format_batches; use arrow_schema::SchemaRef; use bytes::{BufMut, BytesMut}; use datafusion_common::config::TableParquetOptions; @@ -62,9 +61,8 @@ mod tests { use datafusion_execution::object_store::ObjectStoreUrl; use datafusion_expr::{col, lit, when, Expr}; use datafusion_physical_expr::planner::logical2physical; - use datafusion_physical_plan::analyze::AnalyzeExec; - use datafusion_physical_plan::collect; use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, MetricsSet}; + use datafusion_physical_plan::{collect, displayable}; use datafusion_physical_plan::{ExecutionPlan, ExecutionPlanProperties}; use chrono::{TimeZone, Utc}; @@ -83,10 +81,10 @@ mod tests { struct RoundTripResult { /// Data that was read back from ParquetFiles batches: Result>, - /// The EXPLAIN ANALYZE output - explain: Result, /// The physical plan that was created (that has statistics, etc) parquet_exec: Arc, + /// The ParquetSource that is used in plan + parquet_source: ParquetSource, } /// round-trip record batches by writing each individual RecordBatch to @@ -139,109 +137,71 @@ mod tests { self.round_trip(batches).await.batches } - fn build_file_source(&self, file_schema: SchemaRef) -> Arc { + /// run the test, returning the `RoundTripResult` + async fn round_trip(self, batches: Vec) -> RoundTripResult { + let Self { + projection, + schema, + predicate, + pushdown_predicate, + page_index_predicate, + } = self; + + let file_schema = match schema { + Some(schema) => schema, + None => Arc::new( + Schema::try_merge( + batches.iter().map(|b| b.schema().as_ref().clone()), + ) + .unwrap(), + ), + }; + // If testing with page_index_predicate, write parquet + // files with multiple pages + let multi_page = page_index_predicate; + let (meta, _files) = store_parquet(batches, multi_page).await.unwrap(); + let file_group = meta.into_iter().map(Into::into).collect(); + // set up predicate (this is normally done by a layer higher up) - let predicate = self - .predicate - .as_ref() - .map(|p| logical2physical(p, &file_schema)); + let predicate = predicate.map(|p| logical2physical(&p, &file_schema)); let mut source = ParquetSource::default(); if let Some(predicate) = predicate { source = source.with_predicate(Arc::clone(&file_schema), predicate); } - if self.pushdown_predicate { + if pushdown_predicate { source = source .with_pushdown_filters(true) .with_reorder_filters(true); } - if self.page_index_predicate { + if page_index_predicate { source = source.with_enable_page_index(true); } - Arc::new(source) - } - - fn build_parquet_exec( - &self, - file_schema: SchemaRef, - file_group: FileGroup, - source: Arc, - ) -> Arc { let base_config = FileScanConfigBuilder::new( ObjectStoreUrl::local_filesystem(), file_schema, - source, + Arc::new(source.clone()), ) .with_file_group(file_group) - .with_projection(self.projection.clone()) + .with_projection(projection) .build(); - DataSourceExec::from_data_source(base_config) - } - - /// run the test, returning the `RoundTripResult` - async fn round_trip(&self, batches: Vec) -> RoundTripResult { - let file_schema = match &self.schema { - Some(schema) => schema, - None => &Arc::new( - Schema::try_merge( - batches.iter().map(|b| b.schema().as_ref().clone()), - ) - .unwrap(), - ), - }; - let file_schema = Arc::clone(file_schema); - // If testing with page_index_predicate, write parquet - // files with multiple pages - let multi_page = self.page_index_predicate; - let (meta, _files) = store_parquet(batches, multi_page).await.unwrap(); - let file_group: FileGroup = meta.into_iter().map(Into::into).collect(); - - // build a ParquetExec to return the results - let parquet_source = self.build_file_source(file_schema.clone()); - let parquet_exec = self.build_parquet_exec( - file_schema.clone(), - file_group.clone(), - Arc::clone(&parquet_source), - ); - - let analyze_exec = Arc::new(AnalyzeExec::new( - false, - false, - // use a new ParquetSource to avoid sharing execution metrics - self.build_parquet_exec( - file_schema.clone(), - file_group.clone(), - self.build_file_source(file_schema.clone()), - ), - Arc::new(Schema::new(vec![ - Field::new("plan_type", DataType::Utf8, true), - Field::new("plan", DataType::Utf8, true), - ])), - )); let session_ctx = SessionContext::new(); let task_ctx = session_ctx.task_ctx(); - let batches = collect( - Arc::clone(&parquet_exec) as Arc, - task_ctx.clone(), - ) - .await; - - let explain = collect(analyze_exec, task_ctx.clone()) - .await - .map(|batches| { - let batches = pretty_format_batches(&batches).unwrap(); - format!("{batches}") - }); - + let parquet_exec = DataSourceExec::from_data_source(base_config.clone()); RoundTripResult { - batches, - explain, + batches: collect(parquet_exec.clone(), task_ctx).await, parquet_exec, + parquet_source: base_config + .file_source() + .as_any() + .downcast_ref::() + .unwrap() + .clone(), } } } @@ -1109,7 +1069,6 @@ mod tests { let parquet_exec = scan_format( &state, &ParquetFormat::default(), - None, &testdata, filename, Some(vec![0, 1, 2]), @@ -1142,92 +1101,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn parquet_exec_with_int96_from_spark() -> Result<()> { - // arrow-rs relies on the chrono library to convert between timestamps and strings, so - // instead compare as Int64. The underlying type should be a PrimitiveArray of Int64 - // anyway, so this should be a zero-copy non-modifying cast at the SchemaAdapter. - - let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)])); - let testdata = datafusion_common::test_util::parquet_test_data(); - let filename = "int96_from_spark.parquet"; - let session_ctx = SessionContext::new(); - let state = session_ctx.state(); - let task_ctx = state.task_ctx(); - - let time_units_and_expected = vec![ - ( - None, // Same as "ns" time_unit - Arc::new(Int64Array::from(vec![ - Some(1704141296123456000), // Reads as nanosecond fine (note 3 extra 0s) - Some(1704070800000000000), // Reads as nanosecond fine (note 3 extra 0s) - Some(-4852191831933722624), // Cannot be represented with nanos timestamp (year 9999) - Some(1735599600000000000), // Reads as nanosecond fine (note 3 extra 0s) - None, - Some(-4864435138808946688), // Cannot be represented with nanos timestamp (year 290000) - ])), - ), - ( - Some("ns".to_string()), - Arc::new(Int64Array::from(vec![ - Some(1704141296123456000), - Some(1704070800000000000), - Some(-4852191831933722624), - Some(1735599600000000000), - None, - Some(-4864435138808946688), - ])), - ), - ( - Some("us".to_string()), - Arc::new(Int64Array::from(vec![ - Some(1704141296123456), - Some(1704070800000000), - Some(253402225200000000), - Some(1735599600000000), - None, - Some(9089380393200000000), - ])), - ), - ]; - - for (time_unit, expected) in time_units_and_expected { - let parquet_exec = scan_format( - &state, - &ParquetFormat::default().with_coerce_int96(time_unit.clone()), - Some(schema.clone()), - &testdata, - filename, - Some(vec![0]), - None, - ) - .await - .unwrap(); - assert_eq!(parquet_exec.output_partitioning().partition_count(), 1); - - let mut results = parquet_exec.execute(0, task_ctx.clone())?; - let batch = results.next().await.unwrap()?; - - assert_eq!(6, batch.num_rows()); - assert_eq!(1, batch.num_columns()); - - assert_eq!(batch.num_columns(), 1); - let column = batch.column(0); - - assert_eq!(column.len(), expected.len()); - - column - .as_primitive::() - .iter() - .zip(expected.iter()) - .for_each(|(lhs, rhs)| { - assert_eq!(lhs, rhs); - }); - } - - Ok(()) - } - #[tokio::test] async fn parquet_exec_with_range() -> Result<()> { fn file_range(meta: &ObjectMeta, start: i64, end: i64) -> PartitionedFile { @@ -1502,6 +1375,26 @@ mod tests { create_batch(vec![("c1", c1.clone())]) } + /// Returns a int64 array with contents: + /// "[-1, 1, null, 2, 3, null, null]" + fn int64_batch() -> RecordBatch { + let contents: ArrayRef = Arc::new(Int64Array::from(vec![ + Some(-1), + Some(1), + None, + Some(2), + Some(3), + None, + None, + ])); + + create_batch(vec![ + ("a", contents.clone()), + ("b", contents.clone()), + ("c", contents.clone()), + ]) + } + #[tokio::test] async fn parquet_exec_metrics() { // batch1: c1(string) @@ -1561,17 +1454,110 @@ mod tests { .round_trip(vec![batch1]) .await; - let explain = rt.explain.unwrap(); + // should have a pruning predicate + let pruning_predicate = rt.parquet_source.pruning_predicate(); + assert!(pruning_predicate.is_some()); + + // convert to explain plan form + let display = displayable(rt.parquet_exec.as_ref()) + .indent(true) + .to_string(); - // check that there was a pruning predicate -> row groups got pruned - assert_contains!(&explain, "predicate=c1@0 != bar"); + assert_contains!( + &display, + "pruning_predicate=c1_null_count@2 != row_count@3 AND (c1_min@0 != bar OR bar != c1_max@1)" + ); - // there's a single row group, but we can check that it matched - // if no pruning was done this would be 0 instead of 1 - assert_contains!(&explain, "row_groups_matched_statistics=1"); + assert_contains!(&display, r#"predicate=c1@0 != bar"#); - // check the projection - assert_contains!(&explain, "projection=[c1]"); + assert_contains!(&display, "projection=[c1]"); + } + + #[tokio::test] + async fn parquet_exec_display_deterministic() { + // batches: a(int64), b(int64), c(int64) + let batches = int64_batch(); + + fn extract_required_guarantees(s: &str) -> Option<&str> { + s.split("required_guarantees=").nth(1) + } + + // Ensuring that the required_guarantees remain consistent across every display plan of the filter conditions + for _ in 0..100 { + // c = 1 AND b = 1 AND a = 1 + let filter0 = col("c") + .eq(lit(1)) + .and(col("b").eq(lit(1))) + .and(col("a").eq(lit(1))); + + let rt0 = RoundTrip::new() + .with_predicate(filter0) + .with_pushdown_predicate() + .round_trip(vec![batches.clone()]) + .await; + + let pruning_predicate = rt0.parquet_source.pruning_predicate(); + assert!(pruning_predicate.is_some()); + + let display0 = displayable(rt0.parquet_exec.as_ref()) + .indent(true) + .to_string(); + + let guarantees0: &str = extract_required_guarantees(&display0) + .expect("Failed to extract required_guarantees"); + // Compare only the required_guarantees part (Because the file_groups part will not be the same) + assert_eq!( + guarantees0.trim(), + "[a in (1), b in (1), c in (1)]", + "required_guarantees don't match" + ); + } + + // c = 1 AND a = 1 AND b = 1 + let filter1 = col("c") + .eq(lit(1)) + .and(col("a").eq(lit(1))) + .and(col("b").eq(lit(1))); + + let rt1 = RoundTrip::new() + .with_predicate(filter1) + .with_pushdown_predicate() + .round_trip(vec![batches.clone()]) + .await; + + // b = 1 AND a = 1 AND c = 1 + let filter2 = col("b") + .eq(lit(1)) + .and(col("a").eq(lit(1))) + .and(col("c").eq(lit(1))); + + let rt2 = RoundTrip::new() + .with_predicate(filter2) + .with_pushdown_predicate() + .round_trip(vec![batches]) + .await; + + // should have a pruning predicate + let pruning_predicate = rt1.parquet_source.pruning_predicate(); + assert!(pruning_predicate.is_some()); + let pruning_predicate = rt2.parquet_source.predicate(); + assert!(pruning_predicate.is_some()); + + // convert to explain plan form + let display1 = displayable(rt1.parquet_exec.as_ref()) + .indent(true) + .to_string(); + let display2 = displayable(rt2.parquet_exec.as_ref()) + .indent(true) + .to_string(); + + let guarantees1 = extract_required_guarantees(&display1) + .expect("Failed to extract required_guarantees"); + let guarantees2 = extract_required_guarantees(&display2) + .expect("Failed to extract required_guarantees"); + + // Compare only the required_guarantees part (Because the predicate part will not be the same) + assert_eq!(guarantees1, guarantees2, "required_guarantees don't match"); } #[tokio::test] @@ -1595,19 +1581,16 @@ mod tests { .await; // Should not contain a pruning predicate (since nothing can be pruned) - let explain = rt.explain.unwrap(); - - // When both matched and pruned are 0, it means that the pruning predicate - // was not used at all. - assert_contains!(&explain, "row_groups_matched_statistics=0"); - assert_contains!(&explain, "row_groups_pruned_statistics=0"); - - // But pushdown predicate should be present - assert_contains!( - &explain, - "predicate=CASE WHEN c1@0 != bar THEN true ELSE false END" + let pruning_predicate = rt.parquet_source.pruning_predicate(); + assert!( + pruning_predicate.is_none(), + "Still had pruning predicate: {pruning_predicate:?}" ); - assert_contains!(&explain, "pushdown_rows_pruned=5"); + + // but does still has a pushdown down predicate + let predicate = rt.parquet_source.predicate(); + let filter_phys = logical2physical(&filter, rt.parquet_exec.schema().as_ref()); + assert_eq!(predicate.unwrap().to_string(), filter_phys.to_string()); } #[tokio::test] @@ -1633,14 +1616,8 @@ mod tests { .await; // Should have a pruning predicate - let explain = rt.explain.unwrap(); - assert_contains!( - &explain, - "predicate=c1@0 = foo AND CASE WHEN c1@0 != bar THEN true ELSE false END" - ); - - // And bloom filters should have been evaluated - assert_contains!(&explain, "row_groups_pruned_bloom_filter=1"); + let pruning_predicate = rt.parquet_source.pruning_predicate(); + assert!(pruning_predicate.is_some()); } /// Returns the sum of all the metrics with the specified name @@ -1873,13 +1850,13 @@ mod tests { path: &str, store: Arc, batch: RecordBatch, - ) -> u64 { + ) -> usize { let mut writer = ArrowWriter::try_new(BytesMut::new().writer(), batch.schema(), None).unwrap(); writer.write(&batch).unwrap(); writer.flush().unwrap(); let bytes = writer.into_inner().unwrap().into_inner().freeze(); - let total_size = bytes.len() as u64; + let total_size = bytes.len(); let path = Path::from(path); let payload = object_store::PutPayload::from_bytes(bytes); store diff --git a/datafusion/core/src/datasource/statistics.rs b/datafusion/core/src/datasource/statistics.rs new file mode 100644 index 0000000000000..cf283ecee0bf7 --- /dev/null +++ b/datafusion/core/src/datasource/statistics.rs @@ -0,0 +1,219 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use std::mem; +use std::sync::Arc; + +use futures::{Stream, StreamExt}; + +use crate::arrow::datatypes::SchemaRef; +use crate::error::Result; +use crate::physical_plan::{ColumnStatistics, Statistics}; +use datafusion_common::stats::Precision; +use datafusion_common::ScalarValue; +use datafusion_datasource::file_groups::FileGroup; + +use super::listing::PartitionedFile; + +/// Get all files as well as the file level summary statistics (no statistic for partition columns). +/// If the optional `limit` is provided, includes only sufficient files. Needed to read up to +/// `limit` number of rows. `collect_stats` is passed down from the configuration parameter on +/// `ListingTable`. If it is false we only construct bare statistics and skip a potentially expensive +/// call to `multiunzip` for constructing file level summary statistics. +pub async fn get_statistics_with_limit( + all_files: impl Stream)>>, + file_schema: SchemaRef, + limit: Option, + collect_stats: bool, +) -> Result<(FileGroup, Statistics)> { + let mut result_files = FileGroup::default(); + // These statistics can be calculated as long as at least one file provides + // useful information. If none of the files provides any information, then + // they will end up having `Precision::Absent` values. Throughout calculations, + // missing values will be imputed as: + // - zero for summations, and + // - neutral element for extreme points. + let size = file_schema.fields().len(); + let mut col_stats_set = vec![ColumnStatistics::default(); size]; + let mut num_rows = Precision::::Absent; + let mut total_byte_size = Precision::::Absent; + + // Fusing the stream allows us to call next safely even once it is finished. + let mut all_files = Box::pin(all_files.fuse()); + + if let Some(first_file) = all_files.next().await { + let (mut file, file_stats) = first_file?; + file.statistics = Some(file_stats.as_ref().clone()); + result_files.push(file); + + // First file, we set them directly from the file statistics. + num_rows = file_stats.num_rows; + total_byte_size = file_stats.total_byte_size; + for (index, file_column) in + file_stats.column_statistics.clone().into_iter().enumerate() + { + col_stats_set[index].null_count = file_column.null_count; + col_stats_set[index].max_value = file_column.max_value; + col_stats_set[index].min_value = file_column.min_value; + col_stats_set[index].sum_value = file_column.sum_value; + } + + // If the number of rows exceeds the limit, we can stop processing + // files. This only applies when we know the number of rows. It also + // currently ignores tables that have no statistics regarding the + // number of rows. + let conservative_num_rows = match num_rows { + Precision::Exact(nr) => nr, + _ => usize::MIN, + }; + if conservative_num_rows <= limit.unwrap_or(usize::MAX) { + while let Some(current) = all_files.next().await { + let (mut file, file_stats) = current?; + file.statistics = Some(file_stats.as_ref().clone()); + result_files.push(file); + if !collect_stats { + continue; + } + + // We accumulate the number of rows, total byte size and null + // counts across all the files in question. If any file does not + // provide any information or provides an inexact value, we demote + // the statistic precision to inexact. + num_rows = add_row_stats(file_stats.num_rows, num_rows); + + total_byte_size = + add_row_stats(file_stats.total_byte_size, total_byte_size); + + for (file_col_stats, col_stats) in file_stats + .column_statistics + .iter() + .zip(col_stats_set.iter_mut()) + { + let ColumnStatistics { + null_count: file_nc, + max_value: file_max, + min_value: file_min, + sum_value: file_sum, + distinct_count: _, + } = file_col_stats; + + col_stats.null_count = add_row_stats(*file_nc, col_stats.null_count); + set_max_if_greater(file_max, &mut col_stats.max_value); + set_min_if_lesser(file_min, &mut col_stats.min_value); + col_stats.sum_value = file_sum.add(&col_stats.sum_value); + } + + // If the number of rows exceeds the limit, we can stop processing + // files. This only applies when we know the number of rows. It also + // currently ignores tables that have no statistics regarding the + // number of rows. + if num_rows.get_value().unwrap_or(&usize::MIN) + > &limit.unwrap_or(usize::MAX) + { + break; + } + } + } + }; + + let mut statistics = Statistics { + num_rows, + total_byte_size, + column_statistics: col_stats_set, + }; + if all_files.next().await.is_some() { + // If we still have files in the stream, it means that the limit kicked + // in, and the statistic could have been different had we processed the + // files in a different order. + statistics = statistics.to_inexact() + } + + Ok((result_files, statistics)) +} + +fn add_row_stats( + file_num_rows: Precision, + num_rows: Precision, +) -> Precision { + match (file_num_rows, &num_rows) { + (Precision::Absent, _) => num_rows.to_inexact(), + (lhs, Precision::Absent) => lhs.to_inexact(), + (lhs, rhs) => lhs.add(rhs), + } +} + +/// If the given value is numerically greater than the original maximum value, +/// return the new maximum value with appropriate exactness information. +fn set_max_if_greater( + max_nominee: &Precision, + max_value: &mut Precision, +) { + match (&max_value, max_nominee) { + (Precision::Exact(val1), Precision::Exact(val2)) if val1 < val2 => { + *max_value = max_nominee.clone(); + } + (Precision::Exact(val1), Precision::Inexact(val2)) + | (Precision::Inexact(val1), Precision::Inexact(val2)) + | (Precision::Inexact(val1), Precision::Exact(val2)) + if val1 < val2 => + { + *max_value = max_nominee.clone().to_inexact(); + } + (Precision::Exact(_), Precision::Absent) => { + let exact_max = mem::take(max_value); + *max_value = exact_max.to_inexact(); + } + (Precision::Absent, Precision::Exact(_)) => { + *max_value = max_nominee.clone().to_inexact(); + } + (Precision::Absent, Precision::Inexact(_)) => { + *max_value = max_nominee.clone(); + } + _ => {} + } +} + +/// If the given value is numerically lesser than the original minimum value, +/// return the new minimum value with appropriate exactness information. +fn set_min_if_lesser( + min_nominee: &Precision, + min_value: &mut Precision, +) { + match (&min_value, min_nominee) { + (Precision::Exact(val1), Precision::Exact(val2)) if val1 > val2 => { + *min_value = min_nominee.clone(); + } + (Precision::Exact(val1), Precision::Inexact(val2)) + | (Precision::Inexact(val1), Precision::Inexact(val2)) + | (Precision::Inexact(val1), Precision::Exact(val2)) + if val1 > val2 => + { + *min_value = min_nominee.clone().to_inexact(); + } + (Precision::Exact(_), Precision::Absent) => { + let exact_min = mem::take(min_value); + *min_value = exact_min.to_inexact(); + } + (Precision::Absent, Precision::Exact(_)) => { + *min_value = min_nominee.clone().to_inexact(); + } + (Precision::Absent, Precision::Inexact(_)) => { + *min_value = min_nominee.clone(); + } + _ => {} + } +} diff --git a/datafusion/core/src/execution/context/mod.rs b/datafusion/core/src/execution/context/mod.rs index 0bb91536da3ca..fc110a0699df2 100644 --- a/datafusion/core/src/execution/context/mod.rs +++ b/datafusion/core/src/execution/context/mod.rs @@ -35,11 +35,7 @@ use crate::{ }, datasource::{provider_as_source, MemTable, ViewTable}, error::{DataFusionError, Result}, - execution::{ - options::ArrowReadOptions, - runtime_env::{RuntimeEnv, RuntimeEnvBuilder}, - FunctionRegistry, - }, + execution::{options::ArrowReadOptions, runtime_env::RuntimeEnv, FunctionRegistry}, logical_expr::AggregateUDF, logical_expr::ScalarUDF, logical_expr::{ @@ -1040,73 +1036,13 @@ impl SessionContext { variable, value, .. } = stmt; - // Check if this is a runtime configuration - if variable.starts_with("datafusion.runtime.") { - self.set_runtime_variable(&variable, &value)?; - } else { - let mut state = self.state.write(); - state.config_mut().options_mut().set(&variable, &value)?; - drop(state); - } + let mut state = self.state.write(); + state.config_mut().options_mut().set(&variable, &value)?; + drop(state); self.return_empty_dataframe() } - fn set_runtime_variable(&self, variable: &str, value: &str) -> Result<()> { - let key = variable.strip_prefix("datafusion.runtime.").unwrap(); - - match key { - "memory_limit" => { - let memory_limit = Self::parse_memory_limit(value)?; - - let mut state = self.state.write(); - let mut builder = - RuntimeEnvBuilder::from_runtime_env(state.runtime_env()); - builder = builder.with_memory_limit(memory_limit, 1.0); - *state = SessionStateBuilder::from(state.clone()) - .with_runtime_env(Arc::new(builder.build()?)) - .build(); - } - _ => { - return Err(DataFusionError::Plan(format!( - "Unknown runtime configuration: {}", - variable - ))) - } - } - Ok(()) - } - - /// Parse memory limit from string to number of bytes - /// Supports formats like '1.5G', '100M', '512K' - /// - /// # Examples - /// ``` - /// use datafusion::execution::context::SessionContext; - /// - /// assert_eq!(SessionContext::parse_memory_limit("1M").unwrap(), 1024 * 1024); - /// assert_eq!(SessionContext::parse_memory_limit("1.5G").unwrap(), (1.5 * 1024.0 * 1024.0 * 1024.0) as usize); - /// ``` - pub fn parse_memory_limit(limit: &str) -> Result { - let (number, unit) = limit.split_at(limit.len() - 1); - let number: f64 = number.parse().map_err(|_| { - DataFusionError::Plan(format!( - "Failed to parse number from memory limit '{}'", - limit - )) - })?; - - match unit { - "K" => Ok((number * 1024.0) as usize), - "M" => Ok((number * 1024.0 * 1024.0) as usize), - "G" => Ok((number * 1024.0 * 1024.0 * 1024.0) as usize), - _ => Err(DataFusionError::Plan(format!( - "Unsupported unit '{}' in memory limit '{}'", - unit, limit - ))), - } - } - async fn create_custom_table( &self, cmd: &CreateExternalTable, @@ -1897,6 +1833,7 @@ mod tests { use crate::test; use crate::test_util::{plan_and_collect, populate_csv_partitions}; use arrow::datatypes::{DataType, TimeUnit}; + use std::env; use std::error::Error; use std::path::PathBuf; diff --git a/datafusion/core/src/execution/session_state.rs b/datafusion/core/src/execution/session_state.rs index 597700bf8be3d..28f599304f8c8 100644 --- a/datafusion/core/src/execution/session_state.rs +++ b/datafusion/core/src/execution/session_state.rs @@ -1348,30 +1348,28 @@ impl SessionStateBuilder { } = self; let config = config.unwrap_or_default(); - let runtime_env = runtime_env.unwrap_or_else(|| Arc::new(RuntimeEnv::default())); + let runtime_env = runtime_env.unwrap_or(Arc::new(RuntimeEnv::default())); let mut state = SessionState { - session_id: session_id.unwrap_or_else(|| Uuid::new_v4().to_string()), + session_id: session_id.unwrap_or(Uuid::new_v4().to_string()), analyzer: analyzer.unwrap_or_default(), expr_planners: expr_planners.unwrap_or_default(), type_planner, optimizer: optimizer.unwrap_or_default(), physical_optimizers: physical_optimizers.unwrap_or_default(), - query_planner: query_planner - .unwrap_or_else(|| Arc::new(DefaultQueryPlanner {})), - catalog_list: catalog_list.unwrap_or_else(|| { - Arc::new(MemoryCatalogProviderList::new()) as Arc - }), + query_planner: query_planner.unwrap_or(Arc::new(DefaultQueryPlanner {})), + catalog_list: catalog_list + .unwrap_or(Arc::new(MemoryCatalogProviderList::new()) + as Arc), table_functions: table_functions.unwrap_or_default(), scalar_functions: HashMap::new(), aggregate_functions: HashMap::new(), window_functions: HashMap::new(), serializer_registry: serializer_registry - .unwrap_or_else(|| Arc::new(EmptySerializerRegistry)), + .unwrap_or(Arc::new(EmptySerializerRegistry)), file_formats: HashMap::new(), - table_options: table_options.unwrap_or_else(|| { - TableOptions::default_from_session_config(config.options()) - }), + table_options: table_options + .unwrap_or(TableOptions::default_from_session_config(config.options())), config, execution_props: execution_props.unwrap_or_default(), table_factories: table_factories.unwrap_or_default(), diff --git a/datafusion/core/src/lib.rs b/datafusion/core/src/lib.rs index 928efd533ca44..cc510bc81f1a8 100644 --- a/datafusion/core/src/lib.rs +++ b/datafusion/core/src/lib.rs @@ -22,18 +22,7 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] // Make sure fast / cheap clones on Arc are explicit: // https://github.com/apache/datafusion/issues/11143 -// -// Eliminate unnecessary function calls(some may be not cheap) due to `xxx_or` -// for performance. Also avoid abusing `xxx_or_else` for readability: -// https://github.com/apache/datafusion/issues/15802 -#![cfg_attr( - not(test), - deny( - clippy::clone_on_ref_ptr, - clippy::or_fun_call, - clippy::unnecessary_lazy_evaluations - ) -)] +#![cfg_attr(not(test), deny(clippy::clone_on_ref_ptr))] #![warn(missing_docs, clippy::needless_borrow)] //! [DataFusion] is an extensible query engine written in Rust that @@ -883,12 +872,6 @@ doc_comment::doctest!( user_guide_configs ); -#[cfg(doctest)] -doc_comment::doctest!( - "../../../docs/source/user-guide/runtime_configs.md", - user_guide_runtime_configs -); - #[cfg(doctest)] doc_comment::doctest!( "../../../docs/source/user-guide/crate-configuration.md", @@ -1038,8 +1021,8 @@ doc_comment::doctest!( #[cfg(doctest)] doc_comment::doctest!( - "../../../docs/source/user-guide/sql/format_options.md", - user_guide_sql_format_options + "../../../docs/source/user-guide/sql/write_options.md", + user_guide_sql_write_options ); #[cfg(doctest)] diff --git a/datafusion/core/src/physical_planner.rs b/datafusion/core/src/physical_planner.rs index be24206c676c6..f1a99a7714ac4 100644 --- a/datafusion/core/src/physical_planner.rs +++ b/datafusion/core/src/physical_planner.rs @@ -81,7 +81,7 @@ use datafusion_expr::{ WindowFrameBound, WriteOp, }; use datafusion_physical_expr::aggregate::{AggregateExprBuilder, AggregateFunctionExpr}; -use datafusion_physical_expr::expressions::{Column, Literal}; +use datafusion_physical_expr::expressions::Literal; use datafusion_physical_expr::LexOrdering; use datafusion_physical_optimizer::PhysicalOptimizerRule; use datafusion_physical_plan::execution_plan::InvariantLevel; @@ -1023,12 +1023,18 @@ impl DefaultPhysicalPlanner { // Collect left & right field indices, the field indices are sorted in ascending order let left_field_indices = cols .iter() - .filter_map(|c| left_df_schema.index_of_column(c).ok()) + .filter_map(|c| match left_df_schema.index_of_column(c) { + Ok(idx) => Some(idx), + _ => None, + }) .sorted() .collect::>(); let right_field_indices = cols .iter() - .filter_map(|c| right_df_schema.index_of_column(c).ok()) + .filter_map(|c| match right_df_schema.index_of_column(c) { + Ok(idx) => Some(idx), + _ => None, + }) .sorted() .collect::>(); @@ -2000,8 +2006,7 @@ impl DefaultPhysicalPlanner { input: &Arc, expr: &[Expr], ) -> Result> { - let input_logical_schema = input.as_ref().schema(); - let input_physical_schema = input_exec.schema(); + let input_schema = input.as_ref().schema(); let physical_exprs = expr .iter() .map(|e| { @@ -2020,7 +2025,7 @@ impl DefaultPhysicalPlanner { // This depends on the invariant that logical schema field index MUST match // with physical schema field index. let physical_name = if let Expr::Column(col) = e { - match input_logical_schema.index_of_column(col) { + match input_schema.index_of_column(col) { Ok(idx) => { // index physical field using logical field index Ok(input_exec.schema().field(idx).name().to_string()) @@ -2033,14 +2038,10 @@ impl DefaultPhysicalPlanner { physical_name(e) }; - let physical_expr = - self.create_physical_expr(e, input_logical_schema, session_state); - - // Check for possible column name mismatches - let final_physical_expr = - maybe_fix_physical_column_name(physical_expr, &input_physical_schema); - - tuple_err((final_physical_expr, physical_name)) + tuple_err(( + self.create_physical_expr(e, input_schema, session_state), + physical_name, + )) }) .collect::>>()?; @@ -2060,40 +2061,6 @@ fn tuple_err(value: (Result, Result)) -> Result<(T, R)> { } } -// Handle the case where the name of a physical column expression does not match the corresponding physical input fields names. -// Physical column names are derived from the physical schema, whereas physical column expressions are derived from the logical column names. -// -// This is a special case that applies only to column expressions. Logical plans may slightly modify column names by appending a suffix (e.g., using ':'), -// to avoid duplicates—since DFSchemas do not allow duplicate names. For example: `count(Int64(1)):1`. -fn maybe_fix_physical_column_name( - expr: Result>, - input_physical_schema: &SchemaRef, -) -> Result> { - if let Ok(e) = &expr { - if let Some(column) = e.as_any().downcast_ref::() { - let physical_field = input_physical_schema.field(column.index()); - let expr_col_name = column.name(); - let physical_name = physical_field.name(); - - if physical_name != expr_col_name { - // handle edge cases where the physical_name contains ':'. - let colon_count = physical_name.matches(':').count(); - let mut splits = expr_col_name.match_indices(':'); - let split_pos = splits.nth(colon_count); - - if let Some((idx, _)) = split_pos { - let base_name = &expr_col_name[..idx]; - if base_name == physical_name { - let updated_column = Column::new(physical_name, column.index()); - return Ok(Arc::new(updated_column)); - } - } - } - } - } - expr -} - struct OptimizationInvariantChecker<'a> { rule: &'a Arc, } @@ -2689,30 +2656,6 @@ mod tests { } } - #[tokio::test] - async fn test_maybe_fix_colon_in_physical_name() { - // The physical schema has a field name with a colon - let schema = Schema::new(vec![Field::new("metric:avg", DataType::Int32, false)]); - let schema_ref: SchemaRef = Arc::new(schema); - - // What might happen after deduplication - let logical_col_name = "metric:avg:1"; - let expr_with_suffix = - Arc::new(Column::new(logical_col_name, 0)) as Arc; - let expr_result = Ok(expr_with_suffix); - - // Call function under test - let fixed_expr = - maybe_fix_physical_column_name(expr_result, &schema_ref).unwrap(); - - // Downcast back to Column so we can check the name - let col = fixed_expr - .as_any() - .downcast_ref::() - .expect("Column"); - - assert_eq!(col.name(), "metric:avg"); - } struct ErrorExtensionPlanner {} #[async_trait] diff --git a/datafusion/core/src/test/object_store.rs b/datafusion/core/src/test/object_store.rs index 8b19658bb1473..e1328770cabdd 100644 --- a/datafusion/core/src/test/object_store.rs +++ b/datafusion/core/src/test/object_store.rs @@ -66,7 +66,7 @@ pub fn local_unpartitioned_file(path: impl AsRef) -> ObjectMeta ObjectMeta { location, last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len(), + size: metadata.len() as usize, e_tag: None, version: None, } @@ -166,7 +166,7 @@ impl ObjectStore for BlockingObjectStore { fn list( &self, prefix: Option<&Path>, - ) -> BoxStream<'static, object_store::Result> { + ) -> BoxStream<'_, object_store::Result> { self.inner.list(prefix) } diff --git a/datafusion/core/src/test_util/parquet.rs b/datafusion/core/src/test_util/parquet.rs index f5753af64d93f..084554eecbdb0 100644 --- a/datafusion/core/src/test_util/parquet.rs +++ b/datafusion/core/src/test_util/parquet.rs @@ -102,7 +102,7 @@ impl TestParquetFile { println!("Generated test dataset with {num_rows} rows"); - let size = std::fs::metadata(&path)?.len(); + let size = std::fs::metadata(&path)?.len() as usize; let mut canonical_path = path.canonicalize()?; diff --git a/datafusion/core/tests/core_integration.rs b/datafusion/core/tests/core_integration.rs index 250538b133703..9bcb9e41f86a9 100644 --- a/datafusion/core/tests/core_integration.rs +++ b/datafusion/core/tests/core_integration.rs @@ -51,9 +51,6 @@ mod serde; /// Run all tests that are found in the `catalog` directory mod catalog; -/// Run all tests that are found in the `tracing` directory -mod tracing; - #[cfg(test)] #[ctor::ctor] fn init() { diff --git a/datafusion/core/tests/dataframe/dataframe_functions.rs b/datafusion/core/tests/dataframe/dataframe_functions.rs index 40590d74ad910..c763d4c8de2d6 100644 --- a/datafusion/core/tests/dataframe/dataframe_functions.rs +++ b/datafusion/core/tests/dataframe/dataframe_functions.rs @@ -384,7 +384,7 @@ async fn test_fn_approx_median() -> Result<()> { #[tokio::test] async fn test_fn_approx_percentile_cont() -> Result<()> { - let expr = approx_percentile_cont(col("b").sort(true, false), lit(0.5), None); + let expr = approx_percentile_cont(col("b"), lit(0.5), None); let df = create_test_table().await?; let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; @@ -392,26 +392,11 @@ async fn test_fn_approx_percentile_cont() -> Result<()> { assert_snapshot!( batches_to_string(&batches), @r" - +---------------------------------------------------------------------------+ - | approx_percentile_cont(Float64(0.5)) WITHIN GROUP [test.b ASC NULLS LAST] | - +---------------------------------------------------------------------------+ - | 10 | - +---------------------------------------------------------------------------+ - "); - - let expr = approx_percentile_cont(col("b").sort(false, false), lit(0.1), None); - - let df = create_test_table().await?; - let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; - - assert_snapshot!( - batches_to_string(&batches), - @r" - +----------------------------------------------------------------------------+ - | approx_percentile_cont(Float64(0.1)) WITHIN GROUP [test.b DESC NULLS LAST] | - +----------------------------------------------------------------------------+ - | 100 | - +----------------------------------------------------------------------------+ + +---------------------------------------------+ + | approx_percentile_cont(test.b,Float64(0.5)) | + +---------------------------------------------+ + | 10 | + +---------------------------------------------+ "); // the arg2 parameter is a complex expr, but it can be evaluated to the literal value @@ -420,59 +405,23 @@ async fn test_fn_approx_percentile_cont() -> Result<()> { None::<&str>, "arg_2".to_string(), )); - let expr = approx_percentile_cont(col("b").sort(true, false), alias_expr, None); + let expr = approx_percentile_cont(col("b"), alias_expr, None); let df = create_test_table().await?; let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; assert_snapshot!( batches_to_string(&batches), @r" - +--------------------------------------------------------------------+ - | approx_percentile_cont(arg_2) WITHIN GROUP [test.b ASC NULLS LAST] | - +--------------------------------------------------------------------+ - | 10 | - +--------------------------------------------------------------------+ - " - ); - - let alias_expr = Expr::Alias(Alias::new( - cast(lit(0.1), DataType::Float32), - None::<&str>, - "arg_2".to_string(), - )); - let expr = approx_percentile_cont(col("b").sort(false, false), alias_expr, None); - let df = create_test_table().await?; - let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; - - assert_snapshot!( - batches_to_string(&batches), - @r" - +---------------------------------------------------------------------+ - | approx_percentile_cont(arg_2) WITHIN GROUP [test.b DESC NULLS LAST] | - +---------------------------------------------------------------------+ - | 100 | - +---------------------------------------------------------------------+ + +--------------------------------------+ + | approx_percentile_cont(test.b,arg_2) | + +--------------------------------------+ + | 10 | + +--------------------------------------+ " ); // with number of centroids set - let expr = approx_percentile_cont(col("b").sort(true, false), lit(0.5), Some(lit(2))); - - let df = create_test_table().await?; - let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; - - assert_snapshot!( - batches_to_string(&batches), - @r" - +------------------------------------------------------------------------------------+ - | approx_percentile_cont(Float64(0.5),Int32(2)) WITHIN GROUP [test.b ASC NULLS LAST] | - +------------------------------------------------------------------------------------+ - | 30 | - +------------------------------------------------------------------------------------+ - "); - - let expr = - approx_percentile_cont(col("b").sort(false, false), lit(0.1), Some(lit(2))); + let expr = approx_percentile_cont(col("b"), lit(0.5), Some(lit(2))); let df = create_test_table().await?; let batches = df.aggregate(vec![], vec![expr]).unwrap().collect().await?; @@ -480,11 +429,11 @@ async fn test_fn_approx_percentile_cont() -> Result<()> { assert_snapshot!( batches_to_string(&batches), @r" - +-------------------------------------------------------------------------------------+ - | approx_percentile_cont(Float64(0.1),Int32(2)) WITHIN GROUP [test.b DESC NULLS LAST] | - +-------------------------------------------------------------------------------------+ - | 69 | - +-------------------------------------------------------------------------------------+ + +------------------------------------------------------+ + | approx_percentile_cont(test.b,Float64(0.5),Int32(2)) | + +------------------------------------------------------+ + | 30 | + +------------------------------------------------------+ "); Ok(()) diff --git a/datafusion/core/tests/dataframe/mod.rs b/datafusion/core/tests/dataframe/mod.rs index 1855a512048d6..b5923269ab8ba 100644 --- a/datafusion/core/tests/dataframe/mod.rs +++ b/datafusion/core/tests/dataframe/mod.rs @@ -5206,40 +5206,6 @@ fn union_fields() -> UnionFields { .collect() } -#[tokio::test] -async fn union_literal_is_null_and_not_null() -> Result<()> { - let str_array_1 = StringArray::from(vec![None::]); - let str_array_2 = StringArray::from(vec![Some("a")]); - - let batch_1 = - RecordBatch::try_from_iter(vec![("arr", Arc::new(str_array_1) as ArrayRef)])?; - let batch_2 = - RecordBatch::try_from_iter(vec![("arr", Arc::new(str_array_2) as ArrayRef)])?; - - let ctx = SessionContext::new(); - ctx.register_batch("union_batch_1", batch_1)?; - ctx.register_batch("union_batch_2", batch_2)?; - - let df1 = ctx.table("union_batch_1").await?; - let df2 = ctx.table("union_batch_2").await?; - - let batches = df1.union(df2)?.collect().await?; - let schema = batches[0].schema(); - - for batch in batches { - // Verify schema is the same for all batches - if !schema.contains(&batch.schema()) { - return Err(DataFusionError::Internal(format!( - "Schema mismatch. Previously had\n{:#?}\n\nGot:\n{:#?}", - &schema, - batch.schema() - ))); - } - } - - Ok(()) -} - #[tokio::test] async fn sparse_union_is_null() { // union of [{A=1}, {A=}, {B=3.2}, {B=}, {C="a"}, {C=}] @@ -5511,64 +5477,6 @@ async fn boolean_dictionary_as_filter() { ); } -#[tokio::test] -async fn test_union_by_name() -> Result<()> { - let df = create_test_table("test") - .await? - .select(vec![col("a"), col("b"), lit(1).alias("c")])? - .alias("table_alias")?; - - let df2 = df.clone().select_columns(&["c", "b", "a"])?; - let result = df.union_by_name(df2)?.sort_by(vec![col("a"), col("b")])?; - - assert_snapshot!( - batches_to_sort_string(&result.collect().await?), - @r" - +-----------+-----+---+ - | a | b | c | - +-----------+-----+---+ - | 123AbcDef | 100 | 1 | - | 123AbcDef | 100 | 1 | - | CBAdef | 10 | 1 | - | CBAdef | 10 | 1 | - | abc123 | 10 | 1 | - | abc123 | 10 | 1 | - | abcDEF | 1 | 1 | - | abcDEF | 1 | 1 | - +-----------+-----+---+ - " - ); - Ok(()) -} - -#[tokio::test] -async fn test_union_by_name_distinct() -> Result<()> { - let df = create_test_table("test") - .await? - .select(vec![col("a"), col("b"), lit(1).alias("c")])? - .alias("table_alias")?; - - let df2 = df.clone().select_columns(&["c", "b", "a"])?; - let result = df - .union_by_name_distinct(df2)? - .sort_by(vec![col("a"), col("b")])?; - - assert_snapshot!( - batches_to_sort_string(&result.collect().await?), - @r" - +-----------+-----+---+ - | a | b | c | - +-----------+-----+---+ - | 123AbcDef | 100 | 1 | - | CBAdef | 10 | 1 | - | abc123 | 10 | 1 | - | abcDEF | 1 | 1 | - +-----------+-----+---+ - " - ); - Ok(()) -} - #[tokio::test] async fn test_alias() -> Result<()> { let df = create_test_table("test") diff --git a/datafusion/core/tests/execution/logical_plan.rs b/datafusion/core/tests/execution/logical_plan.rs index fdee6fd5dbbce..b30636ddf6a81 100644 --- a/datafusion/core/tests/execution/logical_plan.rs +++ b/datafusion/core/tests/execution/logical_plan.rs @@ -19,19 +19,15 @@ //! create them and depend on them. Test executable semantics of logical plans. use arrow::array::Int64Array; -use arrow::datatypes::{DataType, Field, Schema}; -use datafusion::datasource::{provider_as_source, ViewTable}; +use arrow::datatypes::{DataType, Field}; use datafusion::execution::session_state::SessionStateBuilder; -use datafusion_common::{Column, DFSchema, DFSchemaRef, Result, ScalarValue, Spans}; +use datafusion_common::{Column, DFSchema, Result, ScalarValue, Spans}; use datafusion_execution::TaskContext; use datafusion_expr::expr::{AggregateFunction, AggregateFunctionParams}; use datafusion_expr::logical_plan::{LogicalPlan, Values}; -use datafusion_expr::{ - Aggregate, AggregateUDF, EmptyRelation, Expr, LogicalPlanBuilder, UNNAMED_TABLE, -}; +use datafusion_expr::{Aggregate, AggregateUDF, Expr}; use datafusion_functions_aggregate::count::Count; use datafusion_physical_plan::collect; -use insta::assert_snapshot; use std::collections::HashMap; use std::fmt::Debug; use std::ops::Deref; @@ -100,37 +96,3 @@ where }; element } - -#[test] -fn inline_scan_projection_test() -> Result<()> { - let name = UNNAMED_TABLE; - let column = "a"; - - let schema = Schema::new(vec![ - Field::new("a", DataType::Int32, false), - Field::new("b", DataType::Int32, false), - ]); - let projection = vec![schema.index_of(column)?]; - - let provider = ViewTable::new( - LogicalPlan::EmptyRelation(EmptyRelation { - produce_one_row: false, - schema: DFSchemaRef::new(DFSchema::try_from(schema)?), - }), - None, - ); - let source = provider_as_source(Arc::new(provider)); - - let plan = LogicalPlanBuilder::scan(name, source, Some(projection))?.build()?; - - assert_snapshot!( - format!("{plan}"), - @r" - SubqueryAlias: ?table? - Projection: a - EmptyRelation - " - ); - - Ok(()) -} diff --git a/datafusion/core/tests/expr_api/simplification.rs b/datafusion/core/tests/expr_api/simplification.rs index 34e0487f312fb..7bb21725ef401 100644 --- a/datafusion/core/tests/expr_api/simplification.rs +++ b/datafusion/core/tests/expr_api/simplification.rs @@ -547,9 +547,9 @@ fn test_simplify_with_cycle_count( }; let simplifier = ExprSimplifier::new(info); let (simplified_expr, count) = simplifier - .simplify_with_cycle_count_transformed(input_expr.clone()) + .simplify_with_cycle_count(input_expr.clone()) .expect("successfully evaluated"); - let simplified_expr = simplified_expr.data; + assert_eq!( simplified_expr, expected_expr, "Mismatch evaluating {input_expr}\n Expected:{expected_expr}\n Got:{simplified_expr}" diff --git a/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs b/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs index ff3b66986ced9..dcf477135a377 100644 --- a/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs +++ b/datafusion/core/tests/fuzz_cases/aggregate_fuzz.rs @@ -18,17 +18,16 @@ use std::sync::Arc; use crate::fuzz_cases::aggregation_fuzzer::{ - AggregationFuzzerBuilder, DatasetGeneratorConfig, QueryBuilder, + AggregationFuzzerBuilder, ColumnDescr, DatasetGeneratorConfig, QueryBuilder, }; -use arrow::array::{ - types::Int64Type, Array, ArrayRef, AsArray, Int32Array, Int64Array, RecordBatch, - StringArray, -}; +use arrow::array::{types::Int64Type, Array, ArrayRef, AsArray, Int64Array, RecordBatch}; use arrow::compute::{concat_batches, SortOptions}; -use arrow::datatypes::DataType; +use arrow::datatypes::{ + DataType, IntervalUnit, TimeUnit, DECIMAL128_MAX_PRECISION, DECIMAL128_MAX_SCALE, + DECIMAL256_MAX_PRECISION, DECIMAL256_MAX_SCALE, +}; use arrow::util::pretty::pretty_format_batches; -use arrow_schema::{Field, Schema, SchemaRef}; use datafusion::common::Result; use datafusion::datasource::memory::MemorySourceConfig; use datafusion::datasource::source::DataSourceExec; @@ -43,20 +42,14 @@ use datafusion_common::tree_node::{TreeNode, TreeNodeRecursion, TreeNodeVisitor} use datafusion_common::HashMap; use datafusion_common_runtime::JoinSet; use datafusion_functions_aggregate::sum::sum_udaf; -use datafusion_physical_expr::expressions::{col, lit, Column}; +use datafusion_physical_expr::expressions::col; use datafusion_physical_expr::PhysicalSortExpr; use datafusion_physical_expr_common::sort_expr::LexOrdering; use datafusion_physical_plan::InputOrderMode; use test_utils::{add_empty_batches, StringBatchGenerator}; -use datafusion_execution::memory_pool::FairSpillPool; -use datafusion_execution::runtime_env::RuntimeEnvBuilder; -use datafusion_execution::TaskContext; -use datafusion_physical_plan::metrics::MetricValue; use rand::rngs::StdRng; -use rand::{random, thread_rng, Rng, SeedableRng}; - -use super::record_batch_generator::get_supported_types_columns; +use rand::{thread_rng, Rng, SeedableRng}; // ======================================================================== // The new aggregation fuzz tests based on [`AggregationFuzzer`] @@ -120,32 +113,6 @@ async fn test_first_val() { .await; } -#[tokio::test(flavor = "multi_thread")] -async fn test_last_val() { - let mut data_gen_config = baseline_config(); - - for i in 0..data_gen_config.columns.len() { - if data_gen_config.columns[i].get_max_num_distinct().is_none() { - data_gen_config.columns[i] = data_gen_config.columns[i] - .clone() - // Minimize the chance of identical values in the order by columns to make the test more stable - .with_max_num_distinct(usize::MAX); - } - } - - let query_builder = QueryBuilder::new() - .with_table_name("fuzz_table") - .with_aggregate_function("last_value") - .with_aggregate_arguments(data_gen_config.all_columns()) - .set_group_by_columns(data_gen_config.all_columns()); - - AggregationFuzzerBuilder::from(data_gen_config) - .add_query_builder(query_builder) - .build() - .run() - .await; -} - #[tokio::test(flavor = "multi_thread")] async fn test_max() { let data_gen_config = baseline_config(); @@ -234,7 +201,81 @@ async fn test_median() { /// 1. structured types fn baseline_config() -> DatasetGeneratorConfig { let mut rng = thread_rng(); - let columns = get_supported_types_columns(rng.gen()); + let columns = vec![ + ColumnDescr::new("i8", DataType::Int8), + ColumnDescr::new("i16", DataType::Int16), + ColumnDescr::new("i32", DataType::Int32), + ColumnDescr::new("i64", DataType::Int64), + ColumnDescr::new("u8", DataType::UInt8), + ColumnDescr::new("u16", DataType::UInt16), + ColumnDescr::new("u32", DataType::UInt32), + ColumnDescr::new("u64", DataType::UInt64), + ColumnDescr::new("date32", DataType::Date32), + ColumnDescr::new("date64", DataType::Date64), + ColumnDescr::new("time32_s", DataType::Time32(TimeUnit::Second)), + ColumnDescr::new("time32_ms", DataType::Time32(TimeUnit::Millisecond)), + ColumnDescr::new("time64_us", DataType::Time64(TimeUnit::Microsecond)), + ColumnDescr::new("time64_ns", DataType::Time64(TimeUnit::Nanosecond)), + // `None` is passed in here however when generating the array, it will generate + // random timezones. + ColumnDescr::new("timestamp_s", DataType::Timestamp(TimeUnit::Second, None)), + ColumnDescr::new( + "timestamp_ms", + DataType::Timestamp(TimeUnit::Millisecond, None), + ), + ColumnDescr::new( + "timestamp_us", + DataType::Timestamp(TimeUnit::Microsecond, None), + ), + ColumnDescr::new( + "timestamp_ns", + DataType::Timestamp(TimeUnit::Nanosecond, None), + ), + ColumnDescr::new("float32", DataType::Float32), + ColumnDescr::new("float64", DataType::Float64), + ColumnDescr::new( + "interval_year_month", + DataType::Interval(IntervalUnit::YearMonth), + ), + ColumnDescr::new( + "interval_day_time", + DataType::Interval(IntervalUnit::DayTime), + ), + ColumnDescr::new( + "interval_month_day_nano", + DataType::Interval(IntervalUnit::MonthDayNano), + ), + // begin decimal columns + ColumnDescr::new("decimal128", { + // Generate valid precision and scale for Decimal128 randomly. + let precision: u8 = rng.gen_range(1..=DECIMAL128_MAX_PRECISION); + // It's safe to cast `precision` to i8 type directly. + let scale: i8 = rng.gen_range( + i8::MIN..=std::cmp::min(precision as i8, DECIMAL128_MAX_SCALE), + ); + DataType::Decimal128(precision, scale) + }), + ColumnDescr::new("decimal256", { + // Generate valid precision and scale for Decimal256 randomly. + let precision: u8 = rng.gen_range(1..=DECIMAL256_MAX_PRECISION); + // It's safe to cast `precision` to i8 type directly. + let scale: i8 = rng.gen_range( + i8::MIN..=std::cmp::min(precision as i8, DECIMAL256_MAX_SCALE), + ); + DataType::Decimal256(precision, scale) + }), + // begin string columns + ColumnDescr::new("utf8", DataType::Utf8), + ColumnDescr::new("largeutf8", DataType::LargeUtf8), + ColumnDescr::new("utf8view", DataType::Utf8View), + // low cardinality columns + ColumnDescr::new("u8_low", DataType::UInt8).with_max_num_distinct(10), + ColumnDescr::new("utf8_low", DataType::Utf8).with_max_num_distinct(10), + ColumnDescr::new("bool", DataType::Boolean), + ColumnDescr::new("binary", DataType::Binary), + ColumnDescr::new("large_binary", DataType::LargeBinary), + ColumnDescr::new("binaryview", DataType::BinaryView), + ]; let min_num_rows = 512; let max_num_rows = 1024; @@ -622,134 +663,3 @@ fn extract_result_counts(results: Vec) -> HashMap, i } output } - -fn assert_spill_count_metric(expect_spill: bool, single_aggregate: Arc) { - if let Some(metrics_set) = single_aggregate.metrics() { - let mut spill_count = 0; - - // Inspect metrics for SpillCount - for metric in metrics_set.iter() { - if let MetricValue::SpillCount(count) = metric.value() { - spill_count = count.value(); - break; - } - } - - if expect_spill && spill_count == 0 { - panic!("Expected spill but SpillCount metric not found or SpillCount was 0."); - } else if !expect_spill && spill_count > 0 { - panic!("Expected no spill but found SpillCount metric with value greater than 0."); - } - } else { - panic!("No metrics returned from the operator; cannot verify spilling."); - } -} - -// Fix for https://github.com/apache/datafusion/issues/15530 -#[tokio::test] -async fn test_single_mode_aggregate_with_spill() -> Result<()> { - let scan_schema = Arc::new(Schema::new(vec![ - Field::new("col_0", DataType::Int64, true), - Field::new("col_1", DataType::Utf8, true), - Field::new("col_2", DataType::Utf8, true), - Field::new("col_3", DataType::Utf8, true), - Field::new("col_4", DataType::Utf8, true), - Field::new("col_5", DataType::Int32, true), - Field::new("col_6", DataType::Utf8, true), - Field::new("col_7", DataType::Utf8, true), - Field::new("col_8", DataType::Utf8, true), - ])); - - let group_by = PhysicalGroupBy::new_single(vec![ - (Arc::new(Column::new("col_1", 1)), "col_1".to_string()), - (Arc::new(Column::new("col_7", 7)), "col_7".to_string()), - (Arc::new(Column::new("col_0", 0)), "col_0".to_string()), - (Arc::new(Column::new("col_8", 8)), "col_8".to_string()), - ]); - - fn generate_int64_array() -> ArrayRef { - Arc::new(Int64Array::from_iter_values( - (0..1024).map(|_| random::()), - )) - } - fn generate_int32_array() -> ArrayRef { - Arc::new(Int32Array::from_iter_values( - (0..1024).map(|_| random::()), - )) - } - - fn generate_string_array() -> ArrayRef { - Arc::new(StringArray::from( - (0..1024) - .map(|_| -> String { - thread_rng() - .sample_iter::(rand::distributions::Standard) - .take(5) - .collect() - }) - .collect::>(), - )) - } - - fn generate_record_batch(schema: &SchemaRef) -> Result { - RecordBatch::try_new( - Arc::clone(schema), - vec![ - generate_int64_array(), - generate_string_array(), - generate_string_array(), - generate_string_array(), - generate_string_array(), - generate_int32_array(), - generate_string_array(), - generate_string_array(), - generate_string_array(), - ], - ) - .map_err(|err| err.into()) - } - - let aggregate_expressions = vec![Arc::new( - AggregateExprBuilder::new(sum_udaf(), vec![lit(1i64)]) - .schema(Arc::clone(&scan_schema)) - .alias("SUM(1i64)") - .build()?, - )]; - - let batches = (0..5) - .map(|_| generate_record_batch(&scan_schema)) - .collect::>>()?; - - let plan: Arc = - MemorySourceConfig::try_new_exec(&[batches], Arc::clone(&scan_schema), None) - .unwrap(); - - let single_aggregate = Arc::new(AggregateExec::try_new( - AggregateMode::Single, - group_by, - aggregate_expressions.clone(), - vec![None; aggregate_expressions.len()], - plan, - Arc::clone(&scan_schema), - )?); - - let memory_pool = Arc::new(FairSpillPool::new(250000)); - let task_ctx = Arc::new( - TaskContext::default() - .with_session_config(SessionConfig::new().with_batch_size(248)) - .with_runtime(Arc::new( - RuntimeEnvBuilder::new() - .with_memory_pool(memory_pool) - .build()?, - )), - ); - - datafusion_physical_plan::common::collect( - single_aggregate.execute(0, Arc::clone(&task_ctx))?, - ) - .await?; - - assert_spill_count_metric(true, single_aggregate); - - Ok(()) -} diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs index 3c9fe2917251c..8a8aa180b3c44 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/context_generator.rs @@ -43,7 +43,7 @@ use crate::fuzz_cases::aggregation_fuzzer::data_generator::Dataset; /// - `skip_partial parameters` /// - hint `sorted` or not /// - `spilling` or not (TODO, I think a special `MemoryPool` may be needed -/// to support this) +/// to support this) /// pub struct SessionContextGenerator { /// Current testing dataset diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs index 82bfe199234ef..d61835a0804ed 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/data_generator.rs @@ -15,15 +15,34 @@ // specific language governing permissions and limitations // under the License. -use arrow::array::RecordBatch; -use arrow::datatypes::DataType; -use datafusion_common::Result; +use std::sync::Arc; + +use arrow::array::{ArrayRef, RecordBatch}; +use arrow::datatypes::{ + BinaryType, BinaryViewType, BooleanType, ByteArrayType, ByteViewType, DataType, + Date32Type, Date64Type, Decimal128Type, Decimal256Type, Field, Float32Type, + Float64Type, Int16Type, Int32Type, Int64Type, Int8Type, IntervalDayTimeType, + IntervalMonthDayNanoType, IntervalUnit, IntervalYearMonthType, LargeBinaryType, + LargeUtf8Type, Schema, StringViewType, Time32MillisecondType, Time32SecondType, + Time64MicrosecondType, Time64NanosecondType, TimeUnit, TimestampMicrosecondType, + TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, UInt16Type, + UInt32Type, UInt64Type, UInt8Type, Utf8Type, +}; +use datafusion_common::{arrow_datafusion_err, DataFusionError, Result}; use datafusion_physical_expr::{expressions::col, PhysicalSortExpr}; use datafusion_physical_expr_common::sort_expr::LexOrdering; use datafusion_physical_plan::sorts::sort::sort_batch; -use test_utils::stagger_batch; - -use crate::fuzz_cases::record_batch_generator::{ColumnDescr, RecordBatchGenerator}; +use rand::{ + rngs::{StdRng, ThreadRng}, + thread_rng, Rng, SeedableRng, +}; +use test_utils::{ + array_gen::{ + BinaryArrayGenerator, BooleanArrayGenerator, DecimalArrayGenerator, + PrimitiveArrayGenerator, StringArrayGenerator, + }, + stagger_batch, +}; /// Config for Dataset generator /// @@ -33,12 +52,12 @@ use crate::fuzz_cases::record_batch_generator::{ColumnDescr, RecordBatchGenerato /// when you call `generate` function /// /// - `rows_num_range`, the number of rows in the datasets will be randomly generated -/// within this range +/// within this range /// /// - `sort_keys`, if `sort_keys` are defined, when you call the `generate` function, the generator -/// will generate one `base dataset` firstly. Then the `base dataset` will be sorted -/// based on each `sort_key` respectively. And finally `len(sort_keys) + 1` datasets -/// will be returned +/// will generate one `base dataset` firstly. Then the `base dataset` will be sorted +/// based on each `sort_key` respectively. And finally `len(sort_keys) + 1` datasets +/// will be returned /// #[derive(Debug, Clone)] pub struct DatasetGeneratorConfig { @@ -135,7 +154,7 @@ impl DatasetGenerator { } } - pub fn generate(&mut self) -> Result> { + pub fn generate(&self) -> Result> { let mut datasets = Vec::with_capacity(self.sort_keys_set.len() + 1); // Generate the base batch (unsorted) @@ -185,6 +204,553 @@ impl Dataset { } } +#[derive(Debug, Clone)] +pub struct ColumnDescr { + /// Column name + name: String, + + /// Data type of this column + column_type: DataType, + + /// The maximum number of distinct values in this column. + /// + /// See [`ColumnDescr::with_max_num_distinct`] for more information + max_num_distinct: Option, +} + +impl ColumnDescr { + #[inline] + pub fn new(name: &str, column_type: DataType) -> Self { + Self { + name: name.to_string(), + column_type, + max_num_distinct: None, + } + } + + pub fn get_max_num_distinct(&self) -> Option { + self.max_num_distinct + } + + /// set the maximum number of distinct values in this column + /// + /// If `None`, the number of distinct values is randomly selected between 1 + /// and the number of rows. + pub fn with_max_num_distinct(mut self, num_distinct: usize) -> Self { + self.max_num_distinct = Some(num_distinct); + self + } +} + +/// Record batch generator +struct RecordBatchGenerator { + min_rows_nun: usize, + + max_rows_num: usize, + + columns: Vec, + + candidate_null_pcts: Vec, +} + +macro_rules! generate_string_array { + ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE: ident) => {{ + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + let max_len = $BATCH_GEN_RNG.gen_range(1..50); + + let mut generator = StringArrayGenerator { + max_len, + num_strings: $NUM_ROWS, + num_distinct_strings: $MAX_NUM_DISTINCT, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + match $ARROW_TYPE::DATA_TYPE { + DataType::Utf8 => generator.gen_data::(), + DataType::LargeUtf8 => generator.gen_data::(), + DataType::Utf8View => generator.gen_string_view(), + _ => unreachable!(), + } + }}; +} + +macro_rules! generate_decimal_array { + ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT: expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $PRECISION: ident, $SCALE: ident, $ARROW_TYPE: ident) => {{ + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + + let mut generator = DecimalArrayGenerator { + precision: $PRECISION, + scale: $SCALE, + num_decimals: $NUM_ROWS, + num_distinct_decimals: $MAX_NUM_DISTINCT, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + generator.gen_data::<$ARROW_TYPE>() + }}; +} + +// Generating `BooleanArray` due to it being a special type in Arrow (bit-packed) +macro_rules! generate_boolean_array { + ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE: ident) => {{ + // Select a null percentage from the candidate percentages + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + + let num_distinct_booleans = if $MAX_NUM_DISTINCT >= 2 { 2 } else { 1 }; + + let mut generator = BooleanArrayGenerator { + num_booleans: $NUM_ROWS, + num_distinct_booleans, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + generator.gen_data::<$ARROW_TYPE>() + }}; +} + +macro_rules! generate_primitive_array { + ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE:ident) => {{ + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + + let mut generator = PrimitiveArrayGenerator { + num_primitives: $NUM_ROWS, + num_distinct_primitives: $MAX_NUM_DISTINCT, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + generator.gen_data::<$ARROW_TYPE>() + }}; +} + +macro_rules! generate_binary_array { + ( + $SELF:ident, + $NUM_ROWS:ident, + $MAX_NUM_DISTINCT:expr, + $BATCH_GEN_RNG:ident, + $ARRAY_GEN_RNG:ident, + $ARROW_TYPE:ident + ) => {{ + let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); + let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; + + let max_len = $BATCH_GEN_RNG.gen_range(1..100); + + let mut generator = BinaryArrayGenerator { + max_len, + num_binaries: $NUM_ROWS, + num_distinct_binaries: $MAX_NUM_DISTINCT, + null_pct, + rng: $ARRAY_GEN_RNG, + }; + + match $ARROW_TYPE::DATA_TYPE { + DataType::Binary => generator.gen_data::(), + DataType::LargeBinary => generator.gen_data::(), + DataType::BinaryView => generator.gen_binary_view(), + _ => unreachable!(), + } + }}; +} + +impl RecordBatchGenerator { + fn new(min_rows_nun: usize, max_rows_num: usize, columns: Vec) -> Self { + let candidate_null_pcts = vec![0.0, 0.01, 0.1, 0.5]; + + Self { + min_rows_nun, + max_rows_num, + columns, + candidate_null_pcts, + } + } + + fn generate(&self) -> Result { + let mut rng = thread_rng(); + let num_rows = rng.gen_range(self.min_rows_nun..=self.max_rows_num); + let array_gen_rng = StdRng::from_seed(rng.gen()); + + // Build arrays + let mut arrays = Vec::with_capacity(self.columns.len()); + for col in self.columns.iter() { + let array = self.generate_array_of_type( + col, + num_rows, + &mut rng, + array_gen_rng.clone(), + ); + arrays.push(array); + } + + // Build schema + let fields = self + .columns + .iter() + .map(|col| Field::new(col.name.clone(), col.column_type.clone(), true)) + .collect::>(); + let schema = Arc::new(Schema::new(fields)); + + RecordBatch::try_new(schema, arrays).map_err(|e| arrow_datafusion_err!(e)) + } + + fn generate_array_of_type( + &self, + col: &ColumnDescr, + num_rows: usize, + batch_gen_rng: &mut ThreadRng, + array_gen_rng: StdRng, + ) -> ArrayRef { + let num_distinct = if num_rows > 1 { + batch_gen_rng.gen_range(1..num_rows) + } else { + num_rows + }; + // cap to at most the num_distinct values + let max_num_distinct = col + .max_num_distinct + .map(|max| num_distinct.min(max)) + .unwrap_or(num_distinct); + + match col.column_type { + DataType::Int8 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int8Type + ) + } + DataType::Int16 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int16Type + ) + } + DataType::Int32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int32Type + ) + } + DataType::Int64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Int64Type + ) + } + DataType::UInt8 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt8Type + ) + } + DataType::UInt16 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt16Type + ) + } + DataType::UInt32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt32Type + ) + } + DataType::UInt64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + UInt64Type + ) + } + DataType::Float32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Float32Type + ) + } + DataType::Float64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Float64Type + ) + } + DataType::Date32 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Date32Type + ) + } + DataType::Date64 => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Date64Type + ) + } + DataType::Time32(TimeUnit::Second) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time32SecondType + ) + } + DataType::Time32(TimeUnit::Millisecond) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time32MillisecondType + ) + } + DataType::Time64(TimeUnit::Microsecond) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time64MicrosecondType + ) + } + DataType::Time64(TimeUnit::Nanosecond) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Time64NanosecondType + ) + } + DataType::Interval(IntervalUnit::YearMonth) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + IntervalYearMonthType + ) + } + DataType::Interval(IntervalUnit::DayTime) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + IntervalDayTimeType + ) + } + DataType::Interval(IntervalUnit::MonthDayNano) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + IntervalMonthDayNanoType + ) + } + DataType::Timestamp(TimeUnit::Second, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampSecondType + ) + } + DataType::Timestamp(TimeUnit::Millisecond, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampMillisecondType + ) + } + DataType::Timestamp(TimeUnit::Microsecond, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampMicrosecondType + ) + } + DataType::Timestamp(TimeUnit::Nanosecond, None) => { + generate_primitive_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + TimestampNanosecondType + ) + } + DataType::Binary => { + generate_binary_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + BinaryType + ) + } + DataType::LargeBinary => { + generate_binary_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + LargeBinaryType + ) + } + DataType::BinaryView => { + generate_binary_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + BinaryViewType + ) + } + DataType::Decimal128(precision, scale) => { + generate_decimal_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + precision, + scale, + Decimal128Type + ) + } + DataType::Decimal256(precision, scale) => { + generate_decimal_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + precision, + scale, + Decimal256Type + ) + } + DataType::Utf8 => { + generate_string_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + Utf8Type + ) + } + DataType::LargeUtf8 => { + generate_string_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + LargeUtf8Type + ) + } + DataType::Utf8View => { + generate_string_array!( + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + StringViewType + ) + } + DataType::Boolean => { + generate_boolean_array! { + self, + num_rows, + max_num_distinct, + batch_gen_rng, + array_gen_rng, + BooleanType + } + } + _ => { + panic!("Unsupported data generator type: {}", col.column_type) + } + } + } +} + #[cfg(test)] mod test { use arrow::array::UInt32Array; @@ -211,7 +777,7 @@ mod test { sort_keys_set: vec![vec!["b".to_string()]], }; - let mut gen = DatasetGenerator::new(config); + let gen = DatasetGenerator::new(config); let datasets = gen.generate().unwrap(); // Should Generate 2 datasets diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs index 53e9288ab4af6..bb24fb554d65a 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/fuzzer.rs @@ -164,7 +164,7 @@ struct QueryGroup { impl AggregationFuzzer { /// Run the fuzzer, printing an error and panicking if any of the tasks fail - pub async fn run(&mut self) { + pub async fn run(&self) { let res = self.run_inner().await; if let Err(e) = res { @@ -176,7 +176,7 @@ impl AggregationFuzzer { } } - async fn run_inner(&mut self) -> Result<()> { + async fn run_inner(&self) -> Result<()> { let mut join_set = JoinSet::new(); let mut rng = thread_rng(); @@ -270,7 +270,7 @@ impl AggregationFuzzer { /// - `sql`, the selected test sql /// /// - `dataset_ref`, the input dataset, store it for error reported when found -/// the inconsistency between the one for `ctx` and `expected results`. +/// the inconsistency between the one for `ctx` and `expected results`. /// struct AggregationFuzzTestTask { /// Generated session context in current test case @@ -503,9 +503,7 @@ impl QueryBuilder { let distinct = if *is_distinct { "DISTINCT " } else { "" }; alias_gen += 1; - let (order_by, null_opt) = if function_name.eq("first_value") - || function_name.eq("last_value") - { + let (order_by, null_opt) = if function_name.eq("first_value") { ( self.order_by(&order_by_black_list), /* Among the order by columns, at most one group by column can be included to avoid all order by column values being identical */ self.null_opt(), diff --git a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs index bfb3bb096326f..1e42ac1f4b30b 100644 --- a/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs +++ b/datafusion/core/tests/fuzz_cases/aggregation_fuzzer/mod.rs @@ -44,8 +44,7 @@ mod context_generator; mod data_generator; mod fuzzer; -pub use crate::fuzz_cases::record_batch_generator::ColumnDescr; -pub use data_generator::DatasetGeneratorConfig; +pub use data_generator::{ColumnDescr, DatasetGeneratorConfig}; pub use fuzzer::*; #[derive(Debug)] diff --git a/datafusion/core/tests/fuzz_cases/mod.rs b/datafusion/core/tests/fuzz_cases/mod.rs index 8ccc2a5bc1310..d5511e2970f4d 100644 --- a/datafusion/core/tests/fuzz_cases/mod.rs +++ b/datafusion/core/tests/fuzz_cases/mod.rs @@ -20,7 +20,6 @@ mod distinct_count_string_fuzz; mod join_fuzz; mod merge_fuzz; mod sort_fuzz; -mod sort_query_fuzz; mod aggregation_fuzzer; mod equivalence; @@ -30,6 +29,3 @@ mod pruning; mod limit_fuzz; mod sort_preserving_repartition_fuzz; mod window_fuzz; - -// Utility modules -mod record_batch_generator; diff --git a/datafusion/core/tests/fuzz_cases/record_batch_generator.rs b/datafusion/core/tests/fuzz_cases/record_batch_generator.rs deleted file mode 100644 index 9a62a6397d822..0000000000000 --- a/datafusion/core/tests/fuzz_cases/record_batch_generator.rs +++ /dev/null @@ -1,644 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::sync::Arc; - -use arrow::array::{ArrayRef, RecordBatch}; -use arrow::datatypes::{ - BooleanType, DataType, Date32Type, Date64Type, Decimal128Type, Decimal256Type, Field, - Float32Type, Float64Type, Int16Type, Int32Type, Int64Type, Int8Type, - IntervalDayTimeType, IntervalMonthDayNanoType, IntervalUnit, IntervalYearMonthType, - Schema, Time32MillisecondType, Time32SecondType, Time64MicrosecondType, - Time64NanosecondType, TimeUnit, TimestampMicrosecondType, TimestampMillisecondType, - TimestampNanosecondType, TimestampSecondType, UInt16Type, UInt32Type, UInt64Type, - UInt8Type, -}; -use arrow_schema::{ - DECIMAL128_MAX_PRECISION, DECIMAL128_MAX_SCALE, DECIMAL256_MAX_PRECISION, - DECIMAL256_MAX_SCALE, -}; -use datafusion_common::{arrow_datafusion_err, DataFusionError, Result}; -use rand::{rngs::StdRng, thread_rng, Rng, SeedableRng}; -use test_utils::array_gen::{ - BinaryArrayGenerator, BooleanArrayGenerator, DecimalArrayGenerator, - PrimitiveArrayGenerator, StringArrayGenerator, -}; - -/// Columns that are supported by the record batch generator -/// The RNG is used to generate the precision and scale for the decimal columns, thread -/// RNG is not used because this is used in fuzzing and deterministic results are preferred -pub fn get_supported_types_columns(rng_seed: u64) -> Vec { - let mut rng = StdRng::seed_from_u64(rng_seed); - vec![ - ColumnDescr::new("i8", DataType::Int8), - ColumnDescr::new("i16", DataType::Int16), - ColumnDescr::new("i32", DataType::Int32), - ColumnDescr::new("i64", DataType::Int64), - ColumnDescr::new("u8", DataType::UInt8), - ColumnDescr::new("u16", DataType::UInt16), - ColumnDescr::new("u32", DataType::UInt32), - ColumnDescr::new("u64", DataType::UInt64), - ColumnDescr::new("date32", DataType::Date32), - ColumnDescr::new("date64", DataType::Date64), - ColumnDescr::new("time32_s", DataType::Time32(TimeUnit::Second)), - ColumnDescr::new("time32_ms", DataType::Time32(TimeUnit::Millisecond)), - ColumnDescr::new("time64_us", DataType::Time64(TimeUnit::Microsecond)), - ColumnDescr::new("time64_ns", DataType::Time64(TimeUnit::Nanosecond)), - ColumnDescr::new("timestamp_s", DataType::Timestamp(TimeUnit::Second, None)), - ColumnDescr::new( - "timestamp_ms", - DataType::Timestamp(TimeUnit::Millisecond, None), - ), - ColumnDescr::new( - "timestamp_us", - DataType::Timestamp(TimeUnit::Microsecond, None), - ), - ColumnDescr::new( - "timestamp_ns", - DataType::Timestamp(TimeUnit::Nanosecond, None), - ), - ColumnDescr::new("float32", DataType::Float32), - ColumnDescr::new("float64", DataType::Float64), - ColumnDescr::new( - "interval_year_month", - DataType::Interval(IntervalUnit::YearMonth), - ), - ColumnDescr::new( - "interval_day_time", - DataType::Interval(IntervalUnit::DayTime), - ), - ColumnDescr::new( - "interval_month_day_nano", - DataType::Interval(IntervalUnit::MonthDayNano), - ), - ColumnDescr::new("decimal128", { - let precision: u8 = rng.gen_range(1..=DECIMAL128_MAX_PRECISION); - let scale: i8 = rng.gen_range( - i8::MIN..=std::cmp::min(precision as i8, DECIMAL128_MAX_SCALE), - ); - DataType::Decimal128(precision, scale) - }), - ColumnDescr::new("decimal256", { - let precision: u8 = rng.gen_range(1..=DECIMAL256_MAX_PRECISION); - let scale: i8 = rng.gen_range( - i8::MIN..=std::cmp::min(precision as i8, DECIMAL256_MAX_SCALE), - ); - DataType::Decimal256(precision, scale) - }), - ColumnDescr::new("utf8", DataType::Utf8), - ColumnDescr::new("largeutf8", DataType::LargeUtf8), - ColumnDescr::new("utf8view", DataType::Utf8View), - ColumnDescr::new("u8_low", DataType::UInt8).with_max_num_distinct(10), - ColumnDescr::new("utf8_low", DataType::Utf8).with_max_num_distinct(10), - ColumnDescr::new("bool", DataType::Boolean), - ColumnDescr::new("binary", DataType::Binary), - ColumnDescr::new("large_binary", DataType::LargeBinary), - ColumnDescr::new("binaryview", DataType::BinaryView), - ] -} - -#[derive(Debug, Clone)] -pub struct ColumnDescr { - /// Column name - pub name: String, - - /// Data type of this column - pub column_type: DataType, - - /// The maximum number of distinct values in this column. - /// - /// See [`ColumnDescr::with_max_num_distinct`] for more information - max_num_distinct: Option, -} - -impl ColumnDescr { - #[inline] - pub fn new(name: &str, column_type: DataType) -> Self { - Self { - name: name.to_string(), - column_type, - max_num_distinct: None, - } - } - - pub fn get_max_num_distinct(&self) -> Option { - self.max_num_distinct - } - - /// set the maximum number of distinct values in this column - /// - /// If `None`, the number of distinct values is randomly selected between 1 - /// and the number of rows. - pub fn with_max_num_distinct(mut self, num_distinct: usize) -> Self { - self.max_num_distinct = Some(num_distinct); - self - } -} - -/// Record batch generator -pub struct RecordBatchGenerator { - pub min_rows_num: usize, - - pub max_rows_num: usize, - - pub columns: Vec, - - pub candidate_null_pcts: Vec, - - /// If a seed is provided when constructing the generator, it will be used to - /// create `rng` and the pseudo-randomly generated batches will be deterministic. - /// Otherwise, `rng` will be initialized using `thread_rng()` and the batches - /// generated will be different each time. - rng: StdRng, -} - -macro_rules! generate_decimal_array { - ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT: expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $PRECISION: ident, $SCALE: ident, $ARROW_TYPE: ident) => {{ - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - - let mut generator = DecimalArrayGenerator { - precision: $PRECISION, - scale: $SCALE, - num_decimals: $NUM_ROWS, - num_distinct_decimals: $MAX_NUM_DISTINCT, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - generator.gen_data::<$ARROW_TYPE>() - }}; -} - -// Generating `BooleanArray` due to it being a special type in Arrow (bit-packed) -macro_rules! generate_boolean_array { - ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE: ident) => {{ - // Select a null percentage from the candidate percentages - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - - let num_distinct_booleans = if $MAX_NUM_DISTINCT >= 2 { 2 } else { 1 }; - - let mut generator = BooleanArrayGenerator { - num_booleans: $NUM_ROWS, - num_distinct_booleans, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - generator.gen_data::<$ARROW_TYPE>() - }}; -} - -macro_rules! generate_primitive_array { - ($SELF:ident, $NUM_ROWS:ident, $MAX_NUM_DISTINCT:expr, $BATCH_GEN_RNG:ident, $ARRAY_GEN_RNG:ident, $ARROW_TYPE:ident) => {{ - let null_pct_idx = $BATCH_GEN_RNG.gen_range(0..$SELF.candidate_null_pcts.len()); - let null_pct = $SELF.candidate_null_pcts[null_pct_idx]; - - let mut generator = PrimitiveArrayGenerator { - num_primitives: $NUM_ROWS, - num_distinct_primitives: $MAX_NUM_DISTINCT, - null_pct, - rng: $ARRAY_GEN_RNG, - }; - - generator.gen_data::<$ARROW_TYPE>() - }}; -} - -impl RecordBatchGenerator { - /// Create a new `RecordBatchGenerator` with a random seed. The generated - /// batches will be different each time. - pub fn new( - min_rows_nun: usize, - max_rows_num: usize, - columns: Vec, - ) -> Self { - let candidate_null_pcts = vec![0.0, 0.01, 0.1, 0.5]; - - Self { - min_rows_num: min_rows_nun, - max_rows_num, - columns, - candidate_null_pcts, - rng: StdRng::from_rng(thread_rng()).unwrap(), - } - } - - /// Set a seed for the generator. The pseudo-randomly generated batches will be - /// deterministic for the same seed. - pub fn with_seed(mut self, seed: u64) -> Self { - self.rng = StdRng::seed_from_u64(seed); - self - } - - pub fn generate(&mut self) -> Result { - let num_rows = self.rng.gen_range(self.min_rows_num..=self.max_rows_num); - let array_gen_rng = StdRng::from_seed(self.rng.gen()); - let mut batch_gen_rng = StdRng::from_seed(self.rng.gen()); - let columns = self.columns.clone(); - - // Build arrays - let mut arrays = Vec::with_capacity(columns.len()); - for col in columns.iter() { - let array = self.generate_array_of_type( - col, - num_rows, - &mut batch_gen_rng, - array_gen_rng.clone(), - ); - arrays.push(array); - } - - // Build schema - let fields = self - .columns - .iter() - .map(|col| Field::new(col.name.clone(), col.column_type.clone(), true)) - .collect::>(); - let schema = Arc::new(Schema::new(fields)); - - RecordBatch::try_new(schema, arrays).map_err(|e| arrow_datafusion_err!(e)) - } - - fn generate_array_of_type( - &mut self, - col: &ColumnDescr, - num_rows: usize, - batch_gen_rng: &mut StdRng, - array_gen_rng: StdRng, - ) -> ArrayRef { - let num_distinct = if num_rows > 1 { - batch_gen_rng.gen_range(1..num_rows) - } else { - num_rows - }; - // cap to at most the num_distinct values - let max_num_distinct = col - .max_num_distinct - .map(|max| num_distinct.min(max)) - .unwrap_or(num_distinct); - - match col.column_type { - DataType::Int8 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int8Type - ) - } - DataType::Int16 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int16Type - ) - } - DataType::Int32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int32Type - ) - } - DataType::Int64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Int64Type - ) - } - DataType::UInt8 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt8Type - ) - } - DataType::UInt16 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt16Type - ) - } - DataType::UInt32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt32Type - ) - } - DataType::UInt64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - UInt64Type - ) - } - DataType::Float32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Float32Type - ) - } - DataType::Float64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Float64Type - ) - } - DataType::Date32 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Date32Type - ) - } - DataType::Date64 => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Date64Type - ) - } - DataType::Time32(TimeUnit::Second) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time32SecondType - ) - } - DataType::Time32(TimeUnit::Millisecond) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time32MillisecondType - ) - } - DataType::Time64(TimeUnit::Microsecond) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time64MicrosecondType - ) - } - DataType::Time64(TimeUnit::Nanosecond) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - Time64NanosecondType - ) - } - DataType::Interval(IntervalUnit::YearMonth) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - IntervalYearMonthType - ) - } - DataType::Interval(IntervalUnit::DayTime) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - IntervalDayTimeType - ) - } - DataType::Interval(IntervalUnit::MonthDayNano) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - IntervalMonthDayNanoType - ) - } - DataType::Timestamp(TimeUnit::Second, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampSecondType - ) - } - DataType::Timestamp(TimeUnit::Millisecond, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampMillisecondType - ) - } - DataType::Timestamp(TimeUnit::Microsecond, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampMicrosecondType - ) - } - DataType::Timestamp(TimeUnit::Nanosecond, None) => { - generate_primitive_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - TimestampNanosecondType - ) - } - DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => { - let null_pct_idx = - batch_gen_rng.gen_range(0..self.candidate_null_pcts.len()); - let null_pct = self.candidate_null_pcts[null_pct_idx]; - let max_len = batch_gen_rng.gen_range(1..50); - - let mut generator = StringArrayGenerator { - max_len, - num_strings: num_rows, - num_distinct_strings: max_num_distinct, - null_pct, - rng: array_gen_rng, - }; - - match col.column_type { - DataType::Utf8 => generator.gen_data::(), - DataType::LargeUtf8 => generator.gen_data::(), - DataType::Utf8View => generator.gen_string_view(), - _ => unreachable!(), - } - } - DataType::Binary | DataType::LargeBinary | DataType::BinaryView => { - let null_pct_idx = - batch_gen_rng.gen_range(0..self.candidate_null_pcts.len()); - let null_pct = self.candidate_null_pcts[null_pct_idx]; - let max_len = batch_gen_rng.gen_range(1..100); - - let mut generator = BinaryArrayGenerator { - max_len, - num_binaries: num_rows, - num_distinct_binaries: max_num_distinct, - null_pct, - rng: array_gen_rng, - }; - - match col.column_type { - DataType::Binary => generator.gen_data::(), - DataType::LargeBinary => generator.gen_data::(), - DataType::BinaryView => generator.gen_binary_view(), - _ => unreachable!(), - } - } - DataType::Decimal128(precision, scale) => { - generate_decimal_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - precision, - scale, - Decimal128Type - ) - } - DataType::Decimal256(precision, scale) => { - generate_decimal_array!( - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - precision, - scale, - Decimal256Type - ) - } - DataType::Boolean => { - generate_boolean_array! { - self, - num_rows, - max_num_distinct, - batch_gen_rng, - array_gen_rng, - BooleanType - } - } - _ => { - panic!("Unsupported data generator type: {}", col.column_type) - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_generator_with_fixed_seed_deterministic() { - let mut gen1 = RecordBatchGenerator::new( - 16, - 32, - vec![ - ColumnDescr::new("a", DataType::Utf8), - ColumnDescr::new("b", DataType::UInt32), - ], - ) - .with_seed(310104); - - let mut gen2 = RecordBatchGenerator::new( - 16, - 32, - vec![ - ColumnDescr::new("a", DataType::Utf8), - ColumnDescr::new("b", DataType::UInt32), - ], - ) - .with_seed(310104); - - let batch1 = gen1.generate().unwrap(); - let batch2 = gen2.generate().unwrap(); - - let batch1_formatted = format!("{:?}", batch1); - let batch2_formatted = format!("{:?}", batch2); - - assert_eq!(batch1_formatted, batch2_formatted); - } -} diff --git a/datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs b/datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs deleted file mode 100644 index 1319d4817326d..0000000000000 --- a/datafusion/core/tests/fuzz_cases/sort_query_fuzz.rs +++ /dev/null @@ -1,625 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//! Fuzz Test for various corner cases sorting RecordBatches exceeds available memory and should spill - -use std::cmp::min; -use std::sync::Arc; - -use arrow::array::RecordBatch; -use arrow_schema::SchemaRef; -use datafusion::datasource::MemTable; -use datafusion::prelude::{SessionConfig, SessionContext}; -use datafusion_common::{instant::Instant, Result}; -use datafusion_execution::memory_pool::{ - human_readable_size, MemoryPool, UnboundedMemoryPool, -}; -use datafusion_expr::display_schema; -use datafusion_physical_plan::spill::get_record_batch_memory_size; -use rand::seq::SliceRandom; -use std::time::Duration; - -use datafusion_execution::{ - disk_manager::DiskManagerConfig, memory_pool::FairSpillPool, - runtime_env::RuntimeEnvBuilder, -}; -use rand::Rng; -use rand::{rngs::StdRng, SeedableRng}; - -use crate::fuzz_cases::aggregation_fuzzer::check_equality_of_batches; - -use super::aggregation_fuzzer::ColumnDescr; -use super::record_batch_generator::{get_supported_types_columns, RecordBatchGenerator}; - -/// Entry point for executing the sort query fuzzer. -/// -/// Now memory limiting is disabled by default. See TODOs in `SortQueryFuzzer`. -#[tokio::test(flavor = "multi_thread")] -async fn sort_query_fuzzer_runner() { - let random_seed = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_secs(); - let test_generator = SortFuzzerTestGenerator::new( - 2000, - 3, - "sort_fuzz_table".to_string(), - get_supported_types_columns(random_seed), - false, - random_seed, - ); - let mut fuzzer = SortQueryFuzzer::new(random_seed) - // Configs for how many random query to test - .with_max_rounds(Some(5)) - .with_queries_per_round(4) - .with_config_variations_per_query(5) - // Will stop early if the time limit is reached - .with_time_limit(Duration::from_secs(5)) - .with_test_generator(test_generator); - - fuzzer.run().await.unwrap(); -} - -/// SortQueryFuzzer holds the runner configuration for executing sort query fuzz tests. The fuzzing details are managed inside `SortFuzzerTestGenerator`. -/// -/// It defines: -/// - `max_rounds`: Maximum number of rounds to run (or None to run until `time_limit`). -/// - `queries_per_round`: Number of different queries to run in each round. -/// - `config_variations_per_query`: Number of different configurations to test per query. -/// - `time_limit`: Time limit for the entire fuzzer execution. -/// -/// TODO: The following improvements are blocked on https://github.com/apache/datafusion/issues/14748: -/// 1. Support generating queries with arbitrary number of ORDER BY clauses -/// Currently limited to be smaller than number of projected columns -/// 2. Enable special type columns like utf8_low to be used in ORDER BY clauses -/// 3. Enable memory limiting functionality in the fuzzer runner -pub struct SortQueryFuzzer { - test_gen: SortFuzzerTestGenerator, - /// Random number generator for the runner, used to generate seeds for inner components. - /// Seeds for each choice (query, config, etc.) are printed out for reproducibility. - runner_rng: StdRng, - - // ======================================================================== - // Runner configurations - // ======================================================================== - /// For each round, a new dataset is generated. If `None`, keep running until - /// the time limit is reached - max_rounds: Option, - /// How many different queries to run in each round - queries_per_round: usize, - /// For each query, how many different configurations to try and make sure their - /// results are consistent - config_variations_per_query: usize, - /// The time limit for the entire sort query fuzzer execution. - time_limit: Option, -} - -impl SortQueryFuzzer { - pub fn new(seed: u64) -> Self { - let max_rounds = Some(2); - let queries_per_round = 3; - let config_variations_per_query = 5; - let time_limit = None; - - // Filtered out one column due to a known bug https://github.com/apache/datafusion/issues/14748 - // TODO: Remove this once the bug is fixed - let candidate_columns = get_supported_types_columns(seed) - .into_iter() - .filter(|col| { - col.name != "utf8_low" - && col.name != "utf8view" - && col.name != "binaryview" - }) - .collect::>(); - - let test_gen = SortFuzzerTestGenerator::new( - 10000, - 4, - "sort_fuzz_table".to_string(), - candidate_columns, - false, - seed, - ); - - Self { - max_rounds, - queries_per_round, - config_variations_per_query, - time_limit, - test_gen, - runner_rng: StdRng::seed_from_u64(seed), - } - } - - pub fn with_test_generator(mut self, test_gen: SortFuzzerTestGenerator) -> Self { - self.test_gen = test_gen; - self - } - - pub fn with_max_rounds(mut self, max_rounds: Option) -> Self { - self.max_rounds = max_rounds; - self - } - - pub fn with_queries_per_round(mut self, queries_per_round: usize) -> Self { - self.queries_per_round = queries_per_round; - self - } - - pub fn with_config_variations_per_query( - mut self, - config_variations_per_query: usize, - ) -> Self { - self.config_variations_per_query = config_variations_per_query; - self - } - - pub fn with_time_limit(mut self, time_limit: Duration) -> Self { - self.time_limit = Some(time_limit); - self - } - - fn should_stop_due_to_time_limit( - &self, - start_time: Instant, - n_round: usize, - n_query: usize, - ) -> bool { - if let Some(time_limit) = self.time_limit { - if Instant::now().duration_since(start_time) > time_limit { - println!( - "[SortQueryFuzzer] Time limit reached: {} queries ({} random configs each) in {} rounds", - n_round * self.queries_per_round + n_query, - self.config_variations_per_query, - n_round - ); - return true; - } - } - false - } - - pub async fn run(&mut self) -> Result<()> { - let start_time = Instant::now(); - - // Execute until either`max_rounds` or `time_limit` is reached - let max_rounds = self.max_rounds.unwrap_or(usize::MAX); - for round in 0..max_rounds { - let init_seed = self.runner_rng.gen(); - for query_i in 0..self.queries_per_round { - let query_seed = self.runner_rng.gen(); - let mut expected_results: Option> = None; // use first config's result as the expected result - for config_i in 0..self.config_variations_per_query { - if self.should_stop_due_to_time_limit(start_time, round, query_i) { - return Ok(()); - } - - let config_seed = self.runner_rng.gen(); - - println!( - "[SortQueryFuzzer] Round {}, Query {} (Config {})", - round, query_i, config_i - ); - println!(" Seeds:"); - println!(" init_seed = {}", init_seed); - println!(" query_seed = {}", query_seed); - println!(" config_seed = {}", config_seed); - - let results = self - .test_gen - .fuzzer_run(init_seed, query_seed, config_seed) - .await?; - println!("\n"); // Seperator between tested runs - - if expected_results.is_none() { - expected_results = Some(results); - } else if let Some(ref expected) = expected_results { - // `fuzzer_run` might append `LIMIT k` to either the - // expected or actual query. The number of results is - // checked inside `fuzzer_run()`. Here we only check - // that the first k rows of each result are consistent. - check_equality_of_batches(expected, &results).unwrap(); - } else { - unreachable!(); - } - } - } - } - Ok(()) - } -} - -/// Struct to generate and manage a random dataset for fuzz testing. -/// It is able to re-run the failed test cases by setting the same seed printed out. -/// See the unit tests for examples. -/// -/// To use this struct: -/// 1. Call `init_partitioned_staggered_batches` to generate a random dataset. -/// 2. Use `generate_random_query` to create a random SQL query. -/// 3. Use `generate_random_config` to create a random configuration. -/// 4. Run the fuzzer check with the generated query and configuration. -pub struct SortFuzzerTestGenerator { - /// The total number of rows for the registered table - num_rows: usize, - /// Max number of partitions for the registered table - max_partitions: usize, - /// The name of the registered table - table_name: String, - /// The selected columns from all available candidate columns to be used for - /// this dataset - selected_columns: Vec, - /// If true, will randomly generate a memory limit for the query. Otherwise - /// the query will run under the context with unlimited memory. - set_memory_limit: bool, - - /// States related to the randomly generated dataset. `None` if not initialized - /// by calling `init_partitioned_staggered_batches()` - dataset_state: Option, -} - -/// Struct to hold states related to the randomly generated dataset -pub struct DatasetState { - /// Dataset to construct the partitioned memory table. Outer vector is the - /// partitions, inner vector is staggered batches within the same partition. - partitioned_staggered_batches: Vec>, - /// Number of rows in the whole dataset - dataset_size: usize, - /// The approximate number of rows of a batch (staggered batches will be generated - /// with random number of rows between 1 and `approx_batch_size`) - approx_batch_num_rows: usize, - /// The schema of the dataset - schema: SchemaRef, - /// The memory size of the whole dataset - mem_size: usize, -} - -impl SortFuzzerTestGenerator { - /// Randomly pick a subset of `candidate_columns` to be used for this dataset - pub fn new( - num_rows: usize, - max_partitions: usize, - table_name: String, - candidate_columns: Vec, - set_memory_limit: bool, - rng_seed: u64, - ) -> Self { - let mut rng = StdRng::seed_from_u64(rng_seed); - let min_ncol = min(candidate_columns.len(), 5); - let max_ncol = min(candidate_columns.len(), 10); - let amount = rng.gen_range(min_ncol..=max_ncol); - let selected_columns = candidate_columns - .choose_multiple(&mut rng, amount) - .cloned() - .collect(); - - Self { - num_rows, - max_partitions, - table_name, - selected_columns, - set_memory_limit, - dataset_state: None, - } - } - - /// The outer vector is the partitions, the inner vector is the chunked batches - /// within each partition. - /// The partition number is determined by `self.max_partitions`. - /// The chunked batch length is a random number between 1 and `self.num_rows` / - /// 100 (make sure a single batch won't exceed memory budget for external sort - /// executions) - /// - /// Hack: If we want the query to run under certain degree of parallelism, the - /// memory table should be generated with more partitions, due to https://github.com/apache/datafusion/issues/15088 - fn init_partitioned_staggered_batches(&mut self, rng_seed: u64) { - let mut rng = StdRng::seed_from_u64(rng_seed); - let num_partitions = rng.gen_range(1..=self.max_partitions); - - let max_batch_size = self.num_rows / num_partitions / 50; - let target_partition_size = self.num_rows / num_partitions; - - let mut partitions = Vec::new(); - let mut schema = None; - for _ in 0..num_partitions { - let mut partition = Vec::new(); - let mut num_rows = 0; - - // For each partition, generate random batches until there is about enough - // rows for the specified total number of rows - while num_rows < target_partition_size { - // Generate a random batch of size between 1 and max_batch_size - - // Let edge case (1-row batch) more common - let (min_nrow, max_nrow) = if rng.gen_bool(0.1) { - (1, 3) - } else { - (1, max_batch_size) - }; - - let mut record_batch_generator = RecordBatchGenerator::new( - min_nrow, - max_nrow, - self.selected_columns.clone(), - ) - .with_seed(rng.gen()); - - let record_batch = record_batch_generator.generate().unwrap(); - num_rows += record_batch.num_rows(); - - if schema.is_none() { - schema = Some(record_batch.schema()); - println!(" Dataset schema:"); - println!(" {}", display_schema(schema.as_ref().unwrap())); - } - - partition.push(record_batch); - } - - partitions.push(partition); - } - - // After all partitions are created, optionally make one partition have 0/1 batch - if num_partitions > 2 && rng.gen_bool(0.1) { - let partition_index = rng.gen_range(0..num_partitions); - if rng.gen_bool(0.5) { - // 0 batch - partitions[partition_index] = Vec::new(); - } else { - // 1 batch, keep the old first batch - let first_batch = partitions[partition_index].first().cloned(); - if let Some(batch) = first_batch { - partitions[partition_index] = vec![batch]; - } - } - } - - // Init self fields - let mem_size: usize = partitions - .iter() - .map(|partition| { - partition - .iter() - .map(get_record_batch_memory_size) - .sum::() - }) - .sum(); - - let dataset_size = partitions - .iter() - .map(|partition| { - partition - .iter() - .map(|batch| batch.num_rows()) - .sum::() - }) - .sum::(); - - let approx_batch_num_rows = max_batch_size; - - self.dataset_state = Some(DatasetState { - partitioned_staggered_batches: partitions, - dataset_size, - approx_batch_num_rows, - schema: schema.unwrap(), - mem_size, - }); - } - - /// Generates a random SQL query string and an optional limit value. - /// Returns a tuple containing the query string and an optional limit. - pub fn generate_random_query(&self, rng_seed: u64) -> (String, Option) { - let mut rng = StdRng::seed_from_u64(rng_seed); - - let num_columns = rng.gen_range(1..=3).min(self.selected_columns.len()); - let selected_columns: Vec<_> = self - .selected_columns - .choose_multiple(&mut rng, num_columns) - .collect(); - - let mut order_by_clauses = Vec::new(); - for col in selected_columns { - let mut clause = col.name.clone(); - if rng.gen_bool(0.5) { - let order = if rng.gen_bool(0.5) { "ASC" } else { "DESC" }; - clause.push_str(&format!(" {}", order)); - } - if rng.gen_bool(0.5) { - let nulls = if rng.gen_bool(0.5) { - "NULLS FIRST" - } else { - "NULLS LAST" - }; - clause.push_str(&format!(" {}", nulls)); - } - order_by_clauses.push(clause); - } - - let dataset_size = self.dataset_state.as_ref().unwrap().dataset_size; - - let limit = if rng.gen_bool(0.2) { - // Prefer edge cases for k like 1, dataset_size, etc. - Some(if rng.gen_bool(0.5) { - let edge_cases = - [1, 2, 3, dataset_size - 1, dataset_size, dataset_size + 1]; - *edge_cases.choose(&mut rng).unwrap() - } else { - rng.gen_range(1..=dataset_size) - }) - } else { - None - }; - - let limit_clause = limit.map_or(String::new(), |l| format!(" LIMIT {}", l)); - - let query = format!( - "SELECT * FROM {} ORDER BY {}{}", - self.table_name, - order_by_clauses.join(", "), - limit_clause - ); - - (query, limit) - } - - pub fn generate_random_config( - &self, - rng_seed: u64, - with_memory_limit: bool, - ) -> Result { - let mut rng = StdRng::seed_from_u64(rng_seed); - let init_state = self.dataset_state.as_ref().unwrap(); - let dataset_size = init_state.mem_size; - let num_partitions = init_state.partitioned_staggered_batches.len(); - - // 30% to 200% of the dataset size (if `with_memory_limit` is false, config - // will use the default unbounded pool to override it later) - let memory_limit = rng.gen_range( - (dataset_size as f64 * 0.5) as usize..=(dataset_size as f64 * 2.0) as usize, - ); - // 10% to 20% of the per-partition memory limit size - let per_partition_mem_limit = memory_limit / num_partitions; - let sort_spill_reservation_bytes = rng.gen_range( - (per_partition_mem_limit as f64 * 0.2) as usize - ..=(per_partition_mem_limit as f64 * 0.3) as usize, - ); - - // 1 to 3 times of the approx batch size. Setting this to a very large nvalue - // will cause external sort to fail. - let sort_in_place_threshold_bytes = if with_memory_limit { - // For memory-limited query, setting `sort_in_place_threshold_bytes` too - // large will cause failure. - 0 - } else { - let dataset_size = self.dataset_state.as_ref().unwrap().dataset_size; - rng.gen_range(0..=dataset_size * 2_usize) - }; - - // Set up strings for printing - let memory_limit_str = if with_memory_limit { - human_readable_size(memory_limit) - } else { - "Unbounded".to_string() - }; - let per_partition_limit_str = if with_memory_limit { - human_readable_size(per_partition_mem_limit) - } else { - "Unbounded".to_string() - }; - - println!(" Config: "); - println!(" Dataset size: {}", human_readable_size(dataset_size)); - println!(" Number of partitions: {}", num_partitions); - println!(" Batch size: {}", init_state.approx_batch_num_rows / 2); - println!(" Memory limit: {}", memory_limit_str); - println!( - " Per partition memory limit: {}", - per_partition_limit_str - ); - println!( - " Sort spill reservation bytes: {}", - human_readable_size(sort_spill_reservation_bytes) - ); - println!( - " Sort in place threshold bytes: {}", - human_readable_size(sort_in_place_threshold_bytes) - ); - - let config = SessionConfig::new() - .with_target_partitions(num_partitions) - .with_batch_size(init_state.approx_batch_num_rows / 2) - .with_sort_spill_reservation_bytes(sort_spill_reservation_bytes) - .with_sort_in_place_threshold_bytes(sort_in_place_threshold_bytes); - - let memory_pool: Arc = if with_memory_limit { - Arc::new(FairSpillPool::new(memory_limit)) - } else { - Arc::new(UnboundedMemoryPool::default()) - }; - - let runtime = RuntimeEnvBuilder::new() - .with_memory_pool(memory_pool) - .with_disk_manager(DiskManagerConfig::NewOs) - .build_arc()?; - - let ctx = SessionContext::new_with_config_rt(config, runtime); - - let dataset = &init_state.partitioned_staggered_batches; - let schema = &init_state.schema; - - let provider = MemTable::try_new(schema.clone(), dataset.clone())?; - ctx.register_table("sort_fuzz_table", Arc::new(provider))?; - - Ok(ctx) - } - - async fn fuzzer_run( - &mut self, - dataset_seed: u64, - query_seed: u64, - config_seed: u64, - ) -> Result> { - self.init_partitioned_staggered_batches(dataset_seed); - let (query_str, limit) = self.generate_random_query(query_seed); - println!(" Query:"); - println!(" {}", query_str); - - // ==== Execute the query ==== - - // Only enable memory limits if: - // 1. Query does not contain LIMIT (since topK does not support external execution) - // 2. Memory limiting is enabled in the test generator config - let with_mem_limit = !query_str.contains("LIMIT") && self.set_memory_limit; - - let ctx = self.generate_random_config(config_seed, with_mem_limit)?; - let df = ctx.sql(&query_str).await.unwrap(); - let results = df.collect().await.unwrap(); - - // ==== Check the result size is consistent with the limit ==== - let result_num_rows = results.iter().map(|batch| batch.num_rows()).sum::(); - let dataset_size = self.dataset_state.as_ref().unwrap().dataset_size; - - if let Some(limit) = limit { - let expected_num_rows = min(limit, dataset_size); - assert_eq!(result_num_rows, expected_num_rows); - } - - Ok(results) - } -} - -#[cfg(test)] -mod test { - use super::*; - - /// Given the same seed, the result should be the same - #[tokio::test] - async fn test_sort_query_fuzzer_deterministic() { - let gen_seed = 310104; - let mut test_generator = SortFuzzerTestGenerator::new( - 2000, - 3, - "sort_fuzz_table".to_string(), - get_supported_types_columns(gen_seed), - false, - gen_seed, - ); - - let res1 = test_generator.fuzzer_run(1, 2, 3).await.unwrap(); - let res2 = test_generator.fuzzer_run(1, 2, 3).await.unwrap(); - check_equality_of_batches(&res1, &res2).unwrap(); - } -} diff --git a/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs b/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs index 7b157b707a6de..bdf30c140afff 100644 --- a/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs +++ b/datafusion/core/tests/memory_limit/memory_limit_validation/utils.rs @@ -18,7 +18,7 @@ use datafusion_common_runtime::SpawnedTask; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, System}; +use sysinfo::System; use tokio::time::{interval, Duration}; use datafusion::prelude::{SessionConfig, SessionContext}; @@ -62,11 +62,7 @@ where loop { interval.tick().await; - sys.refresh_processes_specifics( - ProcessesToUpdate::Some(&[pid]), - true, - ProcessRefreshKind::nothing().with_memory(), - ); + sys.refresh_all(); if let Some(process) = sys.process(pid) { let rss_bytes = process.memory(); max_rss_clone @@ -120,8 +116,8 @@ where /// # Example /// /// utils::validate_query_with_memory_limits( -/// 40_000_000 * 2, -/// Some(40_000_000), +/// 40_000_000 * 2, +/// Some(40_000_000), /// "SELECT * FROM generate_series(1, 100000000) AS t(i) ORDER BY i", /// "SELECT * FROM generate_series(1, 10000000) AS t(i) ORDER BY i" /// ); diff --git a/datafusion/core/tests/memory_limit/mod.rs b/datafusion/core/tests/memory_limit/mod.rs index 01342d1604fca..dd5acc8d8908a 100644 --- a/datafusion/core/tests/memory_limit/mod.rs +++ b/datafusion/core/tests/memory_limit/mod.rs @@ -44,14 +44,11 @@ use datafusion_common::{assert_contains, Result}; use datafusion_execution::memory_pool::{ FairSpillPool, GreedyMemoryPool, MemoryPool, TrackConsumersPool, }; -use datafusion_execution::runtime_env::RuntimeEnv; -use datafusion_execution::{DiskManager, TaskContext}; +use datafusion_execution::TaskContext; use datafusion_expr::{Expr, TableType}; use datafusion_physical_expr::{LexOrdering, PhysicalSortExpr}; use datafusion_physical_optimizer::join_selection::JoinSelection; use datafusion_physical_optimizer::PhysicalOptimizerRule; -use datafusion_physical_plan::collect as collect_batches; -use datafusion_physical_plan::common::collect; use datafusion_physical_plan::spill::get_record_batch_memory_size; use rand::Rng; use test_utils::AccessLogGenerator; @@ -496,125 +493,6 @@ async fn test_in_mem_buffer_almost_full() { let _ = df.collect().await.unwrap(); } -/// External sort should be able to run if there is very little pre-reserved memory -/// for merge (set configuration sort_spill_reservation_bytes to 0). -#[tokio::test] -async fn test_external_sort_zero_merge_reservation() { - let config = SessionConfig::new() - .with_sort_spill_reservation_bytes(0) - .with_target_partitions(14); - let runtime = RuntimeEnvBuilder::new() - .with_memory_pool(Arc::new(FairSpillPool::new(10 * 1024 * 1024))) - .build_arc() - .unwrap(); - - let ctx = SessionContext::new_with_config_rt(config, runtime); - - let query = "select * from generate_series(1,10000000) as t1(v1) order by v1;"; - let df = ctx.sql(query).await.unwrap(); - - let physical_plan = df.create_physical_plan().await.unwrap(); - let task_ctx = Arc::new(TaskContext::from(&ctx.state())); - let stream = physical_plan.execute(0, task_ctx).unwrap(); - - // Ensures execution succeed - let _result = collect(stream).await; - - // Ensures the query spilled during execution - let metrics = physical_plan.metrics().unwrap(); - let spill_count = metrics.spill_count().unwrap(); - assert!(spill_count > 0); -} - -// Tests for disk limit (`max_temp_directory_size` in `DiskManager`) -// ------------------------------------------------------------------ - -// Create a new `SessionContext` with speicified disk limit and memory pool limit -async fn setup_context( - disk_limit: u64, - memory_pool_limit: usize, -) -> Result { - let disk_manager = DiskManager::try_new(DiskManagerConfig::NewOs)?; - - let disk_manager = Arc::try_unwrap(disk_manager) - .expect("DiskManager should be a single instance") - .with_max_temp_directory_size(disk_limit)?; - - let runtime = RuntimeEnvBuilder::new() - .with_memory_pool(Arc::new(FairSpillPool::new(memory_pool_limit))) - .build_arc() - .unwrap(); - - let runtime = Arc::new(RuntimeEnv { - memory_pool: runtime.memory_pool.clone(), - disk_manager: Arc::new(disk_manager), - cache_manager: runtime.cache_manager.clone(), - object_store_registry: runtime.object_store_registry.clone(), - }); - - let config = SessionConfig::new() - .with_sort_spill_reservation_bytes(64 * 1024) // 256KB - .with_sort_in_place_threshold_bytes(0) - .with_batch_size(64) // To reduce test memory usage - .with_target_partitions(1); - - Ok(SessionContext::new_with_config_rt(config, runtime)) -} - -/// If the spilled bytes exceed the disk limit, the query should fail -/// (specified by `max_temp_directory_size` in `DiskManager`) -#[tokio::test] -async fn test_disk_spill_limit_reached() -> Result<()> { - let ctx = setup_context(1024 * 1024, 1024 * 1024).await?; // 1MB disk limit, 1MB memory limit - - let df = ctx - .sql("select * from generate_series(1, 1000000000000) as t1(v1) order by v1") - .await - .unwrap(); - - let err = df.collect().await.unwrap_err(); - assert_contains!( - err.to_string(), - "The used disk space during the spilling process has exceeded the allowable limit" - ); - - Ok(()) -} - -/// External query should succeed, if the spilled bytes is less than the disk limit -/// Also verify that after the query is finished, all the disk usage accounted by -/// tempfiles are cleaned up. -#[tokio::test] -async fn test_disk_spill_limit_not_reached() -> Result<()> { - let disk_spill_limit = 1024 * 1024; // 1MB - let ctx = setup_context(disk_spill_limit, 128 * 1024).await?; // 1MB disk limit, 128KB memory limit - - let df = ctx - .sql("select * from generate_series(1, 10000) as t1(v1) order by v1") - .await - .unwrap(); - let plan = df.create_physical_plan().await.unwrap(); - - let task_ctx = ctx.task_ctx(); - let _ = collect_batches(Arc::clone(&plan), task_ctx) - .await - .expect("Query execution failed"); - - let spill_count = plan.metrics().unwrap().spill_count().unwrap(); - let spilled_bytes = plan.metrics().unwrap().spilled_bytes().unwrap(); - - println!("spill count {}, spill bytes {}", spill_count, spilled_bytes); - assert!(spill_count > 0); - assert!((spilled_bytes as u64) < disk_spill_limit); - - // Verify that all temporary files have been properly cleaned up by checking - // that the total disk usage tracked by the disk manager is zero - let current_disk_usage = ctx.runtime_env().disk_manager.used_disk_space(); - assert_eq!(current_disk_usage, 0); - - Ok(()) -} - /// Run the query with the specified memory limit, /// and verifies the expected errors are returned #[derive(Clone, Debug)] @@ -863,10 +741,11 @@ impl Scenario { single_row_batches, } => { use datafusion::physical_expr::expressions::col; - let batches: Vec> = std::iter::repeat_n( - maybe_split_batches(dict_batches(), *single_row_batches), - *partitions, - ) + let batches: Vec> = std::iter::repeat(maybe_split_batches( + dict_batches(), + *single_row_batches, + )) + .take(*partitions) .collect(); let schema = batches[0][0].schema(); diff --git a/datafusion/core/tests/parquet/custom_reader.rs b/datafusion/core/tests/parquet/custom_reader.rs index 761a78a29fd3a..ce5c0d720174d 100644 --- a/datafusion/core/tests/parquet/custom_reader.rs +++ b/datafusion/core/tests/parquet/custom_reader.rs @@ -44,7 +44,6 @@ use insta::assert_snapshot; use object_store::memory::InMemory; use object_store::path::Path; use object_store::{ObjectMeta, ObjectStore}; -use parquet::arrow::arrow_reader::ArrowReaderOptions; use parquet::arrow::async_reader::AsyncFileReader; use parquet::arrow::ArrowWriter; use parquet::errors::ParquetError; @@ -187,7 +186,7 @@ async fn store_parquet_in_memory( location: Path::parse(format!("file-{offset}.parquet")) .expect("creating path"), last_modified: chrono::DateTime::from(SystemTime::now()), - size: buf.len() as u64, + size: buf.len(), e_tag: None, version: None, }; @@ -219,10 +218,9 @@ struct ParquetFileReader { impl AsyncFileReader for ParquetFileReader { fn get_bytes( &mut self, - range: Range, + range: Range, ) -> BoxFuture<'_, parquet::errors::Result> { - let bytes_scanned = range.end - range.start; - self.metrics.bytes_scanned.add(bytes_scanned as usize); + self.metrics.bytes_scanned.add(range.end - range.start); self.store .get_range(&self.meta.location, range) @@ -234,7 +232,6 @@ impl AsyncFileReader for ParquetFileReader { fn get_metadata( &mut self, - _options: Option<&ArrowReaderOptions>, ) -> BoxFuture<'_, parquet::errors::Result>> { Box::pin(async move { let metadata = fetch_parquet_metadata( diff --git a/datafusion/core/tests/parquet/mod.rs b/datafusion/core/tests/parquet/mod.rs index 87a5ed33f127d..f45eacce18df5 100644 --- a/datafusion/core/tests/parquet/mod.rs +++ b/datafusion/core/tests/parquet/mod.rs @@ -611,7 +611,7 @@ fn make_bytearray_batch( large_binary_values: Vec<&[u8]>, ) -> RecordBatch { let num_rows = string_values.len(); - let name: StringArray = std::iter::repeat_n(Some(name), num_rows).collect(); + let name: StringArray = std::iter::repeat(Some(name)).take(num_rows).collect(); let service_string: StringArray = string_values.iter().map(Some).collect(); let service_binary: BinaryArray = binary_values.iter().map(Some).collect(); let service_fixedsize: FixedSizeBinaryArray = fixedsize_values @@ -659,7 +659,7 @@ fn make_bytearray_batch( /// name | service.name fn make_names_batch(name: &str, service_name_values: Vec<&str>) -> RecordBatch { let num_rows = service_name_values.len(); - let name: StringArray = std::iter::repeat_n(Some(name), num_rows).collect(); + let name: StringArray = std::iter::repeat(Some(name)).take(num_rows).collect(); let service_name: StringArray = service_name_values.iter().map(Some).collect(); let schema = Schema::new(vec![ @@ -698,7 +698,7 @@ fn make_int_batches_with_null( Int8Array::from_iter( v8.into_iter() .map(Some) - .chain(std::iter::repeat_n(None, null_values)), + .chain(std::iter::repeat(None).take(null_values)), ) .to_data(), ), @@ -706,7 +706,7 @@ fn make_int_batches_with_null( Int16Array::from_iter( v16.into_iter() .map(Some) - .chain(std::iter::repeat_n(None, null_values)), + .chain(std::iter::repeat(None).take(null_values)), ) .to_data(), ), @@ -714,7 +714,7 @@ fn make_int_batches_with_null( Int32Array::from_iter( v32.into_iter() .map(Some) - .chain(std::iter::repeat_n(None, null_values)), + .chain(std::iter::repeat(None).take(null_values)), ) .to_data(), ), @@ -722,7 +722,7 @@ fn make_int_batches_with_null( Int64Array::from_iter( v64.into_iter() .map(Some) - .chain(std::iter::repeat_n(None, null_values)), + .chain(std::iter::repeat(None).take(null_values)), ) .to_data(), ), diff --git a/datafusion/core/tests/parquet/page_pruning.rs b/datafusion/core/tests/parquet/page_pruning.rs index f693485cbe018..7006bf083eeed 100644 --- a/datafusion/core/tests/parquet/page_pruning.rs +++ b/datafusion/core/tests/parquet/page_pruning.rs @@ -52,7 +52,7 @@ async fn get_parquet_exec(state: &SessionState, filter: Expr) -> DataSourceExec let meta = ObjectMeta { location, last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len(), + size: metadata.len() as usize, e_tag: None, version: None, }; diff --git a/datafusion/core/tests/physical_optimizer/enforce_distribution.rs b/datafusion/core/tests/physical_optimizer/enforce_distribution.rs index 5e182cb93b39c..9898f6204e880 100644 --- a/datafusion/core/tests/physical_optimizer/enforce_distribution.rs +++ b/datafusion/core/tests/physical_optimizer/enforce_distribution.rs @@ -52,7 +52,6 @@ use datafusion_physical_plan::aggregates::{ AggregateExec, AggregateMode, PhysicalGroupBy, }; use datafusion_physical_plan::coalesce_batches::CoalesceBatchesExec; -use datafusion_physical_plan::coalesce_partitions::CoalescePartitionsExec; use datafusion_physical_plan::execution_plan::ExecutionPlan; use datafusion_physical_plan::expressions::col; use datafusion_physical_plan::filter::FilterExec; @@ -3472,47 +3471,3 @@ fn optimize_away_unnecessary_repartition2() -> Result<()> { Ok(()) } - -#[test] -fn test_replace_order_preserving_variants_with_fetch() -> Result<()> { - // Create a base plan - let parquet_exec = parquet_exec(); - - let sort_expr = PhysicalSortExpr { - expr: Arc::new(Column::new("id", 0)), - options: SortOptions::default(), - }; - - let ordering = LexOrdering::new(vec![sort_expr]); - - // Create a SortPreservingMergeExec with fetch=5 - let spm_exec = Arc::new( - SortPreservingMergeExec::new(ordering, parquet_exec.clone()).with_fetch(Some(5)), - ); - - // Create distribution context - let dist_context = DistributionContext::new( - spm_exec, - true, - vec![DistributionContext::new(parquet_exec, false, vec![])], - ); - - // Apply the function - let result = replace_order_preserving_variants(dist_context)?; - - // Verify the plan was transformed to CoalescePartitionsExec - result - .plan - .as_any() - .downcast_ref::() - .expect("Expected CoalescePartitionsExec"); - - // Verify fetch was preserved - assert_eq!( - result.plan.fetch(), - Some(5), - "Fetch value was not preserved after transformation" - ); - - Ok(()) -} diff --git a/datafusion/core/tests/physical_optimizer/enforce_sorting.rs b/datafusion/core/tests/physical_optimizer/enforce_sorting.rs index 052db454ef3f5..4d2c875d3f1d4 100644 --- a/datafusion/core/tests/physical_optimizer/enforce_sorting.rs +++ b/datafusion/core/tests/physical_optimizer/enforce_sorting.rs @@ -1652,7 +1652,7 @@ async fn test_remove_unnecessary_sort7() -> Result<()> { ) as Arc; let expected_input = [ - "SortExec: TopK(fetch=2), expr=[non_nullable_col@1 ASC], preserve_partitioning=[false], sort_prefix=[non_nullable_col@1 ASC]", + "SortExec: TopK(fetch=2), expr=[non_nullable_col@1 ASC], preserve_partitioning=[false]", " SortExec: expr=[non_nullable_col@1 ASC, nullable_col@0 ASC], preserve_partitioning=[false]", " DataSourceExec: partitions=1, partition_sizes=[0]", ]; @@ -3440,38 +3440,3 @@ fn test_handles_multiple_orthogonal_sorts() -> Result<()> { Ok(()) } - -#[test] -fn test_parallelize_sort_preserves_fetch() -> Result<()> { - // Create a schema - let schema = create_test_schema3()?; - let parquet_exec = parquet_exec(&schema); - let coalesced = Arc::new(CoalescePartitionsExec::new(parquet_exec.clone())); - let top_coalesced = CoalescePartitionsExec::new(coalesced.clone()) - .with_fetch(Some(10)) - .unwrap(); - - let requirements = PlanWithCorrespondingCoalescePartitions::new( - top_coalesced.clone(), - true, - vec![PlanWithCorrespondingCoalescePartitions::new( - coalesced, - true, - vec![PlanWithCorrespondingCoalescePartitions::new( - parquet_exec, - false, - vec![], - )], - )], - ); - - let res = parallelize_sorts(requirements)?; - - // Verify fetch was preserved - assert_eq!( - res.data.plan.fetch(), - Some(10), - "Fetch value was not preserved after transformation" - ); - Ok(()) -} diff --git a/datafusion/core/tests/physical_optimizer/mod.rs b/datafusion/core/tests/physical_optimizer/mod.rs index 6643e7fd59b7a..7d5d07715eebc 100644 --- a/datafusion/core/tests/physical_optimizer/mod.rs +++ b/datafusion/core/tests/physical_optimizer/mod.rs @@ -25,7 +25,6 @@ mod join_selection; mod limit_pushdown; mod limited_distinct_aggregation; mod projection_pushdown; -mod push_down_filter; mod replace_with_order_preserving_variants; mod sanity_checker; mod test_utils; diff --git a/datafusion/core/tests/physical_optimizer/push_down_filter.rs b/datafusion/core/tests/physical_optimizer/push_down_filter.rs deleted file mode 100644 index b19144f1bcffe..0000000000000 --- a/datafusion/core/tests/physical_optimizer/push_down_filter.rs +++ /dev/null @@ -1,542 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::sync::{Arc, OnceLock}; -use std::{ - any::Any, - fmt::{Display, Formatter}, -}; - -use arrow::datatypes::{DataType, Field, Schema, SchemaRef}; -use datafusion::{ - datasource::object_store::ObjectStoreUrl, - logical_expr::Operator, - physical_plan::{ - expressions::{BinaryExpr, Column, Literal}, - PhysicalExpr, - }, - scalar::ScalarValue, -}; -use datafusion_common::{config::ConfigOptions, Statistics}; -use datafusion_common::{internal_err, Result}; -use datafusion_datasource::file_scan_config::FileScanConfigBuilder; -use datafusion_datasource::source::DataSourceExec; -use datafusion_datasource::{ - file::FileSource, file_scan_config::FileScanConfig, file_stream::FileOpener, -}; -use datafusion_expr::test::function_stub::count_udaf; -use datafusion_physical_expr::expressions::col; -use datafusion_physical_expr::{ - aggregate::AggregateExprBuilder, conjunction, Partitioning, -}; -use datafusion_physical_expr_common::physical_expr::fmt_sql; -use datafusion_physical_optimizer::push_down_filter::PushdownFilter; -use datafusion_physical_optimizer::PhysicalOptimizerRule; -use datafusion_physical_plan::filter_pushdown::{ - filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, - FilterPushdownSupport, -}; -use datafusion_physical_plan::{ - aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy}, - coalesce_batches::CoalesceBatchesExec, - filter::FilterExec, - repartition::RepartitionExec, -}; -use datafusion_physical_plan::{ - displayable, metrics::ExecutionPlanMetricsSet, DisplayFormatType, ExecutionPlan, -}; - -use object_store::ObjectStore; - -/// A placeholder data source that accepts filter pushdown -#[derive(Clone, Default)] -struct TestSource { - support: bool, - predicate: Option>, - statistics: Option, -} - -impl TestSource { - fn new(support: bool) -> Self { - Self { - support, - predicate: None, - statistics: None, - } - } -} - -impl FileSource for TestSource { - fn create_file_opener( - &self, - _object_store: Arc, - _base_config: &FileScanConfig, - _partition: usize, - ) -> Arc { - todo!("should not be called") - } - - fn as_any(&self) -> &dyn Any { - todo!("should not be called") - } - - fn with_batch_size(&self, _batch_size: usize) -> Arc { - todo!("should not be called") - } - - fn with_schema(&self, _schema: SchemaRef) -> Arc { - todo!("should not be called") - } - - fn with_projection(&self, _config: &FileScanConfig) -> Arc { - todo!("should not be called") - } - - fn with_statistics(&self, statistics: Statistics) -> Arc { - Arc::new(TestSource { - statistics: Some(statistics), - ..self.clone() - }) - } - - fn metrics(&self) -> &ExecutionPlanMetricsSet { - todo!("should not be called") - } - - fn statistics(&self) -> Result { - Ok(self - .statistics - .as_ref() - .expect("statistics not set") - .clone()) - } - - fn file_type(&self) -> &str { - "test" - } - - fn fmt_extra(&self, t: DisplayFormatType, f: &mut Formatter) -> std::fmt::Result { - match t { - DisplayFormatType::Default | DisplayFormatType::Verbose => { - let support = format!(", pushdown_supported={}", self.support); - - let predicate_string = self - .predicate - .as_ref() - .map(|p| format!(", predicate={p}")) - .unwrap_or_default(); - - write!(f, "{}{}", support, predicate_string) - } - DisplayFormatType::TreeRender => { - if let Some(predicate) = &self.predicate { - writeln!(f, "pushdown_supported={}", fmt_sql(predicate.as_ref()))?; - writeln!(f, "predicate={}", fmt_sql(predicate.as_ref()))?; - } - Ok(()) - } - } - } - - fn try_pushdown_filters( - &self, - mut fd: FilterDescription, - config: &ConfigOptions, - ) -> Result>> { - if self.support && config.execution.parquet.pushdown_filters { - if let Some(internal) = self.predicate.as_ref() { - fd.filters.push(Arc::clone(internal)); - } - let all_filters = fd.take_description(); - - Ok(FilterPushdownResult { - support: FilterPushdownSupport::Supported { - child_descriptions: vec![], - op: Arc::new(TestSource { - support: true, - predicate: Some(conjunction(all_filters)), - statistics: self.statistics.clone(), // should be updated in reality - }), - revisit: false, - }, - remaining_description: FilterDescription::empty(), - }) - } else { - Ok(filter_pushdown_not_supported(fd)) - } - } -} - -fn test_scan(support: bool) -> Arc { - let schema = schema(); - let source = Arc::new(TestSource::new(support)); - let base_config = FileScanConfigBuilder::new( - ObjectStoreUrl::parse("test://").unwrap(), - Arc::clone(schema), - source, - ) - .build(); - DataSourceExec::from_data_source(base_config) -} - -#[test] -fn test_pushdown_into_scan() { - let scan = test_scan(true); - let predicate = col_lit_predicate("a", "foo", schema()); - let plan = Arc::new(FilterExec::try_new(predicate, scan).unwrap()); - - // expect the predicate to be pushed down into the DataSource - insta::assert_snapshot!( - OptimizationTest::new(plan, PushdownFilter{}, true), - @r" - OptimizationTest: - input: - - FilterExec: a@0 = foo - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo - " - ); -} - -/// Show that we can use config options to determine how to do pushdown. -#[test] -fn test_pushdown_into_scan_with_config_options() { - let scan = test_scan(true); - let predicate = col_lit_predicate("a", "foo", schema()); - let plan = Arc::new(FilterExec::try_new(predicate, scan).unwrap()) as _; - - let mut cfg = ConfigOptions::default(); - insta::assert_snapshot!( - OptimizationTest::new( - Arc::clone(&plan), - PushdownFilter {}, - false - ), - @r" - OptimizationTest: - input: - - FilterExec: a@0 = foo - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - FilterExec: a@0 = foo - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - " - ); - - cfg.execution.parquet.pushdown_filters = true; - insta::assert_snapshot!( - OptimizationTest::new( - plan, - PushdownFilter {}, - true - ), - @r" - OptimizationTest: - input: - - FilterExec: a@0 = foo - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo - " - ); -} - -#[test] -fn test_filter_collapse() { - // filter should be pushed down into the parquet scan with two filters - let scan = test_scan(true); - let predicate1 = col_lit_predicate("a", "foo", schema()); - let filter1 = Arc::new(FilterExec::try_new(predicate1, scan).unwrap()); - let predicate2 = col_lit_predicate("b", "bar", schema()); - let plan = Arc::new(FilterExec::try_new(predicate2, filter1).unwrap()); - - insta::assert_snapshot!( - OptimizationTest::new(plan, PushdownFilter{}, true), - @r" - OptimizationTest: - input: - - FilterExec: b@1 = bar - - FilterExec: a@0 = foo - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=b@1 = bar AND a@0 = foo - " - ); -} - -#[test] -fn test_filter_with_projection() { - let scan = test_scan(true); - let projection = vec![1, 0]; - let predicate = col_lit_predicate("a", "foo", schema()); - let plan = Arc::new( - FilterExec::try_new(predicate, Arc::clone(&scan)) - .unwrap() - .with_projection(Some(projection)) - .unwrap(), - ); - - // expect the predicate to be pushed down into the DataSource but the FilterExec to be converted to ProjectionExec - insta::assert_snapshot!( - OptimizationTest::new(plan, PushdownFilter{}, true), - @r" - OptimizationTest: - input: - - FilterExec: a@0 = foo, projection=[b@1, a@0] - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - ProjectionExec: expr=[b@1 as b, a@0 as a] - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo - ", - ); - - // add a test where the filter is on a column that isn't included in the output - let projection = vec![1]; - let predicate = col_lit_predicate("a", "foo", schema()); - let plan = Arc::new( - FilterExec::try_new(predicate, scan) - .unwrap() - .with_projection(Some(projection)) - .unwrap(), - ); - insta::assert_snapshot!( - OptimizationTest::new(plan, PushdownFilter{},true), - @r" - OptimizationTest: - input: - - FilterExec: a@0 = foo, projection=[b@1] - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - ProjectionExec: expr=[b@1 as b] - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo - " - ); -} - -#[test] -fn test_push_down_through_transparent_nodes() { - // expect the predicate to be pushed down into the DataSource - let scan = test_scan(true); - let coalesce = Arc::new(CoalesceBatchesExec::new(scan, 1)); - let predicate = col_lit_predicate("a", "foo", schema()); - let filter = Arc::new(FilterExec::try_new(predicate, coalesce).unwrap()); - let repartition = Arc::new( - RepartitionExec::try_new(filter, Partitioning::RoundRobinBatch(1)).unwrap(), - ); - let predicate = col_lit_predicate("b", "bar", schema()); - let plan = Arc::new(FilterExec::try_new(predicate, repartition).unwrap()); - - // expect the predicate to be pushed down into the DataSource - insta::assert_snapshot!( - OptimizationTest::new(plan, PushdownFilter{},true), - @r" - OptimizationTest: - input: - - FilterExec: b@1 = bar - - RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=0 - - FilterExec: a@0 = foo - - CoalesceBatchesExec: target_batch_size=1 - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - RepartitionExec: partitioning=RoundRobinBatch(1), input_partitions=0 - - CoalesceBatchesExec: target_batch_size=1 - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=b@1 = bar AND a@0 = foo - " - ); -} - -#[test] -fn test_no_pushdown_through_aggregates() { - // There are 2 important points here: - // 1. The outer filter **is not** pushed down at all because we haven't implemented pushdown support - // yet for AggregateExec. - // 2. The inner filter **is** pushed down into the DataSource. - let scan = test_scan(true); - - let coalesce = Arc::new(CoalesceBatchesExec::new(scan, 10)); - - let filter = Arc::new( - FilterExec::try_new(col_lit_predicate("a", "foo", schema()), coalesce).unwrap(), - ); - - let aggregate_expr = - vec![ - AggregateExprBuilder::new(count_udaf(), vec![col("a", schema()).unwrap()]) - .schema(Arc::clone(schema())) - .alias("cnt") - .build() - .map(Arc::new) - .unwrap(), - ]; - let group_by = PhysicalGroupBy::new_single(vec![ - (col("a", schema()).unwrap(), "a".to_string()), - (col("b", schema()).unwrap(), "b".to_string()), - ]); - let aggregate = Arc::new( - AggregateExec::try_new( - AggregateMode::Final, - group_by, - aggregate_expr.clone(), - vec![None], - filter, - Arc::clone(schema()), - ) - .unwrap(), - ); - - let coalesce = Arc::new(CoalesceBatchesExec::new(aggregate, 100)); - - let predicate = col_lit_predicate("b", "bar", schema()); - let plan = Arc::new(FilterExec::try_new(predicate, coalesce).unwrap()); - - // expect the predicate to be pushed down into the DataSource - insta::assert_snapshot!( - OptimizationTest::new(plan, PushdownFilter{}, true), - @r" - OptimizationTest: - input: - - FilterExec: b@1 = bar - - CoalesceBatchesExec: target_batch_size=100 - - AggregateExec: mode=Final, gby=[a@0 as a, b@1 as b], aggr=[cnt], ordering_mode=PartiallySorted([0]) - - FilterExec: a@0 = foo - - CoalesceBatchesExec: target_batch_size=10 - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true - output: - Ok: - - FilterExec: b@1 = bar - - CoalesceBatchesExec: target_batch_size=100 - - AggregateExec: mode=Final, gby=[a@0 as a, b@1 as b], aggr=[cnt] - - CoalesceBatchesExec: target_batch_size=10 - - DataSourceExec: file_groups={0 groups: []}, projection=[a, b, c], file_type=test, pushdown_supported=true, predicate=a@0 = foo - " - ); -} - -/// Schema: -/// a: String -/// b: String -/// c: f64 -static TEST_SCHEMA: OnceLock = OnceLock::new(); - -fn schema() -> &'static SchemaRef { - TEST_SCHEMA.get_or_init(|| { - let fields = vec![ - Field::new("a", DataType::Utf8, false), - Field::new("b", DataType::Utf8, false), - Field::new("c", DataType::Float64, false), - ]; - Arc::new(Schema::new(fields)) - }) -} - -/// Returns a predicate that is a binary expression col = lit -fn col_lit_predicate( - column_name: &str, - scalar_value: impl Into, - schema: &Schema, -) -> Arc { - let scalar_value = scalar_value.into(); - Arc::new(BinaryExpr::new( - Arc::new(Column::new_with_schema(column_name, schema).unwrap()), - Operator::Eq, - Arc::new(Literal::new(scalar_value)), - )) -} - -/// A harness for testing physical optimizers. -/// -/// You can use this to test the output of a physical optimizer rule using insta snapshots -#[derive(Debug)] -pub struct OptimizationTest { - input: Vec, - output: Result, String>, -} - -impl OptimizationTest { - pub fn new( - input_plan: Arc, - opt: O, - allow_pushdown_filters: bool, - ) -> Self - where - O: PhysicalOptimizerRule, - { - let mut parquet_pushdown_config = ConfigOptions::default(); - parquet_pushdown_config.execution.parquet.pushdown_filters = - allow_pushdown_filters; - - let input = format_execution_plan(&input_plan); - let input_schema = input_plan.schema(); - - let output_result = opt.optimize(input_plan, &parquet_pushdown_config); - let output = output_result - .and_then(|plan| { - if opt.schema_check() && (plan.schema() != input_schema) { - internal_err!( - "Schema mismatch:\n\nBefore:\n{:?}\n\nAfter:\n{:?}", - input_schema, - plan.schema() - ) - } else { - Ok(plan) - } - }) - .map(|plan| format_execution_plan(&plan)) - .map_err(|e| e.to_string()); - - Self { input, output } - } -} - -impl Display for OptimizationTest { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - writeln!(f, "OptimizationTest:")?; - writeln!(f, " input:")?; - for line in &self.input { - writeln!(f, " - {line}")?; - } - writeln!(f, " output:")?; - match &self.output { - Ok(output) => { - writeln!(f, " Ok:")?; - for line in output { - writeln!(f, " - {line}")?; - } - } - Err(err) => { - writeln!(f, " Err: {err}")?; - } - } - Ok(()) - } -} - -pub fn format_execution_plan(plan: &Arc) -> Vec { - format_lines(&displayable(plan.as_ref()).indent(false).to_string()) -} - -fn format_lines(s: &str) -> Vec { - s.trim().split('\n').map(|s| s.to_string()).collect() -} diff --git a/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs b/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs index eb517c42b0ebb..58eb866c590cc 100644 --- a/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs +++ b/datafusion/core/tests/physical_optimizer/replace_with_order_preserving_variants.rs @@ -18,8 +18,7 @@ use std::sync::Arc; use crate::physical_optimizer::test_utils::{ - check_integrity, create_test_schema3, sort_preserving_merge_exec, - stream_exec_ordered_with_projection, + check_integrity, sort_preserving_merge_exec, stream_exec_ordered_with_projection, }; use datafusion::prelude::SessionContext; @@ -41,14 +40,13 @@ use datafusion_physical_plan::{ }; use datafusion::datasource::source::DataSourceExec; use datafusion_common::tree_node::{TransformedResult, TreeNode}; -use datafusion_common::{assert_contains, Result}; +use datafusion_common::Result; use datafusion_expr::{JoinType, Operator}; use datafusion_physical_expr::expressions::{self, col, Column}; use datafusion_physical_expr::PhysicalSortExpr; -use datafusion_physical_optimizer::enforce_sorting::replace_with_order_preserving_variants::{plan_with_order_preserving_variants, replace_with_order_preserving_variants, OrderPreservationContext}; +use datafusion_physical_optimizer::enforce_sorting::replace_with_order_preserving_variants::{replace_with_order_preserving_variants, OrderPreservationContext}; use datafusion_common::config::ConfigOptions; -use crate::physical_optimizer::enforce_sorting::parquet_exec_sorted; use object_store::memory::InMemory; use object_store::ObjectStore; use rstest::rstest; @@ -1261,52 +1259,3 @@ fn memory_exec_sorted( )) }) } - -#[test] -fn test_plan_with_order_preserving_variants_preserves_fetch() -> Result<()> { - // Create a schema - let schema = create_test_schema3()?; - let parquet_sort_exprs = vec![crate::physical_optimizer::test_utils::sort_expr( - "a", &schema, - )]; - let parquet_exec = parquet_exec_sorted(&schema, parquet_sort_exprs); - let coalesced = CoalescePartitionsExec::new(parquet_exec.clone()) - .with_fetch(Some(10)) - .unwrap(); - - // Test sort's fetch is greater than coalesce fetch, return error because it's not reasonable - let requirements = OrderPreservationContext::new( - coalesced.clone(), - false, - vec![OrderPreservationContext::new( - parquet_exec.clone(), - false, - vec![], - )], - ); - let res = plan_with_order_preserving_variants(requirements, false, true, Some(15)); - assert_contains!(res.unwrap_err().to_string(), "CoalescePartitionsExec fetch [10] should be greater than or equal to SortExec fetch [15]"); - - // Test sort is without fetch, expected to get the fetch value from the coalesced - let requirements = OrderPreservationContext::new( - coalesced.clone(), - false, - vec![OrderPreservationContext::new( - parquet_exec.clone(), - false, - vec![], - )], - ); - let res = plan_with_order_preserving_variants(requirements, false, true, None)?; - assert_eq!(res.plan.fetch(), Some(10),); - - // Test sort's fetch is less than coalesces fetch, expected to get the fetch value from the sort - let requirements = OrderPreservationContext::new( - coalesced, - false, - vec![OrderPreservationContext::new(parquet_exec, false, vec![])], - ); - let res = plan_with_order_preserving_variants(requirements, false, true, Some(5))?; - assert_eq!(res.plan.fetch(), Some(5),); - Ok(()) -} diff --git a/datafusion/core/tests/sql/mod.rs b/datafusion/core/tests/sql/mod.rs index 2a5597b9fb7ee..579049692e7dc 100644 --- a/datafusion/core/tests/sql/mod.rs +++ b/datafusion/core/tests/sql/mod.rs @@ -63,7 +63,6 @@ pub mod create_drop; pub mod explain_analyze; pub mod joins; mod path_partition; -mod runtime_config; pub mod select; mod sql_api; diff --git a/datafusion/core/tests/sql/path_partition.rs b/datafusion/core/tests/sql/path_partition.rs index fa6c7432413f1..bf8466d849f25 100644 --- a/datafusion/core/tests/sql/path_partition.rs +++ b/datafusion/core/tests/sql/path_partition.rs @@ -712,7 +712,7 @@ impl ObjectStore for MirroringObjectStore { let meta = ObjectMeta { location: location.clone(), last_modified: metadata.modified().map(chrono::DateTime::from).unwrap(), - size: metadata.len(), + size: metadata.len() as usize, e_tag: None, version: None, }; @@ -728,15 +728,14 @@ impl ObjectStore for MirroringObjectStore { async fn get_range( &self, location: &Path, - range: Range, + range: Range, ) -> object_store::Result { self.files.iter().find(|x| *x == location).unwrap(); let path = std::path::PathBuf::from(&self.mirrored_file); let mut file = File::open(path).unwrap(); - file.seek(SeekFrom::Start(range.start)).unwrap(); + file.seek(SeekFrom::Start(range.start as u64)).unwrap(); let to_read = range.end - range.start; - let to_read: usize = to_read.try_into().unwrap(); let mut data = Vec::with_capacity(to_read); let read = file.take(to_read as u64).read_to_end(&mut data).unwrap(); assert_eq!(read, to_read); @@ -751,10 +750,9 @@ impl ObjectStore for MirroringObjectStore { fn list( &self, prefix: Option<&Path>, - ) -> BoxStream<'static, object_store::Result> { + ) -> BoxStream<'_, object_store::Result> { let prefix = prefix.cloned().unwrap_or_default(); - let size = self.file_size; - Box::pin(stream::iter(self.files.clone().into_iter().filter_map( + Box::pin(stream::iter(self.files.iter().filter_map( move |location| { // Don't return for exact prefix match let filter = location @@ -764,9 +762,9 @@ impl ObjectStore for MirroringObjectStore { filter.then(|| { Ok(ObjectMeta { - location, + location: location.clone(), last_modified: Utc.timestamp_nanos(0), - size, + size: self.file_size as usize, e_tag: None, version: None, }) @@ -804,7 +802,7 @@ impl ObjectStore for MirroringObjectStore { let object = ObjectMeta { location: k.clone(), last_modified: Utc.timestamp_nanos(0), - size: self.file_size, + size: self.file_size as usize, e_tag: None, version: None, }; diff --git a/datafusion/core/tests/sql/runtime_config.rs b/datafusion/core/tests/sql/runtime_config.rs deleted file mode 100644 index 18e07bb61ed94..0000000000000 --- a/datafusion/core/tests/sql/runtime_config.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//! Tests for runtime configuration SQL interface - -use std::sync::Arc; - -use datafusion::execution::context::SessionContext; -use datafusion::execution::context::TaskContext; -use datafusion_physical_plan::common::collect; - -#[tokio::test] -async fn test_memory_limit_with_spill() { - let ctx = SessionContext::new(); - - ctx.sql("SET datafusion.runtime.memory_limit = '1M'") - .await - .unwrap() - .collect() - .await - .unwrap(); - - ctx.sql("SET datafusion.execution.sort_spill_reservation_bytes = 0") - .await - .unwrap() - .collect() - .await - .unwrap(); - - let query = "select * from generate_series(1,10000000) as t1(v1) order by v1;"; - let df = ctx.sql(query).await.unwrap(); - - let plan = df.create_physical_plan().await.unwrap(); - let task_ctx = Arc::new(TaskContext::from(&ctx.state())); - let stream = plan.execute(0, task_ctx).unwrap(); - - let _results = collect(stream).await; - let metrics = plan.metrics().unwrap(); - let spill_count = metrics.spill_count().unwrap(); - assert!(spill_count > 0, "Expected spills but none occurred"); -} - -#[tokio::test] -async fn test_no_spill_with_adequate_memory() { - let ctx = SessionContext::new(); - - ctx.sql("SET datafusion.runtime.memory_limit = '10M'") - .await - .unwrap() - .collect() - .await - .unwrap(); - ctx.sql("SET datafusion.execution.sort_spill_reservation_bytes = 0") - .await - .unwrap() - .collect() - .await - .unwrap(); - - let query = "select * from generate_series(1,100000) as t1(v1) order by v1;"; - let df = ctx.sql(query).await.unwrap(); - - let plan = df.create_physical_plan().await.unwrap(); - let task_ctx = Arc::new(TaskContext::from(&ctx.state())); - let stream = plan.execute(0, task_ctx).unwrap(); - - let _results = collect(stream).await; - let metrics = plan.metrics().unwrap(); - let spill_count = metrics.spill_count().unwrap(); - assert_eq!(spill_count, 0, "Expected no spills but some occurred"); -} - -#[tokio::test] -async fn test_multiple_configs() { - let ctx = SessionContext::new(); - - ctx.sql("SET datafusion.runtime.memory_limit = '100M'") - .await - .unwrap() - .collect() - .await - .unwrap(); - ctx.sql("SET datafusion.execution.batch_size = '2048'") - .await - .unwrap() - .collect() - .await - .unwrap(); - - let query = "select * from generate_series(1,100000) as t1(v1) order by v1;"; - let result = ctx.sql(query).await.unwrap().collect().await; - - assert!(result.is_ok(), "Should not fail due to memory limit"); - - let state = ctx.state(); - let batch_size = state.config().options().execution.batch_size; - assert_eq!(batch_size, 2048); -} - -#[tokio::test] -async fn test_memory_limit_enforcement() { - let ctx = SessionContext::new(); - - ctx.sql("SET datafusion.runtime.memory_limit = '1M'") - .await - .unwrap() - .collect() - .await - .unwrap(); - - let query = "select * from generate_series(1,100000) as t1(v1) order by v1;"; - let result = ctx.sql(query).await.unwrap().collect().await; - - assert!(result.is_err(), "Should fail due to memory limit"); - - ctx.sql("SET datafusion.runtime.memory_limit = '100M'") - .await - .unwrap() - .collect() - .await - .unwrap(); - - let result = ctx.sql(query).await.unwrap().collect().await; - - assert!(result.is_ok(), "Should not fail due to memory limit"); -} - -#[tokio::test] -async fn test_invalid_memory_limit() { - let ctx = SessionContext::new(); - - let result = ctx - .sql("SET datafusion.runtime.memory_limit = '100X'") - .await; - - assert!(result.is_err()); - let error_message = result.unwrap_err().to_string(); - assert!(error_message.contains("Unsupported unit 'X'")); -} - -#[tokio::test] -async fn test_unknown_runtime_config() { - let ctx = SessionContext::new(); - - let result = ctx - .sql("SET datafusion.runtime.unknown_config = 'value'") - .await; - - assert!(result.is_err()); - let error_message = result.unwrap_err().to_string(); - assert!(error_message.contains("Unknown runtime configuration")); -} diff --git a/datafusion/core/tests/sql/sql_api.rs b/datafusion/core/tests/sql/sql_api.rs index ec086bcc50c76..034d6fa23d9cb 100644 --- a/datafusion/core/tests/sql/sql_api.rs +++ b/datafusion/core/tests/sql/sql_api.rs @@ -19,23 +19,6 @@ use datafusion::prelude::*; use tempfile::TempDir; -#[tokio::test] -async fn test_window_function() { - let ctx = SessionContext::new(); - let df = ctx - .sql( - r#"SELECT - t1.v1, - SUM(t1.v1) OVER w + 1 - FROM - generate_series(1, 10000) AS t1(v1) - WINDOW - w AS (ORDER BY t1.v1 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW);"#, - ) - .await; - assert!(df.is_ok()); -} - #[tokio::test] async fn unsupported_ddl_returns_error() { // Verify SessionContext::with_sql_options errors appropriately diff --git a/datafusion/core/tests/tracing/asserting_tracer.rs b/datafusion/core/tests/tracing/asserting_tracer.rs deleted file mode 100644 index 292e066e5f121..0000000000000 --- a/datafusion/core/tests/tracing/asserting_tracer.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::any::Any; -use std::collections::VecDeque; -use std::ops::Deref; -use std::sync::{Arc, LazyLock}; - -use datafusion_common::{HashMap, HashSet}; -use datafusion_common_runtime::{set_join_set_tracer, JoinSetTracer}; -use futures::future::BoxFuture; -use tokio::sync::{Mutex, MutexGuard}; - -/// Initializes the global join set tracer with the asserting tracer. -/// Call this function before spawning any tasks that should be traced. -pub fn init_asserting_tracer() { - set_join_set_tracer(ASSERTING_TRACER.deref()) - .expect("Failed to initialize asserting tracer"); -} - -/// Verifies that the current task has a traceable ancestry back to "root". -/// -/// The function performs a breadth-first search (BFS) in the global spawn graph: -/// - It starts at the current task and follows parent links. -/// - If it reaches the "root" task, the ancestry is valid. -/// - If a task is missing from the graph, it panics. -/// -/// Note: Tokio task IDs are unique only while a task is active. -/// Once a task completes, its ID may be reused. -pub async fn assert_traceability() { - // Acquire the spawn graph lock. - let spawn_graph = acquire_spawn_graph().await; - - // Start BFS with the current task. - let mut tasks_to_check = VecDeque::from(vec![current_task()]); - - while let Some(task_id) = tasks_to_check.pop_front() { - if task_id == "root" { - // Ancestry reached the root. - continue; - } - // Obtain parent tasks, panicking if the task is not present. - let parents = spawn_graph - .get(&task_id) - .expect("Task ID not found in spawn graph"); - // Queue each parent for checking. - for parent in parents { - tasks_to_check.push_back(parent.clone()); - } - } -} - -/// Tracer that maintains a graph of task ancestry for tracing purposes. -/// -/// For each task, it records a set of parent task IDs to ensure that every -/// asynchronous task can be traced back to "root". -struct AssertingTracer { - /// An asynchronous map from task IDs to their parent task IDs. - spawn_graph: Arc>>>, -} - -/// Lazily initialized global instance of `AssertingTracer`. -static ASSERTING_TRACER: LazyLock = LazyLock::new(AssertingTracer::new); - -impl AssertingTracer { - /// Creates a new `AssertingTracer` with an empty spawn graph. - fn new() -> Self { - Self { - spawn_graph: Arc::default(), - } - } -} - -/// Returns the current task's ID as a string, or "root" if unavailable. -/// -/// Tokio guarantees task IDs are unique only among active tasks, -/// so completed tasks may have their IDs reused. -fn current_task() -> String { - tokio::task::try_id() - .map(|id| format!("{id}")) - .unwrap_or_else(|| "root".to_string()) -} - -/// Asynchronously locks and returns the spawn graph. -/// -/// The returned guard allows inspection or modification of task ancestry. -async fn acquire_spawn_graph<'a>() -> MutexGuard<'a, HashMap>> { - ASSERTING_TRACER.spawn_graph.lock().await -} - -/// Registers the current task as a child of `parent_id` in the spawn graph. -async fn register_task(parent_id: String) { - acquire_spawn_graph() - .await - .entry(current_task()) - .or_insert_with(HashSet::new) - .insert(parent_id); -} - -impl JoinSetTracer for AssertingTracer { - /// Wraps an asynchronous future to record its parent task before execution. - fn trace_future( - &self, - fut: BoxFuture<'static, Box>, - ) -> BoxFuture<'static, Box> { - // Capture the parent task ID. - let parent_id = current_task(); - Box::pin(async move { - // Register the parent-child relationship. - register_task(parent_id).await; - // Execute the wrapped future. - fut.await - }) - } - - /// Wraps a blocking closure to record its parent task before execution. - fn trace_block( - &self, - f: Box Box + Send>, - ) -> Box Box + Send> { - let parent_id = current_task(); - Box::new(move || { - // Synchronously record the task relationship. - futures::executor::block_on(register_task(parent_id)); - f() - }) - } -} diff --git a/datafusion/core/tests/tracing/mod.rs b/datafusion/core/tests/tracing/mod.rs deleted file mode 100644 index 787dd9f4f3cbc..0000000000000 --- a/datafusion/core/tests/tracing/mod.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//! # JoinSetTracer Integration Tests -//! -//! These are smoke tests that verify `JoinSetTracer` can be correctly injected into DataFusion. -//! -//! They run a SQL query that reads Parquet data and performs an aggregation, -//! which causes DataFusion to spawn multiple tasks. -//! The object store is wrapped to assert that every task can be traced back to the root. -//! -//! These tests don't cover all edge cases, but they should fail if changes to -//! DataFusion's task spawning break tracing. - -mod asserting_tracer; -mod traceable_object_store; - -use asserting_tracer::init_asserting_tracer; -use datafusion::datasource::file_format::parquet::ParquetFormat; -use datafusion::datasource::listing::ListingOptions; -use datafusion::prelude::*; -use datafusion::test_util::parquet_test_data; -use datafusion_common::assert_contains; -use datafusion_common_runtime::SpawnedTask; -use log::info; -use object_store::local::LocalFileSystem; -use std::sync::Arc; -use traceable_object_store::traceable_object_store; -use url::Url; - -/// Combined test that first verifies the query panics when no tracer is registered, -/// then initializes the tracer and confirms the query runs successfully. -/// -/// Using a single test function prevents global tracer leakage between tests. -#[tokio::test(flavor = "multi_thread", worker_threads = 8)] -async fn test_tracer_injection() { - // Without initializing the tracer, run the query. - // Spawn the query in a separate task so we can catch its panic. - info!("Running query without tracer"); - // The absence of the tracer should cause the task to panic inside the `TraceableObjectStore`. - let untraced_result = SpawnedTask::spawn(run_query()).join().await; - if let Err(e) = untraced_result { - // Check if the error message contains the expected error. - assert!(e.is_panic(), "Expected a panic, but got: {:?}", e); - assert_contains!(e.to_string(), "Task ID not found in spawn graph"); - info!("Caught expected panic: {}", e); - } else { - panic!("Expected the task to panic, but it completed successfully"); - }; - - // Initialize the asserting tracer and run the query. - info!("Initializing tracer and re-running query"); - init_asserting_tracer(); - SpawnedTask::spawn(run_query()).join().await.unwrap(); // Should complete without panics or errors. -} - -/// Executes a sample task-spawning SQL query using a traceable object store. -async fn run_query() { - info!("Starting query execution"); - - // Create a new session context - let ctx = SessionContext::new(); - - // Get the test data directory - let test_data = parquet_test_data(); - - // Define a Parquet file format with pruning enabled - let file_format = ParquetFormat::default().with_enable_pruning(true); - - // Set listing options for the parquet file with a specific extension - let listing_options = ListingOptions::new(Arc::new(file_format)) - .with_file_extension("alltypes_tiny_pages_plain.parquet"); - - // Wrap the local file system in a traceable object store to verify task traceability. - let local_fs = Arc::new(LocalFileSystem::new()); - let traceable_store = traceable_object_store(local_fs); - - // Register the traceable object store with a test URL. - let url = Url::parse("test://").unwrap(); - ctx.register_object_store(&url, traceable_store.clone()); - - // Register a listing table from the test data directory. - let table_path = format!("test://{}/", test_data); - ctx.register_listing_table("alltypes", &table_path, listing_options, None, None) - .await - .expect("Failed to register table"); - - // Define and execute an SQL query against the registered table, which should - // spawn multiple tasks due to the aggregation and parquet file read. - let sql = "SELECT COUNT(*), string_col FROM alltypes GROUP BY string_col"; - let result_batches = ctx.sql(sql).await.unwrap().collect().await.unwrap(); - - info!("Query complete: {} batches returned", result_batches.len()); -} diff --git a/datafusion/core/tests/tracing/traceable_object_store.rs b/datafusion/core/tests/tracing/traceable_object_store.rs deleted file mode 100644 index dfcafc3a63da1..0000000000000 --- a/datafusion/core/tests/tracing/traceable_object_store.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -//! Object store implementation used for testing - -use crate::tracing::asserting_tracer::assert_traceability; -use futures::stream::BoxStream; -use object_store::{ - path::Path, GetOptions, GetResult, ListResult, MultipartUpload, ObjectMeta, - ObjectStore, PutMultipartOpts, PutOptions, PutPayload, PutResult, -}; -use std::fmt::{Debug, Display, Formatter}; -use std::sync::Arc; - -/// Returns an `ObjectStore` that asserts it can trace its calls back to the root tokio task. -pub fn traceable_object_store( - object_store: Arc, -) -> Arc { - Arc::new(TraceableObjectStore::new(object_store)) -} - -/// An object store that asserts it can trace all its calls back to the root tokio task. -#[derive(Debug)] -struct TraceableObjectStore { - inner: Arc, -} - -impl TraceableObjectStore { - fn new(inner: Arc) -> Self { - Self { inner } - } -} - -impl Display for TraceableObjectStore { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Display::fmt(&self.inner, f) - } -} - -/// All trait methods are forwarded to the inner object store, -/// after asserting they can trace their calls back to the root tokio task. -#[async_trait::async_trait] -impl ObjectStore for TraceableObjectStore { - async fn put_opts( - &self, - location: &Path, - payload: PutPayload, - opts: PutOptions, - ) -> object_store::Result { - assert_traceability().await; - self.inner.put_opts(location, payload, opts).await - } - - async fn put_multipart_opts( - &self, - location: &Path, - opts: PutMultipartOpts, - ) -> object_store::Result> { - assert_traceability().await; - self.inner.put_multipart_opts(location, opts).await - } - - async fn get_opts( - &self, - location: &Path, - options: GetOptions, - ) -> object_store::Result { - assert_traceability().await; - self.inner.get_opts(location, options).await - } - - async fn head(&self, location: &Path) -> object_store::Result { - assert_traceability().await; - self.inner.head(location).await - } - - async fn delete(&self, location: &Path) -> object_store::Result<()> { - assert_traceability().await; - self.inner.delete(location).await - } - - fn list( - &self, - prefix: Option<&Path>, - ) -> BoxStream<'static, object_store::Result> { - futures::executor::block_on(assert_traceability()); - self.inner.list(prefix) - } - - async fn list_with_delimiter( - &self, - prefix: Option<&Path>, - ) -> object_store::Result { - assert_traceability().await; - self.inner.list_with_delimiter(prefix).await - } - - async fn copy(&self, from: &Path, to: &Path) -> object_store::Result<()> { - assert_traceability().await; - self.inner.copy(from, to).await - } - - async fn copy_if_not_exists( - &self, - from: &Path, - to: &Path, - ) -> object_store::Result<()> { - assert_traceability().await; - self.inner.copy_if_not_exists(from, to).await - } -} diff --git a/datafusion/core/tests/user_defined/user_defined_window_functions.rs b/datafusion/core/tests/user_defined/user_defined_window_functions.rs index 7c56507acd451..28394f0b9dfaf 100644 --- a/datafusion/core/tests/user_defined/user_defined_window_functions.rs +++ b/datafusion/core/tests/user_defined/user_defined_window_functions.rs @@ -633,7 +633,7 @@ fn odd_count(arr: &Int64Array) -> i64 { /// returns an array of num_rows that has the number of odd values in `arr` fn odd_count_arr(arr: &Int64Array, num_rows: usize) -> ArrayRef { - let array: Int64Array = std::iter::repeat_n(odd_count(arr), num_rows).collect(); + let array: Int64Array = std::iter::repeat(odd_count(arr)).take(num_rows).collect(); Arc::new(array) } diff --git a/datafusion/datasource-csv/src/source.rs b/datafusion/datasource-csv/src/source.rs index f5d45cd3fc881..6db4d18703204 100644 --- a/datafusion/datasource-csv/src/source.rs +++ b/datafusion/datasource-csv/src/source.rs @@ -704,7 +704,6 @@ impl FileOpener for CsvOpener { let result = store.get_opts(file_meta.location(), options).await?; match result.payload { - #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(mut file, _) => { let is_whole_file_scanned = file_meta.range.is_none(); let decoder = if is_whole_file_scanned { diff --git a/datafusion/datasource-json/src/file_format.rs b/datafusion/datasource-json/src/file_format.rs index 8d0515804fc7b..a6c52312e4127 100644 --- a/datafusion/datasource-json/src/file_format.rs +++ b/datafusion/datasource-json/src/file_format.rs @@ -209,7 +209,6 @@ impl FileFormat for JsonFormat { let r = store.as_ref().get(&object.location).await?; let schema = match r.payload { - #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(file, _) => { let decoder = file_compression_type.convert_read(file)?; let mut reader = BufReader::new(decoder); diff --git a/datafusion/datasource-json/src/source.rs b/datafusion/datasource-json/src/source.rs index ee96d050966d6..f1adccf9ded7d 100644 --- a/datafusion/datasource-json/src/source.rs +++ b/datafusion/datasource-json/src/source.rs @@ -355,7 +355,6 @@ impl FileOpener for JsonOpener { let result = store.get_opts(file_meta.location(), options).await?; match result.payload { - #[cfg(not(target_arch = "wasm32"))] GetResultPayload::File(mut file, _) => { let bytes = match file_meta.range { None => file_compression_type.convert_read(file)?, diff --git a/datafusion/datasource-parquet/src/file_format.rs b/datafusion/datasource-parquet/src/file_format.rs index ee4db50a6eda5..1d9a67fd2eb6d 100644 --- a/datafusion/datasource-parquet/src/file_format.rs +++ b/datafusion/datasource-parquet/src/file_format.rs @@ -24,18 +24,9 @@ use std::ops::Range; use std::sync::Arc; use arrow::array::RecordBatch; -use arrow::datatypes::{Fields, Schema, SchemaRef, TimeUnit}; -use datafusion_datasource::file_compression_type::FileCompressionType; -use datafusion_datasource::file_sink_config::{FileSink, FileSinkConfig}; -use datafusion_datasource::write::{create_writer, get_writer_schema, SharedBuffer}; - -use datafusion_datasource::file_format::{ - FileFormat, FileFormatFactory, FilePushdownSupport, -}; -use datafusion_datasource::write::demux::DemuxedStreamReceiver; - use arrow::compute::sum; use arrow::datatypes::{DataType, Field, FieldRef}; +use arrow::datatypes::{Fields, Schema, SchemaRef}; use datafusion_common::config::{ConfigField, ConfigFileType, TableParquetOptions}; use datafusion_common::parsers::CompressionTypeVariant; use datafusion_common::stats::Precision; @@ -47,8 +38,15 @@ use datafusion_common::{HashMap, Statistics}; use datafusion_common_runtime::{JoinSet, SpawnedTask}; use datafusion_datasource::display::FileGroupDisplay; use datafusion_datasource::file::FileSource; +use datafusion_datasource::file_compression_type::FileCompressionType; +use datafusion_datasource::file_format::{ + FileFormat, FileFormatFactory, FilePushdownSupport, +}; use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder}; +use datafusion_datasource::file_sink_config::{FileSink, FileSinkConfig}; use datafusion_datasource::sink::{DataSink, DataSinkExec}; +use datafusion_datasource::write::demux::DemuxedStreamReceiver; +use datafusion_datasource::write::{create_writer, get_writer_schema, SharedBuffer}; use datafusion_execution::memory_pool::{MemoryConsumer, MemoryPool, MemoryReservation}; use datafusion_execution::{SendableRecordBatchStream, TaskContext}; use datafusion_expr::dml::InsertOp; @@ -61,7 +59,7 @@ use datafusion_physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan}; use datafusion_session::Session; use crate::can_expr_be_pushed_down_with_schemas; -use crate::source::{parse_coerce_int96_string, ParquetSource}; +use crate::source::ParquetSource; use async_trait::async_trait; use bytes::Bytes; use datafusion_datasource::source::DataSourceExec; @@ -78,13 +76,11 @@ use parquet::arrow::arrow_writer::{ }; use parquet::arrow::async_reader::MetadataFetch; use parquet::arrow::{parquet_to_arrow_schema, ArrowSchemaConverter, AsyncArrowWriter}; -use parquet::basic::Type; use parquet::errors::ParquetError; use parquet::file::metadata::{ParquetMetaData, ParquetMetaDataReader, RowGroupMetaData}; use parquet::file::properties::{WriterProperties, WriterPropertiesBuilder}; use parquet::file::writer::SerializedFileWriter; use parquet::format::FileMetaData; -use parquet::schema::types::SchemaDescriptor; use tokio::io::{AsyncWrite, AsyncWriteExt}; use tokio::sync::mpsc::{self, Receiver, Sender}; @@ -272,15 +268,6 @@ impl ParquetFormat { self.options.global.binary_as_string = binary_as_string; self } - - pub fn coerce_int96(&self) -> Option { - self.options.global.coerce_int96.clone() - } - - pub fn with_coerce_int96(mut self, time_unit: Option) -> Self { - self.options.global.coerce_int96 = time_unit; - self - } } /// Clears all metadata (Schema level and field level) on an iterator @@ -304,10 +291,9 @@ async fn fetch_schema_with_location( store: &dyn ObjectStore, file: &ObjectMeta, metadata_size_hint: Option, - coerce_int96: Option, ) -> Result<(Path, Schema)> { let loc_path = file.location.clone(); - let schema = fetch_schema(store, file, metadata_size_hint, coerce_int96).await?; + let schema = fetch_schema(store, file, metadata_size_hint).await?; Ok((loc_path, schema)) } @@ -338,17 +324,12 @@ impl FileFormat for ParquetFormat { store: &Arc, objects: &[ObjectMeta], ) -> Result { - let coerce_int96 = match self.coerce_int96() { - Some(time_unit) => Some(parse_coerce_int96_string(time_unit.as_str())?), - None => None, - }; let mut schemas: Vec<_> = futures::stream::iter(objects) .map(|object| { fetch_schema_with_location( store.as_ref(), object, self.metadata_size_hint(), - coerce_int96, ) }) .boxed() // Workaround https://github.com/rust-lang/rust/issues/64552 @@ -588,46 +569,6 @@ pub fn apply_file_schema_type_coercions( )) } -/// Coerces the file schema's Timestamps to the provided TimeUnit if Parquet schema contains INT96. -pub fn coerce_int96_to_resolution( - parquet_schema: &SchemaDescriptor, - file_schema: &Schema, - time_unit: &TimeUnit, -) -> Option { - let mut transform = false; - let parquet_fields: HashMap<_, _> = parquet_schema - .columns() - .iter() - .map(|f| { - let dt = f.physical_type(); - if dt.eq(&Type::INT96) { - transform = true; - } - (f.name(), dt) - }) - .collect(); - - if !transform { - return None; - } - - let transformed_fields: Vec> = file_schema - .fields - .iter() - .map(|field| match parquet_fields.get(field.name().as_str()) { - Some(Type::INT96) => { - field_with_new_type(field, DataType::Timestamp(*time_unit, None)) - } - _ => Arc::clone(field), - }) - .collect(); - - Some(Schema::new_with_metadata( - transformed_fields, - file_schema.metadata.clone(), - )) -} - /// Coerces the file schema if the table schema uses a view type. #[deprecated( since = "47.0.0", @@ -794,7 +735,10 @@ impl<'a> ObjectStoreFetch<'a> { } impl MetadataFetch for ObjectStoreFetch<'_> { - fn fetch(&mut self, range: Range) -> BoxFuture<'_, Result> { + fn fetch( + &mut self, + range: Range, + ) -> BoxFuture<'_, Result> { async { self.store .get_range(&self.meta.location, range) @@ -831,7 +775,6 @@ async fn fetch_schema( store: &dyn ObjectStore, file: &ObjectMeta, metadata_size_hint: Option, - coerce_int96: Option, ) -> Result { let metadata = fetch_parquet_metadata(store, file, metadata_size_hint).await?; let file_metadata = metadata.file_metadata(); @@ -839,11 +782,6 @@ async fn fetch_schema( file_metadata.schema_descr(), file_metadata.key_value_metadata(), )?; - let schema = coerce_int96 - .and_then(|time_unit| { - coerce_int96_to_resolution(file_metadata.schema_descr(), &schema, &time_unit) - }) - .unwrap_or(schema); Ok(schema) } diff --git a/datafusion/datasource-parquet/src/opener.rs b/datafusion/datasource-parquet/src/opener.rs index cfe8213f86e4b..732fef47d5a75 100644 --- a/datafusion/datasource-parquet/src/opener.rs +++ b/datafusion/datasource-parquet/src/opener.rs @@ -22,27 +22,25 @@ use std::sync::Arc; use crate::page_filter::PagePruningAccessPlanFilter; use crate::row_group_filter::RowGroupAccessPlanFilter; use crate::{ - apply_file_schema_type_coercions, coerce_int96_to_resolution, row_filter, - should_enable_page_index, ParquetAccessPlan, ParquetFileMetrics, - ParquetFileReaderFactory, + apply_file_schema_type_coercions, row_filter, should_enable_page_index, + ParquetAccessPlan, ParquetFileMetrics, ParquetFileReaderFactory, }; use datafusion_datasource::file_meta::FileMeta; use datafusion_datasource::file_stream::{FileOpenFuture, FileOpener}; use datafusion_datasource::schema_adapter::SchemaAdapterFactory; -use arrow::datatypes::{SchemaRef, TimeUnit}; +use arrow::datatypes::SchemaRef; use arrow::error::ArrowError; use datafusion_common::{exec_err, Result}; use datafusion_physical_expr_common::physical_expr::PhysicalExpr; use datafusion_physical_optimizer::pruning::PruningPredicate; -use datafusion_physical_plan::metrics::{Count, ExecutionPlanMetricsSet, MetricBuilder}; +use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet; use futures::{StreamExt, TryStreamExt}; use log::debug; use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ArrowReaderOptions}; use parquet::arrow::async_reader::AsyncFileReader; use parquet::arrow::{ParquetRecordBatchStreamBuilder, ProjectionMask}; -use parquet::file::metadata::ParquetMetaDataReader; /// Implements [`FileOpener`] for a parquet file pub(super) struct ParquetOpener { @@ -56,6 +54,10 @@ pub(super) struct ParquetOpener { pub limit: Option, /// Optional predicate to apply during the scan pub predicate: Option>, + /// Optional pruning predicate applied to row group statistics + pub pruning_predicate: Option>, + /// Optional pruning predicate applied to data page statistics + pub page_pruning_predicate: Option>, /// Schema of the output table pub table_schema: SchemaRef, /// Optional hint for how large the initial request to read parquet metadata @@ -78,10 +80,6 @@ pub(super) struct ParquetOpener { pub enable_bloom_filter: bool, /// Schema adapter factory pub schema_adapter_factory: Arc, - /// Should row group pruning be applied - pub enable_row_group_stats_pruning: bool, - /// Coerce INT96 timestamps to specific TimeUnit - pub coerce_int96: Option, } impl FileOpener for ParquetOpener { @@ -94,7 +92,7 @@ impl FileOpener for ParquetOpener { let metadata_size_hint = file_meta.metadata_size_hint.or(self.metadata_size_hint); - let mut async_file_reader: Box = + let mut reader: Box = self.parquet_file_reader_factory.create_reader( self.partition_index, file_meta, @@ -111,100 +109,47 @@ impl FileOpener for ParquetOpener { .schema_adapter_factory .create(projected_schema, Arc::clone(&self.table_schema)); let predicate = self.predicate.clone(); + let pruning_predicate = self.pruning_predicate.clone(); + let page_pruning_predicate = self.page_pruning_predicate.clone(); let table_schema = Arc::clone(&self.table_schema); let reorder_predicates = self.reorder_filters; let pushdown_filters = self.pushdown_filters; - let coerce_int96 = self.coerce_int96; + let enable_page_index = should_enable_page_index( + self.enable_page_index, + &self.page_pruning_predicate, + ); let enable_bloom_filter = self.enable_bloom_filter; - let enable_row_group_stats_pruning = self.enable_row_group_stats_pruning; let limit = self.limit; - let predicate_creation_errors = MetricBuilder::new(&self.metrics) - .global_counter("num_predicate_creation_errors"); - - let enable_page_index = self.enable_page_index; - Ok(Box::pin(async move { - // Don't load the page index yet. Since it is not stored inline in - // the footer, loading the page index if it is not needed will do - // unecessary I/O. We decide later if it is needed to evaluate the - // pruning predicates. Thus default to not requesting if from the - // underlying reader. - let mut options = ArrowReaderOptions::new().with_page_index(false); + let options = ArrowReaderOptions::new().with_page_index(enable_page_index); + let mut metadata_timer = file_metrics.metadata_load_time.timer(); + let metadata = + ArrowReaderMetadata::load_async(&mut reader, options.clone()).await?; + let mut schema = Arc::clone(metadata.schema()); - // Begin by loading the metadata from the underlying reader (note - // the returned metadata may actually include page indexes as some - // readers may return page indexes even when not requested -- for - // example when they are cached) - let mut reader_metadata = - ArrowReaderMetadata::load_async(&mut async_file_reader, options.clone()) - .await?; - - // Note about schemas: we are actually dealing with **3 different schemas** here: - // - The table schema as defined by the TableProvider. This is what the user sees, what they get when they `SELECT * FROM table`, etc. - // - The "virtual" file schema: this is the table schema minus any hive partition columns and projections. This is what the file schema is coerced to. - // - The physical file schema: this is the schema as defined by the parquet file. This is what the parquet file actually contains. - let mut physical_file_schema = Arc::clone(reader_metadata.schema()); - - // The schema loaded from the file may not be the same as the - // desired schema (for example if we want to instruct the parquet - // reader to read strings using Utf8View instead). Update if necessary - if let Some(merged) = - apply_file_schema_type_coercions(&table_schema, &physical_file_schema) + // read with view types + if let Some(merged) = apply_file_schema_type_coercions(&table_schema, &schema) { - physical_file_schema = Arc::new(merged); - options = options.with_schema(Arc::clone(&physical_file_schema)); - reader_metadata = ArrowReaderMetadata::try_new( - Arc::clone(reader_metadata.metadata()), - options.clone(), - )?; - } - - if coerce_int96.is_some() { - if let Some(merged) = coerce_int96_to_resolution( - reader_metadata.parquet_schema(), - &physical_file_schema, - &(coerce_int96.unwrap()), - ) { - physical_file_schema = Arc::new(merged); - options = options.with_schema(Arc::clone(&physical_file_schema)); - reader_metadata = ArrowReaderMetadata::try_new( - Arc::clone(reader_metadata.metadata()), - options.clone(), - )?; - } + schema = Arc::new(merged); } - // Build predicates for this specific file - let (pruning_predicate, page_pruning_predicate) = build_pruning_predicates( - &predicate, - &physical_file_schema, - &predicate_creation_errors, - ); - - // The page index is not stored inline in the parquet footer so the - // code above may not have read the page index structures yet. If we - // need them for reading and they aren't yet loaded, we need to load them now. - if should_enable_page_index(enable_page_index, &page_pruning_predicate) { - reader_metadata = load_page_index( - reader_metadata, - &mut async_file_reader, - // Since we're manually loading the page index the option here should not matter but we pass it in for consistency - options.with_page_index(true), - ) - .await?; - } + let options = ArrowReaderOptions::new() + .with_page_index(enable_page_index) + .with_schema(Arc::clone(&schema)); + let metadata = + ArrowReaderMetadata::try_new(Arc::clone(metadata.metadata()), options)?; metadata_timer.stop(); - let mut builder = ParquetRecordBatchStreamBuilder::new_with_metadata( - async_file_reader, - reader_metadata, - ); + let mut builder = + ParquetRecordBatchStreamBuilder::new_with_metadata(reader, metadata); + + let file_schema = Arc::clone(builder.schema()); let (schema_mapping, adapted_projections) = - schema_adapter.map_schema(&physical_file_schema)?; + schema_adapter.map_schema(&file_schema)?; let mask = ProjectionMask::roots( builder.parquet_schema(), @@ -215,7 +160,7 @@ impl FileOpener for ParquetOpener { if let Some(predicate) = pushdown_filters.then_some(predicate).flatten() { let row_filter = row_filter::build_row_filter( &predicate, - &physical_file_schema, + &file_schema, &table_schema, builder.metadata(), reorder_predicates, @@ -252,20 +197,18 @@ impl FileOpener for ParquetOpener { } // If there is a predicate that can be evaluated against the metadata if let Some(predicate) = predicate.as_ref() { - if enable_row_group_stats_pruning { - row_groups.prune_by_statistics( - &physical_file_schema, - builder.parquet_schema(), - rg_metadata, - predicate, - &file_metrics, - ); - } + row_groups.prune_by_statistics( + &file_schema, + builder.parquet_schema(), + rg_metadata, + predicate, + &file_metrics, + ); if enable_bloom_filter && !row_groups.is_empty() { row_groups .prune_by_bloom_filters( - &physical_file_schema, + &file_schema, &mut builder, predicate, &file_metrics, @@ -283,7 +226,7 @@ impl FileOpener for ParquetOpener { if let Some(p) = page_pruning_predicate { access_plan = p.prune_plan_with_page_index( access_plan, - &physical_file_schema, + &file_schema, builder.parquet_schema(), file_metadata.as_ref(), &file_metrics, @@ -352,91 +295,3 @@ fn create_initial_plan( // default to scanning all row groups Ok(ParquetAccessPlan::new_all(row_group_count)) } - -/// Build a pruning predicate from an optional predicate expression. -/// If the predicate is None or the predicate cannot be converted to a pruning -/// predicate, return None. -/// If there is an error creating the pruning predicate it is recorded by incrementing -/// the `predicate_creation_errors` counter. -pub(crate) fn build_pruning_predicate( - predicate: Arc, - file_schema: &SchemaRef, - predicate_creation_errors: &Count, -) -> Option> { - match PruningPredicate::try_new(predicate, Arc::clone(file_schema)) { - Ok(pruning_predicate) => { - if !pruning_predicate.always_true() { - return Some(Arc::new(pruning_predicate)); - } - } - Err(e) => { - debug!("Could not create pruning predicate for: {e}"); - predicate_creation_errors.add(1); - } - } - None -} - -/// Build a page pruning predicate from an optional predicate expression. -/// If the predicate is None or the predicate cannot be converted to a page pruning -/// predicate, return None. -pub(crate) fn build_page_pruning_predicate( - predicate: &Arc, - file_schema: &SchemaRef, -) -> Arc { - Arc::new(PagePruningAccessPlanFilter::new( - predicate, - Arc::clone(file_schema), - )) -} - -fn build_pruning_predicates( - predicate: &Option>, - file_schema: &SchemaRef, - predicate_creation_errors: &Count, -) -> ( - Option>, - Option>, -) { - let Some(predicate) = predicate.as_ref() else { - return (None, None); - }; - let pruning_predicate = build_pruning_predicate( - Arc::clone(predicate), - file_schema, - predicate_creation_errors, - ); - let page_pruning_predicate = build_page_pruning_predicate(predicate, file_schema); - (pruning_predicate, Some(page_pruning_predicate)) -} - -/// Returns a `ArrowReaderMetadata` with the page index loaded, loading -/// it from the underlying `AsyncFileReader` if necessary. -async fn load_page_index( - reader_metadata: ArrowReaderMetadata, - input: &mut T, - options: ArrowReaderOptions, -) -> Result { - let parquet_metadata = reader_metadata.metadata(); - let missing_column_index = parquet_metadata.column_index().is_none(); - let missing_offset_index = parquet_metadata.offset_index().is_none(); - // You may ask yourself: why are we even checking if the page index is already loaded here? - // Didn't we explicitly *not* load it above? - // Well it's possible that a custom implementation of `AsyncFileReader` gives you - // the page index even if you didn't ask for it (e.g. because it's cached) - // so it's important to check that here to avoid extra work. - if missing_column_index || missing_offset_index { - let m = Arc::try_unwrap(Arc::clone(parquet_metadata)) - .unwrap_or_else(|e| e.as_ref().clone()); - let mut reader = - ParquetMetaDataReader::new_with_metadata(m).with_page_indexes(true); - reader.load_page_index(input).await?; - let new_parquet_metadata = reader.finish()?; - let new_arrow_reader = - ArrowReaderMetadata::try_new(Arc::new(new_parquet_metadata), options)?; - Ok(new_arrow_reader) - } else { - // No need to load the page index again, just return the existing metadata - Ok(reader_metadata) - } -} diff --git a/datafusion/datasource-parquet/src/page_filter.rs b/datafusion/datasource-parquet/src/page_filter.rs index 148527998ab53..ef832d808647c 100644 --- a/datafusion/datasource-parquet/src/page_filter.rs +++ b/datafusion/datasource-parquet/src/page_filter.rs @@ -249,9 +249,9 @@ impl PagePruningAccessPlanFilter { } if let Some(overall_selection) = overall_selection { - let rows_selected = overall_selection.row_count(); - if rows_selected > 0 { - let rows_skipped = overall_selection.skipped_row_count(); + if overall_selection.selects_any() { + let rows_skipped = rows_skipped(&overall_selection); + let rows_selected = rows_selected(&overall_selection); trace!("Overall selection from predicate skipped {rows_skipped}, selected {rows_selected}: {overall_selection:?}"); total_skip += rows_skipped; total_select += rows_selected; @@ -280,6 +280,22 @@ impl PagePruningAccessPlanFilter { } } +/// returns the number of rows skipped in the selection +/// TODO should this be upstreamed to RowSelection? +fn rows_skipped(selection: &RowSelection) -> usize { + selection + .iter() + .fold(0, |acc, x| if x.skip { acc + x.row_count } else { acc }) +} + +/// returns the number of rows not skipped in the selection +/// TODO should this be upstreamed to RowSelection? +fn rows_selected(selection: &RowSelection) -> usize { + selection + .iter() + .fold(0, |acc, x| if x.skip { acc } else { acc + x.row_count }) +} + fn update_selection( current_selection: Option, row_selection: RowSelection, diff --git a/datafusion/datasource-parquet/src/reader.rs b/datafusion/datasource-parquet/src/reader.rs index 27ec843c1991d..5924a5b5038fc 100644 --- a/datafusion/datasource-parquet/src/reader.rs +++ b/datafusion/datasource-parquet/src/reader.rs @@ -18,19 +18,19 @@ //! [`ParquetFileReaderFactory`] and [`DefaultParquetFileReaderFactory`] for //! low level control of parquet file readers -use crate::ParquetFileMetrics; use bytes::Bytes; use datafusion_datasource::file_meta::FileMeta; use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet; use futures::future::BoxFuture; use object_store::ObjectStore; -use parquet::arrow::arrow_reader::ArrowReaderOptions; use parquet::arrow::async_reader::{AsyncFileReader, ParquetObjectReader}; use parquet::file::metadata::ParquetMetaData; use std::fmt::Debug; use std::ops::Range; use std::sync::Arc; +use crate::ParquetFileMetrics; + /// Interface for reading parquet files. /// /// The combined implementations of [`ParquetFileReaderFactory`] and @@ -96,30 +96,28 @@ pub(crate) struct ParquetFileReader { impl AsyncFileReader for ParquetFileReader { fn get_bytes( &mut self, - range: Range, + range: Range, ) -> BoxFuture<'_, parquet::errors::Result> { - let bytes_scanned = range.end - range.start; - self.file_metrics.bytes_scanned.add(bytes_scanned as usize); + self.file_metrics.bytes_scanned.add(range.end - range.start); self.inner.get_bytes(range) } fn get_byte_ranges( &mut self, - ranges: Vec>, + ranges: Vec>, ) -> BoxFuture<'_, parquet::errors::Result>> where Self: Send, { - let total: u64 = ranges.iter().map(|r| r.end - r.start).sum(); - self.file_metrics.bytes_scanned.add(total as usize); + let total = ranges.iter().map(|r| r.end - r.start).sum(); + self.file_metrics.bytes_scanned.add(total); self.inner.get_byte_ranges(ranges) } - fn get_metadata<'a>( - &'a mut self, - options: Option<&'a ArrowReaderOptions>, - ) -> BoxFuture<'a, parquet::errors::Result>> { - self.inner.get_metadata(options) + fn get_metadata( + &mut self, + ) -> BoxFuture<'_, parquet::errors::Result>> { + self.inner.get_metadata() } } @@ -137,8 +135,7 @@ impl ParquetFileReaderFactory for DefaultParquetFileReaderFactory { metrics, ); let store = Arc::clone(&self.store); - let mut inner = ParquetObjectReader::new(store, file_meta.object_meta.location) - .with_file_size(file_meta.object_meta.size); + let mut inner = ParquetObjectReader::new(store, file_meta.object_meta); if let Some(hint) = metadata_size_hint { inner = inner.with_footer_size_hint(hint) diff --git a/datafusion/datasource-parquet/src/row_filter.rs b/datafusion/datasource-parquet/src/row_filter.rs index 2d2993c29a6f2..da6bf114d71dd 100644 --- a/datafusion/datasource-parquet/src/row_filter.rs +++ b/datafusion/datasource-parquet/src/row_filter.rs @@ -449,7 +449,7 @@ fn columns_sorted(_columns: &[usize], _metadata: &ParquetMetaData) -> Result, - physical_file_schema: &SchemaRef, + file_schema: &SchemaRef, table_schema: &SchemaRef, metadata: &ParquetMetaData, reorder_predicates: bool, @@ -470,7 +470,7 @@ pub fn build_row_filter( .map(|expr| { FilterCandidateBuilder::new( Arc::clone(expr), - Arc::clone(physical_file_schema), + Arc::clone(file_schema), Arc::clone(table_schema), Arc::clone(schema_adapter_factory), ) diff --git a/datafusion/datasource-parquet/src/row_group_filter.rs b/datafusion/datasource-parquet/src/row_group_filter.rs index 13418cdeee223..9d5f9fa16b6eb 100644 --- a/datafusion/datasource-parquet/src/row_group_filter.rs +++ b/datafusion/datasource-parquet/src/row_group_filter.rs @@ -1513,7 +1513,7 @@ mod tests { let object_meta = ObjectMeta { location: object_store::path::Path::parse(file_name).expect("creating path"), last_modified: chrono::DateTime::from(std::time::SystemTime::now()), - size: data.len() as u64, + size: data.len(), e_tag: None, version: None, }; @@ -1526,11 +1526,8 @@ mod tests { let metrics = ExecutionPlanMetricsSet::new(); let file_metrics = ParquetFileMetrics::new(0, object_meta.location.as_ref(), &metrics); - let inner = ParquetObjectReader::new(Arc::new(in_memory), object_meta.location) - .with_file_size(object_meta.size); - let reader = ParquetFileReader { - inner, + inner: ParquetObjectReader::new(Arc::new(in_memory), object_meta), file_metrics: file_metrics.clone(), }; let mut builder = ParquetRecordBatchStreamBuilder::new(reader).await.unwrap(); diff --git a/datafusion/datasource-parquet/src/source.rs b/datafusion/datasource-parquet/src/source.rs index e15f5243cd27e..66d4d313d5a61 100644 --- a/datafusion/datasource-parquet/src/source.rs +++ b/datafusion/datasource-parquet/src/source.rs @@ -17,12 +17,9 @@ //! ParquetSource implementation for reading parquet files use std::any::Any; -use std::fmt::Debug; use std::fmt::Formatter; use std::sync::Arc; -use crate::opener::build_page_pruning_predicate; -use crate::opener::build_pruning_predicate; use crate::opener::ParquetOpener; use crate::page_filter::PagePruningAccessPlanFilter; use crate::DefaultParquetFileReaderFactory; @@ -32,9 +29,9 @@ use datafusion_datasource::schema_adapter::{ DefaultSchemaAdapterFactory, SchemaAdapterFactory, }; -use arrow::datatypes::{Schema, SchemaRef, TimeUnit}; +use arrow::datatypes::{Schema, SchemaRef}; use datafusion_common::config::TableParquetOptions; -use datafusion_common::{DataFusionError, Statistics}; +use datafusion_common::Statistics; use datafusion_datasource::file::FileSource; use datafusion_datasource::file_scan_config::FileScanConfig; use datafusion_physical_expr_common::physical_expr::fmt_sql; @@ -44,6 +41,7 @@ use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, MetricBuilder}; use datafusion_physical_plan::DisplayFormatType; use itertools::Itertools; +use log::debug; use object_store::ObjectStore; /// Execution plan for reading one or more Parquet files. @@ -318,10 +316,24 @@ impl ParquetSource { conf = conf.with_metrics(metrics); conf.predicate = Some(Arc::clone(&predicate)); - conf.page_pruning_predicate = - Some(build_page_pruning_predicate(&predicate, &file_schema)); - conf.pruning_predicate = - build_pruning_predicate(predicate, &file_schema, &predicate_creation_errors); + match PruningPredicate::try_new(Arc::clone(&predicate), Arc::clone(&file_schema)) + { + Ok(pruning_predicate) => { + if !pruning_predicate.always_true() { + conf.pruning_predicate = Some(Arc::new(pruning_predicate)); + } + } + Err(e) => { + debug!("Could not create pruning predicate for: {e}"); + predicate_creation_errors.add(1); + } + }; + + let page_pruning_predicate = Arc::new(PagePruningAccessPlanFilter::new( + &predicate, + Arc::clone(&file_schema), + )); + conf.page_pruning_predicate = Some(page_pruning_predicate); conf } @@ -336,6 +348,16 @@ impl ParquetSource { self.predicate.as_ref() } + /// Optional reference to this parquet scan's pruning predicate + pub fn pruning_predicate(&self) -> Option<&Arc> { + self.pruning_predicate.as_ref() + } + + /// Optional reference to this parquet scan's page pruning predicate + pub fn page_pruning_predicate(&self) -> Option<&Arc> { + self.page_pruning_predicate.as_ref() + } + /// return the optional file reader factory pub fn parquet_file_reader_factory( &self, @@ -438,24 +460,6 @@ impl ParquetSource { } } -/// Parses datafusion.common.config.ParquetOptions.coerce_int96 String to a arrow_schema.datatype.TimeUnit -pub(crate) fn parse_coerce_int96_string( - str_setting: &str, -) -> datafusion_common::Result { - let str_setting_lower: &str = &str_setting.to_lowercase(); - - match str_setting_lower { - "ns" => Ok(TimeUnit::Nanosecond), - "us" => Ok(TimeUnit::Microsecond), - "ms" => Ok(TimeUnit::Millisecond), - "s" => Ok(TimeUnit::Second), - _ => Err(DataFusionError::Configuration(format!( - "Unknown or unsupported parquet coerce_int96: \ - {str_setting}. Valid values are: ns, us, ms, and s." - ))), - } -} - impl FileSource for ParquetSource { fn create_file_opener( &self, @@ -476,13 +480,6 @@ impl FileSource for ParquetSource { Arc::new(DefaultParquetFileReaderFactory::new(object_store)) as _ }); - let coerce_int96 = self - .table_parquet_options - .global - .coerce_int96 - .as_ref() - .map(|time_unit| parse_coerce_int96_string(time_unit.as_str()).unwrap()); - Arc::new(ParquetOpener { partition_index: partition, projection: Arc::from(projection), @@ -491,6 +488,8 @@ impl FileSource for ParquetSource { .expect("Batch size must set before creating ParquetOpener"), limit: base_config.limit, predicate: self.predicate.clone(), + pruning_predicate: self.pruning_predicate.clone(), + page_pruning_predicate: self.page_pruning_predicate.clone(), table_schema: Arc::clone(&base_config.file_schema), metadata_size_hint: self.metadata_size_hint, metrics: self.metrics().clone(), @@ -499,9 +498,7 @@ impl FileSource for ParquetSource { reorder_filters: self.reorder_filters(), enable_page_index: self.enable_page_index(), enable_bloom_filter: self.bloom_filter_on_read(), - enable_row_group_stats_pruning: self.table_parquet_options.global.pruning, schema_adapter_factory, - coerce_int96, }) } @@ -540,10 +537,11 @@ impl FileSource for ParquetSource { .expect("projected_statistics must be set"); // When filters are pushed down, we have no way of knowing the exact statistics. // Note that pruning predicate is also a kind of filter pushdown. - // (bloom filters use `pruning_predicate` too). - // Because filter pushdown may happen dynamically as long as there is a predicate - // if we have *any* predicate applied, we can't guarantee the statistics are exact. - if self.predicate().is_some() { + // (bloom filters use `pruning_predicate` too) + if self.pruning_predicate().is_some() + || self.page_pruning_predicate().is_some() + || (self.predicate().is_some() && self.pushdown_filters()) + { Ok(statistics.to_inexact()) } else { Ok(statistics) @@ -562,8 +560,7 @@ impl FileSource for ParquetSource { .map(|p| format!(", predicate={p}")) .unwrap_or_default(); let pruning_predicate_string = self - .pruning_predicate - .as_ref() + .pruning_predicate() .map(|pre| { let mut guarantees = pre .literal_guarantees() diff --git a/datafusion/datasource/Cargo.toml b/datafusion/datasource/Cargo.toml index 1088efc268c9b..2132272b5768d 100644 --- a/datafusion/datasource/Cargo.toml +++ b/datafusion/datasource/Cargo.toml @@ -56,7 +56,7 @@ datafusion-physical-expr = { workspace = true } datafusion-physical-expr-common = { workspace = true } datafusion-physical-plan = { workspace = true } datafusion-session = { workspace = true } -flate2 = { version = "1.1.1", optional = true } +flate2 = { version = "1.0.24", optional = true } futures = { workspace = true } glob = "0.3.0" itertools = { workspace = true } @@ -72,7 +72,6 @@ xz2 = { version = "0.1", optional = true, features = ["static"] } zstd = { version = "0.13", optional = true, default-features = false } [dev-dependencies] -criterion = { workspace = true } tempfile = { workspace = true } [lints] @@ -81,7 +80,3 @@ workspace = true [lib] name = "datafusion_datasource" path = "src/mod.rs" - -[[bench]] -name = "split_groups_by_statistics" -harness = false diff --git a/datafusion/datasource/benches/split_groups_by_statistics.rs b/datafusion/datasource/benches/split_groups_by_statistics.rs deleted file mode 100644 index f7c5e1b44ae00..0000000000000 --- a/datafusion/datasource/benches/split_groups_by_statistics.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use arrow::datatypes::{DataType, Field, Schema}; -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; -use datafusion_datasource::file_scan_config::FileScanConfig; -use datafusion_datasource::{generate_test_files, verify_sort_integrity}; -use datafusion_physical_expr::PhysicalSortExpr; -use datafusion_physical_expr_common::sort_expr::LexOrdering; -use std::sync::Arc; -use std::time::Duration; - -pub fn compare_split_groups_by_statistics_algorithms(c: &mut Criterion) { - let file_schema = Arc::new(Schema::new(vec![Field::new( - "value", - DataType::Float64, - false, - )])); - - let sort_expr = PhysicalSortExpr { - expr: Arc::new(datafusion_physical_expr::expressions::Column::new( - "value", 0, - )), - options: arrow::compute::SortOptions::default(), - }; - let sort_ordering = LexOrdering::from(vec![sort_expr]); - - // Small, medium, large number of files - let file_counts = [10, 100, 1000]; - let overlap_factors = [0.0, 0.2, 0.5, 0.8]; // No, low, medium, high overlap - - let target_partitions: [usize; 4] = [4, 8, 16, 32]; - - let mut group = c.benchmark_group("split_groups"); - group.measurement_time(Duration::from_secs(10)); - - for &num_files in &file_counts { - for &overlap in &overlap_factors { - let file_groups = generate_test_files(num_files, overlap); - // Benchmark original algorithm - group.bench_with_input( - BenchmarkId::new( - "original", - format!("files={},overlap={:.1}", num_files, overlap), - ), - &( - file_groups.clone(), - file_schema.clone(), - sort_ordering.clone(), - ), - |b, (fg, schema, order)| { - let mut result = Vec::new(); - b.iter(|| { - result = - FileScanConfig::split_groups_by_statistics(schema, fg, order) - .unwrap(); - }); - assert!(verify_sort_integrity(&result)); - }, - ); - - // Benchmark new algorithm with different target partitions - for &tp in &target_partitions { - group.bench_with_input( - BenchmarkId::new( - format!("v2_partitions={}", tp), - format!("files={},overlap={:.1}", num_files, overlap), - ), - &( - file_groups.clone(), - file_schema.clone(), - sort_ordering.clone(), - tp, - ), - |b, (fg, schema, order, target)| { - let mut result = Vec::new(); - b.iter(|| { - result = FileScanConfig::split_groups_by_statistics_with_target_partitions( - schema, fg, order, *target, - ) - .unwrap(); - }); - assert!(verify_sort_integrity(&result)); - }, - ); - } - } - } - - group.finish(); -} - -criterion_group!(benches, compare_split_groups_by_statistics_algorithms); -criterion_main!(benches); diff --git a/datafusion/datasource/src/file.rs b/datafusion/datasource/src/file.rs index 835285b21e38a..0066f39801a1b 100644 --- a/datafusion/datasource/src/file.rs +++ b/datafusion/datasource/src/file.rs @@ -26,12 +26,8 @@ use crate::file_groups::FileGroupPartitioner; use crate::file_scan_config::FileScanConfig; use crate::file_stream::FileOpener; use arrow::datatypes::SchemaRef; -use datafusion_common::config::ConfigOptions; -use datafusion_common::{Result, Statistics}; +use datafusion_common::Statistics; use datafusion_physical_expr::LexOrdering; -use datafusion_physical_plan::filter_pushdown::{ - filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, -}; use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet; use datafusion_physical_plan::DisplayFormatType; @@ -61,7 +57,7 @@ pub trait FileSource: Send + Sync { /// Return execution plan metrics fn metrics(&self) -> &ExecutionPlanMetricsSet; /// Return projected statistics - fn statistics(&self) -> Result; + fn statistics(&self) -> datafusion_common::Result; /// String representation of file source such as "csv", "json", "parquet" fn file_type(&self) -> &str; /// Format FileType specific information @@ -79,7 +75,7 @@ pub trait FileSource: Send + Sync { repartition_file_min_size: usize, output_ordering: Option, config: &FileScanConfig, - ) -> Result> { + ) -> datafusion_common::Result> { if config.file_compression_type.is_compressed() || config.new_lines_in_values { return Ok(None); } @@ -97,16 +93,4 @@ pub trait FileSource: Send + Sync { } Ok(None) } - - /// Try to push down filters into this FileSource. - /// See [`ExecutionPlan::try_pushdown_filters`] for more details. - /// - /// [`ExecutionPlan::try_pushdown_filters`]: datafusion_physical_plan::ExecutionPlan::try_pushdown_filters - fn try_pushdown_filters( - &self, - fd: FilterDescription, - _config: &ConfigOptions, - ) -> Result>> { - Ok(filter_pushdown_not_supported(fd)) - } } diff --git a/datafusion/datasource/src/file_groups.rs b/datafusion/datasource/src/file_groups.rs index 15c86427ed00a..5fe3e25eaa1fe 100644 --- a/datafusion/datasource/src/file_groups.rs +++ b/datafusion/datasource/src/file_groups.rs @@ -25,7 +25,6 @@ use std::collections::BinaryHeap; use std::iter::repeat_with; use std::mem; use std::ops::{Index, IndexMut}; -use std::sync::Arc; /// Repartition input files into `target_partitions` partitions, if total file size exceed /// `repartition_file_min_size` @@ -224,11 +223,10 @@ impl FileGroupPartitioner { return None; } - let target_partition_size = - (total_size as u64).div_ceil(target_partitions as u64); + let target_partition_size = (total_size as usize).div_ceil(target_partitions); let current_partition_index: usize = 0; - let current_partition_size: u64 = 0; + let current_partition_size: usize = 0; // Partition byte range evenly for all `PartitionedFile`s let repartitioned_files = flattened_files @@ -370,7 +368,7 @@ pub struct FileGroup { /// The files in this group files: Vec, /// Optional statistics for the data across all files in the group - statistics: Option>, + statistics: Option, } impl FileGroup { @@ -388,7 +386,7 @@ impl FileGroup { } /// Set the statistics for this group - pub fn with_statistics(mut self, statistics: Arc) -> Self { + pub fn with_statistics(mut self, statistics: Statistics) -> Self { self.statistics = Some(statistics); self } @@ -420,11 +418,6 @@ impl FileGroup { self.files.push(file); } - /// Get the statistics for this group - pub fn statistics(&self) -> Option<&Statistics> { - self.statistics.as_deref() - } - /// Partition the list of files into `n` groups pub fn split_files(mut self, n: usize) -> Vec { if self.is_empty() { @@ -498,15 +491,15 @@ struct ToRepartition { /// the index from which the original file will be taken source_index: usize, /// the size of the original file - file_size: u64, + file_size: usize, /// indexes of which group(s) will this be distributed to (including `source_index`) new_groups: Vec, } impl ToRepartition { - /// How big will each file range be when this file is read in its new groups? - fn range_size(&self) -> u64 { - self.file_size / (self.new_groups.len() as u64) + // how big will each file range be when this file is read in its new groups? + fn range_size(&self) -> usize { + self.file_size / self.new_groups.len() } } diff --git a/datafusion/datasource/src/file_scan_config.rs b/datafusion/datasource/src/file_scan_config.rs index fb756cc11fbbc..5172dafb1f91e 100644 --- a/datafusion/datasource/src/file_scan_config.rs +++ b/datafusion/datasource/src/file_scan_config.rs @@ -23,16 +23,6 @@ use std::{ fmt::Result as FmtResult, marker::PhantomData, sync::Arc, }; -use crate::file_groups::FileGroup; -use crate::{ - display::FileGroupsDisplay, - file::FileSource, - file_compression_type::FileCompressionType, - file_stream::FileStream, - source::{DataSource, DataSourceExec}, - statistics::MinMaxStatistics, - PartitionedFile, -}; use arrow::{ array::{ ArrayData, ArrayRef, BufferBuilder, DictionaryArray, RecordBatch, @@ -41,9 +31,7 @@ use arrow::{ buffer::Buffer, datatypes::{ArrowNativeType, DataType, Field, Schema, SchemaRef, UInt16Type}, }; -use datafusion_common::{ - config::ConfigOptions, exec_err, ColumnStatistics, Constraints, Result, Statistics, -}; +use datafusion_common::{exec_err, ColumnStatistics, Constraints, Result, Statistics}; use datafusion_common::{DataFusionError, ScalarValue}; use datafusion_execution::{ object_store::ObjectStoreUrl, SendableRecordBatchStream, TaskContext, @@ -52,10 +40,6 @@ use datafusion_physical_expr::{ expressions::Column, EquivalenceProperties, LexOrdering, Partitioning, PhysicalSortExpr, }; -use datafusion_physical_plan::filter_pushdown::{ - filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, - FilterPushdownSupport, -}; use datafusion_physical_plan::{ display::{display_orderings, ProjectSchemaDisplay}, metrics::ExecutionPlanMetricsSet, @@ -64,6 +48,17 @@ use datafusion_physical_plan::{ }; use log::{debug, warn}; +use crate::file_groups::FileGroup; +use crate::{ + display::FileGroupsDisplay, + file::FileSource, + file_compression_type::FileCompressionType, + file_stream::FileStream, + source::{DataSource, DataSourceExec}, + statistics::MinMaxStatistics, + PartitionedFile, +}; + /// The base configurations for a [`DataSourceExec`], the a physical plan for /// any given file format. /// @@ -143,9 +138,6 @@ pub struct FileScanConfig { /// Schema before `projection` is applied. It contains the all columns that may /// appear in the files. It does not include table partition columns /// that may be added. - /// Note that this is **not** the schema of the physical files. - /// This is the schema that the physical file schema will be - /// mapped onto, and the schema that the [`DataSourceExec`] will return. pub file_schema: SchemaRef, /// List of files to be processed, grouped into partitions /// @@ -159,6 +151,9 @@ pub struct FileScanConfig { pub file_groups: Vec, /// Table constraints pub constraints: Constraints, + /// Estimated overall statistics of the files, taking `filters` into account. + /// Defaults to [`Statistics::new_unknown`]. + pub statistics: Statistics, /// Columns on which to project the data. Indexes that are higher than the /// number of columns of `file_schema` refer to `table_partition_cols`. pub projection: Option>, @@ -232,10 +227,6 @@ pub struct FileScanConfig { #[derive(Clone)] pub struct FileScanConfigBuilder { object_store_url: ObjectStoreUrl, - /// Table schema before any projections or partition columns are applied. - /// This schema is used to read the files, but is **not** necessarily the schema of the physical files. - /// Rather this is the schema that the physical file schema will be mapped onto, and the schema that the - /// [`DataSourceExec`] will return. file_schema: SchemaRef, file_source: Arc, @@ -421,6 +412,7 @@ impl FileScanConfigBuilder { table_partition_cols, constraints, file_groups, + statistics, output_ordering, file_compression_type, new_lines_in_values, @@ -434,9 +426,9 @@ impl From for FileScanConfigBuilder { Self { object_store_url: config.object_store_url, file_schema: config.file_schema, - file_source: Arc::::clone(&config.file_source), + file_source: config.file_source, file_groups: config.file_groups, - statistics: config.file_source.statistics().ok(), + statistics: Some(config.statistics), output_ordering: config.output_ordering, file_compression_type: Some(config.file_compression_type), new_lines_in_values: Some(config.new_lines_in_values), @@ -479,8 +471,7 @@ impl DataSource for FileScanConfig { fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult { match t { DisplayFormatType::Default | DisplayFormatType::Verbose => { - let schema = self.projected_schema(); - let orderings = get_projected_output_ordering(self, &schema); + let (schema, _, _, orderings) = self.project(); write!(f, "file_groups=")?; FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?; @@ -593,46 +584,6 @@ impl DataSource for FileScanConfig { ) as _ })) } - - fn try_pushdown_filters( - &self, - fd: FilterDescription, - config: &ConfigOptions, - ) -> Result>> { - let FilterPushdownResult { - support, - remaining_description, - } = self.file_source.try_pushdown_filters(fd, config)?; - - match support { - FilterPushdownSupport::Supported { - child_descriptions, - op, - revisit, - } => { - let new_data_source = Arc::new( - FileScanConfigBuilder::from(self.clone()) - .with_source(op) - .build(), - ); - - debug_assert!(child_descriptions.is_empty()); - debug_assert!(!revisit); - - Ok(FilterPushdownResult { - support: FilterPushdownSupport::Supported { - child_descriptions, - op: new_data_source, - revisit, - }, - remaining_description, - }) - } - FilterPushdownSupport::NotSupported => { - Ok(filter_pushdown_not_supported(remaining_description)) - } - } - } } impl FileScanConfig { @@ -659,6 +610,7 @@ impl FileScanConfig { file_schema, file_groups: vec![], constraints: Constraints::empty(), + statistics, projection: None, limit: None, table_partition_cols: vec![], @@ -673,8 +625,7 @@ impl FileScanConfig { /// Set the file source #[deprecated(since = "47.0.0", note = "use FileScanConfigBuilder instead")] pub fn with_source(mut self, file_source: Arc) -> Self { - self.file_source = - file_source.with_statistics(Statistics::new_unknown(&self.file_schema)); + self.file_source = file_source.with_statistics(self.statistics.clone()); self } @@ -688,6 +639,7 @@ impl FileScanConfig { /// Set the statistics of the files #[deprecated(since = "47.0.0", note = "use FileScanConfigBuilder instead")] pub fn with_statistics(mut self, statistics: Statistics) -> Self { + self.statistics = statistics.clone(); self.file_source = self.file_source.with_statistics(statistics); self } @@ -701,8 +653,11 @@ impl FileScanConfig { } } - pub fn projected_stats(&self) -> Statistics { - let statistics = self.file_source.statistics().unwrap(); + fn projected_stats(&self) -> Statistics { + let statistics = self + .file_source + .statistics() + .unwrap_or(self.statistics.clone()); let table_cols_stats = self .projection_indices() @@ -725,7 +680,7 @@ impl FileScanConfig { } } - pub fn projected_schema(&self) -> Arc { + fn projected_schema(&self) -> Arc { let table_fields: Vec<_> = self .projection_indices() .into_iter() @@ -745,7 +700,7 @@ impl FileScanConfig { )) } - pub fn projected_constraints(&self) -> Constraints { + fn projected_constraints(&self) -> Constraints { let indexes = self.projection_indices(); self.constraints @@ -849,7 +804,7 @@ impl FileScanConfig { return ( Arc::clone(&self.file_schema), self.constraints.clone(), - self.file_source.statistics().unwrap().clone(), + self.statistics.clone(), self.output_ordering.clone(), ); } @@ -903,96 +858,6 @@ impl FileScanConfig { }) } - /// Splits file groups into new groups based on statistics to enable efficient parallel processing. - /// - /// The method distributes files across a target number of partitions while ensuring - /// files within each partition maintain sort order based on their min/max statistics. - /// - /// The algorithm works by: - /// 1. Takes files sorted by minimum values - /// 2. For each file: - /// - Finds eligible groups (empty or where file's min > group's last max) - /// - Selects the smallest eligible group - /// - Creates a new group if needed - /// - /// # Parameters - /// * `table_schema`: Schema containing information about the columns - /// * `file_groups`: The original file groups to split - /// * `sort_order`: The lexicographical ordering to maintain within each group - /// * `target_partitions`: The desired number of output partitions - /// - /// # Returns - /// A new set of file groups, where files within each group are non-overlapping with respect to - /// their min/max statistics and maintain the specified sort order. - pub fn split_groups_by_statistics_with_target_partitions( - table_schema: &SchemaRef, - file_groups: &[FileGroup], - sort_order: &LexOrdering, - target_partitions: usize, - ) -> Result> { - if target_partitions == 0 { - return Err(DataFusionError::Internal( - "target_partitions must be greater than 0".to_string(), - )); - } - - let flattened_files = file_groups - .iter() - .flat_map(FileGroup::iter) - .collect::>(); - - if flattened_files.is_empty() { - return Ok(vec![]); - } - - let statistics = MinMaxStatistics::new_from_files( - sort_order, - table_schema, - None, - flattened_files.iter().copied(), - )?; - - let indices_sorted_by_min = statistics.min_values_sorted(); - - // Initialize with target_partitions empty groups - let mut file_groups_indices: Vec> = vec![vec![]; target_partitions]; - - for (idx, min) in indices_sorted_by_min { - if let Some((_, group)) = file_groups_indices - .iter_mut() - .enumerate() - .filter(|(_, group)| { - group.is_empty() - || min - > statistics - .max(*group.last().expect("groups should not be empty")) - }) - .min_by_key(|(_, group)| group.len()) - { - group.push(idx); - } else { - // Create a new group if no existing group fits - file_groups_indices.push(vec![idx]); - } - } - - // Remove any empty groups - file_groups_indices.retain(|group| !group.is_empty()); - - // Assemble indices back into groups of PartitionedFiles - Ok(file_groups_indices - .into_iter() - .map(|file_group_indices| { - FileGroup::new( - file_group_indices - .into_iter() - .map(|idx| flattened_files[idx].clone()) - .collect(), - ) - }) - .collect()) - } - /// Attempts to do a bin-packing on files into file groups, such that any two files /// in a file group are ordered and non-overlapping with respect to their statistics. /// It will produce the smallest number of file groups possible. @@ -1084,11 +949,7 @@ impl Debug for FileScanConfig { write!(f, "FileScanConfig {{")?; write!(f, "object_store_url={:?}, ", self.object_store_url)?; - write!( - f, - "statistics={:?}, ", - self.file_source.statistics().unwrap() - )?; + write!(f, "statistics={:?}, ", self.statistics)?; DisplayAs::fmt_as(self, DisplayFormatType::Verbose, f)?; write!(f, "}}") @@ -1097,8 +958,7 @@ impl Debug for FileScanConfig { impl DisplayAs for FileScanConfig { fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> FmtResult { - let schema = self.projected_schema(); - let orderings = get_projected_output_ordering(self, &schema); + let (schema, _, _, orderings) = self.project(); write!(f, "file_groups=")?; FileGroupsDisplay(&self.file_groups).fmt_as(t, f)?; @@ -1517,10 +1377,7 @@ pub fn wrap_partition_value_in_dict(val: ScalarValue) -> ScalarValue { #[cfg(test)] mod tests { - use crate::{ - generate_test_files, test_util::MockSource, tests::aggr_test_schema, - verify_sort_integrity, - }; + use crate::{test_util::MockSource, tests::aggr_test_schema}; use super::*; use arrow::{ @@ -1611,7 +1468,7 @@ mod tests { ); // verify the proj_schema includes the last column and exactly the same the field it is defined - let proj_schema = conf.projected_schema(); + let (proj_schema, _, _, _) = conf.project(); assert_eq!(proj_schema.fields().len(), file_schema.fields().len() + 1); assert_eq!( *proj_schema.field(file_schema.fields().len()), @@ -1717,7 +1574,7 @@ mod tests { assert_eq!(source_statistics, statistics); assert_eq!(source_statistics.column_statistics.len(), 3); - let proj_schema = conf.projected_schema(); + let (proj_schema, ..) = conf.project(); // created a projector for that projected schema let mut proj = PartitionColumnProjector::new( proj_schema, @@ -2143,7 +2000,7 @@ mod tests { }, partition_values: vec![ScalarValue::from(file.date)], range: None, - statistics: Some(Arc::new(Statistics { + statistics: Some(Statistics { num_rows: Precision::Absent, total_byte_size: Precision::Absent, column_statistics: file @@ -2163,7 +2020,7 @@ mod tests { .unwrap_or_default() }) .collect::>(), - })), + }), extensions: None, metadata_size_hint: None, } @@ -2304,24 +2161,13 @@ mod tests { assert!(config.constraints.is_empty()); // Verify statistics are set to unknown + assert_eq!(config.statistics.num_rows, Precision::Absent); + assert_eq!(config.statistics.total_byte_size, Precision::Absent); assert_eq!( - config.file_source.statistics().unwrap().num_rows, - Precision::Absent - ); - assert_eq!( - config.file_source.statistics().unwrap().total_byte_size, - Precision::Absent - ); - assert_eq!( - config - .file_source - .statistics() - .unwrap() - .column_statistics - .len(), + config.statistics.column_statistics.len(), file_schema.fields().len() ); - for stat in config.file_source.statistics().unwrap().column_statistics { + for stat in config.statistics.column_statistics { assert_eq!(stat.distinct_count, Precision::Absent); assert_eq!(stat.min_value, Precision::Absent); assert_eq!(stat.max_value, Precision::Absent); @@ -2376,163 +2222,4 @@ mod tests { assert_eq!(new_config.constraints, Constraints::default()); assert!(new_config.new_lines_in_values); } - - #[test] - fn test_split_groups_by_statistics_with_target_partitions() -> Result<()> { - use datafusion_common::DFSchema; - use datafusion_expr::{col, execution_props::ExecutionProps}; - - let schema = Arc::new(Schema::new(vec![Field::new( - "value", - DataType::Float64, - false, - )])); - - // Setup sort expression - let exec_props = ExecutionProps::new(); - let df_schema = DFSchema::try_from_qualified_schema("test", schema.as_ref())?; - let sort_expr = vec![col("value").sort(true, false)]; - - let physical_sort_exprs: Vec<_> = sort_expr - .iter() - .map(|expr| create_physical_sort_expr(expr, &df_schema, &exec_props).unwrap()) - .collect(); - - let sort_ordering = LexOrdering::from(physical_sort_exprs); - - // Test case parameters - struct TestCase { - name: String, - file_count: usize, - overlap_factor: f64, - target_partitions: usize, - expected_partition_count: usize, - } - - let test_cases = vec![ - // Basic cases - TestCase { - name: "no_overlap_10_files_4_partitions".to_string(), - file_count: 10, - overlap_factor: 0.0, - target_partitions: 4, - expected_partition_count: 4, - }, - TestCase { - name: "medium_overlap_20_files_5_partitions".to_string(), - file_count: 20, - overlap_factor: 0.5, - target_partitions: 5, - expected_partition_count: 5, - }, - TestCase { - name: "high_overlap_30_files_3_partitions".to_string(), - file_count: 30, - overlap_factor: 0.8, - target_partitions: 3, - expected_partition_count: 7, - }, - // Edge cases - TestCase { - name: "fewer_files_than_partitions".to_string(), - file_count: 3, - overlap_factor: 0.0, - target_partitions: 10, - expected_partition_count: 3, // Should only create as many partitions as files - }, - TestCase { - name: "single_file".to_string(), - file_count: 1, - overlap_factor: 0.0, - target_partitions: 5, - expected_partition_count: 1, // Should create only one partition - }, - TestCase { - name: "empty_files".to_string(), - file_count: 0, - overlap_factor: 0.0, - target_partitions: 3, - expected_partition_count: 0, // Empty result for empty input - }, - ]; - - for case in test_cases { - println!("Running test case: {}", case.name); - - // Generate files using bench utility function - let file_groups = generate_test_files(case.file_count, case.overlap_factor); - - // Call the function under test - let result = - FileScanConfig::split_groups_by_statistics_with_target_partitions( - &schema, - &file_groups, - &sort_ordering, - case.target_partitions, - )?; - - // Verify results - println!( - "Created {} partitions (target was {})", - result.len(), - case.target_partitions - ); - - // Check partition count - assert_eq!( - result.len(), - case.expected_partition_count, - "Case '{}': Unexpected partition count", - case.name - ); - - // Verify sort integrity - assert!( - verify_sort_integrity(&result), - "Case '{}': Files within partitions are not properly ordered", - case.name - ); - - // Distribution check for partitions - if case.file_count > 1 && case.expected_partition_count > 1 { - let group_sizes: Vec = result.iter().map(FileGroup::len).collect(); - let max_size = *group_sizes.iter().max().unwrap(); - let min_size = *group_sizes.iter().min().unwrap(); - - // Check partition balancing - difference shouldn't be extreme - let avg_files_per_partition = - case.file_count as f64 / case.expected_partition_count as f64; - assert!( - (max_size as f64) < 2.0 * avg_files_per_partition, - "Case '{}': Unbalanced distribution. Max partition size {} exceeds twice the average {}", - case.name, - max_size, - avg_files_per_partition - ); - - println!( - "Distribution - min files: {}, max files: {}", - min_size, max_size - ); - } - } - - // Test error case: zero target partitions - let empty_groups: Vec = vec![]; - let err = FileScanConfig::split_groups_by_statistics_with_target_partitions( - &schema, - &empty_groups, - &sort_ordering, - 0, - ) - .unwrap_err(); - - assert!( - err.to_string() - .contains("target_partitions must be greater than 0"), - "Expected error for zero target partitions" - ); - - Ok(()) - } } diff --git a/datafusion/datasource/src/file_sink_config.rs b/datafusion/datasource/src/file_sink_config.rs index 2968bd1ee0449..465167fea9546 100644 --- a/datafusion/datasource/src/file_sink_config.rs +++ b/datafusion/datasource/src/file_sink_config.rs @@ -89,7 +89,6 @@ pub trait FileSink: DataSink { /// The base configurations to provide when creating a physical plan for /// writing to any given file format. -#[derive(Debug, Clone)] pub struct FileSinkConfig { /// The unresolved URL specified by the user pub original_url: String, diff --git a/datafusion/datasource/src/file_stream.rs b/datafusion/datasource/src/file_stream.rs index 1dc53bd6b9319..1caefc3277aca 100644 --- a/datafusion/datasource/src/file_stream.rs +++ b/datafusion/datasource/src/file_stream.rs @@ -78,7 +78,7 @@ impl FileStream { file_opener: Arc, metrics: &ExecutionPlanMetricsSet, ) -> Result { - let projected_schema = config.projected_schema(); + let (projected_schema, ..) = config.project(); let pc_projector = PartitionColumnProjector::new( Arc::clone(&projected_schema), &config diff --git a/datafusion/datasource/src/memory.rs b/datafusion/datasource/src/memory.rs index 6d0e16ef4b916..f2e36672cd5c9 100644 --- a/datafusion/datasource/src/memory.rs +++ b/datafusion/datasource/src/memory.rs @@ -19,12 +19,9 @@ use std::any::Any; use std::fmt; -use std::fmt::Debug; use std::sync::Arc; -use crate::sink::DataSink; use crate::source::{DataSource, DataSourceExec}; -use async_trait::async_trait; use datafusion_physical_plan::execution_plan::{Boundedness, EmissionType}; use datafusion_physical_plan::memory::MemoryStream; use datafusion_physical_plan::projection::{ @@ -45,8 +42,6 @@ use datafusion_physical_expr::equivalence::ProjectionMapping; use datafusion_physical_expr::expressions::Column; use datafusion_physical_expr::utils::collect_columns; use datafusion_physical_expr::{EquivalenceProperties, LexOrdering}; -use futures::StreamExt; -use tokio::sync::RwLock; /// Execution plan for reading in-memory batches of data #[derive(Clone)] @@ -67,7 +62,7 @@ pub struct MemoryExec { } #[allow(unused, deprecated)] -impl Debug for MemoryExec { +impl fmt::Debug for MemoryExec { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.inner.fmt_as(DisplayFormatType::Default, f) } @@ -725,91 +720,6 @@ impl MemorySourceConfig { } } -/// Type alias for partition data -pub type PartitionData = Arc>>; - -/// Implements for writing to a [`MemTable`] -/// -/// [`MemTable`]: -pub struct MemSink { - /// Target locations for writing data - batches: Vec, - schema: SchemaRef, -} - -impl Debug for MemSink { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("MemSink") - .field("num_partitions", &self.batches.len()) - .finish() - } -} - -impl DisplayAs for MemSink { - fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match t { - DisplayFormatType::Default | DisplayFormatType::Verbose => { - let partition_count = self.batches.len(); - write!(f, "MemoryTable (partitions={partition_count})") - } - DisplayFormatType::TreeRender => { - // TODO: collect info - write!(f, "") - } - } - } -} - -impl MemSink { - /// Creates a new [`MemSink`]. - /// - /// The caller is responsible for ensuring that there is at least one partition to insert into. - pub fn try_new(batches: Vec, schema: SchemaRef) -> Result { - if batches.is_empty() { - return plan_err!("Cannot insert into MemTable with zero partitions"); - } - Ok(Self { batches, schema }) - } -} - -#[async_trait] -impl DataSink for MemSink { - fn as_any(&self) -> &dyn Any { - self - } - - fn schema(&self) -> &SchemaRef { - &self.schema - } - - async fn write_all( - &self, - mut data: SendableRecordBatchStream, - _context: &Arc, - ) -> Result { - let num_partitions = self.batches.len(); - - // buffer up the data round robin style into num_partitions - - let mut new_batches = vec![vec![]; num_partitions]; - let mut i = 0; - let mut row_count = 0; - while let Some(batch) = data.next().await.transpose()? { - row_count += batch.num_rows(); - new_batches[i].push(batch); - i = (i + 1) % num_partitions; - } - - // write the outputs into the batches - for (target, mut batches) in self.batches.iter().zip(new_batches.into_iter()) { - // Append all the new batches in one go to minimize locking overhead - target.write().await.append(&mut batches); - } - - Ok(row_count as u64) - } -} - #[cfg(test)] mod memory_source_tests { use std::sync::Arc; diff --git a/datafusion/datasource/src/mod.rs b/datafusion/datasource/src/mod.rs index 3e44851d145b8..fb119d1b3d2db 100644 --- a/datafusion/datasource/src/mod.rs +++ b/datafusion/datasource/src/mod.rs @@ -44,28 +44,23 @@ pub mod source; mod statistics; #[cfg(test)] -pub mod test_util; +mod test_util; pub mod url; pub mod write; -pub use self::url::ListingTableUrl; -use crate::file_groups::FileGroup; use chrono::TimeZone; -use datafusion_common::stats::Precision; -use datafusion_common::{exec_datafusion_err, ColumnStatistics, Result}; +use datafusion_common::Result; use datafusion_common::{ScalarValue, Statistics}; use file_meta::FileMeta; use futures::{Stream, StreamExt}; use object_store::{path::Path, ObjectMeta}; use object_store::{GetOptions, GetRange, ObjectStore}; -// Remove when add_row_stats is remove -#[allow(deprecated)] -pub use statistics::add_row_stats; -pub use statistics::compute_all_files_statistics; use std::ops::Range; use std::pin::Pin; use std::sync::Arc; +pub use self::url::ListingTableUrl; + /// Stream of files get listed from object store pub type PartitionedFileStream = Pin> + Send + Sync + 'static>>; @@ -111,7 +106,7 @@ pub struct PartitionedFile { /// /// DataFusion relies on these statistics for planning (in particular to sort file groups), /// so if they are incorrect, incorrect answers may result. - pub statistics: Option>, + pub statistics: Option, /// An optional field for user defined per object metadata pub extensions: Option>, /// The estimated size of the parquet metadata, in bytes @@ -125,7 +120,7 @@ impl PartitionedFile { object_meta: ObjectMeta { location: Path::from(path.into()), last_modified: chrono::Utc.timestamp_nanos(0), - size, + size: size as usize, e_tag: None, version: None, }, @@ -143,7 +138,7 @@ impl PartitionedFile { object_meta: ObjectMeta { location: Path::from(path), last_modified: chrono::Utc.timestamp_nanos(0), - size, + size: size as usize, e_tag: None, version: None, }, @@ -191,12 +186,6 @@ impl PartitionedFile { self.extensions = Some(extensions); self } - - // Update the statistics for this file. - pub fn with_statistics(mut self, statistics: Arc) -> Self { - self.statistics = Some(statistics); - self - } } impl From for PartitionedFile { @@ -226,7 +215,7 @@ impl From for PartitionedFile { /// Indicates that the range calculation determined no further action is /// necessary, possibly because the calculated range is empty or invalid. pub enum RangeCalculation { - Range(Option>), + Range(Option>), TerminateEarly, } @@ -252,12 +241,7 @@ pub async fn calculate_range( match file_meta.range { None => Ok(RangeCalculation::Range(None)), Some(FileRange { start, end }) => { - let start: u64 = start.try_into().map_err(|_| { - exec_datafusion_err!("Expect start range to fit in u64, got {start}") - })?; - let end: u64 = end.try_into().map_err(|_| { - exec_datafusion_err!("Expect end range to fit in u64, got {end}") - })?; + let (start, end) = (start as usize, end as usize); let start_delta = if start != 0 { find_first_newline(store, location, start - 1, file_size, newline).await? @@ -296,10 +280,10 @@ pub async fn calculate_range( async fn find_first_newline( object_store: &Arc, location: &Path, - start: u64, - end: u64, + start: usize, + end: usize, newline: u8, -) -> Result { +) -> Result { let options = GetOptions { range: Some(GetRange::Bounded(start..end)), ..Default::default() @@ -312,125 +296,15 @@ async fn find_first_newline( while let Some(chunk) = result_stream.next().await.transpose()? { if let Some(position) = chunk.iter().position(|&byte| byte == newline) { - let position = position as u64; return Ok(index + position); } - index += chunk.len() as u64; + index += chunk.len(); } Ok(index) } -/// Generates test files with min-max statistics in different overlap patterns. -/// -/// Used by tests and benchmarks. -/// -/// # Overlap Factors -/// -/// The `overlap_factor` parameter controls how much the value ranges in generated test files overlap: -/// - `0.0`: No overlap between files (completely disjoint ranges) -/// - `0.2`: Low overlap (20% of the range size overlaps with adjacent files) -/// - `0.5`: Medium overlap (50% of ranges overlap) -/// - `0.8`: High overlap (80% of ranges overlap between files) -/// -/// # Examples -/// -/// With 5 files and different overlap factors showing `[min, max]` ranges: -/// -/// overlap_factor = 0.0 (no overlap): -/// -/// File 0: [0, 20] -/// File 1: [20, 40] -/// File 2: [40, 60] -/// File 3: [60, 80] -/// File 4: [80, 100] -/// -/// overlap_factor = 0.5 (50% overlap): -/// -/// File 0: [0, 40] -/// File 1: [20, 60] -/// File 2: [40, 80] -/// File 3: [60, 100] -/// File 4: [80, 120] -/// -/// overlap_factor = 0.8 (80% overlap): -/// -/// File 0: [0, 100] -/// File 1: [20, 120] -/// File 2: [40, 140] -/// File 3: [60, 160] -/// File 4: [80, 180] -pub fn generate_test_files(num_files: usize, overlap_factor: f64) -> Vec { - let mut files = Vec::with_capacity(num_files); - if num_files == 0 { - return vec![]; - } - let range_size = if overlap_factor == 0.0 { - 100 / num_files as i64 - } else { - (100.0 / (overlap_factor * num_files as f64)).max(1.0) as i64 - }; - - for i in 0..num_files { - let base = (i as f64 * range_size as f64 * (1.0 - overlap_factor)) as i64; - let min = base as f64; - let max = (base + range_size) as f64; - - let file = PartitionedFile { - object_meta: ObjectMeta { - location: Path::from(format!("file_{}.parquet", i)), - last_modified: chrono::Utc::now(), - size: 1000, - e_tag: None, - version: None, - }, - partition_values: vec![], - range: None, - statistics: Some(Arc::new(Statistics { - num_rows: Precision::Exact(100), - total_byte_size: Precision::Exact(1000), - column_statistics: vec![ColumnStatistics { - null_count: Precision::Exact(0), - max_value: Precision::Exact(ScalarValue::Float64(Some(max))), - min_value: Precision::Exact(ScalarValue::Float64(Some(min))), - sum_value: Precision::Absent, - distinct_count: Precision::Absent, - }], - })), - extensions: None, - metadata_size_hint: None, - }; - files.push(file); - } - - vec![FileGroup::new(files)] -} - -// Helper function to verify that files within each group maintain sort order -/// Used by tests and benchmarks -pub fn verify_sort_integrity(file_groups: &[FileGroup]) -> bool { - for group in file_groups { - let files = group.iter().collect::>(); - for i in 1..files.len() { - let prev_file = files[i - 1]; - let curr_file = files[i]; - - // Check if the min value of current file is greater than max value of previous file - if let (Some(prev_stats), Some(curr_stats)) = - (&prev_file.statistics, &curr_file.statistics) - { - let prev_max = &prev_stats.column_statistics[0].max_value; - let curr_min = &curr_stats.column_statistics[0].min_value; - if curr_min.get_value().unwrap() <= prev_max.get_value().unwrap() { - return false; - } - } - } - } - true -} - #[cfg(test)] mod tests { use super::ListingTableUrl; diff --git a/datafusion/datasource/src/schema_adapter.rs b/datafusion/datasource/src/schema_adapter.rs index eafddecd05f50..4164cda8cba11 100644 --- a/datafusion/datasource/src/schema_adapter.rs +++ b/datafusion/datasource/src/schema_adapter.rs @@ -42,7 +42,7 @@ pub trait SchemaAdapterFactory: Debug + Send + Sync + 'static { /// Arguments: /// /// * `projected_table_schema`: The schema for the table, projected to - /// include only the fields being output (projected) by the this mapping. + /// include only the fields being output (projected) by the this mapping. /// /// * `table_schema`: The entire table schema for the table fn create( diff --git a/datafusion/datasource/src/source.rs b/datafusion/datasource/src/source.rs index 2d6ea1a8b3915..6c9122ce1ac10 100644 --- a/datafusion/datasource/src/source.rs +++ b/datafusion/datasource/src/source.rs @@ -31,14 +31,10 @@ use datafusion_physical_plan::{ use crate::file_scan_config::FileScanConfig; use datafusion_common::config::ConfigOptions; -use datafusion_common::{Constraints, Result, Statistics}; +use datafusion_common::{Constraints, Statistics}; use datafusion_execution::{SendableRecordBatchStream, TaskContext}; use datafusion_physical_expr::{EquivalenceProperties, Partitioning}; use datafusion_physical_expr_common::sort_expr::LexOrdering; -use datafusion_physical_plan::filter_pushdown::{ - filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, - FilterPushdownSupport, -}; /// Common behaviors in Data Sources for both from Files and Memory. /// @@ -55,7 +51,7 @@ pub trait DataSource: Send + Sync + Debug { &self, partition: usize, context: Arc, - ) -> Result; + ) -> datafusion_common::Result; fn as_any(&self) -> &dyn Any; /// Format this source for display in explain plans fn fmt_as(&self, t: DisplayFormatType, f: &mut Formatter) -> fmt::Result; @@ -66,13 +62,13 @@ pub trait DataSource: Send + Sync + Debug { _target_partitions: usize, _repartition_file_min_size: usize, _output_ordering: Option, - ) -> Result>> { + ) -> datafusion_common::Result>> { Ok(None) } fn output_partitioning(&self) -> Partitioning; fn eq_properties(&self) -> EquivalenceProperties; - fn statistics(&self) -> Result; + fn statistics(&self) -> datafusion_common::Result; /// Return a copy of this DataSource with a new fetch limit fn with_fetch(&self, _limit: Option) -> Option>; fn fetch(&self) -> Option; @@ -82,16 +78,7 @@ pub trait DataSource: Send + Sync + Debug { fn try_swapping_with_projection( &self, _projection: &ProjectionExec, - ) -> Result>>; - /// Try to push down filters into this DataSource. - /// See [`ExecutionPlan::try_pushdown_filters`] for more details. - fn try_pushdown_filters( - &self, - fd: FilterDescription, - _config: &ConfigOptions, - ) -> Result>> { - Ok(filter_pushdown_not_supported(fd)) - } + ) -> datafusion_common::Result>>; } /// [`ExecutionPlan`] handles different file formats like JSON, CSV, AVRO, ARROW, PARQUET @@ -144,7 +131,7 @@ impl ExecutionPlan for DataSourceExec { fn with_new_children( self: Arc, _: Vec>, - ) -> Result> { + ) -> datafusion_common::Result> { Ok(self) } @@ -152,7 +139,7 @@ impl ExecutionPlan for DataSourceExec { &self, target_partitions: usize, config: &ConfigOptions, - ) -> Result>> { + ) -> datafusion_common::Result>> { let data_source = self.data_source.repartitioned( target_partitions, config.optimizer.repartition_file_min_size, @@ -176,7 +163,7 @@ impl ExecutionPlan for DataSourceExec { &self, partition: usize, context: Arc, - ) -> Result { + ) -> datafusion_common::Result { self.data_source.open(partition, context) } @@ -184,7 +171,7 @@ impl ExecutionPlan for DataSourceExec { Some(self.data_source.metrics().clone_inner()) } - fn statistics(&self) -> Result { + fn statistics(&self) -> datafusion_common::Result { self.data_source.statistics() } @@ -202,45 +189,9 @@ impl ExecutionPlan for DataSourceExec { fn try_swapping_with_projection( &self, projection: &ProjectionExec, - ) -> Result>> { + ) -> datafusion_common::Result>> { self.data_source.try_swapping_with_projection(projection) } - - fn try_pushdown_filters( - &self, - fd: FilterDescription, - config: &ConfigOptions, - ) -> Result>> { - let FilterPushdownResult { - support, - remaining_description, - } = self.data_source.try_pushdown_filters(fd, config)?; - - match support { - FilterPushdownSupport::Supported { - child_descriptions, - op, - revisit, - } => { - let new_exec = Arc::new(DataSourceExec::new(op)); - - debug_assert!(child_descriptions.is_empty()); - debug_assert!(!revisit); - - Ok(FilterPushdownResult { - support: FilterPushdownSupport::Supported { - child_descriptions, - op: new_exec, - revisit, - }, - remaining_description, - }) - } - FilterPushdownSupport::NotSupported => { - Ok(filter_pushdown_not_supported(remaining_description)) - } - } - } } impl DataSourceExec { @@ -303,13 +254,3 @@ impl DataSourceExec { }) } } - -/// Create a new `DataSourceExec` from a `DataSource` -impl From for DataSourceExec -where - S: DataSource + 'static, -{ - fn from(source: S) -> Self { - Self::new(Arc::new(source)) - } -} diff --git a/datafusion/datasource/src/statistics.rs b/datafusion/datasource/src/statistics.rs index 8a04d77b273d4..cd002a96683a5 100644 --- a/datafusion/datasource/src/statistics.rs +++ b/datafusion/datasource/src/statistics.rs @@ -20,10 +20,8 @@ //! Currently, this module houses code to sort file groups if they are non-overlapping with //! respect to the required sort order. See [`MinMaxStatistics`] -use futures::{Stream, StreamExt}; use std::sync::Arc; -use crate::file_groups::FileGroup; use crate::PartitionedFile; use arrow::array::RecordBatch; @@ -32,11 +30,9 @@ use arrow::{ compute::SortColumn, row::{Row, Rows}, }; -use datafusion_common::stats::Precision; use datafusion_common::{plan_datafusion_err, plan_err, DataFusionError, Result}; use datafusion_physical_expr::{expressions::Column, PhysicalSortExpr}; use datafusion_physical_expr_common::sort_expr::LexOrdering; -use datafusion_physical_plan::{ColumnStatistics, Statistics}; /// A normalized representation of file min/max statistics that allows for efficient sorting & comparison. /// The min/max values are ordered by [`Self::sort_order`]. @@ -285,213 +281,3 @@ fn sort_columns_from_physical_sort_exprs( .map(|expr| expr.expr.as_any().downcast_ref::()) .collect::>>() } - -/// Get all files as well as the file level summary statistics (no statistic for partition columns). -/// If the optional `limit` is provided, includes only sufficient files. Needed to read up to -/// `limit` number of rows. `collect_stats` is passed down from the configuration parameter on -/// `ListingTable`. If it is false we only construct bare statistics and skip a potentially expensive -/// call to `multiunzip` for constructing file level summary statistics. -#[deprecated( - since = "47.0.0", - note = "Please use `get_files_with_limit` and `compute_all_files_statistics` instead" -)] -#[allow(unused)] -pub async fn get_statistics_with_limit( - all_files: impl Stream)>>, - file_schema: SchemaRef, - limit: Option, - collect_stats: bool, -) -> Result<(FileGroup, Statistics)> { - let mut result_files = FileGroup::default(); - // These statistics can be calculated as long as at least one file provides - // useful information. If none of the files provides any information, then - // they will end up having `Precision::Absent` values. Throughout calculations, - // missing values will be imputed as: - // - zero for summations, and - // - neutral element for extreme points. - let size = file_schema.fields().len(); - let mut col_stats_set = vec![ColumnStatistics::default(); size]; - let mut num_rows = Precision::::Absent; - let mut total_byte_size = Precision::::Absent; - - // Fusing the stream allows us to call next safely even once it is finished. - let mut all_files = Box::pin(all_files.fuse()); - - if let Some(first_file) = all_files.next().await { - let (mut file, file_stats) = first_file?; - file.statistics = Some(Arc::clone(&file_stats)); - result_files.push(file); - - // First file, we set them directly from the file statistics. - num_rows = file_stats.num_rows; - total_byte_size = file_stats.total_byte_size; - for (index, file_column) in - file_stats.column_statistics.clone().into_iter().enumerate() - { - col_stats_set[index].null_count = file_column.null_count; - col_stats_set[index].max_value = file_column.max_value; - col_stats_set[index].min_value = file_column.min_value; - col_stats_set[index].sum_value = file_column.sum_value; - } - - // If the number of rows exceeds the limit, we can stop processing - // files. This only applies when we know the number of rows. It also - // currently ignores tables that have no statistics regarding the - // number of rows. - let conservative_num_rows = match num_rows { - Precision::Exact(nr) => nr, - _ => usize::MIN, - }; - if conservative_num_rows <= limit.unwrap_or(usize::MAX) { - while let Some(current) = all_files.next().await { - let (mut file, file_stats) = current?; - file.statistics = Some(Arc::clone(&file_stats)); - result_files.push(file); - if !collect_stats { - continue; - } - - // We accumulate the number of rows, total byte size and null - // counts across all the files in question. If any file does not - // provide any information or provides an inexact value, we demote - // the statistic precision to inexact. - num_rows = num_rows.add(&file_stats.num_rows); - - total_byte_size = total_byte_size.add(&file_stats.total_byte_size); - - for (file_col_stats, col_stats) in file_stats - .column_statistics - .iter() - .zip(col_stats_set.iter_mut()) - { - let ColumnStatistics { - null_count: file_nc, - max_value: file_max, - min_value: file_min, - sum_value: file_sum, - distinct_count: _, - } = file_col_stats; - - col_stats.null_count = col_stats.null_count.add(file_nc); - col_stats.max_value = col_stats.max_value.max(file_max); - col_stats.min_value = col_stats.min_value.min(file_min); - col_stats.sum_value = col_stats.sum_value.add(file_sum); - } - - // If the number of rows exceeds the limit, we can stop processing - // files. This only applies when we know the number of rows. It also - // currently ignores tables that have no statistics regarding the - // number of rows. - if num_rows.get_value().unwrap_or(&usize::MIN) - > &limit.unwrap_or(usize::MAX) - { - break; - } - } - } - }; - - let mut statistics = Statistics { - num_rows, - total_byte_size, - column_statistics: col_stats_set, - }; - if all_files.next().await.is_some() { - // If we still have files in the stream, it means that the limit kicked - // in, and the statistic could have been different had we processed the - // files in a different order. - statistics = statistics.to_inexact() - } - - Ok((result_files, statistics)) -} - -/// Computes the summary statistics for a group of files(`FileGroup` level's statistics). -/// -/// This function combines statistics from all files in the file group to create -/// summary statistics. It handles the following aspects: -/// - Merges row counts and byte sizes across files -/// - Computes column-level statistics like min/max values -/// - Maintains appropriate precision information (exact, inexact, absent) -/// -/// # Parameters -/// * `file_group` - The group of files to process -/// * `file_schema` - Schema of the files -/// * `collect_stats` - Whether to collect statistics (if false, returns original file group) -/// -/// # Returns -/// A new file group with summary statistics attached -pub fn compute_file_group_statistics( - file_group: FileGroup, - file_schema: SchemaRef, - collect_stats: bool, -) -> Result { - if !collect_stats { - return Ok(file_group); - } - - let file_group_stats = file_group.iter().filter_map(|file| { - let stats = file.statistics.as_ref()?; - Some(stats.as_ref()) - }); - let statistics = Statistics::try_merge_iter(file_group_stats, &file_schema)?; - - Ok(file_group.with_statistics(Arc::new(statistics))) -} - -/// Computes statistics for all files across multiple file groups. -/// -/// This function: -/// 1. Computes statistics for each individual file group -/// 2. Summary statistics across all file groups -/// 3. Optionally marks statistics as inexact -/// -/// # Parameters -/// * `file_groups` - Vector of file groups to process -/// * `table_schema` - Schema of the table -/// * `collect_stats` - Whether to collect statistics -/// * `inexact_stats` - Whether to mark the resulting statistics as inexact -/// -/// # Returns -/// A tuple containing: -/// * The processed file groups with their individual statistics attached -/// * The summary statistics across all file groups, aka all files summary statistics -pub fn compute_all_files_statistics( - file_groups: Vec, - table_schema: SchemaRef, - collect_stats: bool, - inexact_stats: bool, -) -> Result<(Vec, Statistics)> { - let file_groups_with_stats = file_groups - .into_iter() - .map(|file_group| { - compute_file_group_statistics( - file_group, - Arc::clone(&table_schema), - collect_stats, - ) - }) - .collect::>>()?; - - // Then summary statistics across all file groups - let file_groups_statistics = file_groups_with_stats - .iter() - .filter_map(|file_group| file_group.statistics()); - - let mut statistics = - Statistics::try_merge_iter(file_groups_statistics, &table_schema)?; - - if inexact_stats { - statistics = statistics.to_inexact() - } - - Ok((file_groups_with_stats, statistics)) -} - -#[deprecated(since = "47.0.0", note = "Use Statistics::add")] -pub fn add_row_stats( - file_num_rows: Precision, - num_rows: Precision, -) -> Precision { - file_num_rows.add(&num_rows) -} diff --git a/datafusion/datasource/src/url.rs b/datafusion/datasource/src/url.rs index bddfdbcc06d13..2dbcfa2ef1fae 100644 --- a/datafusion/datasource/src/url.rs +++ b/datafusion/datasource/src/url.rs @@ -209,10 +209,10 @@ impl ListingTableUrl { /// assert_eq!(url.file_extension(), None); /// ``` pub fn file_extension(&self) -> Option<&str> { - if let Some(mut segments) = self.url.path_segments() { - if let Some(last_segment) = segments.next_back() { + if let Some(segments) = self.url.path_segments() { + if let Some(last_segment) = segments.last() { if last_segment.contains(".") && !last_segment.ends_with(".") { - return last_segment.split('.').next_back(); + return last_segment.split('.').last(); } } } diff --git a/datafusion/datasource/src/write/demux.rs b/datafusion/datasource/src/write/demux.rs index 49c3a64d24aa8..fc2e5daf92b66 100644 --- a/datafusion/datasource/src/write/demux.rs +++ b/datafusion/datasource/src/write/demux.rs @@ -28,8 +28,8 @@ use datafusion_common::error::Result; use datafusion_physical_plan::SendableRecordBatchStream; use arrow::array::{ - builder::UInt64Builder, cast::AsArray, downcast_dictionary_array, ArrayAccessor, - RecordBatch, StringArray, StructArray, + builder::UInt64Builder, cast::AsArray, downcast_dictionary_array, RecordBatch, + StringArray, StructArray, }; use arrow::datatypes::{DataType, Schema}; use datafusion_common::cast::{ @@ -482,8 +482,10 @@ fn compute_partition_keys_by_row<'a>( .ok_or(exec_datafusion_err!("it is not yet supported to write to hive partitions with datatype {}", dtype))?; - for i in 0..rb.num_rows() { - partition_values.push(Cow::from(array.value(i))); + for val in array.values() { + partition_values.push( + Cow::from(val.ok_or(exec_datafusion_err!("Cannot partition by null value for column {}", col))?), + ); } }, _ => unreachable!(), diff --git a/datafusion/execution/Cargo.toml b/datafusion/execution/Cargo.toml index 20e507e98b68a..8f642f3384d2e 100644 --- a/datafusion/execution/Cargo.toml +++ b/datafusion/execution/Cargo.toml @@ -44,7 +44,7 @@ datafusion-common = { workspace = true, default-features = true } datafusion-expr = { workspace = true } futures = { workspace = true } log = { workspace = true } -object_store = { workspace = true, features = ["fs"] } +object_store = { workspace = true } parking_lot = { workspace = true } rand = { workspace = true } tempfile = { workspace = true } diff --git a/datafusion/execution/src/config.rs b/datafusion/execution/src/config.rs index 1e00a1ce4725e..53646dc5b468e 100644 --- a/datafusion/execution/src/config.rs +++ b/datafusion/execution/src/config.rs @@ -193,11 +193,9 @@ impl SessionConfig { /// /// [`target_partitions`]: datafusion_common::config::ExecutionOptions::target_partitions pub fn with_target_partitions(mut self, n: usize) -> Self { - self.options.execution.target_partitions = if n == 0 { - datafusion_common::config::ExecutionOptions::default().target_partitions - } else { - n - }; + // partition count must be greater than zero + assert!(n > 0); + self.options.execution.target_partitions = n; self } diff --git a/datafusion/execution/src/disk_manager.rs b/datafusion/execution/src/disk_manager.rs index 2b21a6dbf175f..caa62eefe14c7 100644 --- a/datafusion/execution/src/disk_manager.rs +++ b/datafusion/execution/src/disk_manager.rs @@ -17,21 +17,14 @@ //! [`DiskManager`]: Manages files generated during query execution -use datafusion_common::{ - config_err, resources_datafusion_err, resources_err, DataFusionError, Result, -}; +use datafusion_common::{resources_datafusion_err, DataFusionError, Result}; use log::debug; use parking_lot::Mutex; use rand::{thread_rng, Rng}; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use tempfile::{Builder, NamedTempFile, TempDir}; -use crate::memory_pool::human_readable_size; - -const DEFAULT_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; // 100GB - /// Configuration for temporary disk access #[derive(Debug, Clone)] pub enum DiskManagerConfig { @@ -82,12 +75,6 @@ pub struct DiskManager { /// If `Some(vec![])` a new OS specified temporary directory will be created /// If `None` an error will be returned (configured not to spill) local_dirs: Mutex>>>, - /// The maximum amount of data (in bytes) stored inside the temporary directories. - /// Default to 100GB - max_temp_directory_size: u64, - /// Used disk space in the temporary directories. Now only spilled data for - /// external executors are counted. - used_disk_space: Arc, } impl DiskManager { @@ -97,8 +84,6 @@ impl DiskManager { DiskManagerConfig::Existing(manager) => Ok(manager), DiskManagerConfig::NewOs => Ok(Arc::new(Self { local_dirs: Mutex::new(Some(vec![])), - max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE, - used_disk_space: Arc::new(AtomicU64::new(0)), })), DiskManagerConfig::NewSpecified(conf_dirs) => { let local_dirs = create_local_dirs(conf_dirs)?; @@ -108,38 +93,14 @@ impl DiskManager { ); Ok(Arc::new(Self { local_dirs: Mutex::new(Some(local_dirs)), - max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE, - used_disk_space: Arc::new(AtomicU64::new(0)), })) } DiskManagerConfig::Disabled => Ok(Arc::new(Self { local_dirs: Mutex::new(None), - max_temp_directory_size: DEFAULT_MAX_TEMP_DIRECTORY_SIZE, - used_disk_space: Arc::new(AtomicU64::new(0)), })), } } - pub fn with_max_temp_directory_size( - mut self, - max_temp_directory_size: u64, - ) -> Result { - // If the disk manager is disabled and `max_temp_directory_size` is not 0, - // this operation is not meaningful, fail early. - if self.local_dirs.lock().is_none() && max_temp_directory_size != 0 { - return config_err!( - "Cannot set max temp directory size for a disk manager that spilling is disabled" - ); - } - - self.max_temp_directory_size = max_temp_directory_size; - Ok(self) - } - - pub fn used_disk_space(&self) -> u64 { - self.used_disk_space.load(Ordering::Relaxed) - } - /// Return true if this disk manager supports creating temporary /// files. If this returns false, any call to `create_tmp_file` /// will error. @@ -152,7 +113,7 @@ impl DiskManager { /// If the file can not be created for some reason, returns an /// error message referencing the request description pub fn create_tmp_file( - self: &Arc, + &self, request_description: &str, ) -> Result { let mut guard = self.local_dirs.lock(); @@ -181,31 +142,18 @@ impl DiskManager { tempfile: Builder::new() .tempfile_in(local_dirs[dir_index].as_ref()) .map_err(DataFusionError::IoError)?, - current_file_disk_usage: 0, - disk_manager: Arc::clone(self), }) } } /// A wrapper around a [`NamedTempFile`] that also contains -/// a reference to its parent temporary directory. -/// -/// # Note -/// After any modification to the underlying file (e.g., writing data to it), the caller -/// must invoke [`Self::update_disk_usage`] to update the global disk usage counter. -/// This ensures the disk manager can properly enforce usage limits configured by -/// [`DiskManager::with_max_temp_directory_size`]. +/// a reference to its parent temporary directory #[derive(Debug)] pub struct RefCountedTempFile { /// The reference to the directory in which temporary files are created to ensure /// it is not cleaned up prior to the NamedTempFile _parent_temp_dir: Arc, tempfile: NamedTempFile, - /// Tracks the current disk usage of this temporary file. See - /// [`Self::update_disk_usage`] for more details. - current_file_disk_usage: u64, - /// The disk manager that created and manages this temporary file - disk_manager: Arc, } impl RefCountedTempFile { @@ -216,50 +164,6 @@ impl RefCountedTempFile { pub fn inner(&self) -> &NamedTempFile { &self.tempfile } - - /// Updates the global disk usage counter after modifications to the underlying file. - /// - /// # Errors - /// - Returns an error if the global disk usage exceeds the configured limit. - pub fn update_disk_usage(&mut self) -> Result<()> { - // Get new file size from OS - let metadata = self.tempfile.as_file().metadata()?; - let new_disk_usage = metadata.len(); - - // Update the global disk usage by: - // 1. Subtracting the old file size from the global counter - self.disk_manager - .used_disk_space - .fetch_sub(self.current_file_disk_usage, Ordering::Relaxed); - // 2. Adding the new file size to the global counter - self.disk_manager - .used_disk_space - .fetch_add(new_disk_usage, Ordering::Relaxed); - - // 3. Check if the updated global disk usage exceeds the configured limit - let global_disk_usage = self.disk_manager.used_disk_space.load(Ordering::Relaxed); - if global_disk_usage > self.disk_manager.max_temp_directory_size { - return resources_err!( - "The used disk space during the spilling process has exceeded the allowable limit of {}. Try increasing the `max_temp_directory_size` in the disk manager configuration.", - human_readable_size(self.disk_manager.max_temp_directory_size as usize) - ); - } - - // 4. Update the local file size tracking - self.current_file_disk_usage = new_disk_usage; - - Ok(()) - } -} - -/// When the temporary file is dropped, subtract its disk usage from the disk manager's total -impl Drop for RefCountedTempFile { - fn drop(&mut self) { - // Subtract the current file's disk usage from the global counter - self.disk_manager - .used_disk_space - .fetch_sub(self.current_file_disk_usage, Ordering::Relaxed); - } } /// Setup local dirs by creating one new dir in each of the given dirs diff --git a/datafusion/execution/src/memory_pool/mod.rs b/datafusion/execution/src/memory_pool/mod.rs index 19e509d263ea2..71d40aeab53c7 100644 --- a/datafusion/execution/src/memory_pool/mod.rs +++ b/datafusion/execution/src/memory_pool/mod.rs @@ -19,8 +19,7 @@ //! help with allocation accounting. use datafusion_common::{internal_err, Result}; -use std::hash::{Hash, Hasher}; -use std::{cmp::Ordering, sync::atomic, sync::Arc}; +use std::{cmp::Ordering, sync::Arc}; mod pool; pub mod proxy { @@ -141,101 +140,30 @@ pub trait MemoryPool: Send + Sync + std::fmt::Debug { /// Return the total amount of memory reserved fn reserved(&self) -> usize; - - /// Return the memory limit of the pool - /// - /// The default implementation of `MemoryPool::memory_limit` - /// will return `MemoryLimit::Unknown`. - /// If you are using your custom memory pool, but have the requirement to - /// know the memory usage limit of the pool, please implement this method - /// to return it(`Memory::Finite(limit)`). - fn memory_limit(&self) -> MemoryLimit { - MemoryLimit::Unknown - } -} - -/// Memory limit of `MemoryPool` -pub enum MemoryLimit { - Infinite, - /// Bounded memory limit in bytes. - Finite(usize), - Unknown, } /// A memory consumer is a named allocation traced by a particular /// [`MemoryReservation`] in a [`MemoryPool`]. All allocations are registered to /// a particular `MemoryConsumer`; /// -/// Each `MemoryConsumer` is identifiable by a process-unique id, and is therefor not cloneable, -/// If you want a clone of a `MemoryConsumer`, you should look into [`MemoryConsumer::clone_with_new_id`], -/// but note that this `MemoryConsumer` may be treated as a separate entity based on the used pool, -/// and is only guaranteed to share the name and inner properties. -/// /// For help with allocation accounting, see the [`proxy`] module. /// /// [proxy]: datafusion_common::utils::proxy -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Hash, Clone)] pub struct MemoryConsumer { name: String, can_spill: bool, - id: usize, -} - -impl PartialEq for MemoryConsumer { - fn eq(&self, other: &Self) -> bool { - let is_same_id = self.id == other.id; - - #[cfg(debug_assertions)] - if is_same_id { - assert_eq!(self.name, other.name); - assert_eq!(self.can_spill, other.can_spill); - } - - is_same_id - } -} - -impl Eq for MemoryConsumer {} - -impl Hash for MemoryConsumer { - fn hash(&self, state: &mut H) { - self.id.hash(state); - self.name.hash(state); - self.can_spill.hash(state); - } } impl MemoryConsumer { - fn new_unique_id() -> usize { - static ID: atomic::AtomicUsize = atomic::AtomicUsize::new(0); - ID.fetch_add(1, atomic::Ordering::Relaxed) - } - /// Create a new empty [`MemoryConsumer`] that can be grown using [`MemoryReservation`] pub fn new(name: impl Into) -> Self { Self { name: name.into(), can_spill: false, - id: Self::new_unique_id(), - } - } - - /// Returns a clone of this [`MemoryConsumer`] with a new unique id, - /// which can be registered with a [`MemoryPool`], - /// This new consumer is separate from the original. - pub fn clone_with_new_id(&self) -> Self { - Self { - name: self.name.clone(), - can_spill: self.can_spill, - id: Self::new_unique_id(), } } - /// Return the unique id of this [`MemoryConsumer`] - pub fn id(&self) -> usize { - self.id - } - /// Set whether this allocation can be spilled to disk pub fn with_can_spill(self, can_spill: bool) -> Self { Self { can_spill, ..self } @@ -421,7 +349,7 @@ pub mod units { pub const KB: u64 = 1 << 10; } -/// Present size in human-readable form +/// Present size in human readable form pub fn human_readable_size(size: usize) -> String { use units::*; @@ -446,15 +374,6 @@ pub fn human_readable_size(size: usize) -> String { mod tests { use super::*; - #[test] - fn test_id_uniqueness() { - let mut ids = std::collections::HashSet::new(); - for _ in 0..100 { - let consumer = MemoryConsumer::new("test"); - assert!(ids.insert(consumer.id())); // Ensures unique insertion - } - } - #[test] fn test_memory_pool_underflow() { let pool = Arc::new(GreedyMemoryPool::new(50)) as _; diff --git a/datafusion/execution/src/memory_pool/pool.rs b/datafusion/execution/src/memory_pool/pool.rs index e623246eb976b..261332180e571 100644 --- a/datafusion/execution/src/memory_pool/pool.rs +++ b/datafusion/execution/src/memory_pool/pool.rs @@ -15,14 +15,14 @@ // specific language governing permissions and limitations // under the License. -use crate::memory_pool::{MemoryConsumer, MemoryLimit, MemoryPool, MemoryReservation}; +use crate::memory_pool::{MemoryConsumer, MemoryPool, MemoryReservation}; use datafusion_common::HashMap; use datafusion_common::{resources_datafusion_err, DataFusionError, Result}; use log::debug; use parking_lot::Mutex; use std::{ num::NonZeroUsize, - sync::atomic::{AtomicUsize, Ordering}, + sync::atomic::{AtomicU64, AtomicUsize, Ordering}, }; /// A [`MemoryPool`] that enforces no limit @@ -48,10 +48,6 @@ impl MemoryPool for UnboundedMemoryPool { fn reserved(&self) -> usize { self.used.load(Ordering::Relaxed) } - - fn memory_limit(&self) -> MemoryLimit { - MemoryLimit::Infinite - } } /// A [`MemoryPool`] that implements a greedy first-come first-serve limit. @@ -104,10 +100,6 @@ impl MemoryPool for GreedyMemoryPool { fn reserved(&self) -> usize { self.used.load(Ordering::Relaxed) } - - fn memory_limit(&self) -> MemoryLimit { - MemoryLimit::Finite(self.pool_size) - } } /// A [`MemoryPool`] that prevents spillable reservations from using more than @@ -241,10 +233,6 @@ impl MemoryPool for FairSpillPool { let state = self.state.lock(); state.spillable + state.unspillable } - - fn memory_limit(&self) -> MemoryLimit { - MemoryLimit::Finite(self.pool_size) - } } /// Constructs a resources error based upon the individual [`MemoryReservation`]. @@ -261,32 +249,6 @@ fn insufficient_capacity_err( resources_datafusion_err!("Failed to allocate additional {} bytes for {} with {} bytes already allocated for this reservation - {} bytes remain available for the total pool", additional, reservation.registration.consumer.name, reservation.size, available) } -#[derive(Debug)] -struct TrackedConsumer { - name: String, - can_spill: bool, - reserved: AtomicUsize, -} - -impl TrackedConsumer { - /// Shorthand to return the currently reserved value - fn reserved(&self) -> usize { - self.reserved.load(Ordering::Relaxed) - } - - /// Grows the tracked consumer's reserved size, - /// should be called after the pool has successfully performed the grow(). - fn grow(&self, additional: usize) { - self.reserved.fetch_add(additional, Ordering::Relaxed); - } - - /// Reduce the tracked consumer's reserved size, - /// should be called after the pool has successfully performed the shrink(). - fn shrink(&self, shrink: usize) { - self.reserved.fetch_sub(shrink, Ordering::Relaxed); - } -} - /// A [`MemoryPool`] that tracks the consumers that have /// reserved memory within the inner memory pool. /// @@ -297,12 +259,9 @@ impl TrackedConsumer { /// The same consumer can have multiple reservations. #[derive(Debug)] pub struct TrackConsumersPool { - /// The wrapped memory pool that actually handles reservation logic inner: I, - /// The amount of consumers to report(ordered top to bottom by reservation size) top: NonZeroUsize, - /// Maps consumer_id --> TrackedConsumer - tracked_consumers: Mutex>, + tracked_consumers: Mutex>, } impl TrackConsumersPool { @@ -318,20 +277,27 @@ impl TrackConsumersPool { } } + /// Determine if there are multiple [`MemoryConsumer`]s registered + /// which have the same name. + /// + /// This is very tied to the implementation of the memory consumer. + fn has_multiple_consumers(&self, name: &String) -> bool { + let consumer = MemoryConsumer::new(name); + let consumer_with_spill = consumer.clone().with_can_spill(true); + let guard = self.tracked_consumers.lock(); + guard.contains_key(&consumer) && guard.contains_key(&consumer_with_spill) + } + /// The top consumers in a report string. pub fn report_top(&self, top: usize) -> String { let mut consumers = self .tracked_consumers .lock() .iter() - .map(|(consumer_id, tracked_consumer)| { + .map(|(consumer, reserved)| { ( - ( - *consumer_id, - tracked_consumer.name.to_owned(), - tracked_consumer.can_spill, - ), - tracked_consumer.reserved(), + (consumer.name().to_owned(), consumer.can_spill()), + reserved.load(Ordering::Acquire), ) }) .collect::>(); @@ -339,8 +305,12 @@ impl TrackConsumersPool { consumers[0..std::cmp::min(top, consumers.len())] .iter() - .map(|((id, name, can_spill), size)| { - format!("{name}#{id}(can spill: {can_spill}) consumed {size} bytes") + .map(|((name, can_spill), size)| { + if self.has_multiple_consumers(name) { + format!("{name}(can_spill={}) consumed {:?} bytes", can_spill, size) + } else { + format!("{name} consumed {:?} bytes", size) + } }) .collect::>() .join(", ") @@ -352,33 +322,29 @@ impl MemoryPool for TrackConsumersPool { self.inner.register(consumer); let mut guard = self.tracked_consumers.lock(); - let existing = guard.insert( - consumer.id(), - TrackedConsumer { - name: consumer.name().to_string(), - can_spill: consumer.can_spill(), - reserved: Default::default(), - }, - ); - - debug_assert!( - existing.is_none(), - "Registered was called twice on the same consumer" - ); + if let Some(already_reserved) = guard.insert(consumer.clone(), Default::default()) + { + guard.entry_ref(consumer).and_modify(|bytes| { + bytes.fetch_add( + already_reserved.load(Ordering::Acquire), + Ordering::AcqRel, + ); + }); + } } fn unregister(&self, consumer: &MemoryConsumer) { self.inner.unregister(consumer); - self.tracked_consumers.lock().remove(&consumer.id()); + self.tracked_consumers.lock().remove(consumer); } fn grow(&self, reservation: &MemoryReservation, additional: usize) { self.inner.grow(reservation, additional); self.tracked_consumers .lock() - .entry(reservation.consumer().id()) - .and_modify(|tracked_consumer| { - tracked_consumer.grow(additional); + .entry_ref(reservation.consumer()) + .and_modify(|bytes| { + bytes.fetch_add(additional as u64, Ordering::AcqRel); }); } @@ -386,9 +352,9 @@ impl MemoryPool for TrackConsumersPool { self.inner.shrink(reservation, shrink); self.tracked_consumers .lock() - .entry(reservation.consumer().id()) - .and_modify(|tracked_consumer| { - tracked_consumer.shrink(shrink); + .entry_ref(reservation.consumer()) + .and_modify(|bytes| { + bytes.fetch_sub(shrink as u64, Ordering::AcqRel); }); } @@ -410,9 +376,9 @@ impl MemoryPool for TrackConsumersPool { self.tracked_consumers .lock() - .entry(reservation.consumer().id()) - .and_modify(|tracked_consumer| { - tracked_consumer.grow(additional); + .entry_ref(reservation.consumer()) + .and_modify(|bytes| { + bytes.fetch_add(additional as u64, Ordering::AcqRel); }); Ok(()) } @@ -420,10 +386,6 @@ impl MemoryPool for TrackConsumersPool { fn reserved(&self) -> usize { self.inner.reserved() } - - fn memory_limit(&self) -> MemoryLimit { - self.inner.memory_limit() - } } fn provide_top_memory_consumers_to_error_msg( @@ -539,12 +501,12 @@ mod tests { // Test: reports if new reservation causes error // using the previously set sizes for other consumers let mut r5 = MemoryConsumer::new("r5").register(&pool); - let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: r1#{}(can spill: false) consumed 50 bytes, r3#{}(can spill: false) consumed 20 bytes, r2#{}(can spill: false) consumed 15 bytes. Error: Failed to allocate additional 150 bytes for r5 with 0 bytes already allocated for this reservation - 5 bytes remain available for the total pool", r1.consumer().id(), r3.consumer().id(), r2.consumer().id()); + let expected = "Additional allocation failed with top memory consumers (across reservations) as: r1 consumed 50 bytes, r3 consumed 20 bytes, r2 consumed 15 bytes. Error: Failed to allocate additional 150 bytes for r5 with 0 bytes already allocated for this reservation - 5 bytes remain available for the total pool"; let res = r5.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) ), "should provide list of top memory consumers, instead found {:?}", res @@ -562,45 +524,45 @@ mod tests { // Test: see error message when no consumers recorded yet let mut r0 = MemoryConsumer::new(same_name).register(&pool); - let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 100 bytes remain available for the total pool", r0.consumer().id()); + let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 100 bytes remain available for the total pool"; let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) ), "should provide proper error when no reservations have been made yet, instead found {:?}", res ); // API: multiple registrations using the same hashed consumer, - // will be recognized *differently* in the TrackConsumersPool. + // will be recognized as the same in the TrackConsumersPool. + // Test: will be the same per Top Consumers reported. r0.grow(10); // make r0=10, pool available=90 let new_consumer_same_name = MemoryConsumer::new(same_name); let mut r1 = new_consumer_same_name.register(&pool); // TODO: the insufficient_capacity_err() message is per reservation, not per consumer. // a followup PR will clarify this message "0 bytes already allocated for this reservation" - let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 10 bytes, foo#{}(can spill: false) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 90 bytes remain available for the total pool", r0.consumer().id(), r1.consumer().id()); + let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo consumed 10 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 90 bytes remain available for the total pool"; let res = r1.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) ), - "should provide proper error for 2 consumers, instead found {:?}", - res + "should provide proper error with same hashed consumer (a single foo=10 bytes, available=90), instead found {:?}", res ); // Test: will accumulate size changes per consumer, not per reservation r1.grow(20); - let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 20 bytes, foo#{}(can spill: false) consumed 10 bytes. Error: Failed to allocate additional 150 bytes for foo with 20 bytes already allocated for this reservation - 70 bytes remain available for the total pool", r1.consumer().id(), r0.consumer().id()); + let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo consumed 30 bytes. Error: Failed to allocate additional 150 bytes for foo with 20 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; let res = r1.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) ), - "should provide proper error for 2 consumers(one foo=20 bytes, another foo=10 bytes, available=70), instead found {:?}", res + "should provide proper error with same hashed consumer (a single foo=30 bytes, available=70), instead found {:?}", res ); // Test: different hashed consumer, (even with the same name), @@ -608,14 +570,14 @@ mod tests { let consumer_with_same_name_but_different_hash = MemoryConsumer::new(same_name).with_can_spill(true); let mut r2 = consumer_with_same_name_but_different_hash.register(&pool); - let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: foo#{}(can spill: false) consumed 20 bytes, foo#{}(can spill: false) consumed 10 bytes, foo#{}(can spill: true) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 70 bytes remain available for the total pool", r1.consumer().id(), r0.consumer().id(), r2.consumer().id()); + let expected = "Additional allocation failed with top memory consumers (across reservations) as: foo(can_spill=false) consumed 30 bytes, foo(can_spill=true) consumed 0 bytes. Error: Failed to allocate additional 150 bytes for foo with 0 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; let res = r2.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) ), - "should provide proper error with 3 separate consumers(1 = 20 bytes, 2 = 10 bytes, 3 = 0 bytes), instead found {:?}", res + "should provide proper error with different hashed consumer (foo(can_spill=false)=30 bytes and foo(can_spill=true)=0 bytes, available=70), instead found {:?}", res ); } @@ -626,15 +588,14 @@ mod tests { let mut r0 = MemoryConsumer::new("r0").register(&pool); r0.grow(10); let r1_consumer = MemoryConsumer::new("r1"); - let mut r1 = r1_consumer.register(&pool); + let mut r1 = r1_consumer.clone().register(&pool); r1.grow(20); - - let expected = format!("Additional allocation failed with top memory consumers (across reservations) as: r1#{}(can spill: false) consumed 20 bytes, r0#{}(can spill: false) consumed 10 bytes. Error: Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 70 bytes remain available for the total pool", r1.consumer().id(), r0.consumer().id()); + let expected = "Additional allocation failed with top memory consumers (across reservations) as: r1 consumed 20 bytes, r0 consumed 10 bytes. Error: Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected) ), "should provide proper error with both consumers, instead found {:?}", res @@ -642,31 +603,32 @@ mod tests { // Test: unregister one // only the remaining one should be listed - drop(r1); - let expected_consumers = format!("Additional allocation failed with top memory consumers (across reservations) as: r0#{}(can spill: false) consumed 10 bytes", r0.consumer().id()); + pool.unregister(&r1_consumer); + let expected_consumers = "Additional allocation failed with top memory consumers (across reservations) as: r0 consumed 10 bytes"; let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(&expected_consumers) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected_consumers) ), "should provide proper error with only 1 consumer left registered, instead found {:?}", res ); // Test: actual message we see is the `available is 70`. When it should be `available is 90`. // This is because the pool.shrink() does not automatically occur within the inner_pool.deregister(). - let expected_90_available = "Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 90 bytes remain available for the total pool"; + let expected_70_available = "Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 70 bytes remain available for the total pool"; let res = r0.try_grow(150); assert!( matches!( &res, - Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected_90_available) + Err(DataFusionError::ResourcesExhausted(ref e)) if e.to_string().contains(expected_70_available) ), "should find that the inner pool will still count all bytes for the deregistered consumer until the reservation is dropped, instead found {:?}", res ); // Test: the registration needs to free itself (or be dropped), // for the proper error message + r1.free(); let expected_90_available = "Failed to allocate additional 150 bytes for r0 with 10 bytes already allocated for this reservation - 90 bytes remain available for the total pool"; let res = r0.try_grow(150); assert!( @@ -716,7 +678,7 @@ mod tests { .unwrap(); // Test: can get runtime metrics, even without an error thrown - let expected = format!("r3#{}(can spill: false) consumed 45 bytes, r1#{}(can spill: false) consumed 20 bytes", r3.consumer().id(), r1.consumer().id()); + let expected = "r3 consumed 45 bytes, r1 consumed 20 bytes"; let res = downcasted.report_top(2); assert_eq!( res, expected, diff --git a/datafusion/execution/src/runtime_env.rs b/datafusion/execution/src/runtime_env.rs index cb085108819eb..95f14f485792a 100644 --- a/datafusion/execution/src/runtime_env.rs +++ b/datafusion/execution/src/runtime_env.rs @@ -27,7 +27,7 @@ use crate::{ }; use crate::cache::cache_manager::{CacheManager, CacheManagerConfig}; -use datafusion_common::{config::ConfigEntry, Result}; +use datafusion_common::Result; use object_store::ObjectStore; use std::path::PathBuf; use std::sync::Arc; @@ -268,56 +268,4 @@ impl RuntimeEnvBuilder { pub fn build_arc(self) -> Result> { self.build().map(Arc::new) } - - /// Create a new RuntimeEnvBuilder from an existing RuntimeEnv - pub fn from_runtime_env(runtime_env: &RuntimeEnv) -> Self { - let cache_config = CacheManagerConfig { - table_files_statistics_cache: runtime_env - .cache_manager - .get_file_statistic_cache(), - list_files_cache: runtime_env.cache_manager.get_list_files_cache(), - }; - - Self { - disk_manager: DiskManagerConfig::Existing(Arc::clone( - &runtime_env.disk_manager, - )), - memory_pool: Some(Arc::clone(&runtime_env.memory_pool)), - cache_manager: cache_config, - object_store_registry: Arc::clone(&runtime_env.object_store_registry), - } - } - - /// Returns a list of all available runtime configurations with their current values and descriptions - pub fn entries(&self) -> Vec { - // Memory pool configuration - vec![ConfigEntry { - key: "datafusion.runtime.memory_limit".to_string(), - value: None, // Default is system-dependent - description: "Maximum memory limit for query execution. Supports suffixes K (kilobytes), M (megabytes), and G (gigabytes). Example: '2G' for 2 gigabytes.", - }] - } - - /// Generate documentation that can be included in the user guide - pub fn generate_config_markdown() -> String { - use std::fmt::Write as _; - - let s = Self::default(); - - let mut docs = "| key | default | description |\n".to_string(); - docs += "|-----|---------|-------------|\n"; - let mut entries = s.entries(); - entries.sort_unstable_by(|a, b| a.key.cmp(&b.key)); - - for entry in &entries { - let _ = writeln!( - &mut docs, - "| {} | {} | {} |", - entry.key, - entry.value.as_deref().unwrap_or("NULL"), - entry.description - ); - } - docs - } } diff --git a/datafusion/expr-common/src/interval_arithmetic.rs b/datafusion/expr-common/src/interval_arithmetic.rs index 6af4322df29ea..9d00b45962bc2 100644 --- a/datafusion/expr-common/src/interval_arithmetic.rs +++ b/datafusion/expr-common/src/interval_arithmetic.rs @@ -174,7 +174,7 @@ macro_rules! value_transition { /// - `INF` values are converted to `NULL`s while constructing an interval to /// ensure consistency, with other data types. /// - `NaN` (Not a Number) results are conservatively result in unbounded -/// endpoints. +/// endpoints. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Interval { lower: ScalarValue, diff --git a/datafusion/expr-common/src/signature.rs b/datafusion/expr-common/src/signature.rs index a7c9330201bc0..063417a254be3 100644 --- a/datafusion/expr-common/src/signature.rs +++ b/datafusion/expr-common/src/signature.rs @@ -391,11 +391,10 @@ impl TypeSignature { vec![format!("{}, ..", Self::join_types(types, "/"))] } TypeSignature::Uniform(arg_count, valid_types) => { - vec![ - std::iter::repeat_n(Self::join_types(valid_types, "/"), *arg_count) - .collect::>() - .join(", "), - ] + vec![std::iter::repeat(Self::join_types(valid_types, "/")) + .take(*arg_count) + .collect::>() + .join(", ")] } TypeSignature::String(num) => { vec![format!("String({num})")] @@ -413,7 +412,8 @@ impl TypeSignature { vec![Self::join_types(types, ", ")] } TypeSignature::Any(arg_count) => { - vec![std::iter::repeat_n("Any", *arg_count) + vec![std::iter::repeat("Any") + .take(*arg_count) .collect::>() .join(", ")] } diff --git a/datafusion/expr-common/src/type_coercion/aggregates.rs b/datafusion/expr-common/src/type_coercion/aggregates.rs index 44839378d52c9..13d52959aba65 100644 --- a/datafusion/expr-common/src/type_coercion/aggregates.rs +++ b/datafusion/expr-common/src/type_coercion/aggregates.rs @@ -210,7 +210,6 @@ pub fn avg_return_type(func_name: &str, arg_type: &DataType) -> Result let new_scale = DECIMAL256_MAX_SCALE.min(*scale + 4); Ok(DataType::Decimal256(new_precision, new_scale)) } - DataType::Duration(time_unit) => Ok(DataType::Duration(*time_unit)), arg_type if NUMERICS.contains(arg_type) => Ok(DataType::Float64), DataType::Dictionary(_, dict_value_type) => { avg_return_type(func_name, dict_value_type.as_ref()) @@ -232,7 +231,6 @@ pub fn avg_sum_type(arg_type: &DataType) -> Result { let new_precision = DECIMAL256_MAX_PRECISION.min(*precision + 10); Ok(DataType::Decimal256(new_precision, *scale)) } - DataType::Duration(time_unit) => Ok(DataType::Duration(*time_unit)), arg_type if NUMERICS.contains(arg_type) => Ok(DataType::Float64), DataType::Dictionary(_, dict_value_type) => { avg_sum_type(dict_value_type.as_ref()) @@ -300,7 +298,6 @@ pub fn coerce_avg_type(func_name: &str, arg_types: &[DataType]) -> Result Ok(DataType::Decimal128(*p, *s)), DataType::Decimal256(p, s) => Ok(DataType::Decimal256(*p, *s)), d if d.is_numeric() => Ok(DataType::Float64), - DataType::Duration(time_unit) => Ok(DataType::Duration(*time_unit)), DataType::Dictionary(_, v) => coerced_type(func_name, v.as_ref()), _ => { plan_err!( diff --git a/datafusion/expr-common/src/type_coercion/binary.rs b/datafusion/expr-common/src/type_coercion/binary.rs index fdee00f81b1e6..c49de3984097f 100644 --- a/datafusion/expr-common/src/type_coercion/binary.rs +++ b/datafusion/expr-common/src/type_coercion/binary.rs @@ -733,7 +733,6 @@ pub fn comparison_coercion(lhs_type: &DataType, rhs_type: &DataType) -> Option Field Arc::new(Field::new(name, common_type, is_nullable)) } -/// coerce two types if they are Maps by coercing their inner 'entries' fields' types -/// using struct coercion -fn map_coercion(lhs_type: &DataType, rhs_type: &DataType) -> Option { - use arrow::datatypes::DataType::*; - match (lhs_type, rhs_type) { - (Map(lhs_field, lhs_ordered), Map(rhs_field, rhs_ordered)) => { - struct_coercion(lhs_field.data_type(), rhs_field.data_type()).map( - |key_value_type| { - Map( - Arc::new((**lhs_field).clone().with_data_type(key_value_type)), - *lhs_ordered && *rhs_ordered, - ) - }, - ) - } - _ => None, - } -} - /// Returns the output type of applying mathematics operations such as /// `+` to arguments of `lhs_type` and `rhs_type`. fn mathematics_numerical_coercion( @@ -1297,10 +1277,6 @@ fn binary_coercion(lhs_type: &DataType, rhs_type: &DataType) -> Option Some(LargeBinary) } (Binary, Utf8) | (Utf8, Binary) => Some(Binary), - - // Cast FixedSizeBinary to Binary - (FixedSizeBinary(_), Binary) | (Binary, FixedSizeBinary(_)) => Some(Binary), - _ => None, } } @@ -2507,49 +2483,4 @@ mod tests { ); Ok(()) } - - #[test] - fn test_map_coercion() -> Result<()> { - let lhs = Field::new_map( - "lhs", - "entries", - Arc::new(Field::new("keys", DataType::Utf8, false)), - Arc::new(Field::new("values", DataType::LargeUtf8, false)), - true, - false, - ); - let rhs = Field::new_map( - "rhs", - "kvp", - Arc::new(Field::new("k", DataType::Utf8, false)), - Arc::new(Field::new("v", DataType::Utf8, true)), - false, - true, - ); - - let expected = Field::new_map( - "expected", - "entries", // struct coercion takes lhs name - Arc::new(Field::new( - "keys", // struct coercion takes lhs name - DataType::Utf8, - false, - )), - Arc::new(Field::new( - "values", // struct coercion takes lhs name - DataType::LargeUtf8, // lhs is large string - true, // rhs is nullable - )), - false, // both sides must be sorted - true, // rhs is nullable - ); - - test_coercion_binary_rule!( - lhs.data_type(), - rhs.data_type(), - Operator::Eq, - expected.data_type().clone() - ); - Ok(()) - } } diff --git a/datafusion/expr/src/logical_plan/builder.rs b/datafusion/expr/src/logical_plan/builder.rs index 24a5c0fe9a211..91a871d52e9ad 100644 --- a/datafusion/expr/src/logical_plan/builder.rs +++ b/datafusion/expr/src/logical_plan/builder.rs @@ -501,21 +501,6 @@ impl LogicalPlanBuilder { if table_scan.filters.is_empty() { if let Some(p) = table_scan.source.get_logical_plan() { let sub_plan = p.into_owned(); - - if let Some(proj) = table_scan.projection { - let projection_exprs = proj - .into_iter() - .map(|i| { - Expr::Column(Column::from( - sub_plan.schema().qualified_field(i), - )) - }) - .collect::>(); - return Self::new(sub_plan) - .project(projection_exprs)? - .alias(table_scan.table_name); - } - // Ensures that the reference to the inlined table remains the // same, meaning we don't have to change any of the parent nodes // that reference this table. @@ -1132,6 +1117,8 @@ impl LogicalPlanBuilder { .collect::>()?; let on: Vec<(_, _)> = left_keys.into_iter().zip(right_keys).collect(); + let join_schema = + build_join_schema(self.plan.schema(), right.schema(), &join_type)?; let mut join_on: Vec<(Expr, Expr)> = vec![]; let mut filters: Option = None; for (l, r) in &on { @@ -1164,33 +1151,33 @@ impl LogicalPlanBuilder { DataFusionError::Internal("filters should not be None here".to_string()) })?) } else { - let join = Join::try_new( - self.plan, - Arc::new(right), - join_on, - filters, + Ok(Self::new(LogicalPlan::Join(Join { + left: self.plan, + right: Arc::new(right), + on: join_on, + filter: filters, join_type, - JoinConstraint::Using, - false, - )?; - - Ok(Self::new(LogicalPlan::Join(join))) + join_constraint: JoinConstraint::Using, + schema: DFSchemaRef::new(join_schema), + null_equals_null: false, + }))) } } /// Apply a cross join pub fn cross_join(self, right: LogicalPlan) -> Result { - let join = Join::try_new( - self.plan, - Arc::new(right), - vec![], - None, - JoinType::Inner, - JoinConstraint::On, - false, - )?; - - Ok(Self::new(LogicalPlan::Join(join))) + let join_schema = + build_join_schema(self.plan.schema(), right.schema(), &JoinType::Inner)?; + Ok(Self::new(LogicalPlan::Join(Join { + left: self.plan, + right: Arc::new(right), + on: vec![], + filter: None, + join_type: JoinType::Inner, + join_constraint: JoinConstraint::On, + null_equals_null: false, + schema: DFSchemaRef::new(join_schema), + }))) } /// Repartition @@ -1351,7 +1338,7 @@ impl LogicalPlanBuilder { /// to columns from the existing input. `r`, the second element of the tuple, /// must only refer to columns from the right input. /// - /// `filter` contains any other filter expression to apply during the + /// `filter` contains any other other filter expression to apply during the /// join. Note that `equi_exprs` predicates are evaluated more efficiently /// than the filter expressions, so they are preferred. pub fn join_with_expr_keys( @@ -1401,17 +1388,19 @@ impl LogicalPlanBuilder { }) .collect::>>()?; - let join = Join::try_new( - self.plan, - Arc::new(right), - join_key_pairs, + let join_schema = + build_join_schema(self.plan.schema(), right.schema(), &join_type)?; + + Ok(Self::new(LogicalPlan::Join(Join { + left: self.plan, + right: Arc::new(right), + on: join_key_pairs, filter, join_type, - JoinConstraint::On, - false, - )?; - - Ok(Self::new(LogicalPlan::Join(join))) + join_constraint: JoinConstraint::On, + schema: DFSchemaRef::new(join_schema), + null_equals_null: false, + }))) } /// Unnest the given column. @@ -1479,37 +1468,19 @@ impl ValuesFields { } } -// `name_map` tracks a mapping between a field name and the number of appearances of that field. -// -// Some field names might already come to this function with the count (number of times it appeared) -// as a sufix e.g. id:1, so there's still a chance of name collisions, for example, -// if these three fields passed to this function: "col:1", "col" and "col", the function -// would rename them to -> col:1, col, col:1 causing a posteriror error when building the DFSchema. -// that's why we need the `seen` set, so the fields are always unique. -// pub fn change_redundant_column(fields: &Fields) -> Vec { let mut name_map = HashMap::new(); - let mut seen: HashSet = HashSet::new(); - fields .into_iter() .map(|field| { - let base_name = field.name(); - let count = name_map.entry(base_name.clone()).or_insert(0); - let mut new_name = base_name.clone(); - - // Loop until we find a name that hasn't been used - while seen.contains(&new_name) { - *count += 1; - new_name = format!("{}:{}", base_name, count); + let counter = name_map.entry(field.name().to_string()).or_insert(0); + *counter += 1; + if *counter > 1 { + let new_name = format!("{}:{}", field.name(), *counter - 1); + Field::new(new_name, field.data_type().clone(), field.is_nullable()) + } else { + field.as_ref().clone() } - - seen.insert(new_name.clone()); - - let mut modified_field = - Field::new(&new_name, field.data_type().clone(), field.is_nullable()); - modified_field.set_metadata(field.metadata().clone()); - modified_field }) .collect() } @@ -2203,7 +2174,7 @@ pub fn unnest_with_options( // new columns dependent on the same original index dependency_indices - .extend(std::iter::repeat_n(index, transformed_columns.len())); + .extend(std::iter::repeat(index).take(transformed_columns.len())); Ok(transformed_columns .iter() .map(|(col, field)| (col.relation.to_owned(), field.to_owned())) @@ -2759,13 +2730,10 @@ mod tests { let t1_field_1 = Field::new("a", DataType::Int32, false); let t2_field_1 = Field::new("a", DataType::Int32, false); let t2_field_3 = Field::new("a", DataType::Int32, false); - let t2_field_4 = Field::new("a:1", DataType::Int32, false); let t1_field_2 = Field::new("b", DataType::Int32, false); let t2_field_2 = Field::new("b", DataType::Int32, false); - let field_vec = vec![ - t1_field_1, t2_field_1, t1_field_2, t2_field_2, t2_field_3, t2_field_4, - ]; + let field_vec = vec![t1_field_1, t2_field_1, t1_field_2, t2_field_2, t2_field_3]; let remove_redundant = change_redundant_column(&Fields::from(field_vec)); assert_eq!( @@ -2776,7 +2744,6 @@ mod tests { Field::new("b", DataType::Int32, false), Field::new("b:1", DataType::Int32, false), Field::new("a:2", DataType::Int32, false), - Field::new("a:1:1", DataType::Int32, false), ] ); Ok(()) diff --git a/datafusion/expr/src/logical_plan/invariants.rs b/datafusion/expr/src/logical_plan/invariants.rs index 0c30c9785766b..d83410bf99c98 100644 --- a/datafusion/expr/src/logical_plan/invariants.rs +++ b/datafusion/expr/src/logical_plan/invariants.rs @@ -112,11 +112,11 @@ fn assert_valid_semantic_plan(plan: &LogicalPlan) -> Result<()> { /// Returns an error if the plan does not have the expected schema. /// Ignores metadata and nullability. pub fn assert_expected_schema(schema: &DFSchemaRef, plan: &LogicalPlan) -> Result<()> { - let compatible = plan.schema().logically_equivalent_names_and_types(schema); + let compatible = plan.schema().has_equivalent_names_and_types(schema); - if !compatible { + if let Err(e) = compatible { internal_err!( - "Failed due to a difference in schemas: original schema: {:?}, new schema: {:?}", + "Failed due to a difference in schemas: {e}, original schema: {:?}, new schema: {:?}", schema, plan.schema() ) diff --git a/datafusion/expr/src/logical_plan/plan.rs b/datafusion/expr/src/logical_plan/plan.rs index edf5f1126be93..76b45d5d723ae 100644 --- a/datafusion/expr/src/logical_plan/plan.rs +++ b/datafusion/expr/src/logical_plan/plan.rs @@ -3709,47 +3709,6 @@ pub struct Join { } impl Join { - /// Creates a new Join operator with automatically computed schema. - /// - /// This constructor computes the schema based on the join type and inputs, - /// removing the need to manually specify the schema or call `recompute_schema`. - /// - /// # Arguments - /// - /// * `left` - Left input plan - /// * `right` - Right input plan - /// * `on` - Join condition as a vector of (left_expr, right_expr) pairs - /// * `filter` - Optional filter expression (for non-equijoin conditions) - /// * `join_type` - Type of join (Inner, Left, Right, etc.) - /// * `join_constraint` - Join constraint (On, Using) - /// * `null_equals_null` - Whether NULL = NULL in join comparisons - /// - /// # Returns - /// - /// A new Join operator with the computed schema - pub fn try_new( - left: Arc, - right: Arc, - on: Vec<(Expr, Expr)>, - filter: Option, - join_type: JoinType, - join_constraint: JoinConstraint, - null_equals_null: bool, - ) -> Result { - let join_schema = build_join_schema(left.schema(), right.schema(), &join_type)?; - - Ok(Join { - left, - right, - on, - filter, - join_type, - join_constraint, - schema: Arc::new(join_schema), - null_equals_null, - }) - } - /// Create Join with input which wrapped with projection, this method is used to help create physical join. pub fn try_new_with_project_input( original: &LogicalPlan, @@ -4957,379 +4916,4 @@ digraph { Ok(()) } - - #[test] - fn test_join_try_new() -> Result<()> { - let schema = Schema::new(vec![ - Field::new("a", DataType::Int32, false), - Field::new("b", DataType::Int32, false), - ]); - - let left_scan = table_scan(Some("t1"), &schema, None)?.build()?; - - let right_scan = table_scan(Some("t2"), &schema, None)?.build()?; - - let join_types = vec![ - JoinType::Inner, - JoinType::Left, - JoinType::Right, - JoinType::Full, - JoinType::LeftSemi, - JoinType::LeftAnti, - JoinType::RightSemi, - JoinType::RightAnti, - JoinType::LeftMark, - ]; - - for join_type in join_types { - let join = Join::try_new( - Arc::new(left_scan.clone()), - Arc::new(right_scan.clone()), - vec![(col("t1.a"), col("t2.a"))], - Some(col("t1.b").gt(col("t2.b"))), - join_type, - JoinConstraint::On, - false, - )?; - - match join_type { - JoinType::LeftSemi | JoinType::LeftAnti => { - assert_eq!(join.schema.fields().len(), 2); - - let fields = join.schema.fields(); - assert_eq!( - fields[0].name(), - "a", - "First field should be 'a' from left table" - ); - assert_eq!( - fields[1].name(), - "b", - "Second field should be 'b' from left table" - ); - } - JoinType::RightSemi | JoinType::RightAnti => { - assert_eq!(join.schema.fields().len(), 2); - - let fields = join.schema.fields(); - assert_eq!( - fields[0].name(), - "a", - "First field should be 'a' from right table" - ); - assert_eq!( - fields[1].name(), - "b", - "Second field should be 'b' from right table" - ); - } - JoinType::LeftMark => { - assert_eq!(join.schema.fields().len(), 3); - - let fields = join.schema.fields(); - assert_eq!( - fields[0].name(), - "a", - "First field should be 'a' from left table" - ); - assert_eq!( - fields[1].name(), - "b", - "Second field should be 'b' from left table" - ); - assert_eq!( - fields[2].name(), - "mark", - "Third field should be the mark column" - ); - - assert!(!fields[0].is_nullable()); - assert!(!fields[1].is_nullable()); - assert!(!fields[2].is_nullable()); - } - _ => { - assert_eq!(join.schema.fields().len(), 4); - - let fields = join.schema.fields(); - assert_eq!( - fields[0].name(), - "a", - "First field should be 'a' from left table" - ); - assert_eq!( - fields[1].name(), - "b", - "Second field should be 'b' from left table" - ); - assert_eq!( - fields[2].name(), - "a", - "Third field should be 'a' from right table" - ); - assert_eq!( - fields[3].name(), - "b", - "Fourth field should be 'b' from right table" - ); - - if join_type == JoinType::Left { - // Left side fields (first two) shouldn't be nullable - assert!(!fields[0].is_nullable()); - assert!(!fields[1].is_nullable()); - // Right side fields (third and fourth) should be nullable - assert!(fields[2].is_nullable()); - assert!(fields[3].is_nullable()); - } else if join_type == JoinType::Right { - // Left side fields (first two) should be nullable - assert!(fields[0].is_nullable()); - assert!(fields[1].is_nullable()); - // Right side fields (third and fourth) shouldn't be nullable - assert!(!fields[2].is_nullable()); - assert!(!fields[3].is_nullable()); - } else if join_type == JoinType::Full { - assert!(fields[0].is_nullable()); - assert!(fields[1].is_nullable()); - assert!(fields[2].is_nullable()); - assert!(fields[3].is_nullable()); - } - } - } - - assert_eq!(join.on, vec![(col("t1.a"), col("t2.a"))]); - assert_eq!(join.filter, Some(col("t1.b").gt(col("t2.b")))); - assert_eq!(join.join_type, join_type); - assert_eq!(join.join_constraint, JoinConstraint::On); - assert!(!join.null_equals_null); - } - - Ok(()) - } - - #[test] - fn test_join_try_new_with_using_constraint_and_overlapping_columns() -> Result<()> { - let left_schema = Schema::new(vec![ - Field::new("id", DataType::Int32, false), // Common column in both tables - Field::new("name", DataType::Utf8, false), // Unique to left - Field::new("value", DataType::Int32, false), // Common column, different meaning - ]); - - let right_schema = Schema::new(vec![ - Field::new("id", DataType::Int32, false), // Common column in both tables - Field::new("category", DataType::Utf8, false), // Unique to right - Field::new("value", DataType::Float64, true), // Common column, different meaning - ]); - - let left_plan = table_scan(Some("t1"), &left_schema, None)?.build()?; - - let right_plan = table_scan(Some("t2"), &right_schema, None)?.build()?; - - // Test 1: USING constraint with a common column - { - // In the logical plan, both copies of the `id` column are preserved - // The USING constraint is handled later during physical execution, where the common column appears once - let join = Join::try_new( - Arc::new(left_plan.clone()), - Arc::new(right_plan.clone()), - vec![(col("t1.id"), col("t2.id"))], - None, - JoinType::Inner, - JoinConstraint::Using, - false, - )?; - - let fields = join.schema.fields(); - - assert_eq!(fields.len(), 6); - - assert_eq!( - fields[0].name(), - "id", - "First field should be 'id' from left table" - ); - assert_eq!( - fields[1].name(), - "name", - "Second field should be 'name' from left table" - ); - assert_eq!( - fields[2].name(), - "value", - "Third field should be 'value' from left table" - ); - assert_eq!( - fields[3].name(), - "id", - "Fourth field should be 'id' from right table" - ); - assert_eq!( - fields[4].name(), - "category", - "Fifth field should be 'category' from right table" - ); - assert_eq!( - fields[5].name(), - "value", - "Sixth field should be 'value' from right table" - ); - - assert_eq!(join.join_constraint, JoinConstraint::Using); - } - - // Test 2: Complex join condition with expressions - { - // Complex condition: join on id equality AND where left.value < right.value - let join = Join::try_new( - Arc::new(left_plan.clone()), - Arc::new(right_plan.clone()), - vec![(col("t1.id"), col("t2.id"))], // Equijoin condition - Some(col("t1.value").lt(col("t2.value"))), // Non-equi filter condition - JoinType::Inner, - JoinConstraint::On, - false, - )?; - - let fields = join.schema.fields(); - assert_eq!(fields.len(), 6); - - assert_eq!( - fields[0].name(), - "id", - "First field should be 'id' from left table" - ); - assert_eq!( - fields[1].name(), - "name", - "Second field should be 'name' from left table" - ); - assert_eq!( - fields[2].name(), - "value", - "Third field should be 'value' from left table" - ); - assert_eq!( - fields[3].name(), - "id", - "Fourth field should be 'id' from right table" - ); - assert_eq!( - fields[4].name(), - "category", - "Fifth field should be 'category' from right table" - ); - assert_eq!( - fields[5].name(), - "value", - "Sixth field should be 'value' from right table" - ); - - assert_eq!(join.filter, Some(col("t1.value").lt(col("t2.value")))); - } - - // Test 3: Join with null equality behavior set to true - { - let join = Join::try_new( - Arc::new(left_plan.clone()), - Arc::new(right_plan.clone()), - vec![(col("t1.id"), col("t2.id"))], - None, - JoinType::Inner, - JoinConstraint::On, - true, - )?; - - assert!(join.null_equals_null); - } - - Ok(()) - } - - #[test] - fn test_join_try_new_schema_validation() -> Result<()> { - let left_schema = Schema::new(vec![ - Field::new("id", DataType::Int32, false), - Field::new("name", DataType::Utf8, false), - Field::new("value", DataType::Float64, true), - ]); - - let right_schema = Schema::new(vec![ - Field::new("id", DataType::Int32, false), - Field::new("category", DataType::Utf8, true), - Field::new("code", DataType::Int16, false), - ]); - - let left_plan = table_scan(Some("t1"), &left_schema, None)?.build()?; - - let right_plan = table_scan(Some("t2"), &right_schema, None)?.build()?; - - let join_types = vec![ - JoinType::Inner, - JoinType::Left, - JoinType::Right, - JoinType::Full, - ]; - - for join_type in join_types { - let join = Join::try_new( - Arc::new(left_plan.clone()), - Arc::new(right_plan.clone()), - vec![(col("t1.id"), col("t2.id"))], - Some(col("t1.value").gt(lit(5.0))), - join_type, - JoinConstraint::On, - false, - )?; - - let fields = join.schema.fields(); - assert_eq!( - fields.len(), - 6, - "Expected 6 fields for {:?} join", - join_type - ); - - for (i, field) in fields.iter().enumerate() { - let expected_nullable = match (i, &join_type) { - // Left table fields (indices 0, 1, 2) - (0, JoinType::Right | JoinType::Full) => true, // id becomes nullable in RIGHT/FULL - (1, JoinType::Right | JoinType::Full) => true, // name becomes nullable in RIGHT/FULL - (2, _) => true, // value is already nullable - - // Right table fields (indices 3, 4, 5) - (3, JoinType::Left | JoinType::Full) => true, // id becomes nullable in LEFT/FULL - (4, _) => true, // category is already nullable - (5, JoinType::Left | JoinType::Full) => true, // code becomes nullable in LEFT/FULL - - _ => false, - }; - - assert_eq!( - field.is_nullable(), - expected_nullable, - "Field {} ({}) nullability incorrect for {:?} join", - i, - field.name(), - join_type - ); - } - } - - let using_join = Join::try_new( - Arc::new(left_plan.clone()), - Arc::new(right_plan.clone()), - vec![(col("t1.id"), col("t2.id"))], - None, - JoinType::Inner, - JoinConstraint::Using, - false, - )?; - - assert_eq!( - using_join.schema.fields().len(), - 6, - "USING join should have all fields" - ); - assert_eq!(using_join.join_constraint, JoinConstraint::Using); - - Ok(()) - } } diff --git a/datafusion/expr/src/type_coercion/functions.rs b/datafusion/expr/src/type_coercion/functions.rs index 3b34718062eb4..0ec017bdc27f6 100644 --- a/datafusion/expr/src/type_coercion/functions.rs +++ b/datafusion/expr/src/type_coercion/functions.rs @@ -49,7 +49,7 @@ pub fn data_types_with_scalar_udf( let signature = func.signature(); let type_signature = &signature.type_signature; - if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { + if current_types.is_empty() { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { @@ -87,7 +87,7 @@ pub fn data_types_with_aggregate_udf( let signature = func.signature(); let type_signature = &signature.type_signature; - if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { + if current_types.is_empty() { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { @@ -124,7 +124,7 @@ pub fn data_types_with_window_udf( let signature = func.signature(); let type_signature = &signature.type_signature; - if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { + if current_types.is_empty() { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { @@ -161,7 +161,7 @@ pub fn data_types( ) -> Result> { let type_signature = &signature.type_signature; - if current_types.is_empty() && type_signature != &TypeSignature::UserDefined { + if current_types.is_empty() { if type_signature.supports_zero_argument() { return Ok(vec![]); } else if type_signature.used_to_support_zero_arguments() { diff --git a/datafusion/expr/src/udaf.rs b/datafusion/expr/src/udaf.rs index 97507433814b9..b75e8fd3cd3c4 100644 --- a/datafusion/expr/src/udaf.rs +++ b/datafusion/expr/src/udaf.rs @@ -315,16 +315,6 @@ impl AggregateUDF { self.inner.default_value(data_type) } - /// See [`AggregateUDFImpl::supports_null_handling_clause`] for more details. - pub fn supports_null_handling_clause(&self) -> bool { - self.inner.supports_null_handling_clause() - } - - /// See [`AggregateUDFImpl::is_ordered_set_aggregate`] for more details. - pub fn is_ordered_set_aggregate(&self) -> bool { - self.inner.is_ordered_set_aggregate() - } - /// Returns the documentation for this Aggregate UDF. /// /// Documentation can be accessed programmatically as well as @@ -442,14 +432,6 @@ pub trait AggregateUDFImpl: Debug + Send + Sync { null_treatment, } = params; - // exclude the first function argument(= column) in ordered set aggregate function, - // because it is duplicated with the WITHIN GROUP clause in schema name. - let args = if self.is_ordered_set_aggregate() { - &args[1..] - } else { - &args[..] - }; - let mut schema_name = String::new(); schema_name.write_fmt(format_args!( @@ -468,14 +450,8 @@ pub trait AggregateUDFImpl: Debug + Send + Sync { }; if let Some(order_by) = order_by { - let clause = match self.is_ordered_set_aggregate() { - true => "WITHIN GROUP", - false => "ORDER BY", - }; - schema_name.write_fmt(format_args!( - " {} [{}]", - clause, + " ORDER BY [{}]", schema_name_from_sorts(order_by)? ))?; }; @@ -915,18 +891,6 @@ pub trait AggregateUDFImpl: Debug + Send + Sync { ScalarValue::try_from(data_type) } - /// If this function supports `[IGNORE NULLS | RESPECT NULLS]` clause, return true - /// If the function does not, return false - fn supports_null_handling_clause(&self) -> bool { - true - } - - /// If this function is ordered-set aggregate function, return true - /// If the function is not, return false - fn is_ordered_set_aggregate(&self) -> bool { - false - } - /// Returns the documentation for this Aggregate UDF. /// /// Documentation can be accessed programmatically as well as diff --git a/datafusion/ffi/Cargo.toml b/datafusion/ffi/Cargo.toml index 29f40df51444c..5c80c1b042256 100644 --- a/datafusion/ffi/Cargo.toml +++ b/datafusion/ffi/Cargo.toml @@ -40,7 +40,6 @@ crate-type = ["cdylib", "rlib"] [dependencies] abi_stable = "0.11.3" arrow = { workspace = true, features = ["ffi"] } -arrow-schema = { workspace = true } async-ffi = { version = "0.5.0", features = ["abi_stable"] } async-trait = { workspace = true } datafusion = { workspace = true, default-features = false } diff --git a/datafusion/ffi/src/lib.rs b/datafusion/ffi/src/lib.rs index d877e182a1d89..877129fc5bb12 100644 --- a/datafusion/ffi/src/lib.rs +++ b/datafusion/ffi/src/lib.rs @@ -35,7 +35,6 @@ pub mod session_config; pub mod table_provider; pub mod table_source; pub mod udf; -pub mod udtf; pub mod util; pub mod volatility; diff --git a/datafusion/ffi/src/table_provider.rs b/datafusion/ffi/src/table_provider.rs index 890511997a706..a7391a85031e0 100644 --- a/datafusion/ffi/src/table_provider.rs +++ b/datafusion/ffi/src/table_provider.rs @@ -110,8 +110,8 @@ pub struct FFI_TableProvider { /// * `session_config` - session configuration /// * `projections` - if specified, only a subset of the columns are returned /// * `filters_serialized` - filters to apply to the scan, which are a - /// [`LogicalExprList`] protobuf message serialized into bytes to pass - /// across the FFI boundary. + /// [`LogicalExprList`] protobuf message serialized into bytes to pass + /// across the FFI boundary. /// * `limit` - if specified, limit the number of rows returned pub scan: unsafe extern "C" fn( provider: &Self, @@ -259,10 +259,14 @@ unsafe extern "C" fn scan_fn_wrapper( }; let projections: Vec<_> = projections.into_iter().collect(); + let maybe_projections = match projections.is_empty() { + true => None, + false => Some(&projections), + }; let plan = rresult_return!( internal_provider - .scan(&ctx.state(), Some(&projections), &filters, limit.into()) + .scan(&ctx.state(), maybe_projections, &filters, limit.into()) .await ); @@ -596,49 +600,4 @@ mod tests { Ok(()) } - - #[tokio::test] - async fn test_aggregation() -> Result<()> { - use arrow::datatypes::Field; - use datafusion::arrow::{ - array::Float32Array, datatypes::DataType, record_batch::RecordBatch, - }; - use datafusion::common::assert_batches_eq; - use datafusion::datasource::MemTable; - - let schema = - Arc::new(Schema::new(vec![Field::new("a", DataType::Float32, false)])); - - // define data in two partitions - let batch1 = RecordBatch::try_new( - Arc::clone(&schema), - vec![Arc::new(Float32Array::from(vec![2.0, 4.0, 8.0]))], - )?; - - let ctx = SessionContext::new(); - - let provider = Arc::new(MemTable::try_new(schema, vec![vec![batch1]])?); - - let ffi_provider = FFI_TableProvider::new(provider, true, None); - - let foreign_table_provider: ForeignTableProvider = (&ffi_provider).into(); - - ctx.register_table("t", Arc::new(foreign_table_provider))?; - - let result = ctx - .sql("SELECT COUNT(*) as cnt FROM t") - .await? - .collect() - .await?; - #[rustfmt::skip] - let expected = [ - "+-----+", - "| cnt |", - "+-----+", - "| 3 |", - "+-----+" - ]; - assert_batches_eq!(expected, &result); - Ok(()) - } } diff --git a/datafusion/ffi/src/tests/mod.rs b/datafusion/ffi/src/tests/mod.rs index 7a36ee52bdb4b..4b4a29276d9a8 100644 --- a/datafusion/ffi/src/tests/mod.rs +++ b/datafusion/ffi/src/tests/mod.rs @@ -27,7 +27,7 @@ use abi_stable::{ }; use catalog::create_catalog_provider; -use crate::{catalog_provider::FFI_CatalogProvider, udtf::FFI_TableFunction}; +use crate::catalog_provider::FFI_CatalogProvider; use super::{table_provider::FFI_TableProvider, udf::FFI_ScalarUDF}; use arrow::array::RecordBatch; @@ -37,13 +37,12 @@ use datafusion::{ common::record_batch, }; use sync_provider::create_sync_table_provider; -use udf_udaf_udwf::{create_ffi_abs_func, create_ffi_random_func, create_ffi_table_func}; +use udf_udaf_udwf::create_ffi_abs_func; mod async_provider; pub mod catalog; mod sync_provider; mod udf_udaf_udwf; -pub mod utils; #[repr(C)] #[derive(StableAbi)] @@ -61,10 +60,6 @@ pub struct ForeignLibraryModule { /// Create a scalar UDF pub create_scalar_udf: extern "C" fn() -> FFI_ScalarUDF, - pub create_nullary_udf: extern "C" fn() -> FFI_ScalarUDF, - - pub create_table_function: extern "C" fn() -> FFI_TableFunction, - pub version: extern "C" fn() -> u64, } @@ -110,8 +105,6 @@ pub fn get_foreign_library_module() -> ForeignLibraryModuleRef { create_catalog: create_catalog_provider, create_table: construct_table_provider, create_scalar_udf: create_ffi_abs_func, - create_nullary_udf: create_ffi_random_func, - create_table_function: create_ffi_table_func, version: super::version, } .leak_into_prefix() diff --git a/datafusion/ffi/src/tests/udf_udaf_udwf.rs b/datafusion/ffi/src/tests/udf_udaf_udwf.rs index c3cb1bcc35338..e8a13aac13081 100644 --- a/datafusion/ffi/src/tests/udf_udaf_udwf.rs +++ b/datafusion/ffi/src/tests/udf_udaf_udwf.rs @@ -15,13 +15,8 @@ // specific language governing permissions and limitations // under the License. -use crate::{udf::FFI_ScalarUDF, udtf::FFI_TableFunction}; -use datafusion::{ - catalog::TableFunctionImpl, - functions::math::{abs::AbsFunc, random::RandomFunc}, - functions_table::generate_series::RangeFunc, - logical_expr::ScalarUDF, -}; +use crate::udf::FFI_ScalarUDF; +use datafusion::{functions::math::abs::AbsFunc, logical_expr::ScalarUDF}; use std::sync::Arc; @@ -30,15 +25,3 @@ pub(crate) extern "C" fn create_ffi_abs_func() -> FFI_ScalarUDF { udf.into() } - -pub(crate) extern "C" fn create_ffi_random_func() -> FFI_ScalarUDF { - let udf: Arc = Arc::new(RandomFunc::new().into()); - - udf.into() -} - -pub(crate) extern "C" fn create_ffi_table_func() -> FFI_TableFunction { - let udtf: Arc = Arc::new(RangeFunc {}); - - FFI_TableFunction::new(udtf, None) -} diff --git a/datafusion/ffi/src/tests/utils.rs b/datafusion/ffi/src/tests/utils.rs deleted file mode 100644 index 6465b17d9b60c..0000000000000 --- a/datafusion/ffi/src/tests/utils.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use crate::tests::ForeignLibraryModuleRef; -use abi_stable::library::RootModule; -use datafusion::error::{DataFusionError, Result}; -use std::path::Path; - -/// Compute the path to the library. It would be preferable to simply use -/// abi_stable::library::development_utils::compute_library_path however -/// our current CI pipeline has a `ci` profile that we need to use to -/// find the library. -pub fn compute_library_path( - target_path: &Path, -) -> std::io::Result { - let debug_dir = target_path.join("debug"); - let release_dir = target_path.join("release"); - let ci_dir = target_path.join("ci"); - - let debug_path = M::get_library_path(&debug_dir.join("deps")); - let release_path = M::get_library_path(&release_dir.join("deps")); - let ci_path = M::get_library_path(&ci_dir.join("deps")); - - let all_paths = vec![ - (debug_dir.clone(), debug_path), - (release_dir, release_path), - (ci_dir, ci_path), - ]; - - let best_path = all_paths - .into_iter() - .filter(|(_, path)| path.exists()) - .filter_map(|(dir, path)| path.metadata().map(|m| (dir, m)).ok()) - .filter_map(|(dir, meta)| meta.modified().map(|m| (dir, m)).ok()) - .max_by_key(|(_, date)| *date) - .map(|(dir, _)| dir) - .unwrap_or(debug_dir); - - Ok(best_path) -} - -pub fn get_module() -> Result { - let expected_version = crate::version(); - - let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); - let target_dir = crate_root - .parent() - .expect("Failed to find crate parent") - .parent() - .expect("Failed to find workspace root") - .join("target"); - - // Find the location of the library. This is specific to the build environment, - // so you will need to change the approach here based on your use case. - // let target: &std::path::Path = "../../../../target/".as_ref(); - let library_path = - compute_library_path::(target_dir.as_path()) - .map_err(|e| DataFusionError::External(Box::new(e)))? - .join("deps"); - - // Load the module - let module = ForeignLibraryModuleRef::load_from_directory(&library_path) - .map_err(|e| DataFusionError::External(Box::new(e)))?; - - assert_eq!( - module - .version() - .expect("Unable to call version on FFI module")(), - expected_version - ); - - Ok(module) -} diff --git a/datafusion/ffi/src/udf/mod.rs b/datafusion/ffi/src/udf.rs similarity index 87% rename from datafusion/ffi/src/udf/mod.rs rename to datafusion/ffi/src/udf.rs index 706b9fabedcb4..bbc9cf936ceec 100644 --- a/datafusion/ffi/src/udf/mod.rs +++ b/datafusion/ffi/src/udf.rs @@ -29,9 +29,7 @@ use arrow::{ }; use datafusion::{ error::DataFusionError, - logical_expr::{ - type_coercion::functions::data_types_with_scalar_udf, ReturnInfo, ReturnTypeArgs, - }, + logical_expr::type_coercion::functions::data_types_with_scalar_udf, }; use datafusion::{ error::Result, @@ -39,10 +37,6 @@ use datafusion::{ ColumnarValue, ScalarFunctionArgs, ScalarUDF, ScalarUDFImpl, Signature, }, }; -use return_info::FFI_ReturnInfo; -use return_type_args::{ - FFI_ReturnTypeArgs, ForeignReturnTypeArgs, ForeignReturnTypeArgsOwned, -}; use crate::{ arrow_wrappers::{WrappedArray, WrappedSchema}, @@ -51,9 +45,6 @@ use crate::{ volatility::FFI_Volatility, }; -pub mod return_info; -pub mod return_type_args; - /// A stable struct for sharing a [`ScalarUDF`] across FFI boundaries. #[repr(C)] #[derive(Debug, StableAbi)] @@ -75,14 +66,6 @@ pub struct FFI_ScalarUDF { arg_types: RVec, ) -> RResult, - /// Determines the return info of the underlying [`ScalarUDF`]. Either this - /// or return_type may be implemented on a UDF. - pub return_type_from_args: unsafe extern "C" fn( - udf: &Self, - args: FFI_ReturnTypeArgs, - ) - -> RResult, - /// Execute the underlying [`ScalarUDF`] and return the result as a `FFI_ArrowArray` /// within an AbiStable wrapper. pub invoke_with_args: unsafe extern "C" fn( @@ -140,23 +123,6 @@ unsafe extern "C" fn return_type_fn_wrapper( rresult!(return_type) } -unsafe extern "C" fn return_type_from_args_fn_wrapper( - udf: &FFI_ScalarUDF, - args: FFI_ReturnTypeArgs, -) -> RResult { - let private_data = udf.private_data as *const ScalarUDFPrivateData; - let udf = &(*private_data).udf; - - let args: ForeignReturnTypeArgsOwned = rresult_return!((&args).try_into()); - let args_ref: ForeignReturnTypeArgs = (&args).into(); - - let return_type = udf - .return_type_from_args((&args_ref).into()) - .and_then(FFI_ReturnInfo::try_from); - - rresult!(return_type) -} - unsafe extern "C" fn coerce_types_fn_wrapper( udf: &FFI_ScalarUDF, arg_types: RVec, @@ -243,7 +209,6 @@ impl From> for FFI_ScalarUDF { short_circuits, invoke_with_args: invoke_with_args_fn_wrapper, return_type: return_type_fn_wrapper, - return_type_from_args: return_type_from_args_fn_wrapper, coerce_types: coerce_types_fn_wrapper, clone: clone_fn_wrapper, release: release_fn_wrapper, @@ -316,16 +281,6 @@ impl ScalarUDFImpl for ForeignScalarUDF { result.and_then(|r| (&r.0).try_into().map_err(DataFusionError::from)) } - fn return_type_from_args(&self, args: ReturnTypeArgs) -> Result { - let args: FFI_ReturnTypeArgs = args.try_into()?; - - let result = unsafe { (self.udf.return_type_from_args)(&self.udf, args) }; - - let result = df_result!(result); - - result.and_then(|r| r.try_into()) - } - fn invoke_with_args(&self, invoke_args: ScalarFunctionArgs) -> Result { let ScalarFunctionArgs { args, diff --git a/datafusion/ffi/src/udf/return_info.rs b/datafusion/ffi/src/udf/return_info.rs deleted file mode 100644 index cf76ddd1db762..0000000000000 --- a/datafusion/ffi/src/udf/return_info.rs +++ /dev/null @@ -1,53 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use abi_stable::StableAbi; -use arrow::{datatypes::DataType, ffi::FFI_ArrowSchema}; -use datafusion::{error::DataFusionError, logical_expr::ReturnInfo}; - -use crate::arrow_wrappers::WrappedSchema; - -/// A stable struct for sharing a [`ReturnInfo`] across FFI boundaries. -#[repr(C)] -#[derive(Debug, StableAbi)] -#[allow(non_camel_case_types)] -pub struct FFI_ReturnInfo { - return_type: WrappedSchema, - nullable: bool, -} - -impl TryFrom for FFI_ReturnInfo { - type Error = DataFusionError; - - fn try_from(value: ReturnInfo) -> Result { - let return_type = WrappedSchema(FFI_ArrowSchema::try_from(value.return_type())?); - Ok(Self { - return_type, - nullable: value.nullable(), - }) - } -} - -impl TryFrom for ReturnInfo { - type Error = DataFusionError; - - fn try_from(value: FFI_ReturnInfo) -> Result { - let return_type = DataType::try_from(&value.return_type.0)?; - - Ok(ReturnInfo::new(return_type, value.nullable)) - } -} diff --git a/datafusion/ffi/src/udf/return_type_args.rs b/datafusion/ffi/src/udf/return_type_args.rs deleted file mode 100644 index a0897630e2ea9..0000000000000 --- a/datafusion/ffi/src/udf/return_type_args.rs +++ /dev/null @@ -1,142 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use abi_stable::{ - std_types::{ROption, RVec}, - StableAbi, -}; -use arrow::datatypes::DataType; -use datafusion::{ - common::exec_datafusion_err, error::DataFusionError, logical_expr::ReturnTypeArgs, - scalar::ScalarValue, -}; - -use crate::{ - arrow_wrappers::WrappedSchema, - util::{rvec_wrapped_to_vec_datatype, vec_datatype_to_rvec_wrapped}, -}; -use prost::Message; - -/// A stable struct for sharing a [`ReturnTypeArgs`] across FFI boundaries. -#[repr(C)] -#[derive(Debug, StableAbi)] -#[allow(non_camel_case_types)] -pub struct FFI_ReturnTypeArgs { - arg_types: RVec, - scalar_arguments: RVec>>, - nullables: RVec, -} - -impl TryFrom> for FFI_ReturnTypeArgs { - type Error = DataFusionError; - - fn try_from(value: ReturnTypeArgs) -> Result { - let arg_types = vec_datatype_to_rvec_wrapped(value.arg_types)?; - let scalar_arguments: Result, Self::Error> = value - .scalar_arguments - .iter() - .map(|maybe_arg| { - maybe_arg - .map(|arg| { - let proto_value: datafusion_proto::protobuf::ScalarValue = - arg.try_into()?; - let proto_bytes: RVec = proto_value.encode_to_vec().into(); - Ok(proto_bytes) - }) - .transpose() - }) - .collect(); - let scalar_arguments = scalar_arguments?.into_iter().map(ROption::from).collect(); - - let nullables = value.nullables.into(); - Ok(Self { - arg_types, - scalar_arguments, - nullables, - }) - } -} - -// TODO(tsaucer) It would be good to find a better way around this, but it -// appears a restriction based on the need to have a borrowed ScalarValue -// in the arguments when converted to ReturnTypeArgs -pub struct ForeignReturnTypeArgsOwned { - arg_types: Vec, - scalar_arguments: Vec>, - nullables: Vec, -} - -pub struct ForeignReturnTypeArgs<'a> { - arg_types: &'a [DataType], - scalar_arguments: Vec>, - nullables: &'a [bool], -} - -impl TryFrom<&FFI_ReturnTypeArgs> for ForeignReturnTypeArgsOwned { - type Error = DataFusionError; - - fn try_from(value: &FFI_ReturnTypeArgs) -> Result { - let arg_types = rvec_wrapped_to_vec_datatype(&value.arg_types)?; - let scalar_arguments: Result, Self::Error> = value - .scalar_arguments - .iter() - .map(|maybe_arg| { - let maybe_arg = maybe_arg.as_ref().map(|arg| { - let proto_value = - datafusion_proto::protobuf::ScalarValue::decode(arg.as_ref()) - .map_err(|err| exec_datafusion_err!("{}", err))?; - let scalar_value: ScalarValue = (&proto_value).try_into()?; - Ok(scalar_value) - }); - Option::from(maybe_arg).transpose() - }) - .collect(); - let scalar_arguments = scalar_arguments?.into_iter().collect(); - - let nullables = value.nullables.iter().cloned().collect(); - - Ok(Self { - arg_types, - scalar_arguments, - nullables, - }) - } -} - -impl<'a> From<&'a ForeignReturnTypeArgsOwned> for ForeignReturnTypeArgs<'a> { - fn from(value: &'a ForeignReturnTypeArgsOwned) -> Self { - Self { - arg_types: &value.arg_types, - scalar_arguments: value - .scalar_arguments - .iter() - .map(|opt| opt.as_ref()) - .collect(), - nullables: &value.nullables, - } - } -} - -impl<'a> From<&'a ForeignReturnTypeArgs<'a>> for ReturnTypeArgs<'a> { - fn from(value: &'a ForeignReturnTypeArgs) -> Self { - ReturnTypeArgs { - arg_types: value.arg_types, - scalar_arguments: &value.scalar_arguments, - nullables: value.nullables, - } - } -} diff --git a/datafusion/ffi/src/udtf.rs b/datafusion/ffi/src/udtf.rs deleted file mode 100644 index 1e06247546be7..0000000000000 --- a/datafusion/ffi/src/udtf.rs +++ /dev/null @@ -1,321 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::{ffi::c_void, sync::Arc}; - -use abi_stable::{ - std_types::{RResult, RString, RVec}, - StableAbi, -}; - -use datafusion::error::Result; -use datafusion::{ - catalog::{TableFunctionImpl, TableProvider}, - prelude::{Expr, SessionContext}, -}; -use datafusion_proto::{ - logical_plan::{ - from_proto::parse_exprs, to_proto::serialize_exprs, DefaultLogicalExtensionCodec, - }, - protobuf::LogicalExprList, -}; -use prost::Message; -use tokio::runtime::Handle; - -use crate::{ - df_result, rresult_return, - table_provider::{FFI_TableProvider, ForeignTableProvider}, -}; - -/// A stable struct for sharing a [`TableFunctionImpl`] across FFI boundaries. -#[repr(C)] -#[derive(Debug, StableAbi)] -#[allow(non_camel_case_types)] -pub struct FFI_TableFunction { - /// Equivalent to the `call` function of the TableFunctionImpl. - /// The arguments are Expr passed as protobuf encoded bytes. - pub call: unsafe extern "C" fn( - udtf: &Self, - args: RVec, - ) -> RResult, - - /// Used to create a clone on the provider of the udtf. This should - /// only need to be called by the receiver of the udtf. - pub clone: unsafe extern "C" fn(udtf: &Self) -> Self, - - /// Release the memory of the private data when it is no longer being used. - pub release: unsafe extern "C" fn(udtf: &mut Self), - - /// Internal data. This is only to be accessed by the provider of the udtf. - /// A [`ForeignTableFunction`] should never attempt to access this data. - pub private_data: *mut c_void, -} - -unsafe impl Send for FFI_TableFunction {} -unsafe impl Sync for FFI_TableFunction {} - -pub struct TableFunctionPrivateData { - udtf: Arc, - runtime: Option, -} - -impl FFI_TableFunction { - fn inner(&self) -> &Arc { - let private_data = self.private_data as *const TableFunctionPrivateData; - unsafe { &(*private_data).udtf } - } - - fn runtime(&self) -> Option { - let private_data = self.private_data as *const TableFunctionPrivateData; - unsafe { (*private_data).runtime.clone() } - } -} - -unsafe extern "C" fn call_fn_wrapper( - udtf: &FFI_TableFunction, - args: RVec, -) -> RResult { - let runtime = udtf.runtime(); - let udtf = udtf.inner(); - - let default_ctx = SessionContext::new(); - let codec = DefaultLogicalExtensionCodec {}; - - let proto_filters = rresult_return!(LogicalExprList::decode(args.as_ref())); - - let args = - rresult_return!(parse_exprs(proto_filters.expr.iter(), &default_ctx, &codec)); - - let table_provider = rresult_return!(udtf.call(&args)); - RResult::ROk(FFI_TableProvider::new(table_provider, false, runtime)) -} - -unsafe extern "C" fn release_fn_wrapper(udtf: &mut FFI_TableFunction) { - let private_data = Box::from_raw(udtf.private_data as *mut TableFunctionPrivateData); - drop(private_data); -} - -unsafe extern "C" fn clone_fn_wrapper(udtf: &FFI_TableFunction) -> FFI_TableFunction { - let runtime = udtf.runtime(); - let udtf = udtf.inner(); - - FFI_TableFunction::new(Arc::clone(udtf), runtime) -} - -impl Clone for FFI_TableFunction { - fn clone(&self) -> Self { - unsafe { (self.clone)(self) } - } -} - -impl FFI_TableFunction { - pub fn new(udtf: Arc, runtime: Option) -> Self { - let private_data = Box::new(TableFunctionPrivateData { udtf, runtime }); - - Self { - call: call_fn_wrapper, - clone: clone_fn_wrapper, - release: release_fn_wrapper, - private_data: Box::into_raw(private_data) as *mut c_void, - } - } -} - -impl From> for FFI_TableFunction { - fn from(udtf: Arc) -> Self { - let private_data = Box::new(TableFunctionPrivateData { - udtf, - runtime: None, - }); - - Self { - call: call_fn_wrapper, - clone: clone_fn_wrapper, - release: release_fn_wrapper, - private_data: Box::into_raw(private_data) as *mut c_void, - } - } -} - -impl Drop for FFI_TableFunction { - fn drop(&mut self) { - unsafe { (self.release)(self) } - } -} - -/// This struct is used to access an UDTF provided by a foreign -/// library across a FFI boundary. -/// -/// The ForeignTableFunction is to be used by the caller of the UDTF, so it has -/// no knowledge or access to the private data. All interaction with the UDTF -/// must occur through the functions defined in FFI_TableFunction. -#[derive(Debug)] -pub struct ForeignTableFunction(FFI_TableFunction); - -unsafe impl Send for ForeignTableFunction {} -unsafe impl Sync for ForeignTableFunction {} - -impl From for ForeignTableFunction { - fn from(value: FFI_TableFunction) -> Self { - Self(value) - } -} - -impl TableFunctionImpl for ForeignTableFunction { - fn call(&self, args: &[Expr]) -> Result> { - let codec = DefaultLogicalExtensionCodec {}; - let expr_list = LogicalExprList { - expr: serialize_exprs(args, &codec)?, - }; - let filters_serialized = expr_list.encode_to_vec().into(); - - let table_provider = unsafe { (self.0.call)(&self.0, filters_serialized) }; - - let table_provider = df_result!(table_provider)?; - let table_provider: ForeignTableProvider = (&table_provider).into(); - - Ok(Arc::new(table_provider)) - } -} - -#[cfg(test)] -mod tests { - use arrow::{ - array::{ - record_batch, ArrayRef, Float64Array, RecordBatch, StringArray, UInt64Array, - }, - datatypes::{DataType, Field, Schema}, - }; - use datafusion::{ - catalog::MemTable, common::exec_err, prelude::lit, scalar::ScalarValue, - }; - - use super::*; - - #[derive(Debug)] - struct TestUDTF {} - - impl TableFunctionImpl for TestUDTF { - fn call(&self, args: &[Expr]) -> Result> { - let args = args - .iter() - .map(|arg| { - if let Expr::Literal(scalar) = arg { - Ok(scalar) - } else { - exec_err!("Expected only literal arguments to table udf") - } - }) - .collect::>>()?; - - if args.len() < 2 { - exec_err!("Expected at least two arguments to table udf")? - } - - let ScalarValue::UInt64(Some(num_rows)) = args[0].to_owned() else { - exec_err!( - "First argument must be the number of elements to create as u64" - )? - }; - let num_rows = num_rows as usize; - - let mut fields = Vec::default(); - let mut arrays1 = Vec::default(); - let mut arrays2 = Vec::default(); - - let split = num_rows / 3; - for (idx, arg) in args[1..].iter().enumerate() { - let (field, array) = match arg { - ScalarValue::Utf8(s) => { - let s_vec = vec![s.to_owned(); num_rows]; - ( - Field::new(format!("field-{}", idx), DataType::Utf8, true), - Arc::new(StringArray::from(s_vec)) as ArrayRef, - ) - } - ScalarValue::UInt64(v) => { - let v_vec = vec![v.to_owned(); num_rows]; - ( - Field::new(format!("field-{}", idx), DataType::UInt64, true), - Arc::new(UInt64Array::from(v_vec)) as ArrayRef, - ) - } - ScalarValue::Float64(v) => { - let v_vec = vec![v.to_owned(); num_rows]; - ( - Field::new(format!("field-{}", idx), DataType::Float64, true), - Arc::new(Float64Array::from(v_vec)) as ArrayRef, - ) - } - _ => exec_err!( - "Test case only supports utf8, u64, and f64. Found {}", - arg.data_type() - )?, - }; - - fields.push(field); - arrays1.push(array.slice(0, split)); - arrays2.push(array.slice(split, num_rows - split)); - } - - let schema = Arc::new(Schema::new(fields)); - let batches = vec![ - RecordBatch::try_new(Arc::clone(&schema), arrays1)?, - RecordBatch::try_new(Arc::clone(&schema), arrays2)?, - ]; - - let table_provider = MemTable::try_new(schema, vec![batches])?; - - Ok(Arc::new(table_provider)) - } - } - - #[tokio::test] - async fn test_round_trip_udtf() -> Result<()> { - let original_udtf = Arc::new(TestUDTF {}) as Arc; - - let local_udtf: FFI_TableFunction = - FFI_TableFunction::new(Arc::clone(&original_udtf), None); - - let foreign_udf: ForeignTableFunction = local_udtf.into(); - - let table = - foreign_udf.call(&vec![lit(6_u64), lit("one"), lit(2.0), lit(3_u64)])?; - - let ctx = SessionContext::default(); - let _ = ctx.register_table("test-table", table)?; - - let returned_batches = ctx.table("test-table").await?.collect().await?; - - assert_eq!(returned_batches.len(), 2); - let expected_batch_0 = record_batch!( - ("field-0", Utf8, ["one", "one"]), - ("field-1", Float64, [2.0, 2.0]), - ("field-2", UInt64, [3, 3]) - )?; - assert_eq!(returned_batches[0], expected_batch_0); - - let expected_batch_1 = record_batch!( - ("field-0", Utf8, ["one", "one", "one", "one"]), - ("field-1", Float64, [2.0, 2.0, 2.0, 2.0]), - ("field-2", UInt64, [3, 3, 3, 3]) - )?; - assert_eq!(returned_batches[1], expected_batch_1); - - Ok(()) - } -} diff --git a/datafusion/ffi/tests/ffi_integration.rs b/datafusion/ffi/tests/ffi_integration.rs index c6df324e9a17c..f610f12c8244e 100644 --- a/datafusion/ffi/tests/ffi_integration.rs +++ b/datafusion/ffi/tests/ffi_integration.rs @@ -20,14 +20,84 @@ #[cfg(feature = "integration-tests")] mod tests { + use abi_stable::library::RootModule; + use datafusion::common::record_batch; use datafusion::error::{DataFusionError, Result}; - use datafusion::prelude::SessionContext; + use datafusion::logical_expr::ScalarUDF; + use datafusion::prelude::{col, SessionContext}; use datafusion_ffi::catalog_provider::ForeignCatalogProvider; use datafusion_ffi::table_provider::ForeignTableProvider; - use datafusion_ffi::tests::create_record_batch; - use datafusion_ffi::tests::utils::get_module; + use datafusion_ffi::tests::{create_record_batch, ForeignLibraryModuleRef}; + use datafusion_ffi::udf::ForeignScalarUDF; + use std::path::Path; use std::sync::Arc; + /// Compute the path to the library. It would be preferable to simply use + /// abi_stable::library::development_utils::compute_library_path however + /// our current CI pipeline has a `ci` profile that we need to use to + /// find the library. + pub fn compute_library_path( + target_path: &Path, + ) -> std::io::Result { + let debug_dir = target_path.join("debug"); + let release_dir = target_path.join("release"); + let ci_dir = target_path.join("ci"); + + let debug_path = M::get_library_path(&debug_dir.join("deps")); + let release_path = M::get_library_path(&release_dir.join("deps")); + let ci_path = M::get_library_path(&ci_dir.join("deps")); + + let all_paths = vec![ + (debug_dir.clone(), debug_path), + (release_dir, release_path), + (ci_dir, ci_path), + ]; + + let best_path = all_paths + .into_iter() + .filter(|(_, path)| path.exists()) + .filter_map(|(dir, path)| path.metadata().map(|m| (dir, m)).ok()) + .filter_map(|(dir, meta)| meta.modified().map(|m| (dir, m)).ok()) + .max_by_key(|(_, date)| *date) + .map(|(dir, _)| dir) + .unwrap_or(debug_dir); + + Ok(best_path) + } + + fn get_module() -> Result { + let expected_version = datafusion_ffi::version(); + + let crate_root = Path::new(env!("CARGO_MANIFEST_DIR")); + let target_dir = crate_root + .parent() + .expect("Failed to find crate parent") + .parent() + .expect("Failed to find workspace root") + .join("target"); + + // Find the location of the library. This is specific to the build environment, + // so you will need to change the approach here based on your use case. + // let target: &std::path::Path = "../../../../target/".as_ref(); + let library_path = + compute_library_path::(target_dir.as_path()) + .map_err(|e| DataFusionError::External(Box::new(e)))? + .join("deps"); + + // Load the module + let module = ForeignLibraryModuleRef::load_from_directory(&library_path) + .map_err(|e| DataFusionError::External(Box::new(e)))?; + + assert_eq!( + module + .version() + .expect("Unable to call version on FFI module")(), + expected_version + ); + + Ok(module) + } + /// It is important that this test is in the `tests` directory and not in the /// library directory so we can verify we are building a dynamic library and /// testing it via a different executable. @@ -71,6 +141,46 @@ mod tests { test_table_provider(true).await } + /// This test validates that we can load an external module and use a scalar + /// udf defined in it via the foreign function interface. In this case we are + /// using the abs() function as our scalar UDF. + #[tokio::test] + async fn test_scalar_udf() -> Result<()> { + let module = get_module()?; + + let ffi_abs_func = + module + .create_scalar_udf() + .ok_or(DataFusionError::NotImplemented( + "External table provider failed to implement create_scalar_udf" + .to_string(), + ))?(); + let foreign_abs_func: ForeignScalarUDF = (&ffi_abs_func).try_into()?; + + let udf: ScalarUDF = foreign_abs_func.into(); + + let ctx = SessionContext::default(); + let df = ctx.read_batch(create_record_batch(-5, 5))?; + + let df = df + .with_column("abs_a", udf.call(vec![col("a")]))? + .with_column("abs_b", udf.call(vec![col("b")]))?; + + let result = df.collect().await?; + + let expected = record_batch!( + ("a", Int32, vec![-5, -4, -3, -2, -1]), + ("b", Float64, vec![-5., -4., -3., -2., -1.]), + ("abs_a", Int32, vec![5, 4, 3, 2, 1]), + ("abs_b", Float64, vec![5., 4., 3., 2., 1.]) + )?; + + assert!(result.len() == 1); + assert!(result[0] == expected); + + Ok(()) + } + #[tokio::test] async fn test_catalog() -> Result<()> { let module = get_module()?; diff --git a/datafusion/ffi/tests/ffi_udf.rs b/datafusion/ffi/tests/ffi_udf.rs deleted file mode 100644 index bbc23552def43..0000000000000 --- a/datafusion/ffi/tests/ffi_udf.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -/// Add an additional module here for convenience to scope this to only -/// when the feature integtation-tests is built -#[cfg(feature = "integration-tests")] -mod tests { - - use arrow::datatypes::DataType; - use datafusion::common::record_batch; - use datafusion::error::{DataFusionError, Result}; - use datafusion::logical_expr::ScalarUDF; - use datafusion::prelude::{col, SessionContext}; - - use datafusion_ffi::tests::create_record_batch; - use datafusion_ffi::tests::utils::get_module; - use datafusion_ffi::udf::ForeignScalarUDF; - - /// This test validates that we can load an external module and use a scalar - /// udf defined in it via the foreign function interface. In this case we are - /// using the abs() function as our scalar UDF. - #[tokio::test] - async fn test_scalar_udf() -> Result<()> { - let module = get_module()?; - - let ffi_abs_func = - module - .create_scalar_udf() - .ok_or(DataFusionError::NotImplemented( - "External table provider failed to implement create_scalar_udf" - .to_string(), - ))?(); - let foreign_abs_func: ForeignScalarUDF = (&ffi_abs_func).try_into()?; - - let udf: ScalarUDF = foreign_abs_func.into(); - - let ctx = SessionContext::default(); - let df = ctx.read_batch(create_record_batch(-5, 5))?; - - let df = df - .with_column("abs_a", udf.call(vec![col("a")]))? - .with_column("abs_b", udf.call(vec![col("b")]))?; - - let result = df.collect().await?; - - let expected = record_batch!( - ("a", Int32, vec![-5, -4, -3, -2, -1]), - ("b", Float64, vec![-5., -4., -3., -2., -1.]), - ("abs_a", Int32, vec![5, 4, 3, 2, 1]), - ("abs_b", Float64, vec![5., 4., 3., 2., 1.]) - )?; - - assert!(result.len() == 1); - assert!(result[0] == expected); - - Ok(()) - } - - /// This test validates nullary input UDFs - #[tokio::test] - async fn test_nullary_scalar_udf() -> Result<()> { - let module = get_module()?; - - let ffi_abs_func = - module - .create_nullary_udf() - .ok_or(DataFusionError::NotImplemented( - "External table provider failed to implement create_scalar_udf" - .to_string(), - ))?(); - let foreign_abs_func: ForeignScalarUDF = (&ffi_abs_func).try_into()?; - - let udf: ScalarUDF = foreign_abs_func.into(); - - let ctx = SessionContext::default(); - let df = ctx.read_batch(create_record_batch(-5, 5))?; - - let df = df.with_column("time_now", udf.call(vec![]))?; - - let result = df.collect().await?; - - assert!(result.len() == 1); - assert_eq!( - result[0].column_by_name("time_now").unwrap().data_type(), - &DataType::Float64 - ); - - Ok(()) - } -} diff --git a/datafusion/ffi/tests/ffi_udtf.rs b/datafusion/ffi/tests/ffi_udtf.rs deleted file mode 100644 index 5a46211d3b9c6..0000000000000 --- a/datafusion/ffi/tests/ffi_udtf.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -/// Add an additional module here for convenience to scope this to only -/// when the feature integtation-tests is built -#[cfg(feature = "integration-tests")] -mod tests { - - use std::sync::Arc; - - use arrow::array::{create_array, ArrayRef}; - use datafusion::error::{DataFusionError, Result}; - use datafusion::prelude::SessionContext; - - use datafusion_ffi::tests::utils::get_module; - use datafusion_ffi::udtf::ForeignTableFunction; - - /// This test validates that we can load an external module and use a scalar - /// udf defined in it via the foreign function interface. In this case we are - /// using the abs() function as our scalar UDF. - #[tokio::test] - async fn test_user_defined_table_function() -> Result<()> { - let module = get_module()?; - - let ffi_table_func = module - .create_table_function() - .ok_or(DataFusionError::NotImplemented( - "External table function provider failed to implement create_table_function" - .to_string(), - ))?(); - let foreign_table_func: ForeignTableFunction = ffi_table_func.into(); - - let udtf = Arc::new(foreign_table_func); - - let ctx = SessionContext::default(); - ctx.register_udtf("my_range", udtf); - - let result = ctx - .sql("SELECT * FROM my_range(5)") - .await? - .collect() - .await?; - let expected = create_array!(Int64, [0, 1, 2, 3, 4]) as ArrayRef; - - assert!(result.len() == 1); - assert!(result[0].column(0) == &expected); - - Ok(()) - } -} diff --git a/datafusion/functions-aggregate/benches/array_agg.rs b/datafusion/functions-aggregate/benches/array_agg.rs index e22be611d8d76..fb605e87ed0cc 100644 --- a/datafusion/functions-aggregate/benches/array_agg.rs +++ b/datafusion/functions-aggregate/benches/array_agg.rs @@ -19,23 +19,17 @@ use std::sync::Arc; use arrow::array::{ Array, ArrayRef, ArrowPrimitiveType, AsArray, ListArray, NullBufferBuilder, - PrimitiveArray, }; use arrow::datatypes::{Field, Int64Type}; +use arrow::util::bench_util::create_primitive_array; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use datafusion_expr::Accumulator; use datafusion_functions_aggregate::array_agg::ArrayAggAccumulator; use arrow::buffer::OffsetBuffer; +use arrow::util::test_util::seedable_rng; use rand::distributions::{Distribution, Standard}; -use rand::prelude::StdRng; use rand::Rng; -use rand::SeedableRng; - -/// Returns fixed seedable RNG -pub fn seedable_rng() -> StdRng { - StdRng::seed_from_u64(42) -} fn merge_batch_bench(c: &mut Criterion, name: &str, values: ArrayRef) { let list_item_data_type = values.as_list::().values().data_type().clone(); @@ -52,24 +46,6 @@ fn merge_batch_bench(c: &mut Criterion, name: &str, values: ArrayRef) { }); } -pub fn create_primitive_array(size: usize, null_density: f32) -> PrimitiveArray -where - T: ArrowPrimitiveType, - Standard: Distribution, -{ - let mut rng = seedable_rng(); - - (0..size) - .map(|_| { - if rng.gen::() < null_density { - None - } else { - Some(rng.gen()) - } - }) - .collect() -} - /// Create List array with the given item data type, null density, null locations and zero length lists density /// Creates an random (but fixed-seeded) array of a given size and null density pub fn create_list_array( diff --git a/datafusion/functions-aggregate/src/approx_median.rs b/datafusion/functions-aggregate/src/approx_median.rs index 9a202879d94ab..787e08bae2867 100644 --- a/datafusion/functions-aggregate/src/approx_median.rs +++ b/datafusion/functions-aggregate/src/approx_median.rs @@ -45,7 +45,7 @@ make_udaf_expr_and_func!( /// APPROX_MEDIAN aggregate expression #[user_doc( doc_section(label = "Approximate Functions"), - description = "Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY x)`.", + description = "Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(x, 0.5)`.", syntax_example = "approx_median(expression)", sql_example = r#"```sql > SELECT approx_median(column_name) FROM table_name; diff --git a/datafusion/functions-aggregate/src/approx_percentile_cont.rs b/datafusion/functions-aggregate/src/approx_percentile_cont.rs index 41281733f5deb..1fad5f73703c7 100644 --- a/datafusion/functions-aggregate/src/approx_percentile_cont.rs +++ b/datafusion/functions-aggregate/src/approx_percentile_cont.rs @@ -34,7 +34,6 @@ use datafusion_common::{ downcast_value, internal_err, not_impl_datafusion_err, not_impl_err, plan_err, Result, ScalarValue, }; -use datafusion_expr::expr::{AggregateFunction, Sort}; use datafusion_expr::function::{AccumulatorArgs, StateFieldsArgs}; use datafusion_expr::type_coercion::aggregates::{INTEGERS, NUMERICS}; use datafusion_expr::utils::format_state_name; @@ -52,39 +51,29 @@ create_func!(ApproxPercentileCont, approx_percentile_cont_udaf); /// Computes the approximate percentile continuous of a set of numbers pub fn approx_percentile_cont( - order_by: Sort, + expression: Expr, percentile: Expr, centroids: Option, ) -> Expr { - let expr = order_by.expr.clone(); - let args = if let Some(centroids) = centroids { - vec![expr, percentile, centroids] + vec![expression, percentile, centroids] } else { - vec![expr, percentile] + vec![expression, percentile] }; - - Expr::AggregateFunction(AggregateFunction::new_udf( - approx_percentile_cont_udaf(), - args, - false, - None, - Some(vec![order_by]), - None, - )) + approx_percentile_cont_udaf().call(args) } #[user_doc( doc_section(label = "Approximate Functions"), description = "Returns the approximate percentile of input values using the t-digest algorithm.", - syntax_example = "approx_percentile_cont(percentile, centroids) WITHIN GROUP (ORDER BY expression)", + syntax_example = "approx_percentile_cont(expression, percentile, centroids)", sql_example = r#"```sql -> SELECT approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) FROM table_name; -+-----------------------------------------------------------------------+ -| approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) | -+-----------------------------------------------------------------------+ -| 65.0 | -+-----------------------------------------------------------------------+ +> SELECT approx_percentile_cont(column_name, 0.75, 100) FROM table_name; ++-------------------------------------------------+ +| approx_percentile_cont(column_name, 0.75, 100) | ++-------------------------------------------------+ +| 65.0 | ++-------------------------------------------------+ ```"#, standard_argument(name = "expression",), argument( @@ -141,19 +130,6 @@ impl ApproxPercentileCont { args: AccumulatorArgs, ) -> Result { let percentile = validate_input_percentile_expr(&args.exprs[1])?; - - let is_descending = args - .ordering_req - .first() - .map(|sort_expr| sort_expr.options.descending) - .unwrap_or(false); - - let percentile = if is_descending { - 1.0 - percentile - } else { - percentile - }; - let tdigest_max_size = if args.exprs.len() == 3 { Some(validate_input_max_size_expr(&args.exprs[2])?) } else { @@ -316,14 +292,6 @@ impl AggregateUDFImpl for ApproxPercentileCont { Ok(arg_types[0].clone()) } - fn supports_null_handling_clause(&self) -> bool { - false - } - - fn is_ordered_set_aggregate(&self) -> bool { - true - } - fn documentation(&self) -> Option<&Documentation> { self.doc() } diff --git a/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs b/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs index 0316757f26d08..16dac2c1b8f04 100644 --- a/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs +++ b/datafusion/functions-aggregate/src/approx_percentile_cont_with_weight.rs @@ -52,14 +52,14 @@ make_udaf_expr_and_func!( #[user_doc( doc_section(label = "Approximate Functions"), description = "Returns the weighted approximate percentile of input values using the t-digest algorithm.", - syntax_example = "approx_percentile_cont_with_weight(weight, percentile) WITHIN GROUP (ORDER BY expression)", + syntax_example = "approx_percentile_cont_with_weight(expression, weight, percentile)", sql_example = r#"```sql -> SELECT approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) FROM table_name; -+---------------------------------------------------------------------------------------------+ -| approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) | -+---------------------------------------------------------------------------------------------+ -| 78.5 | -+---------------------------------------------------------------------------------------------+ +> SELECT approx_percentile_cont_with_weight(column_name, weight_column, 0.90) FROM table_name; ++----------------------------------------------------------------------+ +| approx_percentile_cont_with_weight(column_name, weight_column, 0.90) | ++----------------------------------------------------------------------+ +| 78.5 | ++----------------------------------------------------------------------+ ```"#, standard_argument(name = "expression", prefix = "The"), argument( @@ -178,14 +178,6 @@ impl AggregateUDFImpl for ApproxPercentileContWithWeight { self.approx_percentile_cont.state_fields(args) } - fn supports_null_handling_clause(&self) -> bool { - false - } - - fn is_ordered_set_aggregate(&self) -> bool { - true - } - fn documentation(&self) -> Option<&Documentation> { self.doc() } diff --git a/datafusion/functions-aggregate/src/array_agg.rs b/datafusion/functions-aggregate/src/array_agg.rs index d658744c1ba5d..573624ce4d491 100644 --- a/datafusion/functions-aggregate/src/array_agg.rs +++ b/datafusion/functions-aggregate/src/array_agg.rs @@ -289,7 +289,7 @@ impl Accumulator for ArrayAggAccumulator { } let val = Arc::clone(&values[0]); - if !val.is_empty() { + if val.len() > 0 { self.values.push(val); } Ok(()) @@ -310,7 +310,7 @@ impl Accumulator for ArrayAggAccumulator { match Self::get_optional_values_to_merge_as_is(list_arr) { Some(values) => { // Make sure we don't insert empty lists - if !values.is_empty() { + if values.len() > 0 { self.values.push(values); } } diff --git a/datafusion/functions-aggregate/src/average.rs b/datafusion/functions-aggregate/src/average.rs index 30d1e09fe3cc0..758a775e06d7c 100644 --- a/datafusion/functions-aggregate/src/average.rs +++ b/datafusion/functions-aggregate/src/average.rs @@ -24,9 +24,8 @@ use arrow::array::{ use arrow::compute::sum; use arrow::datatypes::{ - i256, ArrowNativeType, DataType, Decimal128Type, Decimal256Type, DecimalType, - DurationMicrosecondType, DurationMillisecondType, DurationNanosecondType, - DurationSecondType, Field, Float64Type, TimeUnit, UInt64Type, + i256, ArrowNativeType, DataType, Decimal128Type, Decimal256Type, DecimalType, Field, + Float64Type, UInt64Type, }; use datafusion_common::{ exec_err, not_impl_err, utils::take_function_args, Result, ScalarValue, @@ -152,16 +151,6 @@ impl AggregateUDFImpl for Avg { target_precision: *target_precision, target_scale: *target_scale, })), - - (Duration(time_unit), Duration(result_unit)) => { - Ok(Box::new(DurationAvgAccumulator { - sum: None, - count: 0, - time_unit: *time_unit, - result_unit: *result_unit, - })) - } - _ => exec_err!( "AvgAccumulator for ({} --> {})", &data_type, @@ -417,105 +406,6 @@ impl Accumulator for DecimalAvgAccumu } } -/// An accumulator to compute the average for duration values -#[derive(Debug)] -struct DurationAvgAccumulator { - sum: Option, - count: u64, - time_unit: TimeUnit, - result_unit: TimeUnit, -} - -impl Accumulator for DurationAvgAccumulator { - fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> { - let array = &values[0]; - self.count += (array.len() - array.null_count()) as u64; - - let sum_value = match self.time_unit { - TimeUnit::Second => sum(array.as_primitive::()), - TimeUnit::Millisecond => sum(array.as_primitive::()), - TimeUnit::Microsecond => sum(array.as_primitive::()), - TimeUnit::Nanosecond => sum(array.as_primitive::()), - }; - - if let Some(x) = sum_value { - let v = self.sum.get_or_insert(0); - *v += x; - } - Ok(()) - } - - fn evaluate(&mut self) -> Result { - let avg = self.sum.map(|sum| sum / self.count as i64); - - match self.result_unit { - TimeUnit::Second => Ok(ScalarValue::DurationSecond(avg)), - TimeUnit::Millisecond => Ok(ScalarValue::DurationMillisecond(avg)), - TimeUnit::Microsecond => Ok(ScalarValue::DurationMicrosecond(avg)), - TimeUnit::Nanosecond => Ok(ScalarValue::DurationNanosecond(avg)), - } - } - - fn size(&self) -> usize { - size_of_val(self) - } - - fn state(&mut self) -> Result> { - let duration_value = match self.time_unit { - TimeUnit::Second => ScalarValue::DurationSecond(self.sum), - TimeUnit::Millisecond => ScalarValue::DurationMillisecond(self.sum), - TimeUnit::Microsecond => ScalarValue::DurationMicrosecond(self.sum), - TimeUnit::Nanosecond => ScalarValue::DurationNanosecond(self.sum), - }; - - Ok(vec![ScalarValue::from(self.count), duration_value]) - } - - fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> { - self.count += sum(states[0].as_primitive::()).unwrap_or_default(); - - let sum_value = match self.time_unit { - TimeUnit::Second => sum(states[1].as_primitive::()), - TimeUnit::Millisecond => { - sum(states[1].as_primitive::()) - } - TimeUnit::Microsecond => { - sum(states[1].as_primitive::()) - } - TimeUnit::Nanosecond => { - sum(states[1].as_primitive::()) - } - }; - - if let Some(x) = sum_value { - let v = self.sum.get_or_insert(0); - *v += x; - } - Ok(()) - } - - fn retract_batch(&mut self, values: &[ArrayRef]) -> Result<()> { - let array = &values[0]; - self.count -= (array.len() - array.null_count()) as u64; - - let sum_value = match self.time_unit { - TimeUnit::Second => sum(array.as_primitive::()), - TimeUnit::Millisecond => sum(array.as_primitive::()), - TimeUnit::Microsecond => sum(array.as_primitive::()), - TimeUnit::Nanosecond => sum(array.as_primitive::()), - }; - - if let Some(x) = sum_value { - self.sum = Some(self.sum.unwrap() - x); - } - Ok(()) - } - - fn supports_retract_batch(&self) -> bool { - true - } -} - /// An accumulator to compute the average of `[PrimitiveArray]`. /// Stores values as native types, and does overflow checking /// diff --git a/datafusion/functions-aggregate/src/first_last.rs b/datafusion/functions-aggregate/src/first_last.rs index ec8c440b77e5f..28e6a8723dfd4 100644 --- a/datafusion/functions-aggregate/src/first_last.rs +++ b/datafusion/functions-aggregate/src/first_last.rs @@ -52,7 +52,6 @@ use datafusion_macros::user_doc; use datafusion_physical_expr_common::sort_expr::LexOrdering; create_func!(FirstValue, first_value_udaf); -create_func!(LastValue, last_value_udaf); /// Returns the first value in a group of values. pub fn first_value(expression: Expr, order_by: Option>) -> Expr { @@ -68,20 +67,6 @@ pub fn first_value(expression: Expr, order_by: Option>) -> Expr { } } -/// Returns the last value in a group of values. -pub fn last_value(expression: Expr, order_by: Option>) -> Expr { - if let Some(order_by) = order_by { - last_value_udaf() - .call(vec![expression]) - .order_by(order_by) - .build() - // guaranteed to be `Expr::AggregateFunction` - .unwrap() - } else { - last_value_udaf().call(vec![expression]) - } -} - #[user_doc( doc_section(label = "General Functions"), description = "Returns the first element in an aggregation group according to the requested ordering. If no ordering is given, returns an arbitrary element from the group.", @@ -181,7 +166,6 @@ impl AggregateUDFImpl for FirstValue { } fn groups_accumulator_supported(&self, args: AccumulatorArgs) -> bool { - // TODO: extract to function use DataType::*; matches!( args.return_type, @@ -209,7 +193,6 @@ impl AggregateUDFImpl for FirstValue { &self, args: AccumulatorArgs, ) -> Result> { - // TODO: extract to function fn create_accumulator( args: AccumulatorArgs, ) -> Result> @@ -227,7 +210,6 @@ impl AggregateUDFImpl for FirstValue { args.ignore_nulls, args.return_type, &ordering_dtypes, - true, )?)) } @@ -276,12 +258,10 @@ impl AggregateUDFImpl for FirstValue { create_accumulator::(args) } - _ => { - internal_err!( - "GroupsAccumulator not supported for first_value({})", - args.return_type - ) - } + _ => internal_err!( + "GroupsAccumulator not supported for first({})", + args.return_type + ), } } @@ -311,7 +291,6 @@ impl AggregateUDFImpl for FirstValue { } } -// TODO: rename to PrimitiveGroupsAccumulator struct FirstPrimitiveGroupsAccumulator where T: ArrowPrimitiveType + Send, @@ -337,16 +316,12 @@ where // buffer for `get_filtered_min_of_each_group` // filter_min_of_each_group_buf.0[group_idx] -> idx_in_val // only valid if filter_min_of_each_group_buf.1[group_idx] == true - // TODO: rename to extreme_of_each_group_buf min_of_each_group_buf: (Vec, BooleanBufferBuilder), // =========== option ============ // Stores the applicable ordering requirement. ordering_req: LexOrdering, - // true: take first element in an aggregation group according to the requested ordering. - // false: take last element in an aggregation group according to the requested ordering. - pick_first_in_group: bool, // derived from `ordering_req`. sort_options: Vec, // Stores whether incoming data already satisfies the ordering requirement. @@ -367,7 +342,6 @@ where ignore_nulls: bool, data_type: &DataType, ordering_dtypes: &[DataType], - pick_first_in_group: bool, ) -> Result { let requirement_satisfied = ordering_req.is_empty(); @@ -391,7 +365,6 @@ where is_sets: BooleanBufferBuilder::new(0), size_of_orderings: 0, min_of_each_group_buf: (Vec::new(), BooleanBufferBuilder::new(0)), - pick_first_in_group, }) } @@ -418,13 +391,8 @@ where assert!(new_ordering_values.len() == self.ordering_req.len()); let current_ordering = &self.orderings[group_idx]; - compare_rows(current_ordering, new_ordering_values, &self.sort_options).map(|x| { - if self.pick_first_in_group { - x.is_gt() - } else { - x.is_lt() - } - }) + compare_rows(current_ordering, new_ordering_values, &self.sort_options) + .map(|x| x.is_gt()) } fn take_orderings(&mut self, emit_to: EmitTo) -> Vec> { @@ -533,10 +501,10 @@ where .map(ScalarValue::size_of_vec) .sum::() } + /// Returns a vector of tuples `(group_idx, idx_in_val)` representing the index of the /// minimum value in `orderings` for each group, using lexicographical comparison. /// Values are filtered using `opt_filter` and `is_set_arr` if provided. - /// TODO: rename to get_filtered_extreme_of_each_group fn get_filtered_min_of_each_group( &mut self, orderings: &[ArrayRef], @@ -588,19 +556,15 @@ where } let is_valid = self.min_of_each_group_buf.1.get_bit(group_idx); - - if !is_valid { + if is_valid + && comparator + .compare(self.min_of_each_group_buf.0[group_idx], idx_in_val) + .is_gt() + { + self.min_of_each_group_buf.0[group_idx] = idx_in_val; + } else if !is_valid { self.min_of_each_group_buf.1.set_bit(group_idx, true); self.min_of_each_group_buf.0[group_idx] = idx_in_val; - } else { - let ordering = comparator - .compare(self.min_of_each_group_buf.0[group_idx], idx_in_val); - - if (ordering.is_gt() && self.pick_first_in_group) - || (ordering.is_lt() && !self.pick_first_in_group) - { - self.min_of_each_group_buf.0[group_idx] = idx_in_val; - } } } @@ -954,6 +918,13 @@ impl Accumulator for FirstValueAccumulator { } } +make_udaf_expr_and_func!( + LastValue, + last_value, + "Returns the last value in a group of values.", + last_value_udaf +); + #[user_doc( doc_section(label = "General Functions"), description = "Returns the last element in an aggregation group according to the requested ordering. If no ordering is given, returns an arbitrary element from the group.", @@ -1081,109 +1052,6 @@ impl AggregateUDFImpl for LastValue { fn documentation(&self) -> Option<&Documentation> { self.doc() } - - fn groups_accumulator_supported(&self, args: AccumulatorArgs) -> bool { - use DataType::*; - matches!( - args.return_type, - Int8 | Int16 - | Int32 - | Int64 - | UInt8 - | UInt16 - | UInt32 - | UInt64 - | Float16 - | Float32 - | Float64 - | Decimal128(_, _) - | Decimal256(_, _) - | Date32 - | Date64 - | Time32(_) - | Time64(_) - | Timestamp(_, _) - ) - } - - fn create_groups_accumulator( - &self, - args: AccumulatorArgs, - ) -> Result> { - fn create_accumulator( - args: AccumulatorArgs, - ) -> Result> - where - T: ArrowPrimitiveType + Send, - { - let ordering_dtypes = args - .ordering_req - .iter() - .map(|e| e.expr.data_type(args.schema)) - .collect::>>()?; - - Ok(Box::new(FirstPrimitiveGroupsAccumulator::::try_new( - args.ordering_req.clone(), - args.ignore_nulls, - args.return_type, - &ordering_dtypes, - false, - )?)) - } - - match args.return_type { - DataType::Int8 => create_accumulator::(args), - DataType::Int16 => create_accumulator::(args), - DataType::Int32 => create_accumulator::(args), - DataType::Int64 => create_accumulator::(args), - DataType::UInt8 => create_accumulator::(args), - DataType::UInt16 => create_accumulator::(args), - DataType::UInt32 => create_accumulator::(args), - DataType::UInt64 => create_accumulator::(args), - DataType::Float16 => create_accumulator::(args), - DataType::Float32 => create_accumulator::(args), - DataType::Float64 => create_accumulator::(args), - - DataType::Decimal128(_, _) => create_accumulator::(args), - DataType::Decimal256(_, _) => create_accumulator::(args), - - DataType::Timestamp(TimeUnit::Second, _) => { - create_accumulator::(args) - } - DataType::Timestamp(TimeUnit::Millisecond, _) => { - create_accumulator::(args) - } - DataType::Timestamp(TimeUnit::Microsecond, _) => { - create_accumulator::(args) - } - DataType::Timestamp(TimeUnit::Nanosecond, _) => { - create_accumulator::(args) - } - - DataType::Date32 => create_accumulator::(args), - DataType::Date64 => create_accumulator::(args), - DataType::Time32(TimeUnit::Second) => { - create_accumulator::(args) - } - DataType::Time32(TimeUnit::Millisecond) => { - create_accumulator::(args) - } - - DataType::Time64(TimeUnit::Microsecond) => { - create_accumulator::(args) - } - DataType::Time64(TimeUnit::Nanosecond) => { - create_accumulator::(args) - } - - _ => { - internal_err!( - "GroupsAccumulator not supported for last_value({})", - args.return_type - ) - } - } - } } #[derive(Debug)] @@ -1543,7 +1411,6 @@ mod tests { true, &DataType::Int64, &[DataType::Int64], - true, )?; let mut val_with_orderings = { @@ -1618,7 +1485,7 @@ mod tests { } #[test] - fn test_group_acc_size_of_ordering() -> Result<()> { + fn test_frist_group_acc_size_of_ordering() -> Result<()> { let schema = Arc::new(Schema::new(vec![ Field::new("a", DataType::Int64, true), Field::new("b", DataType::Int64, true), @@ -1637,7 +1504,6 @@ mod tests { true, &DataType::Int64, &[DataType::Int64], - true, )?; let val_with_orderings = { @@ -1697,79 +1563,4 @@ mod tests { Ok(()) } - - #[test] - fn test_last_group_acc() -> Result<()> { - let schema = Arc::new(Schema::new(vec![ - Field::new("a", DataType::Int64, true), - Field::new("b", DataType::Int64, true), - Field::new("c", DataType::Int64, true), - Field::new("d", DataType::Int32, true), - Field::new("e", DataType::Boolean, true), - ])); - - let sort_key = LexOrdering::new(vec![PhysicalSortExpr { - expr: col("c", &schema).unwrap(), - options: SortOptions::default(), - }]); - - let mut group_acc = FirstPrimitiveGroupsAccumulator::::try_new( - sort_key, - true, - &DataType::Int64, - &[DataType::Int64], - false, - )?; - - let mut val_with_orderings = { - let mut val_with_orderings = Vec::::new(); - - let vals = Arc::new(Int64Array::from(vec![Some(1), None, Some(3), Some(-6)])); - let orderings = Arc::new(Int64Array::from(vec![1, -9, 3, -6])); - - val_with_orderings.push(vals); - val_with_orderings.push(orderings); - - val_with_orderings - }; - - group_acc.update_batch( - &val_with_orderings, - &[0, 1, 2, 1], - Some(&BooleanArray::from(vec![true, true, false, true])), - 3, - )?; - - let state = group_acc.state(EmitTo::All)?; - - let expected_state: Vec> = vec![ - Arc::new(Int64Array::from(vec![Some(1), Some(-6), None])), - Arc::new(Int64Array::from(vec![Some(1), Some(-6), None])), - Arc::new(BooleanArray::from(vec![true, true, false])), - ]; - assert_eq!(state, expected_state); - - group_acc.merge_batch( - &state, - &[0, 1, 2], - Some(&BooleanArray::from(vec![true, false, false])), - 3, - )?; - - val_with_orderings.clear(); - val_with_orderings.push(Arc::new(Int64Array::from(vec![66, 6]))); - val_with_orderings.push(Arc::new(Int64Array::from(vec![66, 6]))); - - group_acc.update_batch(&val_with_orderings, &[1, 2], None, 4)?; - - let binding = group_acc.evaluate(EmitTo::All)?; - let eval_result = binding.as_any().downcast_ref::().unwrap(); - - let expect: PrimitiveArray = - Int64Array::from(vec![Some(1), Some(66), Some(6), None]); - - assert_eq!(eval_result, &expect); - - Ok(()) - } } diff --git a/datafusion/functions-aggregate/src/string_agg.rs b/datafusion/functions-aggregate/src/string_agg.rs index a7594b9ccb01f..64314ef6df687 100644 --- a/datafusion/functions-aggregate/src/string_agg.rs +++ b/datafusion/functions-aggregate/src/string_agg.rs @@ -17,17 +17,15 @@ //! [`StringAgg`] accumulator for the `string_agg` function -use crate::array_agg::ArrayAgg; use arrow::array::ArrayRef; -use arrow::datatypes::{DataType, Field}; +use arrow::datatypes::DataType; use datafusion_common::cast::as_generic_string_array; use datafusion_common::Result; -use datafusion_common::{internal_err, not_impl_err, ScalarValue}; +use datafusion_common::{not_impl_err, ScalarValue}; use datafusion_expr::function::AccumulatorArgs; use datafusion_expr::{ Accumulator, AggregateUDFImpl, Documentation, Signature, TypeSignature, Volatility, }; -use datafusion_functions_aggregate_common::accumulator::StateFieldsArgs; use datafusion_macros::user_doc; use datafusion_physical_expr::expressions::Literal; use std::any::Any; @@ -43,31 +41,15 @@ make_udaf_expr_and_func!( #[user_doc( doc_section(label = "General Functions"), - description = "Concatenates the values of string expressions and places separator values between them. \ -If ordering is required, strings are concatenated in the specified order. \ -This aggregation function can only mix DISTINCT and ORDER BY if the ordering expression is exactly the same as the first argument expression.", - syntax_example = "string_agg([DISTINCT] expression, delimiter [ORDER BY expression])", + description = "Concatenates the values of string expressions and places separator values between them.", + syntax_example = "string_agg(expression, delimiter)", sql_example = r#"```sql > SELECT string_agg(name, ', ') AS names_list FROM employee; +--------------------------+ | names_list | +--------------------------+ -| Alice, Bob, Bob, Charlie | -+--------------------------+ -> SELECT string_agg(name, ', ' ORDER BY name DESC) AS names_list - FROM employee; -+--------------------------+ -| names_list | -+--------------------------+ -| Charlie, Bob, Bob, Alice | -+--------------------------+ -> SELECT string_agg(DISTINCT name, ', ' ORDER BY name DESC) AS names_list - FROM employee; -+--------------------------+ -| names_list | -+--------------------------+ -| Charlie, Bob, Alice | +| Alice, Bob, Charlie | +--------------------------+ ```"#, argument( @@ -83,7 +65,6 @@ This aggregation function can only mix DISTINCT and ORDER BY if the ordering exp #[derive(Debug)] pub struct StringAgg { signature: Signature, - array_agg: ArrayAgg, } impl StringAgg { @@ -95,13 +76,9 @@ impl StringAgg { TypeSignature::Exact(vec![DataType::LargeUtf8, DataType::Utf8]), TypeSignature::Exact(vec![DataType::LargeUtf8, DataType::LargeUtf8]), TypeSignature::Exact(vec![DataType::LargeUtf8, DataType::Null]), - TypeSignature::Exact(vec![DataType::Utf8, DataType::Utf8]), - TypeSignature::Exact(vec![DataType::Utf8, DataType::LargeUtf8]), - TypeSignature::Exact(vec![DataType::Utf8, DataType::Null]), ], Volatility::Immutable, ), - array_agg: Default::default(), } } } @@ -129,40 +106,20 @@ impl AggregateUDFImpl for StringAgg { Ok(DataType::LargeUtf8) } - fn state_fields(&self, args: StateFieldsArgs) -> Result> { - self.array_agg.state_fields(args) - } - fn accumulator(&self, acc_args: AccumulatorArgs) -> Result> { - let Some(lit) = acc_args.exprs[1].as_any().downcast_ref::() else { - return not_impl_err!( - "The second argument of the string_agg function must be a string literal" - ); - }; - - let delimiter = if lit.value().is_null() { - // If the second argument (the delimiter that joins strings) is NULL, join - // on an empty string. (e.g. [a, b, c] => "abc"). - "" - } else if let Some(lit_string) = lit.value().try_as_str() { - lit_string.unwrap_or("") - } else { - return not_impl_err!( - "StringAgg not supported for delimiter \"{}\"", - lit.value() - ); - }; - - let array_agg_acc = self.array_agg.accumulator(AccumulatorArgs { - return_type: &DataType::new_list(acc_args.return_type.clone(), true), - exprs: &filter_index(acc_args.exprs, 1), - ..acc_args - })?; + if let Some(lit) = acc_args.exprs[1].as_any().downcast_ref::() { + return match lit.value().try_as_str() { + Some(Some(delimiter)) => { + Ok(Box::new(StringAggAccumulator::new(delimiter))) + } + Some(None) => Ok(Box::new(StringAggAccumulator::new(""))), + None => { + not_impl_err!("StringAgg not supported for delimiter {}", lit.value()) + } + }; + } - Ok(Box::new(StringAggAccumulator::new( - array_agg_acc, - delimiter, - ))) + not_impl_err!("expect literal") } fn documentation(&self) -> Option<&Documentation> { @@ -172,14 +129,14 @@ impl AggregateUDFImpl for StringAgg { #[derive(Debug)] pub(crate) struct StringAggAccumulator { - array_agg_acc: Box, + values: Option, delimiter: String, } impl StringAggAccumulator { - pub fn new(array_agg_acc: Box, delimiter: &str) -> Self { + pub fn new(delimiter: &str) -> Self { Self { - array_agg_acc, + values: None, delimiter: delimiter.to_string(), } } @@ -187,311 +144,37 @@ impl StringAggAccumulator { impl Accumulator for StringAggAccumulator { fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> { - self.array_agg_acc.update_batch(&filter_index(values, 1)) - } - - fn evaluate(&mut self) -> Result { - let scalar = self.array_agg_acc.evaluate()?; - - let ScalarValue::List(list) = scalar else { - return internal_err!("Expected a DataType::List while evaluating underlying ArrayAggAccumulator, but got {}", scalar.data_type()); - }; - - let string_arr: Vec<_> = match list.value_type() { - DataType::LargeUtf8 => as_generic_string_array::(list.values())? - .iter() - .flatten() - .collect(), - DataType::Utf8 => as_generic_string_array::(list.values())? - .iter() - .flatten() - .collect(), - _ => { - return internal_err!( - "Expected elements to of type Utf8 or LargeUtf8, but got {}", - list.value_type() - ) + let string_array: Vec<_> = as_generic_string_array::(&values[0])? + .iter() + .filter_map(|v| v.as_ref().map(ToString::to_string)) + .collect(); + if !string_array.is_empty() { + let s = string_array.join(self.delimiter.as_str()); + let v = self.values.get_or_insert("".to_string()); + if !v.is_empty() { + v.push_str(self.delimiter.as_str()); } - }; - - if string_arr.is_empty() { - return Ok(ScalarValue::LargeUtf8(None)); + v.push_str(s.as_str()); } - - Ok(ScalarValue::LargeUtf8(Some( - string_arr.join(&self.delimiter), - ))) - } - - fn size(&self) -> usize { - size_of_val(self) - size_of_val(&self.array_agg_acc) - + self.array_agg_acc.size() - + self.delimiter.capacity() - } - - fn state(&mut self) -> Result> { - self.array_agg_acc.state() - } - - fn merge_batch(&mut self, values: &[ArrayRef]) -> Result<()> { - self.array_agg_acc.merge_batch(values) - } -} - -fn filter_index(values: &[T], index: usize) -> Vec { - values - .iter() - .enumerate() - .filter(|(i, _)| *i != index) - .map(|(_, v)| v) - .cloned() - .collect::>() -} - -#[cfg(test)] -mod tests { - use super::*; - use arrow::array::LargeStringArray; - use arrow::compute::SortOptions; - use arrow::datatypes::{Fields, Schema}; - use datafusion_common::internal_err; - use datafusion_physical_expr::expressions::Column; - use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr}; - use std::sync::Arc; - - #[test] - fn no_duplicates_no_distinct() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",").build_two()?; - - acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; - acc2.update_batch(&[data(["d", "e", "f"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str(acc1.evaluate()?); - - assert_eq!(result, "a,b,c,d,e,f"); - - Ok(()) - } - - #[test] - fn no_duplicates_distinct() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") - .distinct() - .build_two()?; - - acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; - acc2.update_batch(&[data(["d", "e", "f"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str_sorted(acc1.evaluate()?, ","); - - assert_eq!(result, "a,b,c,d,e,f"); - - Ok(()) - } - - #[test] - fn duplicates_no_distinct() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",").build_two()?; - - acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; - acc2.update_batch(&[data(["a", "b", "c"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str(acc1.evaluate()?); - - assert_eq!(result, "a,b,c,a,b,c"); - Ok(()) } - #[test] - fn duplicates_distinct() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") - .distinct() - .build_two()?; - - acc1.update_batch(&[data(["a", "b", "c"]), data([","])])?; - acc2.update_batch(&[data(["a", "b", "c"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str_sorted(acc1.evaluate()?, ","); - - assert_eq!(result, "a,b,c"); - - Ok(()) - } - - #[test] - fn no_duplicates_distinct_sort_asc() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") - .distinct() - .order_by_col("col", SortOptions::new(false, false)) - .build_two()?; - - acc1.update_batch(&[data(["e", "b", "d"]), data([","])])?; - acc2.update_batch(&[data(["f", "a", "c"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str(acc1.evaluate()?); - - assert_eq!(result, "a,b,c,d,e,f"); - - Ok(()) - } - - #[test] - fn no_duplicates_distinct_sort_desc() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") - .distinct() - .order_by_col("col", SortOptions::new(true, false)) - .build_two()?; - - acc1.update_batch(&[data(["e", "b", "d"]), data([","])])?; - acc2.update_batch(&[data(["f", "a", "c"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str(acc1.evaluate()?); - - assert_eq!(result, "f,e,d,c,b,a"); - - Ok(()) - } - - #[test] - fn duplicates_distinct_sort_asc() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") - .distinct() - .order_by_col("col", SortOptions::new(false, false)) - .build_two()?; - - acc1.update_batch(&[data(["a", "c", "b"]), data([","])])?; - acc2.update_batch(&[data(["b", "c", "a"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str(acc1.evaluate()?); - - assert_eq!(result, "a,b,c"); - - Ok(()) - } - - #[test] - fn duplicates_distinct_sort_desc() -> Result<()> { - let (mut acc1, mut acc2) = StringAggAccumulatorBuilder::new(",") - .distinct() - .order_by_col("col", SortOptions::new(true, false)) - .build_two()?; - - acc1.update_batch(&[data(["a", "c", "b"]), data([","])])?; - acc2.update_batch(&[data(["b", "c", "a"]), data([","])])?; - acc1 = merge(acc1, acc2)?; - - let result = some_str(acc1.evaluate()?); - - assert_eq!(result, "c,b,a"); - + fn merge_batch(&mut self, values: &[ArrayRef]) -> Result<()> { + self.update_batch(values)?; Ok(()) } - struct StringAggAccumulatorBuilder { - sep: String, - distinct: bool, - ordering: LexOrdering, - schema: Schema, - } - - impl StringAggAccumulatorBuilder { - fn new(sep: &str) -> Self { - Self { - sep: sep.to_string(), - distinct: Default::default(), - ordering: Default::default(), - schema: Schema { - fields: Fields::from(vec![Field::new( - "col", - DataType::LargeUtf8, - true, - )]), - metadata: Default::default(), - }, - } - } - fn distinct(mut self) -> Self { - self.distinct = true; - self - } - - fn order_by_col(mut self, col: &str, sort_options: SortOptions) -> Self { - self.ordering.extend([PhysicalSortExpr::new( - Arc::new( - Column::new_with_schema(col, &self.schema) - .expect("column not available in schema"), - ), - sort_options, - )]); - self - } - - fn build(&self) -> Result> { - StringAgg::new().accumulator(AccumulatorArgs { - return_type: &DataType::LargeUtf8, - schema: &self.schema, - ignore_nulls: false, - ordering_req: &self.ordering, - is_reversed: false, - name: "", - is_distinct: self.distinct, - exprs: &[ - Arc::new(Column::new("col", 0)), - Arc::new(Literal::new(ScalarValue::Utf8(Some(self.sep.to_string())))), - ], - }) - } - - fn build_two(&self) -> Result<(Box, Box)> { - Ok((self.build()?, self.build()?)) - } - } - - fn some_str(value: ScalarValue) -> String { - str(value) - .expect("ScalarValue was not a String") - .expect("ScalarValue was None") - } - - fn some_str_sorted(value: ScalarValue, sep: &str) -> String { - let value = some_str(value); - let mut parts: Vec<&str> = value.split(sep).collect(); - parts.sort(); - parts.join(sep) - } - - fn str(value: ScalarValue) -> Result> { - match value { - ScalarValue::LargeUtf8(v) => Ok(v), - _ => internal_err!( - "Expected ScalarValue::LargeUtf8, got {}", - value.data_type() - ), - } + fn state(&mut self) -> Result> { + Ok(vec![self.evaluate()?]) } - fn data(list: [&str; N]) -> ArrayRef { - Arc::new(LargeStringArray::from(list.to_vec())) + fn evaluate(&mut self) -> Result { + Ok(ScalarValue::LargeUtf8(self.values.clone())) } - fn merge( - mut acc1: Box, - mut acc2: Box, - ) -> Result> { - let intermediate_state = acc2.state().and_then(|e| { - e.iter() - .map(|v| v.to_array()) - .collect::>>() - })?; - acc1.merge_batch(&intermediate_state)?; - Ok(acc1) + fn size(&self) -> usize { + size_of_val(self) + + self.values.as_ref().map(|v| v.capacity()).unwrap_or(0) + + self.delimiter.capacity() } } diff --git a/datafusion/functions-nested/src/array_has.rs b/datafusion/functions-nested/src/array_has.rs index 5ef1491313b13..48ee341566b90 100644 --- a/datafusion/functions-nested/src/array_has.rs +++ b/datafusion/functions-nested/src/array_has.rs @@ -271,7 +271,7 @@ fn array_has_dispatch_for_scalar( let offsets = haystack.value_offsets(); // If first argument is empty list (second argument is non-null), return false // i.e. array_has([], non-null element) -> false - if values.is_empty() { + if values.len() == 0 { return Ok(Arc::new(BooleanArray::new( BooleanBuffer::new_unset(haystack.len()), None, @@ -488,7 +488,7 @@ fn array_has_all_and_any_dispatch( ) -> Result { let haystack = as_generic_list_array::(haystack)?; let needle = as_generic_list_array::(needle)?; - if needle.values().is_empty() { + if needle.values().len() == 0 { let buffer = match comparison_type { ComparisonType::All => BooleanBuffer::new_set(haystack.len()), ComparisonType::Any => BooleanBuffer::new_unset(haystack.len()), diff --git a/datafusion/functions-nested/src/flatten.rs b/datafusion/functions-nested/src/flatten.rs index 4279f04e3dc44..f288035948dcb 100644 --- a/datafusion/functions-nested/src/flatten.rs +++ b/datafusion/functions-nested/src/flatten.rs @@ -18,18 +18,19 @@ //! [`ScalarUDFImpl`] definitions for flatten function. use crate::utils::make_scalar_function; -use arrow::array::{Array, ArrayRef, GenericListArray, OffsetSizeTrait}; +use arrow::array::{ArrayRef, GenericListArray, OffsetSizeTrait}; use arrow::buffer::OffsetBuffer; use arrow::datatypes::{ DataType, DataType::{FixedSizeList, LargeList, List, Null}, }; -use datafusion_common::cast::{as_large_list_array, as_list_array}; -use datafusion_common::utils::ListCoercion; +use datafusion_common::cast::{ + as_generic_list_array, as_large_list_array, as_list_array, +}; use datafusion_common::{exec_err, utils::take_function_args, Result}; use datafusion_expr::{ - ArrayFunctionArgument, ArrayFunctionSignature, ColumnarValue, Documentation, - ScalarUDFImpl, Signature, TypeSignature, Volatility, + ArrayFunctionSignature, ColumnarValue, Documentation, ScalarUDFImpl, Signature, + TypeSignature, Volatility, }; use datafusion_macros::user_doc; use std::any::Any; @@ -76,11 +77,9 @@ impl Flatten { pub fn new() -> Self { Self { signature: Signature { + // TODO (https://github.com/apache/datafusion/issues/13757) flatten should be single-step, not recursive type_signature: TypeSignature::ArraySignature( - ArrayFunctionSignature::Array { - arguments: vec![ArrayFunctionArgument::Array], - array_coercion: Some(ListCoercion::FixedSizedListToList), - }, + ArrayFunctionSignature::RecursiveArray, ), volatility: Volatility::Immutable, }, @@ -103,23 +102,25 @@ impl ScalarUDFImpl for Flatten { } fn return_type(&self, arg_types: &[DataType]) -> Result { - let data_type = match &arg_types[0] { - List(field) | FixedSizeList(field, _) => match field.data_type() { - List(field) | FixedSizeList(field, _) => List(Arc::clone(field)), - _ => arg_types[0].clone(), - }, - LargeList(field) => match field.data_type() { - List(field) | LargeList(field) | FixedSizeList(field, _) => { - LargeList(Arc::clone(field)) + fn get_base_type(data_type: &DataType) -> Result { + match data_type { + List(field) | FixedSizeList(field, _) + if matches!(field.data_type(), List(_) | FixedSizeList(_, _)) => + { + get_base_type(field.data_type()) } - _ => arg_types[0].clone(), - }, - Null => Null, - _ => exec_err!( - "Not reachable, data_type should be List, LargeList or FixedSizeList" - )?, - }; + LargeList(field) if matches!(field.data_type(), LargeList(_)) => { + get_base_type(field.data_type()) + } + Null | List(_) | LargeList(_) => Ok(data_type.to_owned()), + FixedSizeList(field, _) => Ok(List(Arc::clone(field))), + _ => exec_err!( + "Not reachable, data_type should be List, LargeList or FixedSizeList" + ), + } + } + let data_type = get_base_type(&arg_types[0])?; Ok(data_type) } @@ -145,62 +146,14 @@ pub fn flatten_inner(args: &[ArrayRef]) -> Result { match array.data_type() { List(_) => { - let (field, offsets, values, nulls) = - as_list_array(&array)?.clone().into_parts(); - - match field.data_type() { - List(_) => { - let (inner_field, inner_offsets, inner_values, _) = - as_list_array(&values)?.clone().into_parts(); - let offsets = get_offsets_for_flatten::(inner_offsets, offsets); - let flattened_array = GenericListArray::::new( - inner_field, - offsets, - inner_values, - nulls, - ); - - Ok(Arc::new(flattened_array) as ArrayRef) - } - LargeList(_) => { - exec_err!("flatten does not support type '{:?}'", array.data_type())? - } - _ => Ok(Arc::clone(array) as ArrayRef), - } + let list_arr = as_list_array(&array)?; + let flattened_array = flatten_internal::(list_arr.clone(), None)?; + Ok(Arc::new(flattened_array) as ArrayRef) } LargeList(_) => { - let (field, offsets, values, nulls) = - as_large_list_array(&array)?.clone().into_parts(); - - match field.data_type() { - List(_) => { - let (inner_field, inner_offsets, inner_values, _) = - as_list_array(&values)?.clone().into_parts(); - let offsets = get_large_offsets_for_flatten(inner_offsets, offsets); - let flattened_array = GenericListArray::::new( - inner_field, - offsets, - inner_values, - nulls, - ); - - Ok(Arc::new(flattened_array) as ArrayRef) - } - LargeList(_) => { - let (inner_field, inner_offsets, inner_values, nulls) = - as_large_list_array(&values)?.clone().into_parts(); - let offsets = get_offsets_for_flatten::(inner_offsets, offsets); - let flattened_array = GenericListArray::::new( - inner_field, - offsets, - inner_values, - nulls, - ); - - Ok(Arc::new(flattened_array) as ArrayRef) - } - _ => Ok(Arc::clone(array) as ArrayRef), - } + let list_arr = as_large_list_array(&array)?; + let flattened_array = flatten_internal::(list_arr.clone(), None)?; + Ok(Arc::new(flattened_array) as ArrayRef) } Null => Ok(Arc::clone(array)), _ => { @@ -209,6 +162,37 @@ pub fn flatten_inner(args: &[ArrayRef]) -> Result { } } +fn flatten_internal( + list_arr: GenericListArray, + indexes: Option>, +) -> Result> { + let (field, offsets, values, _) = list_arr.clone().into_parts(); + let data_type = field.data_type(); + + match data_type { + // Recursively get the base offsets for flattened array + List(_) | LargeList(_) => { + let sub_list = as_generic_list_array::(&values)?; + if let Some(indexes) = indexes { + let offsets = get_offsets_for_flatten(offsets, indexes); + flatten_internal::(sub_list.clone(), Some(offsets)) + } else { + flatten_internal::(sub_list.clone(), Some(offsets)) + } + } + // Reach the base level, create a new list array + _ => { + if let Some(indexes) = indexes { + let offsets = get_offsets_for_flatten(offsets, indexes); + let list_arr = GenericListArray::::new(field, offsets, values, None); + Ok(list_arr) + } else { + Ok(list_arr) + } + } + } +} + // Create new offsets that are equivalent to `flatten` the array. fn get_offsets_for_flatten( offsets: OffsetBuffer, @@ -221,16 +205,3 @@ fn get_offsets_for_flatten( .collect(); OffsetBuffer::new(offsets.into()) } - -// Create new large offsets that are equivalent to `flatten` the array. -fn get_large_offsets_for_flatten( - offsets: OffsetBuffer, - indexes: OffsetBuffer

, -) -> OffsetBuffer { - let buffer = offsets.into_inner(); - let offsets: Vec = indexes - .iter() - .map(|i| buffer[i.to_usize().unwrap()].to_i64().unwrap()) - .collect(); - OffsetBuffer::new(offsets.into()) -} diff --git a/datafusion/functions-nested/src/sort.rs b/datafusion/functions-nested/src/sort.rs index 85737ef135bce..1db245fe52fed 100644 --- a/datafusion/functions-nested/src/sort.rs +++ b/datafusion/functions-nested/src/sort.rs @@ -20,7 +20,6 @@ use crate::utils::make_scalar_function; use arrow::array::{new_null_array, Array, ArrayRef, ListArray, NullBufferBuilder}; use arrow::buffer::OffsetBuffer; -use arrow::compute::SortColumn; use arrow::datatypes::DataType::{FixedSizeList, LargeList, List}; use arrow::datatypes::{DataType, Field}; use arrow::{compute, compute::SortOptions}; @@ -208,24 +207,9 @@ pub fn array_sort_inner(args: &[ArrayRef]) -> Result { valid.append_null(); } else { let arr_ref = list_array.value(i); + let arr_ref = arr_ref.as_ref(); - // arrow sort kernel does not support Structs, so use - // lexsort_to_indices instead: - // https://github.com/apache/arrow-rs/issues/6911#issuecomment-2562928843 - let sorted_array = match arr_ref.data_type() { - DataType::Struct(_) => { - let sort_columns: Vec = vec![SortColumn { - values: Arc::clone(&arr_ref), - options: sort_option, - }]; - let indices = compute::lexsort_to_indices(&sort_columns, None)?; - compute::take(arr_ref.as_ref(), &indices, None)? - } - _ => { - let arr_ref = arr_ref.as_ref(); - compute::sort(arr_ref, sort_option)? - } - }; + let sorted_array = compute::sort(arr_ref, sort_option)?; array_lengths.push(sorted_array.len()); arrays.push(sorted_array); valid.append_non_null(); diff --git a/datafusion/functions-table/src/generate_series.rs b/datafusion/functions-table/src/generate_series.rs index ee95567ab73dc..5bb56f28bc8d3 100644 --- a/datafusion/functions-table/src/generate_series.rs +++ b/datafusion/functions-table/src/generate_series.rs @@ -138,15 +138,12 @@ impl TableProvider for GenerateSeriesTable { async fn scan( &self, state: &dyn Session, - projection: Option<&Vec>, + _projection: Option<&Vec>, _filters: &[Expr], _limit: Option, ) -> Result> { let batch_size = state.config_options().execution.batch_size; - let schema = match projection { - Some(projection) => Arc::new(self.schema.project(projection)?), - None => self.schema(), - }; + let state = match self.args { // if args have null, then return 0 row GenSeriesArgs::ContainsNull { include_end, name } => GenerateSeriesState { @@ -178,7 +175,7 @@ impl TableProvider for GenerateSeriesTable { }; Ok(Arc::new(LazyMemoryExec::try_new( - schema, + self.schema(), vec![Arc::new(RwLock::new(state))], )?)) } diff --git a/datafusion/functions-window-common/src/expr.rs b/datafusion/functions-window-common/src/expr.rs index 76e27b045b0a3..1d99fe7acf152 100644 --- a/datafusion/functions-window-common/src/expr.rs +++ b/datafusion/functions-window-common/src/expr.rs @@ -36,9 +36,9 @@ impl<'a> ExpressionArgs<'a> { /// # Arguments /// /// * `input_exprs` - The expressions passed as arguments - /// to the user-defined window function. + /// to the user-defined window function. /// * `input_types` - The data types corresponding to the - /// arguments to the user-defined window function. + /// arguments to the user-defined window function. /// pub fn new( input_exprs: &'a [Arc], diff --git a/datafusion/functions-window-common/src/field.rs b/datafusion/functions-window-common/src/field.rs index 03f88b0b95cc8..8011b7b0f05f0 100644 --- a/datafusion/functions-window-common/src/field.rs +++ b/datafusion/functions-window-common/src/field.rs @@ -33,9 +33,9 @@ impl<'a> WindowUDFFieldArgs<'a> { /// # Arguments /// /// * `input_types` - The data types corresponding to the - /// arguments to the user-defined window function. + /// arguments to the user-defined window function. /// * `function_name` - The qualified schema name of the - /// user-defined window function expression. + /// user-defined window function expression. /// pub fn new(input_types: &'a [DataType], display_name: &'a str) -> Self { WindowUDFFieldArgs { diff --git a/datafusion/functions-window-common/src/partition.rs b/datafusion/functions-window-common/src/partition.rs index e853aa8fb05d5..64786d2fe7c70 100644 --- a/datafusion/functions-window-common/src/partition.rs +++ b/datafusion/functions-window-common/src/partition.rs @@ -41,13 +41,13 @@ impl<'a> PartitionEvaluatorArgs<'a> { /// # Arguments /// /// * `input_exprs` - The expressions passed as arguments - /// to the user-defined window function. + /// to the user-defined window function. /// * `input_types` - The data types corresponding to the - /// arguments to the user-defined window function. + /// arguments to the user-defined window function. /// * `is_reversed` - Set to `true` if and only if the user-defined - /// window function is reversible and is reversed. + /// window function is reversible and is reversed. /// * `ignore_nulls` - Set to `true` when `IGNORE NULLS` is - /// specified. + /// specified. /// pub fn new( input_exprs: &'a [Arc], diff --git a/datafusion/functions-window/src/cume_dist.rs b/datafusion/functions-window/src/cume_dist.rs index d156416a82a4b..d777f7932b0e6 100644 --- a/datafusion/functions-window/src/cume_dist.rs +++ b/datafusion/functions-window/src/cume_dist.rs @@ -43,23 +43,8 @@ define_udwf_and_expr!( /// CumeDist calculates the cume_dist in the window function with order by #[user_doc( doc_section(label = "Ranking Functions"), - description = "Relative rank of the current row: (number of rows preceding or peer with the current row) / (total rows).", - syntax_example = "cume_dist()", - sql_example = r#"```sql - --Example usage of the cume_dist window function: - SELECT salary, - cume_dist() OVER (ORDER BY salary) AS cume_dist - FROM employees; -``` -```sql -+--------+-----------+ -| salary | cume_dist | -+--------+-----------+ -| 30000 | 0.33 | -| 50000 | 0.67 | -| 70000 | 1.00 | -+--------+-----------+ -```"# + description = "Relative rank of the current row: (number of rows preceding or peer with current row) / (total rows).", + syntax_example = "cume_dist()" )] #[derive(Debug)] pub struct CumeDist { @@ -128,7 +113,7 @@ impl PartitionEvaluator for CumeDistEvaluator { let len = range.end - range.start; *acc += len as u64; let value: f64 = (*acc as f64) / scalar; - let result = iter::repeat_n(value, len); + let result = iter::repeat(value).take(len); Some(result) }) .flatten(), diff --git a/datafusion/functions-window/src/macros.rs b/datafusion/functions-window/src/macros.rs index 2ef1eacba953d..0a86ba6255330 100644 --- a/datafusion/functions-window/src/macros.rs +++ b/datafusion/functions-window/src/macros.rs @@ -29,12 +29,12 @@ /// # Parameters /// /// * `$UDWF`: The struct which defines the [`Signature`](datafusion_expr::Signature) -/// of the user-defined window function. +/// of the user-defined window function. /// * `$OUT_FN_NAME`: The basename to generate a unique function name like -/// `$OUT_FN_NAME_udwf`. +/// `$OUT_FN_NAME_udwf`. /// * `$DOC`: Doc comments for UDWF. /// * (optional) `$CTOR`: Pass a custom constructor. When omitted it -/// automatically resolves to `$UDWF::default()`. +/// automatically resolves to `$UDWF::default()`. /// /// # Example /// @@ -122,13 +122,13 @@ macro_rules! get_or_init_udwf { /// # Parameters /// /// * `$UDWF`: The struct which defines the [`Signature`] of the -/// user-defined window function. +/// user-defined window function. /// * `$OUT_FN_NAME`: The basename to generate a unique function name like -/// `$OUT_FN_NAME_udwf`. +/// `$OUT_FN_NAME_udwf`. /// * `$DOC`: Doc comments for UDWF. /// * (optional) `[$($PARAM:ident),+]`: An array of 1 or more parameters -/// for the generated function. The type of parameters is [`Expr`]. -/// When omitted this creates a function with zero parameters. +/// for the generated function. The type of parameters is [`Expr`]. +/// When omitted this creates a function with zero parameters. /// /// [`Signature`]: datafusion_expr::Signature /// [`Expr`]: datafusion_expr::Expr @@ -332,15 +332,15 @@ macro_rules! create_udwf_expr { /// # Arguments /// /// * `$UDWF`: The struct which defines the [`Signature`] of the -/// user-defined window function. +/// user-defined window function. /// * `$OUT_FN_NAME`: The basename to generate a unique function name like -/// `$OUT_FN_NAME_udwf`. +/// `$OUT_FN_NAME_udwf`. /// * (optional) `[$($PARAM:ident),+]`: An array of 1 or more parameters -/// for the generated function. The type of parameters is [`Expr`]. -/// When omitted this creates a function with zero parameters. +/// for the generated function. The type of parameters is [`Expr`]. +/// When omitted this creates a function with zero parameters. /// * `$DOC`: Doc comments for UDWF. /// * (optional) `$CTOR`: Pass a custom constructor. When omitted it -/// automatically resolves to `$UDWF::default()`. +/// automatically resolves to `$UDWF::default()`. /// /// [`Signature`]: datafusion_expr::Signature /// [`Expr`]: datafusion_expr::Expr diff --git a/datafusion/functions-window/src/nth_value.rs b/datafusion/functions-window/src/nth_value.rs index 36e6b83d61ce4..1c781bd8e5f3f 100644 --- a/datafusion/functions-window/src/nth_value.rs +++ b/datafusion/functions-window/src/nth_value.rs @@ -160,49 +160,16 @@ fn get_last_value_doc() -> &'static Documentation { static NTH_VALUE_DOCUMENTATION: LazyLock = LazyLock::new(|| { Documentation::builder( DOC_SECTION_ANALYTICAL, - "Returns the value evaluated at the nth row of the window frame \ - (counting from 1). Returns NULL if no such row exists.", + "Returns value evaluated at the row that is the nth row of the window \ + frame (counting from 1); null if no such row.", "nth_value(expression, n)", ) .with_argument( "expression", - "The column from which to retrieve the nth value.", - ) - .with_argument( - "n", - "Integer. Specifies the row number (starting from 1) in the window frame.", - ) - .with_sql_example( - r#"```sql --- Sample employees table: -CREATE TABLE employees (id INT, salary INT); -INSERT INTO employees (id, salary) VALUES -(1, 30000), -(2, 40000), -(3, 50000), -(4, 60000), -(5, 70000); - --- Example usage of nth_value: -SELECT nth_value(salary, 2) OVER ( - ORDER BY salary - ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -) AS nth_value -FROM employees; -``` - -```text -+-----------+ -| nth_value | -+-----------+ -| 40000 | -| 40000 | -| 40000 | -| 40000 | -| 40000 | -+-----------+ -```"#, + "The name the column of which nth \ + value to retrieve", ) + .with_argument("n", "Integer. Specifies the n in nth") .build() }); diff --git a/datafusion/functions-window/src/rank.rs b/datafusion/functions-window/src/rank.rs index 2ff2c31d8c2aa..bd2edc5722eb6 100644 --- a/datafusion/functions-window/src/rank.rs +++ b/datafusion/functions-window/src/rank.rs @@ -261,7 +261,7 @@ impl PartitionEvaluator for RankEvaluator { .iter() .scan(1_u64, |acc, range| { let len = range.end - range.start; - let result = iter::repeat_n(*acc, len); + let result = iter::repeat(*acc).take(len); *acc += len as u64; Some(result) }) @@ -274,7 +274,7 @@ impl PartitionEvaluator for RankEvaluator { .zip(1u64..) .flat_map(|(range, rank)| { let len = range.end - range.start; - iter::repeat_n(rank, len) + iter::repeat(rank).take(len) }), )), @@ -287,7 +287,7 @@ impl PartitionEvaluator for RankEvaluator { .scan(0_u64, |acc, range| { let len = range.end - range.start; let value = (*acc as f64) / (denominator - 1.0).max(1.0); - let result = iter::repeat_n(value, len); + let result = iter::repeat(value).take(len); *acc += len as u64; Some(result) }) diff --git a/datafusion/functions/Cargo.toml b/datafusion/functions/Cargo.toml index 729770b8a65c6..31ff55121b771 100644 --- a/datafusion/functions/Cargo.toml +++ b/datafusion/functions/Cargo.toml @@ -66,7 +66,7 @@ arrow = { workspace = true } arrow-buffer = { workspace = true } base64 = { version = "0.22", optional = true } blake2 = { version = "^0.10.2", optional = true } -blake3 = { version = "1.8", optional = true } +blake3 = { version = "1.7", optional = true } chrono = { workspace = true } datafusion-common = { workspace = true } datafusion-doc = { workspace = true } diff --git a/datafusion/functions/benches/chr.rs b/datafusion/functions/benches/chr.rs index 8575809c21c8b..4750fb4666532 100644 --- a/datafusion/functions/benches/chr.rs +++ b/datafusion/functions/benches/chr.rs @@ -17,21 +17,15 @@ extern crate criterion; -use arrow::{array::PrimitiveArray, datatypes::Int64Type}; +use arrow::{array::PrimitiveArray, datatypes::Int64Type, util::test_util::seedable_rng}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; use datafusion_expr::{ColumnarValue, ScalarFunctionArgs}; use datafusion_functions::string::chr; -use rand::{Rng, SeedableRng}; +use rand::Rng; use arrow::datatypes::DataType; -use rand::rngs::StdRng; use std::sync::Arc; -/// Returns fixed seedable RNG -pub fn seedable_rng() -> StdRng { - StdRng::seed_from_u64(42) -} - fn criterion_benchmark(c: &mut Criterion) { let cot_fn = chr(); let size = 1024; diff --git a/datafusion/functions/benches/regx.rs b/datafusion/functions/benches/regx.rs index 3a1a6a71173e8..1f99cc3a5f0bc 100644 --- a/datafusion/functions/benches/regx.rs +++ b/datafusion/functions/benches/regx.rs @@ -197,7 +197,7 @@ fn criterion_benchmark(c: &mut Criterion) { let regex = Arc::new(regex(&mut rng)) as ArrayRef; let flags = Arc::new(flags(&mut rng)) as ArrayRef; let replacement = - Arc::new(StringArray::from_iter_values(iter::repeat_n("XX", 1000))) + Arc::new(StringArray::from_iter_values(iter::repeat("XX").take(1000))) as ArrayRef; b.iter(|| { @@ -219,9 +219,9 @@ fn criterion_benchmark(c: &mut Criterion) { let regex = cast(®ex(&mut rng), &DataType::Utf8View).unwrap(); // flags are not allowed to be utf8view according to the function let flags = Arc::new(flags(&mut rng)) as ArrayRef; - let replacement = Arc::new(StringViewArray::from_iter_values(iter::repeat_n( - "XX", 1000, - ))); + let replacement = Arc::new(StringViewArray::from_iter_values( + iter::repeat("XX").take(1000), + )); b.iter(|| { black_box( diff --git a/datafusion/functions/src/datetime/to_timestamp.rs b/datafusion/functions/src/datetime/to_timestamp.rs index 52c86733f3327..f1c61fe2b964d 100644 --- a/datafusion/functions/src/datetime/to_timestamp.rs +++ b/datafusion/functions/src/datetime/to_timestamp.rs @@ -18,14 +18,15 @@ use std::any::Any; use std::sync::Arc; -use crate::datetime::common::*; use arrow::datatypes::DataType::*; use arrow::datatypes::TimeUnit::{Microsecond, Millisecond, Nanosecond, Second}; use arrow::datatypes::{ ArrowTimestampType, DataType, TimeUnit, TimestampMicrosecondType, TimestampMillisecondType, TimestampNanosecondType, TimestampSecondType, }; -use datafusion_common::{exec_err, Result, ScalarType, ScalarValue}; + +use crate::datetime::common::*; +use datafusion_common::{exec_err, Result, ScalarType}; use datafusion_expr::{ ColumnarValue, Documentation, ScalarUDFImpl, Signature, Volatility, }; @@ -328,30 +329,6 @@ impl ScalarUDFImpl for ToTimestampFunc { Utf8View | LargeUtf8 | Utf8 => { to_timestamp_impl::(&args, "to_timestamp") } - Decimal128(_, _) => { - match &args[0] { - ColumnarValue::Scalar(ScalarValue::Decimal128( - Some(value), - _, - scale, - )) => { - // Convert decimal to seconds and nanoseconds - let scale_factor = 10_i128.pow(*scale as u32); - let seconds = value / scale_factor; - let fraction = value % scale_factor; - - let nanos = (fraction * 1_000_000_000) / scale_factor; - - let timestamp_nanos = seconds * 1_000_000_000 + nanos; - - Ok(ColumnarValue::Scalar(ScalarValue::TimestampNanosecond( - Some(timestamp_nanos as i64), - None, - ))) - } - _ => exec_err!("Invalid decimal value"), - } - } other => { exec_err!( "Unsupported data type {:?} for function to_timestamp", @@ -400,7 +377,7 @@ impl ScalarUDFImpl for ToTimestampSecondsFunc { } match args[0].data_type() { - Null | Int32 | Int64 | Timestamp(_, None) | Decimal128(_, _) => { + Null | Int32 | Int64 | Timestamp(_, None) => { args[0].cast_to(&Timestamp(Second, None), None) } Timestamp(_, Some(tz)) => args[0].cast_to(&Timestamp(Second, Some(tz)), None), diff --git a/datafusion/optimizer/Cargo.toml b/datafusion/optimizer/Cargo.toml index 60358d20e2a1a..3413b365f67de 100644 --- a/datafusion/optimizer/Cargo.toml +++ b/datafusion/optimizer/Cargo.toml @@ -55,15 +55,9 @@ regex-syntax = "0.8.0" [dev-dependencies] async-trait = { workspace = true } -criterion = { workspace = true } ctor = { workspace = true } datafusion-functions-aggregate = { workspace = true } datafusion-functions-window = { workspace = true } datafusion-functions-window-common = { workspace = true } datafusion-sql = { workspace = true } env_logger = { workspace = true } -insta = { workspace = true } - -[[bench]] -name = "projection_unnecessary" -harness = false diff --git a/datafusion/optimizer/benches/projection_unnecessary.rs b/datafusion/optimizer/benches/projection_unnecessary.rs deleted file mode 100644 index 100ee97542ebb..0000000000000 --- a/datafusion/optimizer/benches/projection_unnecessary.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use arrow::datatypes::{DataType, Field, Schema}; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use datafusion_common::ToDFSchema; -use datafusion_common::{Column, TableReference}; -use datafusion_expr::{logical_plan::LogicalPlan, projection_schema, Expr}; -use datafusion_optimizer::optimize_projections::is_projection_unnecessary; -use std::sync::Arc; - -fn is_projection_unnecessary_old( - input: &LogicalPlan, - proj_exprs: &[Expr], -) -> datafusion_common::Result { - // First check if all expressions are trivial (cheaper operation than `projection_schema`) - if !proj_exprs - .iter() - .all(|expr| matches!(expr, Expr::Column(_) | Expr::Literal(_))) - { - return Ok(false); - } - let proj_schema = projection_schema(input, proj_exprs)?; - Ok(&proj_schema == input.schema()) -} - -fn create_plan_with_many_exprs(num_exprs: usize) -> (LogicalPlan, Vec) { - // Create schema with many fields - let fields = (0..num_exprs) - .map(|i| Field::new(format!("col{}", i), DataType::Int32, false)) - .collect::>(); - let schema = Schema::new(fields); - - // Create table scan - let table_scan = LogicalPlan::EmptyRelation(datafusion_expr::EmptyRelation { - produce_one_row: true, - schema: Arc::new(schema.clone().to_dfschema().unwrap()), - }); - - // Create projection expressions (just column references) - let exprs = (0..num_exprs) - .map(|i| Expr::Column(Column::new(None::, format!("col{}", i)))) - .collect(); - - (table_scan, exprs) -} - -fn benchmark_is_projection_unnecessary(c: &mut Criterion) { - let (plan, exprs) = create_plan_with_many_exprs(1000); - - let mut group = c.benchmark_group("projection_unnecessary_comparison"); - - group.bench_function("is_projection_unnecessary_new", |b| { - b.iter(|| black_box(is_projection_unnecessary(&plan, &exprs).unwrap())) - }); - - group.bench_function("is_projection_unnecessary_old", |b| { - b.iter(|| black_box(is_projection_unnecessary_old(&plan, &exprs).unwrap())) - }); - - group.finish(); -} - -criterion_group!(benches, benchmark_is_projection_unnecessary); -criterion_main!(benches); diff --git a/datafusion/optimizer/src/decorrelate.rs b/datafusion/optimizer/src/decorrelate.rs index 418619c8399e3..71ff863b51a18 100644 --- a/datafusion/optimizer/src/decorrelate.rs +++ b/datafusion/optimizer/src/decorrelate.rs @@ -501,7 +501,10 @@ fn agg_exprs_evaluation_result_on_empty_batch( let info = SimplifyContext::new(&props).with_schema(Arc::clone(schema)); let simplifier = ExprSimplifier::new(info); let result_expr = simplifier.simplify(result_expr)?; - expr_result_map_for_count_bug.insert(e.schema_name().to_string(), result_expr); + if matches!(result_expr, Expr::Literal(ScalarValue::Int64(_))) { + expr_result_map_for_count_bug + .insert(e.schema_name().to_string(), result_expr); + } } Ok(()) } diff --git a/datafusion/optimizer/src/optimize_projections/mod.rs b/datafusion/optimizer/src/optimize_projections/mod.rs index 4452b2d4ce034..b3a09e2dcbcc7 100644 --- a/datafusion/optimizer/src/optimize_projections/mod.rs +++ b/datafusion/optimizer/src/optimize_projections/mod.rs @@ -31,7 +31,8 @@ use datafusion_common::{ use datafusion_expr::expr::Alias; use datafusion_expr::Unnest; use datafusion_expr::{ - logical_plan::LogicalPlan, Aggregate, Distinct, Expr, Projection, TableScan, Window, + logical_plan::LogicalPlan, projection_schema, Aggregate, Distinct, Expr, Projection, + TableScan, Window, }; use crate::optimize_projections::required_indices::RequiredIndices; @@ -454,17 +455,6 @@ fn merge_consecutive_projections(proj: Projection) -> Result::new(); expr.iter() @@ -784,24 +774,9 @@ fn rewrite_projection_given_requirements( /// Projection is unnecessary, when /// - input schema of the projection, output schema of the projection are same, and /// - all projection expressions are either Column or Literal -pub fn is_projection_unnecessary( - input: &LogicalPlan, - proj_exprs: &[Expr], -) -> Result { - // First check if the number of expressions is equal to the number of fields in the input schema. - if proj_exprs.len() != input.schema().fields().len() { - return Ok(false); - } - Ok(input.schema().iter().zip(proj_exprs.iter()).all( - |((field_relation, field_name), expr)| { - // Check if the expression is a column and if it matches the field name - if let Expr::Column(col) = expr { - col.relation.as_ref() == field_relation && col.name.eq(field_name.name()) - } else { - false - } - }, - )) +fn is_projection_unnecessary(input: &LogicalPlan, proj_exprs: &[Expr]) -> Result { + let proj_schema = projection_schema(input, proj_exprs)?; + Ok(&proj_schema == input.schema() && proj_exprs.iter().all(is_expr_trivial)) } #[cfg(test)] diff --git a/datafusion/optimizer/src/optimizer.rs b/datafusion/optimizer/src/optimizer.rs index b40121dbfeb7e..ffbb95cb7f74e 100644 --- a/datafusion/optimizer/src/optimizer.rs +++ b/datafusion/optimizer/src/optimizer.rs @@ -506,11 +506,8 @@ mod tests { }); let err = opt.optimize(plan, &config, &observe).unwrap_err(); - // Simplify assert to check the error message contains the expected message - assert_contains!( - err.strip_backtrace(), - "Failed due to a difference in schemas: original schema: DFSchema" - ); + // Simplify assert to check the error message contains the expected message, which is only the schema length mismatch + assert_contains!(err.strip_backtrace(), "Schema mismatch: the schema length are not same Expected schema length: 3, got: 0"); } #[test] diff --git a/datafusion/optimizer/src/scalar_subquery_to_join.rs b/datafusion/optimizer/src/scalar_subquery_to_join.rs index 5c89bc29a596a..499447861a58b 100644 --- a/datafusion/optimizer/src/scalar_subquery_to_join.rs +++ b/datafusion/optimizer/src/scalar_subquery_to_join.rs @@ -22,10 +22,9 @@ use std::sync::Arc; use crate::decorrelate::{PullUpCorrelatedExpr, UN_MATCHED_ROW_INDICATOR}; use crate::optimizer::ApplyOrder; -use crate::utils::{evaluates_to_null, replace_qualified_name}; +use crate::utils::replace_qualified_name; use crate::{OptimizerConfig, OptimizerRule}; -use crate::analyzer::type_coercion::TypeCoercionRewriter; use datafusion_common::alias::AliasGenerator; use datafusion_common::tree_node::{ Transformed, TransformedResult, TreeNode, TreeNodeRecursion, TreeNodeRewriter, @@ -349,10 +348,6 @@ fn build_join( let mut computation_project_expr = HashMap::new(); if let Some(expr_map) = collected_count_expr_map { for (name, result) in expr_map { - if evaluates_to_null(result.clone(), result.column_refs())? { - // If expr always returns null when column is null, skip processing - continue; - } let computer_expr = if let Some(filter) = &pull_up.pull_up_having_expr { Expr::Case(expr::Case { expr: None, @@ -386,11 +381,7 @@ fn build_join( )))), }) }; - let mut expr_rewrite = TypeCoercionRewriter { - schema: new_plan.schema(), - }; - computation_project_expr - .insert(name, computer_expr.rewrite(&mut expr_rewrite).data()?); + computation_project_expr.insert(name, computer_expr); } } @@ -434,18 +425,18 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: Int32(1) < __scalar_sq_1.max(orders.o_custkey) AND Int32(1) < __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: __scalar_sq_2.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: Int32(1) < __scalar_sq_1.max(orders.o_custkey) AND Int32(1) < __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: __scalar_sq_2.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], plan, @@ -489,19 +480,19 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_acctbal < __scalar_sq_1.sum(orders.o_totalprice) [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [sum(orders.o_totalprice):Float64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: sum(orders.o_totalprice), orders.o_custkey, __always_true [sum(orders.o_totalprice):Float64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[sum(orders.o_totalprice)]] [o_custkey:Int64, __always_true:Boolean, sum(orders.o_totalprice):Float64;N]\ - \n Filter: orders.o_totalprice < __scalar_sq_2.sum(lineitem.l_extendedprice) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: __scalar_sq_2.l_orderkey = orders.o_orderkey [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n SubqueryAlias: __scalar_sq_2 [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64, __always_true:Boolean]\ - \n Projection: sum(lineitem.l_extendedprice), lineitem.l_orderkey, __always_true [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[lineitem.l_orderkey, Boolean(true) AS __always_true]], aggr=[[sum(lineitem.l_extendedprice)]] [l_orderkey:Int64, __always_true:Boolean, sum(lineitem.l_extendedprice):Float64;N]\ - \n TableScan: lineitem [l_orderkey:Int64, l_partkey:Int64, l_suppkey:Int64, l_linenumber:Int32, l_quantity:Float64, l_extendedprice:Float64]"; + \n Filter: customer.c_acctbal < __scalar_sq_1.sum(orders.o_totalprice) [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: __scalar_sq_1.o_custkey = customer.c_custkey [c_custkey:Int64, c_name:Utf8, sum(orders.o_totalprice):Float64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [sum(orders.o_totalprice):Float64;N, o_custkey:Int64]\ + \n Projection: sum(orders.o_totalprice), orders.o_custkey [sum(orders.o_totalprice):Float64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[sum(orders.o_totalprice)]] [o_custkey:Int64, sum(orders.o_totalprice):Float64;N]\ + \n Filter: orders.o_totalprice < __scalar_sq_2.sum(lineitem.l_extendedprice) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N]\ + \n Left Join: Filter: __scalar_sq_2.l_orderkey = orders.o_orderkey [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N, sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n SubqueryAlias: __scalar_sq_2 [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64]\ + \n Projection: sum(lineitem.l_extendedprice), lineitem.l_orderkey [sum(lineitem.l_extendedprice):Float64;N, l_orderkey:Int64]\ + \n Aggregate: groupBy=[[lineitem.l_orderkey]], aggr=[[sum(lineitem.l_extendedprice)]] [l_orderkey:Int64, sum(lineitem.l_extendedprice):Float64;N]\ + \n TableScan: lineitem [l_orderkey:Int64, l_partkey:Int64, l_suppkey:Int64, l_linenumber:Int32, l_quantity:Float64, l_extendedprice:Float64]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], plan, @@ -531,14 +522,14 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n Filter: orders.o_orderkey = Int32(1) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n Filter: orders.o_orderkey = Int32(1) [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -769,56 +760,13 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) + Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey) + Int32(1), orders.o_custkey, __always_true [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; - - assert_multi_rules_optimized_plan_eq_display_indent( - vec![Arc::new(ScalarSubqueryToJoin::new())], - plan, - expected, - ); - Ok(()) - } - - /// Test for correlated scalar subquery with non-strong project - #[test] - fn scalar_subquery_with_non_strong_project() -> Result<()> { - let case = Expr::Case(expr::Case { - expr: None, - when_then_expr: vec![( - Box::new(col("max(orders.o_totalprice)")), - Box::new(lit("a")), - )], - else_expr: Some(Box::new(lit("b"))), - }); - - let sq = Arc::new( - LogicalPlanBuilder::from(scan_tpch_table("orders")) - .filter( - out_ref_col(DataType::Int64, "customer.c_custkey") - .eq(col("orders.o_custkey")), - )? - .aggregate(Vec::::new(), vec![max(col("orders.o_totalprice"))])? - .project(vec![case])? - .build()?, - ); - - let plan = LogicalPlanBuilder::from(scan_tpch_table("customer")) - .project(vec![col("customer.c_custkey"), scalar_subquery(sq)])? - .build()?; - - let expected = "Projection: customer.c_custkey, CASE WHEN __scalar_sq_1.__always_true IS NULL THEN CASE WHEN CAST(NULL AS Boolean) THEN Utf8(\"a\") ELSE Utf8(\"b\") END ELSE __scalar_sq_1.CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END END AS CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END [c_custkey:Int64, CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END, orders.o_custkey, __always_true [CASE WHEN max(orders.o_totalprice) THEN Utf8(\"a\") ELSE Utf8(\"b\") END:Utf8, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_totalprice)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_totalprice):Float64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) + Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey) + Int32(1), orders.o_custkey [max(orders.o_custkey) + Int32(1):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -876,13 +824,13 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey >= __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey >= __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -915,13 +863,13 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) AND customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -955,13 +903,13 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) OR customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey = __scalar_sq_1.max(orders.o_custkey) OR customer.c_custkey = Int32(1) [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -988,13 +936,13 @@ mod tests { .build()?; let expected = "Projection: test.c [c:UInt32]\ - \n Filter: test.c < __scalar_sq_1.min(sq.c) [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N, __always_true:Boolean;N]\ - \n Left Join: Filter: test.a = __scalar_sq_1.a [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N, __always_true:Boolean;N]\ - \n TableScan: test [a:UInt32, b:UInt32, c:UInt32]\ - \n SubqueryAlias: __scalar_sq_1 [min(sq.c):UInt32;N, a:UInt32, __always_true:Boolean]\ - \n Projection: min(sq.c), sq.a, __always_true [min(sq.c):UInt32;N, a:UInt32, __always_true:Boolean]\ - \n Aggregate: groupBy=[[sq.a, Boolean(true) AS __always_true]], aggr=[[min(sq.c)]] [a:UInt32, __always_true:Boolean, min(sq.c):UInt32;N]\ - \n TableScan: sq [a:UInt32, b:UInt32, c:UInt32]"; + \n Filter: test.c < __scalar_sq_1.min(sq.c) [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N]\ + \n Left Join: Filter: test.a = __scalar_sq_1.a [a:UInt32, b:UInt32, c:UInt32, min(sq.c):UInt32;N, a:UInt32;N]\ + \n TableScan: test [a:UInt32, b:UInt32, c:UInt32]\ + \n SubqueryAlias: __scalar_sq_1 [min(sq.c):UInt32;N, a:UInt32]\ + \n Projection: min(sq.c), sq.a [min(sq.c):UInt32;N, a:UInt32]\ + \n Aggregate: groupBy=[[sq.a]], aggr=[[min(sq.c)]] [a:UInt32, min(sq.c):UInt32;N]\ + \n TableScan: sq [a:UInt32, b:UInt32, c:UInt32]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], @@ -1103,18 +1051,18 @@ mod tests { .build()?; let expected = "Projection: customer.c_custkey [c_custkey:Int64]\ - \n Filter: customer.c_custkey BETWEEN __scalar_sq_1.min(orders.o_custkey) AND __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_2.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, __always_true:Boolean;N]\ - \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ - \n SubqueryAlias: __scalar_sq_1 [min(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: min(orders.o_custkey), orders.o_custkey, __always_true [min(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[min(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, min(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ - \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Projection: max(orders.o_custkey), orders.o_custkey, __always_true [max(orders.o_custkey):Int64;N, o_custkey:Int64, __always_true:Boolean]\ - \n Aggregate: groupBy=[[orders.o_custkey, Boolean(true) AS __always_true]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, __always_true:Boolean, max(orders.o_custkey):Int64;N]\ - \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; + \n Filter: customer.c_custkey BETWEEN __scalar_sq_1.min(orders.o_custkey) AND __scalar_sq_2.max(orders.o_custkey) [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_2.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N, max(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n Left Join: Filter: customer.c_custkey = __scalar_sq_1.o_custkey [c_custkey:Int64, c_name:Utf8, min(orders.o_custkey):Int64;N, o_custkey:Int64;N]\ + \n TableScan: customer [c_custkey:Int64, c_name:Utf8]\ + \n SubqueryAlias: __scalar_sq_1 [min(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: min(orders.o_custkey), orders.o_custkey [min(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[min(orders.o_custkey)]] [o_custkey:Int64, min(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]\ + \n SubqueryAlias: __scalar_sq_2 [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Projection: max(orders.o_custkey), orders.o_custkey [max(orders.o_custkey):Int64;N, o_custkey:Int64]\ + \n Aggregate: groupBy=[[orders.o_custkey]], aggr=[[max(orders.o_custkey)]] [o_custkey:Int64, max(orders.o_custkey):Int64;N]\ + \n TableScan: orders [o_orderkey:Int64, o_custkey:Int64, o_orderstatus:Utf8, o_totalprice:Float64;N]"; assert_multi_rules_optimized_plan_eq_display_indent( vec![Arc::new(ScalarSubqueryToJoin::new())], diff --git a/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs b/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs index b1c03dcd00aaa..9003467703df2 100644 --- a/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs +++ b/datafusion/optimizer/src/simplify_expressions/expr_simplifier.rs @@ -33,8 +33,8 @@ use datafusion_common::{ }; use datafusion_common::{internal_err, DFSchema, DataFusionError, Result, ScalarValue}; use datafusion_expr::{ - and, binary::BinaryTypeCoercer, lit, or, BinaryExpr, Case, ColumnarValue, Expr, Like, - Operator, Volatility, WindowFunctionDefinition, + and, lit, or, BinaryExpr, Case, ColumnarValue, Expr, Like, Operator, Volatility, + WindowFunctionDefinition, }; use datafusion_expr::{expr::ScalarFunction, interval_arithmetic::NullableInterval}; use datafusion_expr::{ @@ -188,7 +188,7 @@ impl ExprSimplifier { /// assert_eq!(expr, b_lt_2); /// ``` pub fn simplify(&self, expr: Expr) -> Result { - Ok(self.simplify_with_cycle_count_transformed(expr)?.0.data) + Ok(self.simplify_with_cycle_count(expr)?.0) } /// Like [Self::simplify], simplifies this [`Expr`] as much as possible, evaluating @@ -198,34 +198,7 @@ impl ExprSimplifier { /// /// See [Self::simplify] for details and usage examples. /// - #[deprecated( - since = "48.0.0", - note = "Use `simplify_with_cycle_count_transformed` instead" - )] - #[allow(unused_mut)] pub fn simplify_with_cycle_count(&self, mut expr: Expr) -> Result<(Expr, u32)> { - let (transformed, cycle_count) = - self.simplify_with_cycle_count_transformed(expr)?; - Ok((transformed.data, cycle_count)) - } - - /// Like [Self::simplify], simplifies this [`Expr`] as much as possible, evaluating - /// constants and applying algebraic simplifications. Additionally returns a `u32` - /// representing the number of simplification cycles performed, which can be useful for testing - /// optimizations. - /// - /// # Returns - /// - /// A tuple containing: - /// - The simplified expression wrapped in a `Transformed` indicating if changes were made - /// - The number of simplification cycles that were performed - /// - /// See [Self::simplify] for details and usage examples. - /// - pub fn simplify_with_cycle_count_transformed( - &self, - mut expr: Expr, - ) -> Result<(Transformed, u32)> { let mut simplifier = Simplifier::new(&self.info); let mut const_evaluator = ConstEvaluator::try_new(self.info.execution_props())?; let mut shorten_in_list_simplifier = ShortenInListSimplifier::new(); @@ -239,7 +212,6 @@ impl ExprSimplifier { // simplifications can enable new constant evaluation // see `Self::with_max_cycles` let mut num_cycles = 0; - let mut has_transformed = false; loop { let Transformed { data, transformed, .. @@ -249,18 +221,13 @@ impl ExprSimplifier { .transform_data(|expr| expr.rewrite(&mut guarantee_rewriter))?; expr = data; num_cycles += 1; - // Track if any transformation occurred - has_transformed = has_transformed || transformed; if !transformed || num_cycles >= self.max_simplifier_cycles { break; } } // shorten inlist should be started after other inlist rules are applied expr = expr.rewrite(&mut shorten_in_list_simplifier).data()?; - Ok(( - Transformed::new_transformed(expr, has_transformed), - num_cycles, - )) + Ok((expr, num_cycles)) } /// Apply type coercion to an [`Expr`] so that it can be @@ -425,15 +392,15 @@ impl ExprSimplifier { /// let expr = col("a").is_not_null(); /// /// // When using default maximum cycles, 2 cycles will be performed. - /// let (simplified_expr, count) = simplifier.simplify_with_cycle_count_transformed(expr.clone()).unwrap(); - /// assert_eq!(simplified_expr.data, lit(true)); + /// let (simplified_expr, count) = simplifier.simplify_with_cycle_count(expr.clone()).unwrap(); + /// assert_eq!(simplified_expr, lit(true)); /// // 2 cycles were executed, but only 1 was needed /// assert_eq!(count, 2); /// /// // Only 1 simplification pass is necessary here, so we can set the maximum cycles to 1. - /// let (simplified_expr, count) = simplifier.with_max_cycles(1).simplify_with_cycle_count_transformed(expr.clone()).unwrap(); + /// let (simplified_expr, count) = simplifier.with_max_cycles(1).simplify_with_cycle_count(expr.clone()).unwrap(); /// // Expression has been rewritten to: (c = a AND b = 1) - /// assert_eq!(simplified_expr.data, lit(true)); + /// assert_eq!(simplified_expr, lit(true)); /// // Only 1 cycle was executed /// assert_eq!(count, 1); /// @@ -793,25 +760,6 @@ impl TreeNodeRewriter for Simplifier<'_, S> { None => lit_bool_null(), }) } - // According to SQL's null semantics, NULL = NULL evaluates to NULL - // Both sides are the same expression (A = A) and A is non-volatile expression - // A = A --> A IS NOT NULL OR NULL - // A = A --> true (if A not nullable) - Expr::BinaryExpr(BinaryExpr { - left, - op: Eq, - right, - }) if (left == right) & !left.is_volatile() => { - Transformed::yes(match !info.nullable(&left)? { - true => lit(true), - false => Expr::BinaryExpr(BinaryExpr { - left: Box::new(Expr::IsNotNull(left)), - op: Or, - right: Box::new(lit_bool_null()), - }), - }) - } - // Rules for NotEq // @@ -1028,39 +976,30 @@ impl TreeNodeRewriter for Simplifier<'_, S> { // Rules for Multiply // - // A * 1 --> A (with type coercion if needed) + // A * 1 --> A Expr::BinaryExpr(BinaryExpr { left, op: Multiply, right, - }) if is_one(&right) => { - simplify_right_is_one_case(info, left, &Multiply, &right)? - } - // A * null --> null + }) if is_one(&right) => Transformed::yes(*left), + // 1 * A --> A Expr::BinaryExpr(BinaryExpr { left, op: Multiply, right, - }) if is_null(&right) => { - simplify_right_is_null_case(info, &left, &Multiply, right)? - } - // 1 * A --> A + }) if is_one(&left) => Transformed::yes(*right), + // A * null --> null Expr::BinaryExpr(BinaryExpr { - left, + left: _, op: Multiply, right, - }) if is_one(&left) => { - // 1 * A is equivalent to A * 1 - simplify_right_is_one_case(info, right, &Multiply, &left)? - } + }) if is_null(&right) => Transformed::yes(*right), // null * A --> null Expr::BinaryExpr(BinaryExpr { left, op: Multiply, - right, - }) if is_null(&left) => { - simplify_right_is_null_case(info, &right, &Multiply, left)? - } + right: _, + }) if is_null(&left) => Transformed::yes(*left), // A * 0 --> 0 (if A is not null and not floating, since NAN * 0 -> NAN) Expr::BinaryExpr(BinaryExpr { @@ -1094,23 +1033,19 @@ impl TreeNodeRewriter for Simplifier<'_, S> { left, op: Divide, right, - }) if is_one(&right) => { - simplify_right_is_one_case(info, left, &Divide, &right)? - } - // A / null --> null + }) if is_one(&right) => Transformed::yes(*left), + // null / A --> null Expr::BinaryExpr(BinaryExpr { left, op: Divide, - right, - }) if is_null(&right) => { - simplify_right_is_null_case(info, &left, &Divide, right)? - } - // null / A --> null + right: _, + }) if is_null(&left) => Transformed::yes(*left), + // A / null --> null Expr::BinaryExpr(BinaryExpr { - left, + left: _, op: Divide, right, - }) if is_null(&left) => simplify_null_div_other_case(info, left, &right)?, + }) if is_null(&right) => Transformed::yes(*right), // // Rules for Modulo @@ -2062,84 +1997,6 @@ fn is_exactly_true(expr: Expr, info: &impl SimplifyInfo) -> Result { } } -// A * 1 -> A -// A / 1 -> A -// -// Move this function body out of the large match branch avoid stack overflow -fn simplify_right_is_one_case( - info: &S, - left: Box, - op: &Operator, - right: &Expr, -) -> Result> { - // Check if resulting type would be different due to coercion - let left_type = info.get_data_type(&left)?; - let right_type = info.get_data_type(right)?; - match BinaryTypeCoercer::new(&left_type, op, &right_type).get_result_type() { - Ok(result_type) => { - // Only cast if the types differ - if left_type != result_type { - Ok(Transformed::yes(Expr::Cast(Cast::new(left, result_type)))) - } else { - Ok(Transformed::yes(*left)) - } - } - Err(_) => Ok(Transformed::yes(*left)), - } -} - -// A * null -> null -// A / null -> null -// -// Move this function body out of the large match branch avoid stack overflow -fn simplify_right_is_null_case( - info: &S, - left: &Expr, - op: &Operator, - right: Box, -) -> Result> { - // Check if resulting type would be different due to coercion - let left_type = info.get_data_type(left)?; - let right_type = info.get_data_type(&right)?; - match BinaryTypeCoercer::new(&left_type, op, &right_type).get_result_type() { - Ok(result_type) => { - // Only cast if the types differ - if right_type != result_type { - Ok(Transformed::yes(Expr::Cast(Cast::new(right, result_type)))) - } else { - Ok(Transformed::yes(*right)) - } - } - Err(_) => Ok(Transformed::yes(*right)), - } -} - -// null / A --> null -// -// Move this function body out of the large match branch avoid stack overflow -fn simplify_null_div_other_case( - info: &S, - left: Box, - right: &Expr, -) -> Result> { - // Check if resulting type would be different due to coercion - let left_type = info.get_data_type(&left)?; - let right_type = info.get_data_type(right)?; - match BinaryTypeCoercer::new(&left_type, &Operator::Divide, &right_type) - .get_result_type() - { - Ok(result_type) => { - // Only cast if the types differ - if left_type != result_type { - Ok(Transformed::yes(Expr::Cast(Cast::new(left, result_type)))) - } else { - Ok(Transformed::yes(*left)) - } - } - Err(_) => Ok(Transformed::yes(*left)), - } -} - #[cfg(test)] mod tests { use crate::simplify_expressions::SimplifyContext; @@ -2295,21 +2152,6 @@ mod tests { } } - #[test] - fn test_simplify_eq_not_self() { - // `expr_a`: column `c2` is nullable, so `c2 = c2` simplifies to `c2 IS NOT NULL OR NULL` - // This ensures the expression is only true when `c2` is not NULL, accounting for SQL's NULL semantics. - let expr_a = col("c2").eq(col("c2")); - let expected_a = col("c2").is_not_null().or(lit_bool_null()); - - // `expr_b`: column `c2_non_null` is explicitly non-nullable, so `c2_non_null = c2_non_null` is always true - let expr_b = col("c2_non_null").eq(col("c2_non_null")); - let expected_b = lit(true); - - assert_eq!(simplify(expr_a), expected_a); - assert_eq!(simplify(expr_b), expected_b); - } - #[test] fn test_simplify_or_true() { let expr_a = col("c2").or(lit(true)); @@ -2474,12 +2316,12 @@ mod tests { // A / null --> null let null = lit(ScalarValue::Null); { - let expr = col("c1") / null.clone(); + let expr = col("c") / null.clone(); assert_eq!(simplify(expr), null); } // null / A --> null { - let expr = null.clone() / col("c1"); + let expr = null.clone() / col("c"); assert_eq!(simplify(expr), null); } } @@ -3343,15 +3185,6 @@ mod tests { simplifier.simplify(expr) } - fn coerce(expr: Expr) -> Expr { - let schema = expr_test_schema(); - let execution_props = ExecutionProps::new(); - let simplifier = ExprSimplifier::new( - SimplifyContext::new(&execution_props).with_schema(Arc::clone(&schema)), - ); - simplifier.coerce(expr, schema.as_ref()).unwrap() - } - fn simplify(expr: Expr) -> Expr { try_simplify(expr).unwrap() } @@ -3362,8 +3195,7 @@ mod tests { let simplifier = ExprSimplifier::new( SimplifyContext::new(&execution_props).with_schema(schema), ); - let (expr, count) = simplifier.simplify_with_cycle_count_transformed(expr)?; - Ok((expr.data, count)) + simplifier.simplify_with_cycle_count(expr) } fn simplify_with_cycle_count(expr: Expr) -> (Expr, u32) { @@ -3395,7 +3227,6 @@ mod tests { Field::new("c2_non_null", DataType::Boolean, false), Field::new("c3_non_null", DataType::Int64, false), Field::new("c4_non_null", DataType::UInt32, false), - Field::new("c5", DataType::FixedSizeBinary(3), true), ] .into(), HashMap::new(), @@ -4519,34 +4350,6 @@ mod tests { } } - #[test] - fn simplify_fixed_size_binary_eq_lit() { - let bytes = [1u8, 2, 3].as_slice(); - - // The expression starts simple. - let expr = col("c5").eq(lit(bytes)); - - // The type coercer introduces a cast. - let coerced = coerce(expr.clone()); - let schema = expr_test_schema(); - assert_eq!( - coerced, - col("c5") - .cast_to(&DataType::Binary, schema.as_ref()) - .unwrap() - .eq(lit(bytes)) - ); - - // The simplifier removes the cast. - assert_eq!( - simplify(coerced), - col("c5").eq(Expr::Literal(ScalarValue::FixedSizeBinary( - 3, - Some(bytes.to_vec()), - ))) - ); - } - fn if_not_null(expr: Expr, then: bool) -> Expr { Expr::Case(Case { expr: Some(expr.is_not_null().into()), diff --git a/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs b/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs index 6314209dc7670..e33869ca2b636 100644 --- a/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs +++ b/datafusion/optimizer/src/simplify_expressions/simplify_exprs.rs @@ -123,11 +123,10 @@ impl SimplifyExpressions { let name_preserver = NamePreserver::new(&plan); let mut rewrite_expr = |expr: Expr| { let name = name_preserver.save(&expr); - let expr = simplifier.simplify_with_cycle_count_transformed(expr)?.0; - Ok(Transformed::new_transformed( - name.restore(expr.data), - expr.transformed, - )) + let expr = simplifier.simplify(expr)?; + // TODO it would be nice to have a way to know if the expression was simplified + // or not. For now conservatively return Transformed::yes + Ok(Transformed::yes(name.restore(expr))) }; plan.map_expressions(|expr| { diff --git a/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs b/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs index 37116018cdca5..be71a8cd19b00 100644 --- a/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs +++ b/datafusion/optimizer/src/simplify_expressions/unwrap_cast.rs @@ -197,7 +197,6 @@ fn is_supported_type(data_type: &DataType) -> bool { is_supported_numeric_type(data_type) || is_supported_string_type(data_type) || is_supported_dictionary_type(data_type) - || is_supported_binary_type(data_type) } /// Returns true if unwrap_cast_in_comparison support this numeric type @@ -231,10 +230,6 @@ fn is_supported_dictionary_type(data_type: &DataType) -> bool { DataType::Dictionary(_, inner) if is_supported_type(inner)) } -fn is_supported_binary_type(data_type: &DataType) -> bool { - matches!(data_type, DataType::Binary | DataType::FixedSizeBinary(_)) -} - ///// Tries to move a cast from an expression (such as column) to the literal other side of a comparison operator./ /// /// Specifically, rewrites @@ -297,7 +292,6 @@ pub(super) fn try_cast_literal_to_type( try_cast_numeric_literal(lit_value, target_type) .or_else(|| try_cast_string_literal(lit_value, target_type)) .or_else(|| try_cast_dictionary(lit_value, target_type)) - .or_else(|| try_cast_binary(lit_value, target_type)) } /// Convert a numeric value from one numeric data type to another @@ -507,20 +501,6 @@ fn cast_between_timestamp(from: &DataType, to: &DataType, value: i128) -> Option } } -fn try_cast_binary( - lit_value: &ScalarValue, - target_type: &DataType, -) -> Option { - match (lit_value, target_type) { - (ScalarValue::Binary(Some(v)), DataType::FixedSizeBinary(n)) - if v.len() == *n as usize => - { - Some(ScalarValue::FixedSizeBinary(*n, Some(v.clone()))) - } - _ => None, - } -} - #[cfg(test)] mod tests { use super::*; @@ -1470,13 +1450,4 @@ mod tests { ) } } - - #[test] - fn try_cast_to_fixed_size_binary() { - expect_cast( - ScalarValue::Binary(Some(vec![1, 2, 3])), - DataType::FixedSizeBinary(3), - ExpectedCast::Value(ScalarValue::FixedSizeBinary(3, Some(vec![1, 2, 3]))), - ) - } } diff --git a/datafusion/optimizer/src/utils.rs b/datafusion/optimizer/src/utils.rs index 41c40ec06d652..c734d908f6d6c 100644 --- a/datafusion/optimizer/src/utils.rs +++ b/datafusion/optimizer/src/utils.rs @@ -79,50 +79,6 @@ pub fn is_restrict_null_predicate<'a>( return Ok(true); } - // If result is single `true`, return false; - // If result is single `NULL` or `false`, return true; - Ok( - match evaluate_expr_with_null_column(predicate, join_cols_of_predicate)? { - ColumnarValue::Array(array) => { - if array.len() == 1 { - let boolean_array = as_boolean_array(&array)?; - boolean_array.is_null(0) || !boolean_array.value(0) - } else { - false - } - } - ColumnarValue::Scalar(scalar) => matches!( - scalar, - ScalarValue::Boolean(None) | ScalarValue::Boolean(Some(false)) - ), - }, - ) -} - -/// Determines if an expression will always evaluate to null. -/// `c0 + 8` return true -/// `c0 IS NULL` return false -/// `CASE WHEN c0 > 1 then 0 else 1` return false -pub fn evaluates_to_null<'a>( - predicate: Expr, - null_columns: impl IntoIterator, -) -> Result { - if matches!(predicate, Expr::Column(_)) { - return Ok(true); - } - - Ok( - match evaluate_expr_with_null_column(predicate, null_columns)? { - ColumnarValue::Array(_) => false, - ColumnarValue::Scalar(scalar) => scalar.is_null(), - }, - ) -} - -fn evaluate_expr_with_null_column<'a>( - predicate: Expr, - null_columns: impl IntoIterator, -) -> Result { static DUMMY_COL_NAME: &str = "?"; let schema = Schema::new(vec![Field::new(DUMMY_COL_NAME, DataType::Null, true)]); let input_schema = DFSchema::try_from(schema.clone())?; @@ -131,15 +87,37 @@ fn evaluate_expr_with_null_column<'a>( let execution_props = ExecutionProps::default(); let null_column = Column::from_name(DUMMY_COL_NAME); - let join_cols_to_replace = null_columns + let join_cols_to_replace = join_cols_of_predicate .into_iter() .map(|column| (column, &null_column)) .collect::>(); let replaced_predicate = replace_col(predicate, &join_cols_to_replace)?; let coerced_predicate = coerce(replaced_predicate, &input_schema)?; - create_physical_expr(&coerced_predicate, &input_schema, &execution_props)? - .evaluate(&input_batch) + let phys_expr = + create_physical_expr(&coerced_predicate, &input_schema, &execution_props)?; + + let result_type = phys_expr.data_type(&schema)?; + if !matches!(&result_type, DataType::Boolean) { + return Ok(false); + } + + // If result is single `true`, return false; + // If result is single `NULL` or `false`, return true; + Ok(match phys_expr.evaluate(&input_batch)? { + ColumnarValue::Array(array) => { + if array.len() == 1 { + let boolean_array = as_boolean_array(&array)?; + boolean_array.is_null(0) || !boolean_array.value(0) + } else { + false + } + } + ColumnarValue::Scalar(scalar) => matches!( + scalar, + ScalarValue::Boolean(None) | ScalarValue::Boolean(Some(false)) + ), + }) } fn coerce(expr: Expr, schema: &DFSchema) -> Result { diff --git a/datafusion/optimizer/tests/optimizer_integration.rs b/datafusion/optimizer/tests/optimizer_integration.rs index 941e5bd7b4d77..098027dd06420 100644 --- a/datafusion/optimizer/tests/optimizer_integration.rs +++ b/datafusion/optimizer/tests/optimizer_integration.rs @@ -37,7 +37,6 @@ use datafusion_sql::planner::{ContextProvider, SqlToRel}; use datafusion_sql::sqlparser::ast::Statement; use datafusion_sql::sqlparser::dialect::GenericDialect; use datafusion_sql::sqlparser::parser::Parser; -use insta::assert_snapshot; #[cfg(test)] #[ctor::ctor] @@ -50,25 +49,16 @@ fn init() { fn case_when() -> Result<()> { let sql = "SELECT CASE WHEN col_int32 > 0 THEN 1 ELSE 0 END FROM test"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" -Projection: CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END - TableScan: test projection=[col_int32] -"# - ); + let expected = + "Projection: CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END\ + \n TableScan: test projection=[col_int32]"; + assert_eq!(expected, format!("{plan}")); let sql = "SELECT CASE WHEN col_uint32 > 0 THEN 1 ELSE 0 END FROM test"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: CASE WHEN test.col_uint32 > UInt32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_uint32 > Int64(0) THEN Int64(1) ELSE Int64(0) END - TableScan: test projection=[col_uint32] - "# - ); + let expected = "Projection: CASE WHEN test.col_uint32 > UInt32(0) THEN Int64(1) ELSE Int64(0) END AS CASE WHEN test.col_uint32 > Int64(0) THEN Int64(1) ELSE Int64(0) END\ + \n TableScan: test projection=[col_uint32]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -82,21 +72,16 @@ fn subquery_filter_with_cast() -> Result<()> { AND (cast('2002-05-08' as date) + interval '5 days')\ )"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: test.col_int32 - Inner Join: Filter: CAST(test.col_int32 AS Float64) > __scalar_sq_1.avg(test.col_int32) - TableScan: test projection=[col_int32] - SubqueryAlias: __scalar_sq_1 - Aggregate: groupBy=[[]], aggr=[[avg(CAST(test.col_int32 AS Float64))]] - Projection: test.col_int32 - Filter: __common_expr_4 >= Date32("2002-05-08") AND __common_expr_4 <= Date32("2002-05-13") - Projection: CAST(test.col_utf8 AS Date32) AS __common_expr_4, test.col_int32 - TableScan: test projection=[col_int32, col_utf8] - "# - ); + let expected = "Projection: test.col_int32\ + \n Inner Join: Filter: CAST(test.col_int32 AS Float64) > __scalar_sq_1.avg(test.col_int32)\ + \n TableScan: test projection=[col_int32]\ + \n SubqueryAlias: __scalar_sq_1\ + \n Aggregate: groupBy=[[]], aggr=[[avg(CAST(test.col_int32 AS Float64))]]\ + \n Projection: test.col_int32\ + \n Filter: __common_expr_4 >= Date32(\"2002-05-08\") AND __common_expr_4 <= Date32(\"2002-05-13\")\ + \n Projection: CAST(test.col_utf8 AS Date32) AS __common_expr_4, test.col_int32\ + \n TableScan: test projection=[col_int32, col_utf8]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -104,15 +89,10 @@ fn subquery_filter_with_cast() -> Result<()> { fn case_when_aggregate() -> Result<()> { let sql = "SELECT col_utf8, sum(CASE WHEN col_int32 > 0 THEN 1 ELSE 0 END) AS n FROM test GROUP BY col_utf8"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: test.col_utf8, sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END) AS n - Aggregate: groupBy=[[test.col_utf8]], aggr=[[sum(CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END) AS sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END)]] - TableScan: test projection=[col_int32, col_utf8] - "# - ); + let expected = "Projection: test.col_utf8, sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END) AS n\ + \n Aggregate: groupBy=[[test.col_utf8]], aggr=[[sum(CASE WHEN test.col_int32 > Int32(0) THEN Int64(1) ELSE Int64(0) END) AS sum(CASE WHEN test.col_int32 > Int64(0) THEN Int64(1) ELSE Int64(0) END)]]\ + \n TableScan: test projection=[col_int32, col_utf8]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -120,15 +100,10 @@ fn case_when_aggregate() -> Result<()> { fn unsigned_target_type() -> Result<()> { let sql = "SELECT col_utf8 FROM test WHERE col_uint32 > 0"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: test.col_utf8 - Filter: test.col_uint32 > UInt32(0) - TableScan: test projection=[col_uint32, col_utf8] - "# - ); + let expected = "Projection: test.col_utf8\ + \n Filter: test.col_uint32 > UInt32(0)\ + \n TableScan: test projection=[col_uint32, col_utf8]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -137,14 +112,9 @@ fn distribute_by() -> Result<()> { // regression test for https://github.com/apache/datafusion/issues/3234 let sql = "SELECT col_int32, col_utf8 FROM test DISTRIBUTE BY (col_utf8)"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" - Repartition: DistributeBy(test.col_utf8) - TableScan: test projection=[col_int32, col_utf8] - "# - ); + let expected = "Repartition: DistributeBy(test.col_utf8)\ + \n TableScan: test projection=[col_int32, col_utf8]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -155,20 +125,15 @@ fn semi_join_with_join_filter() -> Result<()> { SELECT col_utf8 FROM test t2 WHERE test.col_int32 = t2.col_int32 \ AND test.col_uint32 != t2.col_uint32)"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: test.col_utf8 - LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32, col_uint32, col_utf8] - SubqueryAlias: __correlated_sq_1 - SubqueryAlias: t2 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32, col_uint32] - "# - ); + let expected = "Projection: test.col_utf8\ + \n LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32, col_uint32, col_utf8]\ + \n SubqueryAlias: __correlated_sq_1\ + \n SubqueryAlias: t2\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32, col_uint32]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -179,19 +144,14 @@ fn anti_join_with_join_filter() -> Result<()> { SELECT col_utf8 FROM test t2 WHERE test.col_int32 = t2.col_int32 \ AND test.col_uint32 != t2.col_uint32)"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" -Projection: test.col_utf8 - LeftAnti Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32 - TableScan: test projection=[col_int32, col_uint32, col_utf8] - SubqueryAlias: __correlated_sq_1 - SubqueryAlias: t2 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32, col_uint32] -"# - ); + let expected = "Projection: test.col_utf8\ + \n LeftAnti Join: test.col_int32 = __correlated_sq_1.col_int32 Filter: test.col_uint32 != __correlated_sq_1.col_uint32\ + \n TableScan: test projection=[col_int32, col_uint32, col_utf8]\ + \n SubqueryAlias: __correlated_sq_1\ + \n SubqueryAlias: t2\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32, col_uint32]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -200,21 +160,15 @@ fn where_exists_distinct() -> Result<()> { let sql = "SELECT col_int32 FROM test WHERE EXISTS (\ SELECT DISTINCT col_int32 FROM test t2 WHERE test.col_int32 = t2.col_int32)"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" -LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32] - SubqueryAlias: __correlated_sq_1 - Aggregate: groupBy=[[t2.col_int32]], aggr=[[]] - SubqueryAlias: t2 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32] -"# - - ); + let expected = "LeftSemi Join: test.col_int32 = __correlated_sq_1.col_int32\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32]\ + \n SubqueryAlias: __correlated_sq_1\ + \n Aggregate: groupBy=[[t2.col_int32]], aggr=[[]]\ + \n SubqueryAlias: t2\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -224,19 +178,15 @@ fn intersect() -> Result<()> { INTERSECT SELECT col_int32, col_utf8 FROM test \ INTERSECT SELECT col_int32, col_utf8 FROM test"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" -LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8 - Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]] - LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8 - Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]] - TableScan: test projection=[col_int32, col_utf8] - TableScan: test projection=[col_int32, col_utf8] - TableScan: test projection=[col_int32, col_utf8] -"# - ); + let expected = + "LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8\ + \n Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]]\ + \n LeftSemi Join: test.col_int32 = test.col_int32, test.col_utf8 = test.col_utf8\ + \n Aggregate: groupBy=[[test.col_int32, test.col_utf8]], aggr=[[]]\ + \n TableScan: test projection=[col_int32, col_utf8]\ + \n TableScan: test projection=[col_int32, col_utf8]\ + \n TableScan: test projection=[col_int32, col_utf8]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -245,16 +195,12 @@ fn between_date32_plus_interval() -> Result<()> { let sql = "SELECT count(1) FROM test \ WHERE col_date32 between '1998-03-18' AND cast('1998-03-18' as date) + INTERVAL '90 days'"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" -Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] - Projection: - Filter: test.col_date32 >= Date32("1998-03-18") AND test.col_date32 <= Date32("1998-06-16") - TableScan: test projection=[col_date32] -"# - ); + let expected = + "Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]]\ + \n Projection: \ + \n Filter: test.col_date32 >= Date32(\"1998-03-18\") AND test.col_date32 <= Date32(\"1998-06-16\")\ + \n TableScan: test projection=[col_date32]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -263,16 +209,12 @@ fn between_date64_plus_interval() -> Result<()> { let sql = "SELECT count(1) FROM test \ WHERE col_date64 between '1998-03-18T00:00:00' AND cast('1998-03-18' as date) + INTERVAL '90 days'"; let plan = test_sql(sql)?; - - assert_snapshot!( - format!("{plan}"), - @r#" - Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] - Projection: - Filter: test.col_date64 >= Date64("1998-03-18") AND test.col_date64 <= Date64("1998-06-16") - TableScan: test projection=[col_date64] - "# - ); + let expected = + "Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]]\ + \n Projection: \ + \n Filter: test.col_date64 >= Date64(\"1998-03-18\") AND test.col_date64 <= Date64(\"1998-06-16\")\ + \n TableScan: test projection=[col_date64]"; + assert_eq!(expected, format!("{plan}")); Ok(()) } @@ -281,73 +223,54 @@ fn propagate_empty_relation() { let sql = "SELECT test.col_int32 FROM test JOIN ( SELECT col_int32 FROM test WHERE false ) AS ta1 ON test.col_int32 = ta1.col_int32;"; let plan = test_sql(sql).unwrap(); // when children exist EmptyRelation, it will bottom-up propagate. - - assert_snapshot!( - format!("{plan}"), - @r#" - EmptyRelation - "# - ); + let expected = "EmptyRelation"; + assert_eq!(expected, format!("{plan}")); } #[test] fn join_keys_in_subquery_alias() { let sql = "SELECT * FROM test AS A, ( SELECT col_int32 as key FROM test ) AS B where A.col_int32 = B.key;"; let plan = test_sql(sql).unwrap(); - - assert_snapshot!( - format!("{plan}"), - @r#" - Inner Join: a.col_int32 = b.key - SubqueryAlias: a - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] - SubqueryAlias: b - Projection: test.col_int32 AS key - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32] - "# - ); + let expected = "Inner Join: a.col_int32 = b.key\ + \n SubqueryAlias: a\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]\ + \n SubqueryAlias: b\ + \n Projection: test.col_int32 AS key\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32]"; + + assert_eq!(expected, format!("{plan}")); } #[test] fn join_keys_in_subquery_alias_1() { let sql = "SELECT * FROM test AS A, ( SELECT test.col_int32 AS key FROM test JOIN test AS C on test.col_int32 = C.col_int32 ) AS B where A.col_int32 = B.key;"; let plan = test_sql(sql).unwrap(); - - assert_snapshot!( - format!("{plan}"), - @r#" - Inner Join: a.col_int32 = b.key - SubqueryAlias: a - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] - SubqueryAlias: b - Projection: test.col_int32 AS key - Inner Join: test.col_int32 = c.col_int32 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32] - SubqueryAlias: c - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32] - "# - ); + let expected = "Inner Join: a.col_int32 = b.key\ + \n SubqueryAlias: a\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]\ + \n SubqueryAlias: b\ + \n Projection: test.col_int32 AS key\ + \n Inner Join: test.col_int32 = c.col_int32\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32]\ + \n SubqueryAlias: c\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32]"; + assert_eq!(expected, format!("{plan}")); } #[test] fn push_down_filter_groupby_expr_contains_alias() { let sql = "SELECT * FROM (SELECT (col_int32 + col_uint32) AS c, count(*) FROM test GROUP BY 1) where c > 3"; let plan = test_sql(sql).unwrap(); - - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: test.col_int32 + test.col_uint32 AS c, count(Int64(1)) AS count(*) - Aggregate: groupBy=[[CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64)]], aggr=[[count(Int64(1))]] - Filter: CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64) > Int64(3) - TableScan: test projection=[col_int32, col_uint32] - "# - ); + let expected = "Projection: test.col_int32 + test.col_uint32 AS c, count(Int64(1)) AS count(*)\ + \n Aggregate: groupBy=[[CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64)]], aggr=[[count(Int64(1))]]\ + \n Filter: CAST(test.col_int32 AS Int64) + CAST(test.col_uint32 AS Int64) > Int64(3)\ + \n TableScan: test projection=[col_int32, col_uint32]"; + assert_eq!(expected, format!("{plan}")); } #[test] @@ -355,18 +278,13 @@ fn push_down_filter_groupby_expr_contains_alias() { fn test_same_name_but_not_ambiguous() { let sql = "SELECT t1.col_int32 AS col_int32 FROM test t1 intersect SELECT col_int32 FROM test t2"; let plan = test_sql(sql).unwrap(); - - assert_snapshot!( - format!("{plan}"), - @r#" - LeftSemi Join: t1.col_int32 = t2.col_int32 - Aggregate: groupBy=[[t1.col_int32]], aggr=[[]] - SubqueryAlias: t1 - TableScan: test projection=[col_int32] - SubqueryAlias: t2 - TableScan: test projection=[col_int32] - "# - ); + let expected = "LeftSemi Join: t1.col_int32 = t2.col_int32\ + \n Aggregate: groupBy=[[t1.col_int32]], aggr=[[]]\ + \n SubqueryAlias: t1\ + \n TableScan: test projection=[col_int32]\ + \n SubqueryAlias: t2\ + \n TableScan: test projection=[col_int32]"; + assert_eq!(expected, format!("{plan}")); } #[test] @@ -377,14 +295,11 @@ fn eliminate_nested_filters() { AND (1=1) AND (1=0 OR 1=1)"; let plan = test_sql(sql).unwrap(); + let expected = "\ + Filter: test.col_int32 > Int32(0)\ + \n TableScan: test projection=[col_int32]"; - assert_snapshot!( - format!("{plan}"), - @r#" -Filter: test.col_int32 > Int32(0) - TableScan: test projection=[col_int32] - "# - ); + assert_eq!(expected, format!("{plan}")); } #[test] @@ -395,15 +310,10 @@ fn eliminate_redundant_null_check_on_count() { GROUP BY col_int32 HAVING c IS NOT NULL"; let plan = test_sql(sql).unwrap(); - - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: test.col_int32, count(Int64(1)) AS count(*) AS c - Aggregate: groupBy=[[test.col_int32]], aggr=[[count(Int64(1))]] - TableScan: test projection=[col_int32] - "# - ); + let expected = "Projection: test.col_int32, count(Int64(1)) AS count(*) AS c\ + \n Aggregate: groupBy=[[test.col_int32]], aggr=[[count(Int64(1))]]\ + \n TableScan: test projection=[col_int32]"; + assert_eq!(expected, format!("{plan}")); } #[test] @@ -423,16 +333,13 @@ fn test_propagate_empty_relation_inner_join_and_unions() { SELECT test.col_int32 FROM test WHERE 1 = 0"; let plan = test_sql(sql).unwrap(); - - assert_snapshot!( - format!("{plan}"), - @r#" -Union - TableScan: test projection=[col_int32] - TableScan: test projection=[col_int32] - Filter: test.col_int32 < Int32(0) - TableScan: test projection=[col_int32] - "#); + let expected = "\ + Union\ + \n TableScan: test projection=[col_int32]\ + \n TableScan: test projection=[col_int32]\ + \n Filter: test.col_int32 < Int32(0)\ + \n TableScan: test projection=[col_int32]"; + assert_eq!(expected, format!("{plan}")); } #[test] @@ -440,14 +347,10 @@ fn select_wildcard_with_repeated_column_but_is_aliased() { let sql = "SELECT *, col_int32 as col_32 FROM test"; let plan = test_sql(sql).unwrap(); + let expected = "Projection: test.col_int32, test.col_uint32, test.col_utf8, test.col_date32, test.col_date64, test.col_ts_nano_none, test.col_ts_nano_utc, test.col_int32 AS col_32\ + \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]"; - assert_snapshot!( - format!("{plan}"), - @r#" - Projection: test.col_int32, test.col_uint32, test.col_utf8, test.col_date32, test.col_date64, test.col_ts_nano_none, test.col_ts_nano_utc, test.col_int32 AS col_32 - TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] - "# - ); + assert_eq!(expected, format!("{plan}")); } #[test] @@ -464,20 +367,15 @@ fn select_correlated_predicate_subquery_with_uppercase_ident() { ) "#; let plan = test_sql(sql).unwrap(); - - assert_snapshot!( - format!("{plan}"), - @r#" - LeftSemi Join: test.col_int32 = __correlated_sq_1.COL_INT32 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc] - SubqueryAlias: __correlated_sq_1 - SubqueryAlias: T1 - Projection: test.col_int32 AS COL_INT32 - Filter: test.col_int32 IS NOT NULL - TableScan: test projection=[col_int32] - "# - ); + let expected = "LeftSemi Join: test.col_int32 = __correlated_sq_1.COL_INT32\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32, col_uint32, col_utf8, col_date32, col_date64, col_ts_nano_none, col_ts_nano_utc]\ + \n SubqueryAlias: __correlated_sq_1\ + \n SubqueryAlias: T1\ + \n Projection: test.col_int32 AS COL_INT32\ + \n Filter: test.col_int32 IS NOT NULL\ + \n TableScan: test projection=[col_int32]"; + assert_eq!(expected, format!("{plan}")); } fn test_sql(sql: &str) -> Result { diff --git a/datafusion/physical-expr-common/src/physical_expr.rs b/datafusion/physical-expr-common/src/physical_expr.rs index 3bc41d2652d9a..43f214607f9fc 100644 --- a/datafusion/physical-expr-common/src/physical_expr.rs +++ b/datafusion/physical-expr-common/src/physical_expr.rs @@ -27,7 +27,6 @@ use arrow::array::BooleanArray; use arrow::compute::filter_record_batch; use arrow::datatypes::{DataType, Schema}; use arrow::record_batch::RecordBatch; -use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode}; use datafusion_common::{internal_err, not_impl_err, Result, ScalarValue}; use datafusion_expr_common::columnar_value::ColumnarValue; use datafusion_expr_common::interval_arithmetic::Interval; @@ -284,55 +283,6 @@ pub trait PhysicalExpr: Send + Sync + Display + Debug + DynEq + DynHash { /// See the [`fmt_sql`] function for an example of printing `PhysicalExpr`s as SQL. /// fn fmt_sql(&self, f: &mut Formatter<'_>) -> fmt::Result; - - /// Take a snapshot of this `PhysicalExpr`, if it is dynamic. - /// - /// "Dynamic" in this case means containing references to structures that may change - /// during plan execution, such as hash tables. - /// - /// This method is used to capture the current state of `PhysicalExpr`s that may contain - /// dynamic references to other operators in order to serialize it over the wire - /// or treat it via downcast matching. - /// - /// You should not call this method directly as it does not handle recursion. - /// Instead use [`snapshot_physical_expr`] to handle recursion and capture the - /// full state of the `PhysicalExpr`. - /// - /// This is expected to return "simple" expressions that do not have mutable state - /// and are composed of DataFusion's built-in `PhysicalExpr` implementations. - /// Callers however should *not* assume anything about the returned expressions - /// since callers and implementers may not agree on what "simple" or "built-in" - /// means. - /// In other words, if you need to serialize a `PhysicalExpr` across the wire - /// you should call this method and then try to serialize the result, - /// but you should handle unknown or unexpected `PhysicalExpr` implementations gracefully - /// just as if you had not called this method at all. - /// - /// In particular, consider: - /// * A `PhysicalExpr` that references the current state of a `datafusion::physical_plan::TopK` - /// that is involved in a query with `SELECT * FROM t1 ORDER BY a LIMIT 10`. - /// This function may return something like `a >= 12`. - /// * A `PhysicalExpr` that references the current state of a `datafusion::physical_plan::joins::HashJoinExec` - /// from a query such as `SELECT * FROM t1 JOIN t2 ON t1.a = t2.b`. - /// This function may return something like `t2.b IN (1, 5, 7)`. - /// - /// A system or function that can only deal with a hardcoded set of `PhysicalExpr` implementations - /// or needs to serialize this state to bytes may not be able to handle these dynamic references. - /// In such cases, we should return a simplified version of the `PhysicalExpr` that does not - /// contain these dynamic references. - /// - /// Systems that implement remote execution of plans, e.g. serialize a portion of the query plan - /// and send it across the wire to a remote executor may want to call this method after - /// every batch on the source side and brodcast / update the current snaphot to the remote executor. - /// - /// Note for implementers: this method should *not* handle recursion. - /// Recursion is handled in [`snapshot_physical_expr`]. - fn snapshot(&self) -> Result>> { - // By default, we return None to indicate that this PhysicalExpr does not - // have any dynamic references or state. - // This is a safe default behavior. - Ok(None) - } } /// [`PhysicalExpr`] can't be constrained by [`Eq`] directly because it must remain object @@ -496,30 +446,3 @@ pub fn fmt_sql(expr: &dyn PhysicalExpr) -> impl Display + '_ { Wrapper { expr } } - -/// Take a snapshot of the given `PhysicalExpr` if it is dynamic. -/// -/// Take a snapshot of this `PhysicalExpr` if it is dynamic. -/// This is used to capture the current state of `PhysicalExpr`s that may contain -/// dynamic references to other operators in order to serialize it over the wire -/// or treat it via downcast matching. -/// -/// See the documentation of [`PhysicalExpr::snapshot`] for more details. -/// -/// # Returns -/// -/// Returns an `Option>` which is the snapshot of the -/// `PhysicalExpr` if it is dynamic. If the `PhysicalExpr` does not have -/// any dynamic references or state, it returns `None`. -pub fn snapshot_physical_expr( - expr: Arc, -) -> Result> { - expr.transform_up(|e| { - if let Some(snapshot) = e.snapshot()? { - Ok(Transformed::yes(snapshot)) - } else { - Ok(Transformed::no(Arc::clone(&e))) - } - }) - .data() -} diff --git a/datafusion/physical-expr/Cargo.toml b/datafusion/physical-expr/Cargo.toml index 47e3291e5cb4d..72baa0db00a21 100644 --- a/datafusion/physical-expr/Cargo.toml +++ b/datafusion/physical-expr/Cargo.toml @@ -57,7 +57,6 @@ petgraph = "0.7.1" arrow = { workspace = true, features = ["test_utils"] } criterion = { workspace = true } datafusion-functions = { workspace = true } -insta = { workspace = true } rand = { workspace = true } rstest = { workspace = true } @@ -72,7 +71,3 @@ name = "case_when" [[bench]] harness = false name = "is_null" - -[[bench]] -harness = false -name = "binary_op" diff --git a/datafusion/physical-expr/benches/binary_op.rs b/datafusion/physical-expr/benches/binary_op.rs deleted file mode 100644 index 216d8a520e489..0000000000000 --- a/datafusion/physical-expr/benches/binary_op.rs +++ /dev/null @@ -1,312 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use arrow::{ - array::BooleanArray, - datatypes::{DataType, Field, Schema}, -}; -use arrow::{array::StringArray, record_batch::RecordBatch}; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use datafusion_expr::{and, binary_expr, col, lit, or, Operator}; -use datafusion_physical_expr::{ - expressions::{BinaryExpr, Column}, - planner::logical2physical, - PhysicalExpr, -}; -use std::sync::Arc; - -/// Generates BooleanArrays with different true/false distributions for benchmarking. -/// -/// Returns a vector of tuples containing scenario name and corresponding BooleanArray. -/// -/// # Arguments -/// - `TEST_ALL_FALSE` - Used to generate what kind of test data -/// - `len` - Length of the BooleanArray to generate -fn generate_boolean_cases( - len: usize, -) -> Vec<(String, BooleanArray)> { - let mut cases = Vec::with_capacity(6); - - // Scenario 1: All elements false or all elements true - if TEST_ALL_FALSE { - let all_false = BooleanArray::from(vec![false; len]); - cases.push(("all_false".to_string(), all_false)); - } else { - let all_true = BooleanArray::from(vec![true; len]); - cases.push(("all_true".to_string(), all_true)); - } - - // Scenario 2: Single true at first position or single false at first position - if TEST_ALL_FALSE { - let mut first_true = vec![false; len]; - first_true[0] = true; - cases.push(("one_true_first".to_string(), BooleanArray::from(first_true))); - } else { - let mut first_false = vec![true; len]; - first_false[0] = false; - cases.push(( - "one_false_first".to_string(), - BooleanArray::from(first_false), - )); - } - - // Scenario 3: Single true at last position or single false at last position - if TEST_ALL_FALSE { - let mut last_true = vec![false; len]; - last_true[len - 1] = true; - cases.push(("one_true_last".to_string(), BooleanArray::from(last_true))); - } else { - let mut last_false = vec![true; len]; - last_false[len - 1] = false; - cases.push(("one_false_last".to_string(), BooleanArray::from(last_false))); - } - - // Scenario 4: Single true at exact middle or single false at exact middle - let mid = len / 2; - if TEST_ALL_FALSE { - let mut mid_true = vec![false; len]; - mid_true[mid] = true; - cases.push(("one_true_middle".to_string(), BooleanArray::from(mid_true))); - } else { - let mut mid_false = vec![true; len]; - mid_false[mid] = false; - cases.push(( - "one_false_middle".to_string(), - BooleanArray::from(mid_false), - )); - } - - // Scenario 5: Single true at 25% position or single false at 25% position - let mid_left = len / 4; - if TEST_ALL_FALSE { - let mut mid_left_true = vec![false; len]; - mid_left_true[mid_left] = true; - cases.push(( - "one_true_middle_left".to_string(), - BooleanArray::from(mid_left_true), - )); - } else { - let mut mid_left_false = vec![true; len]; - mid_left_false[mid_left] = false; - cases.push(( - "one_false_middle_left".to_string(), - BooleanArray::from(mid_left_false), - )); - } - - // Scenario 6: Single true at 75% position or single false at 75% position - let mid_right = (3 * len) / 4; - if TEST_ALL_FALSE { - let mut mid_right_true = vec![false; len]; - mid_right_true[mid_right] = true; - cases.push(( - "one_true_middle_right".to_string(), - BooleanArray::from(mid_right_true), - )); - } else { - let mut mid_right_false = vec![true; len]; - mid_right_false[mid_right] = false; - cases.push(( - "one_false_middle_right".to_string(), - BooleanArray::from(mid_right_false), - )); - } - - // Scenario 7: Test all true or all false in AND/OR - // This situation won't cause a short circuit, but it can skip the bool calculation - if TEST_ALL_FALSE { - let all_true = vec![true; len]; - cases.push(("all_true_in_and".to_string(), BooleanArray::from(all_true))); - } else { - let all_false = vec![false; len]; - cases.push(("all_false_in_or".to_string(), BooleanArray::from(all_false))); - } - - cases -} - -/// Benchmarks AND/OR operator short-circuiting by evaluating complex regex conditions. -/// -/// Creates 7 test scenarios per operator: -/// 1. All values enable short-circuit (all_true/all_false) -/// 2. 2-6 Single true/false value at different positions to measure early exit -/// 3. Test all true or all false in AND/OR -/// -/// You can run this benchmark with: -/// ```sh -/// cargo bench --bench binary_op -- short_circuit -/// ``` -fn benchmark_binary_op_in_short_circuit(c: &mut Criterion) { - // Create schema with three columns - let schema = Arc::new(Schema::new(vec![ - Field::new("a", DataType::Boolean, false), - Field::new("b", DataType::Utf8, false), - Field::new("c", DataType::Utf8, false), - ])); - - // Generate test data with extended content - let (b_values, c_values) = generate_test_strings(8192); - - let batches_and = - create_record_batch::(schema.clone(), &b_values, &c_values).unwrap(); - let batches_or = - create_record_batch::(schema.clone(), &b_values, &c_values).unwrap(); - - // Build complex string matching conditions - let right_condition_and = and( - // Check for API endpoint pattern in URLs - binary_expr( - col("b"), - Operator::RegexMatch, - lit(r#"^https://(\w+\.)?example\.(com|org)/"#), - ), - // Check for markdown code blocks and summary section - binary_expr( - col("c"), - Operator::RegexMatch, - lit("```(rust|python|go)\nfn? main$$"), - ), - ); - - let right_condition_or = or( - // Check for secure HTTPS protocol - binary_expr( - col("b"), - Operator::RegexMatch, - lit(r#"^https://(\w+\.)?example\.(com|org)/"#), - ), - // Check for Rust code examples - binary_expr( - col("c"), - Operator::RegexMatch, - lit("```(rust|python|go)\nfn? main$$"), - ), - ); - - // Create physical binary expressions - // a AND ((b ~ regex) AND (c ~ regex)) - let expr_and = BinaryExpr::new( - Arc::new(Column::new("a", 0)), - Operator::And, - logical2physical(&right_condition_and, &schema), - ); - - // a OR ((b ~ regex) OR (c ~ regex)) - let expr_or = BinaryExpr::new( - Arc::new(Column::new("a", 0)), - Operator::Or, - logical2physical(&right_condition_or, &schema), - ); - - // Each scenario when the test operator is `and` - { - for (name, batch) in batches_and.into_iter() { - c.bench_function(&format!("short_circuit/and/{}", name), |b| { - b.iter(|| expr_and.evaluate(black_box(&batch)).unwrap()) - }); - } - } - // Each scenario when the test operator is `or` - { - for (name, batch) in batches_or.into_iter() { - c.bench_function(&format!("short_circuit/or/{}", name), |b| { - b.iter(|| expr_or.evaluate(black_box(&batch)).unwrap()) - }); - } - } -} - -/// Generate test data with computationally expensive patterns -fn generate_test_strings(num_rows: usize) -> (Vec, Vec) { - // Extended URL patterns with query parameters and paths - let base_urls = [ - "https://api.example.com/v2/users/12345/posts?category=tech&sort=date&lang=en-US", - "https://cdn.example.net/assets/images/2023/08/15/sample-image-highres.jpg?width=1920&quality=85", - "http://service.demo.org:8080/api/data/transactions/20230815123456.csv", - "ftp://legacy.archive.example/backups/2023/Q3/database-dump.sql.gz", - "https://docs.example.co.uk/reference/advanced-topics/concurrency/parallel-processing.md#implementation-details", - ]; - - // Extended markdown content with code blocks and structure - let base_markdowns = [ - concat!( - "# Advanced Topics in Computer Science\n\n", - "## Summary\nThis article explores complex system design patterns and...\n\n", - "```rust\nfn process_data(data: &mut [i32]) {\n // Parallel processing example\n data.par_iter_mut().for_each(|x| *x *= 2);\n}\n```\n\n", - "## Performance Considerations\nWhen implementing concurrent systems...\n" - ), - concat!( - "## API Documentation\n\n", - "```json\n{\n \"endpoint\": \"/api/v2/users\",\n \"methods\": [\"GET\", \"POST\"],\n \"parameters\": {\n \"page\": \"number\"\n }\n}\n```\n\n", - "# Authentication Guide\nSecure your API access using OAuth 2.0...\n" - ), - concat!( - "# Data Processing Pipeline\n\n", - "```python\nfrom multiprocessing import Pool\n\ndef main():\n with Pool(8) as p:\n results = p.map(process_item, data)\n```\n\n", - "## Summary of Optimizations\n1. Batch processing\n2. Memory pooling\n3. Concurrent I/O operations\n" - ), - concat!( - "# System Architecture Overview\n\n", - "## Components\n- Load Balancer\n- Database Cluster\n- Cache Service\n\n", - "```go\nfunc main() {\n router := gin.Default()\n router.GET(\"/api/health\", healthCheck)\n router.Run(\":8080\")\n}\n```\n" - ), - concat!( - "## Configuration Reference\n\n", - "```yaml\nserver:\n port: 8080\n max_threads: 32\n\ndatabase:\n url: postgres://user@prod-db:5432/main\n```\n\n", - "# Deployment Strategies\nBlue-green deployment patterns with...\n" - ), - ]; - - let mut urls = Vec::with_capacity(num_rows); - let mut markdowns = Vec::with_capacity(num_rows); - - for i in 0..num_rows { - urls.push(base_urls[i % 5].to_string()); - markdowns.push(base_markdowns[i % 5].to_string()); - } - - (urls, markdowns) -} - -/// Creates record batches with boolean arrays that test different short-circuit scenarios. -/// When TEST_ALL_FALSE = true: creates data for AND operator benchmarks (needs early false exit) -/// When TEST_ALL_FALSE = false: creates data for OR operator benchmarks (needs early true exit) -fn create_record_batch( - schema: Arc, - b_values: &[String], - c_values: &[String], -) -> arrow::error::Result> { - // Generate data for six scenarios, but only the data for the "all_false" and "all_true" cases can be optimized through short-circuiting - let boolean_array = generate_boolean_cases::(b_values.len()); - let mut rbs = Vec::with_capacity(boolean_array.len()); - for (name, a_array) in boolean_array { - let b_array = StringArray::from(b_values.to_vec()); - let c_array = StringArray::from(c_values.to_vec()); - rbs.push(( - name, - RecordBatch::try_new( - schema.clone(), - vec![Arc::new(a_array), Arc::new(b_array), Arc::new(c_array)], - )?, - )); - } - Ok(rbs) -} - -criterion_group!(benches, benchmark_binary_op_in_short_circuit); - -criterion_main!(benches); diff --git a/datafusion/physical-expr/src/aggregate.rs b/datafusion/physical-expr/src/aggregate.rs index 49912954ac81c..ae3d9050fa628 100644 --- a/datafusion/physical-expr/src/aggregate.rs +++ b/datafusion/physical-expr/src/aggregate.rs @@ -97,94 +97,6 @@ impl AggregateExprBuilder { /// Constructs an `AggregateFunctionExpr` from the builder /// /// Note that an [`Self::alias`] must be provided before calling this method. - /// - /// # Example: Create an [`AggregateUDF`] - /// - /// In the following example, [`AggregateFunctionExpr`] will be built using [`AggregateExprBuilder`] - /// which provides a build function. Full example could be accessed from the source file. - /// - /// ``` - /// # use std::any::Any; - /// # use std::sync::Arc; - /// # use arrow::datatypes::DataType; - /// # use datafusion_common::{Result, ScalarValue}; - /// # use datafusion_expr::{col, ColumnarValue, Documentation, Signature, Volatility, Expr}; - /// # use datafusion_expr::{AggregateUDFImpl, AggregateUDF, Accumulator, function::{AccumulatorArgs, StateFieldsArgs}}; - /// # use arrow::datatypes::Field; - /// # - /// # #[derive(Debug, Clone)] - /// # struct FirstValueUdf { - /// # signature: Signature, - /// # } - /// # - /// # impl FirstValueUdf { - /// # fn new() -> Self { - /// # Self { - /// # signature: Signature::any(1, Volatility::Immutable), - /// # } - /// # } - /// # } - /// # - /// # impl AggregateUDFImpl for FirstValueUdf { - /// # fn as_any(&self) -> &dyn Any { - /// # unimplemented!() - /// # } - /// # fn name(&self) -> &str { - /// # unimplemented!() - /// } - /// # fn signature(&self) -> &Signature { - /// # unimplemented!() - /// # } - /// # fn return_type(&self, args: &[DataType]) -> Result { - /// # unimplemented!() - /// # } - /// # - /// # fn accumulator(&self, acc_args: AccumulatorArgs) -> Result> { - /// # unimplemented!() - /// # } - /// # - /// # fn state_fields(&self, args: StateFieldsArgs) -> Result> { - /// # unimplemented!() - /// # } - /// # - /// # fn documentation(&self) -> Option<&Documentation> { - /// # unimplemented!() - /// # } - /// # } - /// # - /// # let first_value = AggregateUDF::from(FirstValueUdf::new()); - /// # let expr = first_value.call(vec![col("a")]); - /// # - /// # use datafusion_physical_expr::expressions::Column; - /// # use datafusion_physical_expr_common::physical_expr::PhysicalExpr; - /// # use datafusion_physical_expr::aggregate::AggregateExprBuilder; - /// # use datafusion_physical_expr::expressions::PhysicalSortExpr; - /// # use datafusion_physical_expr::PhysicalSortRequirement; - /// # - /// fn build_aggregate_expr() -> Result<()> { - /// let args = vec![Arc::new(Column::new("a", 0)) as Arc]; - /// let order_by = vec![PhysicalSortExpr { - /// expr: Arc::new(Column::new("x", 1)) as Arc, - /// options: Default::default(), - /// }]; - /// - /// let first_value = AggregateUDF::from(FirstValueUdf::new()); - /// - /// let aggregate_expr = AggregateExprBuilder::new( - /// Arc::new(first_value), - /// args - /// ) - /// .order_by(order_by.into()) - /// .alias("first_a_by_x") - /// .ignore_nulls() - /// .build()?; - /// - /// Ok(()) - /// } - /// ``` - /// - /// This creates a physical expression equivalent to SQL: - /// `first_value(a ORDER BY x) IGNORE NULLS AS first_a_by_x` pub fn build(self) -> Result { let Self { fun, diff --git a/datafusion/physical-expr/src/equivalence/projection.rs b/datafusion/physical-expr/src/equivalence/projection.rs index a33339091c85d..035678fbf1f39 100644 --- a/datafusion/physical-expr/src/equivalence/projection.rs +++ b/datafusion/physical-expr/src/equivalence/projection.rs @@ -67,8 +67,8 @@ impl ProjectionMapping { let matching_input_field = input_schema.field(idx); if col.name() != matching_input_field.name() { return internal_err!("Input field name {} does not match with the projection expression {}", - matching_input_field.name(),col.name()) - } + matching_input_field.name(),col.name()) + } let matching_input_column = Column::new(matching_input_field.name(), idx); Ok(Transformed::yes(Arc::new(matching_input_column))) diff --git a/datafusion/physical-expr/src/equivalence/properties/mod.rs b/datafusion/physical-expr/src/equivalence/properties/mod.rs index 5b34a02a91424..c7c33ba5b2ba5 100644 --- a/datafusion/physical-expr/src/equivalence/properties/mod.rs +++ b/datafusion/physical-expr/src/equivalence/properties/mod.rs @@ -546,26 +546,22 @@ impl EquivalenceProperties { self.ordering_satisfy_requirement(&sort_requirements) } - /// Returns the number of consecutive requirements (starting from the left) - /// that are satisfied by the plan ordering. - fn compute_common_sort_prefix_length( - &self, - normalized_reqs: &LexRequirement, - ) -> usize { + /// Checks whether the given sort requirements are satisfied by any of the + /// existing orderings. + pub fn ordering_satisfy_requirement(&self, reqs: &LexRequirement) -> bool { + let mut eq_properties = self.clone(); + // First, standardize the given requirement: + let normalized_reqs = eq_properties.normalize_sort_requirements(reqs); + // Check whether given ordering is satisfied by constraints first - if self.satisfied_by_constraints(normalized_reqs) { - // If the constraints satisfy all requirements, return the full normalized requirements length - return normalized_reqs.len(); + if self.satisfied_by_constraints(&normalized_reqs) { + return true; } - let mut eq_properties = self.clone(); - - for (i, normalized_req) in normalized_reqs.iter().enumerate() { + for normalized_req in normalized_reqs { // Check whether given ordering is satisfied - if !eq_properties.ordering_satisfy_single(normalized_req) { - // As soon as one requirement is not satisfied, return - // how many we've satisfied so far - return i; + if !eq_properties.ordering_satisfy_single(&normalized_req) { + return false; } // Treat satisfied keys as constants in subsequent iterations. We // can do this because the "next" key only matters in a lexicographical @@ -579,35 +575,10 @@ impl EquivalenceProperties { // From the analysis above, we know that `[a ASC]` is satisfied. Then, // we add column `a` as constant to the algorithm state. This enables us // to deduce that `(b + c) ASC` is satisfied, given `a` is constant. - eq_properties = eq_properties.with_constants(std::iter::once( - ConstExpr::from(Arc::clone(&normalized_req.expr)), - )); + eq_properties = eq_properties + .with_constants(std::iter::once(ConstExpr::from(normalized_req.expr))); } - - // All requirements are satisfied. - normalized_reqs.len() - } - - /// Determines the longest prefix of `reqs` that is satisfied by the existing ordering. - /// Returns that prefix as a new `LexRequirement`, and a boolean indicating if all the requirements are satisfied. - pub fn extract_common_sort_prefix( - &self, - reqs: &LexRequirement, - ) -> (LexRequirement, bool) { - // First, standardize the given requirement: - let normalized_reqs = self.normalize_sort_requirements(reqs); - - let prefix_len = self.compute_common_sort_prefix_length(&normalized_reqs); - ( - LexRequirement::new(normalized_reqs[..prefix_len].to_vec()), - prefix_len == normalized_reqs.len(), - ) - } - - /// Checks whether the given sort requirements are satisfied by any of the - /// existing orderings. - pub fn ordering_satisfy_requirement(&self, reqs: &LexRequirement) -> bool { - self.extract_common_sort_prefix(reqs).1 + true } /// Checks if the sort requirements are satisfied by any of the table constraints (primary key or unique). @@ -1112,7 +1083,7 @@ impl EquivalenceProperties { /// # Arguments /// /// * `mapping` - A reference to `ProjectionMapping` that defines how expressions are mapped - /// in the projection operation + /// in the projection operation /// /// # Returns /// diff --git a/datafusion/physical-expr/src/expressions/binary.rs b/datafusion/physical-expr/src/expressions/binary.rs index 6c68d11e2c94c..f21d3e7652cdc 100644 --- a/datafusion/physical-expr/src/expressions/binary.rs +++ b/datafusion/physical-expr/src/expressions/binary.rs @@ -29,9 +29,7 @@ use arrow::compute::kernels::boolean::{and_kleene, not, or_kleene}; use arrow::compute::kernels::cmp::*; use arrow::compute::kernels::comparison::{regexp_is_match, regexp_is_match_scalar}; use arrow::compute::kernels::concat_elements::concat_elements_utf8; -use arrow::compute::{ - cast, filter_record_batch, ilike, like, nilike, nlike, SlicesIterator, -}; +use arrow::compute::{cast, ilike, like, nilike, nlike}; use arrow::datatypes::*; use arrow::error::ArrowError; use datafusion_common::cast::as_boolean_array; @@ -360,26 +358,7 @@ impl PhysicalExpr for BinaryExpr { fn evaluate(&self, batch: &RecordBatch) -> Result { use arrow::compute::kernels::numeric::*; - // Evaluate left-hand side expression. let lhs = self.left.evaluate(batch)?; - - // Check if we can apply short-circuit evaluation. - match check_short_circuit(&lhs, &self.op) { - ShortCircuitStrategy::None => {} - ShortCircuitStrategy::ReturnLeft => return Ok(lhs), - ShortCircuitStrategy::ReturnRight => { - let rhs = self.right.evaluate(batch)?; - return Ok(rhs); - } - ShortCircuitStrategy::PreSelection(selection) => { - // The function `evaluate_selection` was not called for filtering and calculation, - // as it takes into account cases where the selection contains null values. - let batch = filter_record_batch(batch, selection)?; - let right_ret = self.right.evaluate(&batch)?; - return pre_selection_scatter(selection, right_ret); - } - } - let rhs = self.right.evaluate(batch)?; let left_data_type = lhs.data_type(); let right_data_type = rhs.data_type(); @@ -420,19 +399,23 @@ impl PhysicalExpr for BinaryExpr { let result_type = self.data_type(input_schema)?; - // If the left-hand side is an array and the right-hand side is a non-null scalar, try the optimized kernel. - if let (ColumnarValue::Array(array), ColumnarValue::Scalar(ref scalar)) = - (&lhs, &rhs) - { - if !scalar.is_null() { - if let Some(result_array) = - self.evaluate_array_scalar(array, scalar.clone())? - { - let final_array = result_array - .and_then(|a| to_result_type_array(&self.op, a, &result_type)); - return final_array.map(ColumnarValue::Array); + // Attempt to use special kernels if one input is scalar and the other is an array + let scalar_result = match (&lhs, &rhs) { + (ColumnarValue::Array(array), ColumnarValue::Scalar(scalar)) => { + // if left is array and right is literal(not NULL) - use scalar operations + if scalar.is_null() { + None + } else { + self.evaluate_array_scalar(array, scalar.clone())?.map(|r| { + r.and_then(|a| to_result_type_array(&self.op, a, &result_type)) + }) } } + (_, _) => None, // default to array implementation + }; + + if let Some(result) = scalar_result { + return result.map(ColumnarValue::Array); } // if both arrays or both literals - extract arrays and continue execution @@ -822,201 +805,6 @@ impl BinaryExpr { } } -enum ShortCircuitStrategy<'a> { - None, - ReturnLeft, - ReturnRight, - PreSelection(&'a BooleanArray), -} - -/// Based on the results calculated from the left side of the short-circuit operation, -/// if the proportion of `true` is less than 0.2 and the current operation is an `and`, -/// the `RecordBatch` will be filtered in advance. -const PRE_SELECTION_THRESHOLD: f32 = 0.2; - -/// Checks if a logical operator (`AND`/`OR`) can short-circuit evaluation based on the left-hand side (lhs) result. -/// -/// Short-circuiting occurs under these circumstances: -/// - For `AND`: -/// - if LHS is all false => short-circuit → return LHS -/// - if LHS is all true => short-circuit → return RHS -/// - if LHS is mixed and true_count/sum_count <= [`PRE_SELECTION_THRESHOLD`] -> pre-selection -/// - For `OR`: -/// - if LHS is all true => short-circuit → return LHS -/// - if LHS is all false => short-circuit → return RHS -/// # Arguments -/// * `lhs` - The left-hand side (lhs) columnar value (array or scalar) -/// * `lhs` - The left-hand side (lhs) columnar value (array or scalar) -/// * `op` - The logical operator (`AND` or `OR`) -/// -/// # Implementation Notes -/// 1. Only works with Boolean-typed arguments (other types automatically return `false`) -/// 2. Handles both scalar values and array values -/// 3. For arrays, uses optimized bit counting techniques for boolean arrays -fn check_short_circuit<'a>( - lhs: &'a ColumnarValue, - op: &Operator, -) -> ShortCircuitStrategy<'a> { - // Quick reject for non-logical operators,and quick judgment when op is and - let is_and = match op { - Operator::And => true, - Operator::Or => false, - _ => return ShortCircuitStrategy::None, - }; - - // Non-boolean types can't be short-circuited - if lhs.data_type() != DataType::Boolean { - return ShortCircuitStrategy::None; - } - - match lhs { - ColumnarValue::Array(array) => { - // Fast path for arrays - try to downcast to boolean array - if let Ok(bool_array) = as_boolean_array(array) { - // Arrays with nulls can't be short-circuited - if bool_array.null_count() > 0 { - return ShortCircuitStrategy::None; - } - - let len = bool_array.len(); - if len == 0 { - return ShortCircuitStrategy::None; - } - - let true_count = bool_array.values().count_set_bits(); - if is_and { - // For AND, prioritize checking for all-false (short circuit case) - // Uses optimized false_count() method provided by Arrow - - // Short circuit if all values are false - if true_count == 0 { - return ShortCircuitStrategy::ReturnLeft; - } - - // If no false values, then all must be true - if true_count == len { - return ShortCircuitStrategy::ReturnRight; - } - - // determine if we can pre-selection - if true_count as f32 / len as f32 <= PRE_SELECTION_THRESHOLD { - return ShortCircuitStrategy::PreSelection(bool_array); - } - } else { - // For OR, prioritize checking for all-true (short circuit case) - // Uses optimized true_count() method provided by Arrow - - // Short circuit if all values are true - if true_count == len { - return ShortCircuitStrategy::ReturnLeft; - } - - // If no true values, then all must be false - if true_count == 0 { - return ShortCircuitStrategy::ReturnRight; - } - } - } - } - ColumnarValue::Scalar(scalar) => { - // Fast path for scalar values - if let ScalarValue::Boolean(Some(is_true)) = scalar { - // Return Left for: - // - AND with false value - // - OR with true value - if (is_and && !is_true) || (!is_and && *is_true) { - return ShortCircuitStrategy::ReturnLeft; - } else { - return ShortCircuitStrategy::ReturnRight; - } - } - } - } - - // If we can't short-circuit, indicate that normal evaluation should continue - ShortCircuitStrategy::None -} - -/// Creates a new boolean array based on the evaluation of the right expression, -/// but only for positions where the left_result is true. -/// -/// This function is used for short-circuit evaluation optimization of logical AND operations: -/// - When left_result has few true values, we only evaluate the right expression for those positions -/// - Values are copied from right_array where left_result is true -/// - All other positions are filled with false values -/// -/// # Parameters -/// - `left_result` Boolean array with selection mask (typically from left side of AND) -/// - `right_result` Result of evaluating right side of expression (only for selected positions) -/// -/// # Returns -/// A combined ColumnarValue with values from right_result where left_result is true -/// -/// # Example -/// Initial Data: { 1, 2, 3, 4, 5 } -/// Left Evaluation -/// (Condition: Equal to 2 or 3) -/// ↓ -/// Filtered Data: {2, 3} -/// Left Bitmap: { 0, 1, 1, 0, 0 } -/// ↓ -/// Right Evaluation -/// (Condition: Even numbers) -/// ↓ -/// Right Data: { 2 } -/// Right Bitmap: { 1, 0 } -/// ↓ -/// Combine Results -/// Final Bitmap: { 0, 1, 0, 0, 0 } -/// -/// # Note -/// Perhaps it would be better to modify `left_result` directly without creating a copy? -/// In practice, `left_result` should have only one owner, so making changes should be safe. -/// However, this is difficult to achieve under the immutable constraints of [`Arc`] and [`BooleanArray`]. -fn pre_selection_scatter( - left_result: &BooleanArray, - right_result: ColumnarValue, -) -> Result { - let right_boolean_array = match &right_result { - ColumnarValue::Array(array) => array.as_boolean(), - ColumnarValue::Scalar(_) => return Ok(right_result), - }; - - let result_len = left_result.len(); - - let mut result_array_builder = BooleanArray::builder(result_len); - - // keep track of current position we have in right boolean array - let mut right_array_pos = 0; - - // keep track of how much is filled - let mut last_end = 0; - SlicesIterator::new(left_result).for_each(|(start, end)| { - // the gap needs to be filled with false - if start > last_end { - result_array_builder.append_n(start - last_end, false); - } - - // copy values from right array for this slice - let len = end - start; - right_boolean_array - .slice(right_array_pos, len) - .iter() - .for_each(|v| result_array_builder.append_option(v)); - - right_array_pos += len; - last_end = end; - }); - - // Fill any remaining positions with false - if last_end < result_len { - result_array_builder.append_n(result_len - last_end, false); - } - let boolean_result = result_array_builder.finish(); - - Ok(ColumnarValue::Array(Arc::new(boolean_result))) -} - fn concat_elements(left: Arc, right: Arc) -> Result { Ok(match left.data_type() { DataType::Utf8 => Arc::new(concat_elements_utf8( @@ -1071,14 +859,10 @@ pub fn similar_to( mod tests { use super::*; use crate::expressions::{col, lit, try_cast, Column, Literal}; - use datafusion_expr::lit as expr_lit; use datafusion_common::plan_datafusion_err; use datafusion_physical_expr_common::physical_expr::fmt_sql; - use crate::planner::logical2physical; - use arrow::array::BooleanArray; - use datafusion_expr::col as logical_col; /// Performs a binary operation, applying any type coercion necessary fn binary_op( left: Arc, @@ -5048,262 +4832,4 @@ mod tests { Ok(()) } - - #[test] - fn test_check_short_circuit() { - // Test with non-nullable arrays - let schema = Arc::new(Schema::new(vec![ - Field::new("a", DataType::Int32, false), - Field::new("b", DataType::Int32, false), - ])); - let a_array = Int32Array::from(vec![1, 3, 4, 5, 6]); - let b_array = Int32Array::from(vec![1, 2, 3, 4, 5]); - let batch = RecordBatch::try_new( - Arc::clone(&schema), - vec![Arc::new(a_array), Arc::new(b_array)], - ) - .unwrap(); - - // op: AND left: all false - let left_expr = logical2physical(&logical_col("a").eq(expr_lit(2)), &schema); - let left_value = left_expr.evaluate(&batch).unwrap(); - assert!(matches!( - check_short_circuit(&left_value, &Operator::And), - ShortCircuitStrategy::ReturnLeft - )); - - // op: AND left: not all false - let left_expr = logical2physical(&logical_col("a").eq(expr_lit(3)), &schema); - let left_value = left_expr.evaluate(&batch).unwrap(); - let ColumnarValue::Array(array) = &left_value else { - panic!("Expected ColumnarValue::Array"); - }; - let ShortCircuitStrategy::PreSelection(value) = - check_short_circuit(&left_value, &Operator::And) - else { - panic!("Expected ShortCircuitStrategy::PreSelection"); - }; - let expected_boolean_arr: Vec<_> = - as_boolean_array(array).unwrap().iter().collect(); - let boolean_arr: Vec<_> = value.iter().collect(); - assert_eq!(expected_boolean_arr, boolean_arr); - - // op: OR left: all true - let left_expr = logical2physical(&logical_col("a").gt(expr_lit(0)), &schema); - let left_value = left_expr.evaluate(&batch).unwrap(); - assert!(matches!( - check_short_circuit(&left_value, &Operator::Or), - ShortCircuitStrategy::ReturnLeft - )); - - // op: OR left: not all true - let left_expr: Arc = - logical2physical(&logical_col("a").gt(expr_lit(2)), &schema); - let left_value = left_expr.evaluate(&batch).unwrap(); - assert!(matches!( - check_short_circuit(&left_value, &Operator::Or), - ShortCircuitStrategy::None - )); - - // Test with nullable arrays and null values - let schema_nullable = Arc::new(Schema::new(vec![ - Field::new("c", DataType::Boolean, true), - Field::new("d", DataType::Boolean, true), - ])); - - // Create arrays with null values - let c_array = Arc::new(BooleanArray::from(vec![ - Some(true), - Some(false), - None, - Some(true), - None, - ])) as ArrayRef; - let d_array = Arc::new(BooleanArray::from(vec![ - Some(false), - Some(true), - Some(false), - None, - Some(true), - ])) as ArrayRef; - - let batch_nullable = RecordBatch::try_new( - Arc::clone(&schema_nullable), - vec![Arc::clone(&c_array), Arc::clone(&d_array)], - ) - .unwrap(); - - // Case: Mixed values with nulls - shouldn't short-circuit for AND - let mixed_nulls = logical2physical(&logical_col("c"), &schema_nullable); - let mixed_nulls_value = mixed_nulls.evaluate(&batch_nullable).unwrap(); - assert!(matches!( - check_short_circuit(&mixed_nulls_value, &Operator::And), - ShortCircuitStrategy::None - )); - - // Case: Mixed values with nulls - shouldn't short-circuit for OR - assert!(matches!( - check_short_circuit(&mixed_nulls_value, &Operator::Or), - ShortCircuitStrategy::None - )); - - // Test with all nulls - let all_nulls = Arc::new(BooleanArray::from(vec![None, None, None])) as ArrayRef; - let null_batch = RecordBatch::try_new( - Arc::new(Schema::new(vec![Field::new("e", DataType::Boolean, true)])), - vec![all_nulls], - ) - .unwrap(); - - let null_expr = logical2physical(&logical_col("e"), &null_batch.schema()); - let null_value = null_expr.evaluate(&null_batch).unwrap(); - - // All nulls shouldn't short-circuit for AND or OR - assert!(matches!( - check_short_circuit(&null_value, &Operator::And), - ShortCircuitStrategy::None - )); - assert!(matches!( - check_short_circuit(&null_value, &Operator::Or), - ShortCircuitStrategy::None - )); - - // Test with scalar values - // Scalar true - let scalar_true = ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))); - assert!(matches!( - check_short_circuit(&scalar_true, &Operator::Or), - ShortCircuitStrategy::ReturnLeft - )); // Should short-circuit OR - assert!(matches!( - check_short_circuit(&scalar_true, &Operator::And), - ShortCircuitStrategy::ReturnRight - )); // Should return the RHS for AND - - // Scalar false - let scalar_false = ColumnarValue::Scalar(ScalarValue::Boolean(Some(false))); - assert!(matches!( - check_short_circuit(&scalar_false, &Operator::And), - ShortCircuitStrategy::ReturnLeft - )); // Should short-circuit AND - assert!(matches!( - check_short_circuit(&scalar_false, &Operator::Or), - ShortCircuitStrategy::ReturnRight - )); // Should return the RHS for OR - - // Scalar null - let scalar_null = ColumnarValue::Scalar(ScalarValue::Boolean(None)); - assert!(matches!( - check_short_circuit(&scalar_null, &Operator::And), - ShortCircuitStrategy::None - )); - assert!(matches!( - check_short_circuit(&scalar_null, &Operator::Or), - ShortCircuitStrategy::None - )); - } - - /// Test for [pre_selection_scatter] - /// Since [check_short_circuit] ensures that the left side does not contain null and is neither all_true nor all_false, as well as not being empty, - /// the following tests have been designed: - /// 1. Test sparse left with interleaved true/false - /// 2. Test multiple consecutive true blocks - /// 3. Test multiple consecutive true blocks - /// 4. Test single true at first position - /// 5. Test single true at last position - /// 6. Test nulls in right array - /// 7. Test scalar right handling - #[test] - fn test_pre_selection_scatter() { - fn create_bool_array(bools: Vec) -> BooleanArray { - BooleanArray::from(bools.into_iter().map(Some).collect::>()) - } - // Test sparse left with interleaved true/false - { - // Left: [T, F, T, F, T] - // Right: [F, T, F] (values for 3 true positions) - let left = create_bool_array(vec![true, false, true, false, true]); - let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![ - false, true, false, - ]))); - - let result = pre_selection_scatter(&left, right).unwrap(); - let result_arr = result.into_array(left.len()).unwrap(); - - let expected = create_bool_array(vec![false, false, true, false, false]); - assert_eq!(&expected, result_arr.as_boolean()); - } - // Test multiple consecutive true blocks - { - // Left: [F, T, T, F, T, T, T] - // Right: [T, F, F, T, F] - let left = - create_bool_array(vec![false, true, true, false, true, true, true]); - let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![ - true, false, false, true, false, - ]))); - - let result = pre_selection_scatter(&left, right).unwrap(); - let result_arr = result.into_array(left.len()).unwrap(); - - let expected = - create_bool_array(vec![false, true, false, false, false, true, false]); - assert_eq!(&expected, result_arr.as_boolean()); - } - // Test single true at first position - { - // Left: [T, F, F] - // Right: [F] - let left = create_bool_array(vec![true, false, false]); - let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![false]))); - - let result = pre_selection_scatter(&left, right).unwrap(); - let result_arr = result.into_array(left.len()).unwrap(); - - let expected = create_bool_array(vec![false, false, false]); - assert_eq!(&expected, result_arr.as_boolean()); - } - // Test single true at last position - { - // Left: [F, F, T] - // Right: [F] - let left = create_bool_array(vec![false, false, true]); - let right = ColumnarValue::Array(Arc::new(create_bool_array(vec![false]))); - - let result = pre_selection_scatter(&left, right).unwrap(); - let result_arr = result.into_array(left.len()).unwrap(); - - let expected = create_bool_array(vec![false, false, false]); - assert_eq!(&expected, result_arr.as_boolean()); - } - // Test nulls in right array - { - // Left: [F, T, F, T] - // Right: [None, Some(false)] (with null at first position) - let left = create_bool_array(vec![false, true, false, true]); - let right_arr = BooleanArray::from(vec![None, Some(false)]); - let right = ColumnarValue::Array(Arc::new(right_arr)); - - let result = pre_selection_scatter(&left, right).unwrap(); - let result_arr = result.into_array(left.len()).unwrap(); - - let expected = BooleanArray::from(vec![ - Some(false), - None, // null from right - Some(false), - Some(false), - ]); - assert_eq!(&expected, result_arr.as_boolean()); - } - // Test scalar right handling - { - // Left: [T, F, T] - // Right: Scalar true - let left = create_bool_array(vec![true, false, true]); - let right = ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))); - - let result = pre_selection_scatter(&left, right).unwrap(); - assert!(matches!(result, ColumnarValue::Scalar(_))); - } - } } diff --git a/datafusion/physical-expr/src/expressions/dynamic_filters.rs b/datafusion/physical-expr/src/expressions/dynamic_filters.rs deleted file mode 100644 index c0a3285f0e781..0000000000000 --- a/datafusion/physical-expr/src/expressions/dynamic_filters.rs +++ /dev/null @@ -1,474 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::{ - any::Any, - fmt::Display, - hash::Hash, - sync::{Arc, RwLock}, -}; - -use crate::PhysicalExpr; -use arrow::datatypes::{DataType, Schema}; -use datafusion_common::{ - tree_node::{Transformed, TransformedResult, TreeNode}, - Result, -}; -use datafusion_expr::ColumnarValue; -use datafusion_physical_expr_common::physical_expr::{DynEq, DynHash}; - -/// A dynamic [`PhysicalExpr`] that can be updated by anyone with a reference to it. -#[derive(Debug)] -pub struct DynamicFilterPhysicalExpr { - /// The original children of this PhysicalExpr, if any. - /// This is necessary because the dynamic filter may be initialized with a placeholder (e.g. `lit(true)`) - /// and later remapped to the actual expressions that are being filtered. - /// But we need to know the children (e.g. columns referenced in the expression) ahead of time to evaluate the expression correctly. - children: Vec>, - /// If any of the children were remapped / modified (e.g. to adjust for projections) we need to keep track of the new children - /// so that when we update `current()` in subsequent iterations we can re-apply the replacements. - remapped_children: Option>>, - /// The source of dynamic filters. - inner: Arc>>, - /// For testing purposes track the data type and nullability to make sure they don't change. - /// If they do, there's a bug in the implementation. - /// But this can have overhead in production, so it's only included in our tests. - data_type: Arc>>, - nullable: Arc>>, -} - -impl Hash for DynamicFilterPhysicalExpr { - fn hash(&self, state: &mut H) { - let inner = self.current().expect("Failed to get current expression"); - inner.dyn_hash(state); - self.children.dyn_hash(state); - self.remapped_children.dyn_hash(state); - } -} - -impl PartialEq for DynamicFilterPhysicalExpr { - fn eq(&self, other: &Self) -> bool { - let inner = self.current().expect("Failed to get current expression"); - let our_children = self.remapped_children.as_ref().unwrap_or(&self.children); - let other_children = other.remapped_children.as_ref().unwrap_or(&other.children); - let other = other.current().expect("Failed to get current expression"); - inner.dyn_eq(other.as_any()) && our_children == other_children - } -} - -impl Eq for DynamicFilterPhysicalExpr {} - -impl Display for DynamicFilterPhysicalExpr { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let inner = self.current().expect("Failed to get current expression"); - write!(f, "DynamicFilterPhysicalExpr [ {} ]", inner) - } -} - -impl DynamicFilterPhysicalExpr { - /// Create a new [`DynamicFilterPhysicalExpr`] - /// from an initial expression and a list of children. - /// The list of children is provided separately because - /// the initial expression may not have the same children. - /// For example, if the initial expression is just `true` - /// it will not reference any columns, but we may know that - /// we are going to replace this expression with a real one - /// that does reference certain columns. - /// In this case you **must** pass in the columns that will be - /// used in the final expression as children to this function - /// since DataFusion is generally not compatible with dynamic - /// *children* in expressions. - /// - /// To determine the children you can: - /// - /// - Use [`collect_columns`] to collect the columns from the expression. - /// - Use existing information, such as the sort columns in a `SortExec`. - /// - /// Generally the important bit is that the *leaf children that reference columns - /// do not change* since those will be used to determine what columns need to read or projected - /// when evaluating the expression. - /// - /// [`collect_columns`]: crate::utils::collect_columns - #[allow(dead_code)] // Only used in tests for now - pub fn new( - children: Vec>, - inner: Arc, - ) -> Self { - Self { - children, - remapped_children: None, // Initially no remapped children - inner: Arc::new(RwLock::new(inner)), - data_type: Arc::new(RwLock::new(None)), - nullable: Arc::new(RwLock::new(None)), - } - } - - fn remap_children( - children: &[Arc], - remapped_children: Option<&Vec>>, - expr: Arc, - ) -> Result> { - if let Some(remapped_children) = remapped_children { - // Remap the children to the new children - // of the expression. - expr.transform_up(|child| { - // Check if this is any of our original children - if let Some(pos) = - children.iter().position(|c| c.as_ref() == child.as_ref()) - { - // If so, remap it to the current children - // of the expression. - let new_child = Arc::clone(&remapped_children[pos]); - Ok(Transformed::yes(new_child)) - } else { - // Otherwise, just return the expression - Ok(Transformed::no(child)) - } - }) - .data() - } else { - // If we don't have any remapped children, just return the expression - Ok(Arc::clone(&expr)) - } - } - - /// Get the current expression. - /// This will return the current expression with any children - /// remapped to match calls to [`PhysicalExpr::with_new_children`]. - pub fn current(&self) -> Result> { - let inner = self - .inner - .read() - .map_err(|_| { - datafusion_common::DataFusionError::Execution( - "Failed to acquire read lock for inner".to_string(), - ) - })? - .clone(); - let inner = - Self::remap_children(&self.children, self.remapped_children.as_ref(), inner)?; - Ok(inner) - } - - /// Update the current expression. - /// Any children of this expression must be a subset of the original children - /// passed to the constructor. - /// This should be called e.g.: - /// - When we've computed the probe side's hash table in a HashJoinExec - /// - After every batch is processed if we update the TopK heap in a SortExec using a TopK approach. - #[allow(dead_code)] // Only used in tests for now - pub fn update(&self, new_expr: Arc) -> Result<()> { - let mut current = self.inner.write().map_err(|_| { - datafusion_common::DataFusionError::Execution( - "Failed to acquire write lock for inner".to_string(), - ) - })?; - // Remap the children of the new expression to match the original children - // We still do this again in `current()` but doing it preventively here - // reduces the work needed in some cases if `current()` is called multiple times - // and the same externally facing `PhysicalExpr` is used for both `with_new_children` and `update()`.` - let new_expr = Self::remap_children( - &self.children, - self.remapped_children.as_ref(), - new_expr, - )?; - *current = new_expr; - Ok(()) - } -} - -impl PhysicalExpr for DynamicFilterPhysicalExpr { - fn as_any(&self) -> &dyn Any { - self - } - - fn children(&self) -> Vec<&Arc> { - self.remapped_children - .as_ref() - .unwrap_or(&self.children) - .iter() - .collect() - } - - fn with_new_children( - self: Arc, - children: Vec>, - ) -> Result> { - Ok(Arc::new(Self { - children: self.children.clone(), - remapped_children: Some(children), - inner: Arc::clone(&self.inner), - data_type: Arc::clone(&self.data_type), - nullable: Arc::clone(&self.nullable), - })) - } - - fn data_type(&self, input_schema: &Schema) -> Result { - let res = self.current()?.data_type(input_schema)?; - #[cfg(test)] - { - use datafusion_common::internal_err; - // Check if the data type has changed. - let mut data_type_lock = self - .data_type - .write() - .expect("Failed to acquire write lock for data_type"); - if let Some(existing) = &*data_type_lock { - if existing != &res { - // If the data type has changed, we have a bug. - return internal_err!( - "DynamicFilterPhysicalExpr data type has changed unexpectedly. \ - Expected: {existing:?}, Actual: {res:?}" - ); - } - } else { - *data_type_lock = Some(res.clone()); - } - } - Ok(res) - } - - fn nullable(&self, input_schema: &Schema) -> Result { - let res = self.current()?.nullable(input_schema)?; - #[cfg(test)] - { - use datafusion_common::internal_err; - // Check if the nullability has changed. - let mut nullable_lock = self - .nullable - .write() - .expect("Failed to acquire write lock for nullable"); - if let Some(existing) = *nullable_lock { - if existing != res { - // If the nullability has changed, we have a bug. - return internal_err!( - "DynamicFilterPhysicalExpr nullability has changed unexpectedly. \ - Expected: {existing}, Actual: {res}" - ); - } - } else { - *nullable_lock = Some(res); - } - } - Ok(res) - } - - fn evaluate( - &self, - batch: &arrow::record_batch::RecordBatch, - ) -> Result { - let current = self.current()?; - #[cfg(test)] - { - // Ensure that we are not evaluating after the expression has changed. - let schema = batch.schema(); - self.nullable(&schema)?; - self.data_type(&schema)?; - }; - current.evaluate(batch) - } - - fn fmt_sql(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let inner = self.current().map_err(|_| std::fmt::Error)?; - inner.fmt_sql(f) - } - - fn snapshot(&self) -> Result>> { - // Return the current expression as a snapshot. - Ok(Some(self.current()?)) - } -} - -#[cfg(test)] -mod test { - use crate::{ - expressions::{col, lit, BinaryExpr}, - utils::reassign_predicate_columns, - }; - use arrow::{ - array::RecordBatch, - datatypes::{DataType, Field, Schema}, - }; - use datafusion_common::ScalarValue; - - use super::*; - - #[test] - fn test_remap_children() { - let table_schema = Arc::new(Schema::new(vec![ - Field::new("a", DataType::Int32, false), - Field::new("b", DataType::Int32, false), - ])); - let expr = Arc::new(BinaryExpr::new( - col("a", &table_schema).unwrap(), - datafusion_expr::Operator::Eq, - lit(42) as Arc, - )); - let dynamic_filter = Arc::new(DynamicFilterPhysicalExpr::new( - vec![col("a", &table_schema).unwrap()], - expr as Arc, - )); - // Simulate two `ParquetSource` files with different filter schemas - // Both of these should hit the same inner `PhysicalExpr` even after `update()` is called - // and be able to remap children independently. - let filter_schema_1 = Arc::new(Schema::new(vec![ - Field::new("a", DataType::Int32, false), - Field::new("b", DataType::Int32, false), - ])); - let filter_schema_2 = Arc::new(Schema::new(vec![ - Field::new("b", DataType::Int32, false), - Field::new("a", DataType::Int32, false), - ])); - // Each ParquetExec calls `with_new_children` on the DynamicFilterPhysicalExpr - // and remaps the children to the file schema. - let dynamic_filter_1 = reassign_predicate_columns( - Arc::clone(&dynamic_filter) as Arc, - &filter_schema_1, - false, - ) - .unwrap(); - let snap = dynamic_filter_1.snapshot().unwrap().unwrap(); - insta::assert_snapshot!(format!("{snap:?}"), @r#"BinaryExpr { left: Column { name: "a", index: 0 }, op: Eq, right: Literal { value: Int32(42) }, fail_on_overflow: false }"#); - let dynamic_filter_2 = reassign_predicate_columns( - Arc::clone(&dynamic_filter) as Arc, - &filter_schema_2, - false, - ) - .unwrap(); - let snap = dynamic_filter_2.snapshot().unwrap().unwrap(); - insta::assert_snapshot!(format!("{snap:?}"), @r#"BinaryExpr { left: Column { name: "a", index: 1 }, op: Eq, right: Literal { value: Int32(42) }, fail_on_overflow: false }"#); - // Both filters allow evaluating the same expression - let batch_1 = RecordBatch::try_new( - Arc::clone(&filter_schema_1), - vec![ - // a - ScalarValue::Int32(Some(42)).to_array_of_size(1).unwrap(), - // b - ScalarValue::Int32(Some(43)).to_array_of_size(1).unwrap(), - ], - ) - .unwrap(); - let batch_2 = RecordBatch::try_new( - Arc::clone(&filter_schema_2), - vec![ - // b - ScalarValue::Int32(Some(43)).to_array_of_size(1).unwrap(), - // a - ScalarValue::Int32(Some(42)).to_array_of_size(1).unwrap(), - ], - ) - .unwrap(); - // Evaluate the expression on both batches - let result_1 = dynamic_filter_1.evaluate(&batch_1).unwrap(); - let result_2 = dynamic_filter_2.evaluate(&batch_2).unwrap(); - // Check that the results are the same - let ColumnarValue::Array(arr_1) = result_1 else { - panic!("Expected ColumnarValue::Array"); - }; - let ColumnarValue::Array(arr_2) = result_2 else { - panic!("Expected ColumnarValue::Array"); - }; - assert!(arr_1.eq(&arr_2)); - let expected = ScalarValue::Boolean(Some(true)) - .to_array_of_size(1) - .unwrap(); - assert!(arr_1.eq(&expected)); - // Now lets update the expression - // Note that we update the *original* expression and that should be reflected in both the derived expressions - let new_expr = Arc::new(BinaryExpr::new( - col("a", &table_schema).unwrap(), - datafusion_expr::Operator::Gt, - lit(43) as Arc, - )); - dynamic_filter - .update(Arc::clone(&new_expr) as Arc) - .expect("Failed to update expression"); - // Now we should be able to evaluate the new expression on both batches - let result_1 = dynamic_filter_1.evaluate(&batch_1).unwrap(); - let result_2 = dynamic_filter_2.evaluate(&batch_2).unwrap(); - // Check that the results are the same - let ColumnarValue::Array(arr_1) = result_1 else { - panic!("Expected ColumnarValue::Array"); - }; - let ColumnarValue::Array(arr_2) = result_2 else { - panic!("Expected ColumnarValue::Array"); - }; - assert!(arr_1.eq(&arr_2)); - let expected = ScalarValue::Boolean(Some(false)) - .to_array_of_size(1) - .unwrap(); - assert!(arr_1.eq(&expected)); - } - - #[test] - fn test_snapshot() { - let expr = lit(42) as Arc; - let dynamic_filter = DynamicFilterPhysicalExpr::new(vec![], Arc::clone(&expr)); - - // Take a snapshot of the current expression - let snapshot = dynamic_filter.snapshot().unwrap(); - assert_eq!(snapshot, Some(expr)); - - // Update the current expression - let new_expr = lit(100) as Arc; - dynamic_filter.update(Arc::clone(&new_expr)).unwrap(); - // Take another snapshot - let snapshot = dynamic_filter.snapshot().unwrap(); - assert_eq!(snapshot, Some(new_expr)); - } - - #[test] - fn test_dynamic_filter_physical_expr_misbehaves_data_type_nullable() { - let dynamic_filter = - DynamicFilterPhysicalExpr::new(vec![], lit(42) as Arc); - - // First call to data_type and nullable should set the initial values. - let initial_data_type = dynamic_filter.data_type(&Schema::empty()).unwrap(); - let initial_nullable = dynamic_filter.nullable(&Schema::empty()).unwrap(); - - // Call again and expect no change. - let second_data_type = dynamic_filter.data_type(&Schema::empty()).unwrap(); - let second_nullable = dynamic_filter.nullable(&Schema::empty()).unwrap(); - assert_eq!( - initial_data_type, second_data_type, - "Data type should not change on second call." - ); - assert_eq!( - initial_nullable, second_nullable, - "Nullability should not change on second call." - ); - - // Now change the current expression to something else. - dynamic_filter - .update(lit(ScalarValue::Utf8(None)) as Arc) - .expect("Failed to update expression"); - // Check that we error if we call data_type, nullable or evaluate after changing the expression. - assert!( - dynamic_filter.data_type(&Schema::empty()).is_err(), - "Expected err when data_type is called after changing the expression." - ); - assert!( - dynamic_filter.nullable(&Schema::empty()).is_err(), - "Expected err when nullable is called after changing the expression." - ); - let batch = RecordBatch::new_empty(Arc::new(Schema::empty())); - assert!( - dynamic_filter.evaluate(&batch).is_err(), - "Expected err when evaluate is called after changing the expression." - ); - } -} diff --git a/datafusion/physical-expr/src/expressions/mod.rs b/datafusion/physical-expr/src/expressions/mod.rs index d77207fbbcd76..f00b49f503141 100644 --- a/datafusion/physical-expr/src/expressions/mod.rs +++ b/datafusion/physical-expr/src/expressions/mod.rs @@ -22,7 +22,6 @@ mod binary; mod case; mod cast; mod column; -mod dynamic_filters; mod in_list; mod is_not_null; mod is_null; diff --git a/datafusion/physical-expr/src/lib.rs b/datafusion/physical-expr/src/lib.rs index 9f795c81fa48e..93ced2eb628d8 100644 --- a/datafusion/physical-expr/src/lib.rs +++ b/datafusion/physical-expr/src/lib.rs @@ -68,7 +68,7 @@ pub use planner::{create_physical_expr, create_physical_exprs}; pub use scalar_function::ScalarFunctionExpr; pub use datafusion_physical_expr_common::utils::reverse_order_bys; -pub use utils::{conjunction, conjunction_opt, split_conjunction}; +pub use utils::split_conjunction; // For backwards compatibility pub mod tree_node { diff --git a/datafusion/physical-expr/src/planner.rs b/datafusion/physical-expr/src/planner.rs index 8660bff796d5a..fac83dfc45247 100644 --- a/datafusion/physical-expr/src/planner.rs +++ b/datafusion/physical-expr/src/planner.rs @@ -102,7 +102,7 @@ use datafusion_expr::{ /// /// * `e` - The logical expression /// * `input_dfschema` - The DataFusion schema for the input, used to resolve `Column` references -/// to qualified or unqualified fields by name. +/// to qualified or unqualified fields by name. pub fn create_physical_expr( e: &Expr, input_dfschema: &DFSchema, diff --git a/datafusion/physical-expr/src/utils/mod.rs b/datafusion/physical-expr/src/utils/mod.rs index b4d0758fd2e81..7e4c7f0e10ba8 100644 --- a/datafusion/physical-expr/src/utils/mod.rs +++ b/datafusion/physical-expr/src/utils/mod.rs @@ -47,31 +47,6 @@ pub fn split_conjunction( split_impl(Operator::And, predicate, vec![]) } -/// Create a conjunction of the given predicates. -/// If the input is empty, return a literal true. -/// If the input contains a single predicate, return the predicate. -/// Otherwise, return a conjunction of the predicates (e.g. `a AND b AND c`). -pub fn conjunction( - predicates: impl IntoIterator>, -) -> Arc { - conjunction_opt(predicates).unwrap_or_else(|| crate::expressions::lit(true)) -} - -/// Create a conjunction of the given predicates. -/// If the input is empty or the return None. -/// If the input contains a single predicate, return Some(predicate). -/// Otherwise, return a Some(..) of a conjunction of the predicates (e.g. `Some(a AND b AND c)`). -pub fn conjunction_opt( - predicates: impl IntoIterator>, -) -> Option> { - predicates - .into_iter() - .fold(None, |acc, predicate| match acc { - None => Some(predicate), - Some(acc) => Some(Arc::new(BinaryExpr::new(acc, Operator::And, predicate))), - }) -} - /// Assume the predicate is in the form of DNF, split the predicate to a Vec of PhysicalExprs. /// /// For example, split "a1 = a2 OR b1 <= b2 OR c1 != c2" into ["a1 = a2", "b1 <= b2", "c1 != c2"] diff --git a/datafusion/physical-optimizer/src/aggregate_statistics.rs b/datafusion/physical-optimizer/src/aggregate_statistics.rs index 28ee10eb650a0..0d3d83c58373f 100644 --- a/datafusion/physical-optimizer/src/aggregate_statistics.rs +++ b/datafusion/physical-optimizer/src/aggregate_statistics.rs @@ -42,7 +42,6 @@ impl AggregateStatistics { impl PhysicalOptimizerRule for AggregateStatistics { #[cfg_attr(feature = "recursive_protection", recursive::recursive)] - #[allow(clippy::only_used_in_recursion)] // See https://github.com/rust-lang/rust-clippy/issues/14566 fn optimize( &self, plan: Arc, diff --git a/datafusion/physical-optimizer/src/enforce_distribution.rs b/datafusion/physical-optimizer/src/enforce_distribution.rs index 523762401dfad..5e76edad1f569 100644 --- a/datafusion/physical-optimizer/src/enforce_distribution.rs +++ b/datafusion/physical-optimizer/src/enforce_distribution.rs @@ -837,7 +837,7 @@ fn new_join_conditions( /// /// * `input`: Current node. /// * `n_target`: desired target partition number, if partition number of the -/// current executor is less than this value. Partition number will be increased. +/// current executor is less than this value. Partition number will be increased. /// /// # Returns /// @@ -880,7 +880,7 @@ fn add_roundrobin_on_top( /// * `input`: Current node. /// * `hash_exprs`: Stores Physical Exprs that are used during hashing. /// * `n_target`: desired target partition number, if partition number of the -/// current executor is less than this value. Partition number will be increased. +/// current executor is less than this value. Partition number will be increased. /// /// # Returns /// @@ -1018,7 +1018,7 @@ fn remove_dist_changing_operators( /// " RepartitionExec: partitioning=RoundRobinBatch(10), input_partitions=2", /// " DataSourceExec: file_groups={2 groups: \[\[x], \[y]]}, projection=\[a, b, c, d, e], output_ordering=\[a@0 ASC], file_type=parquet", /// ``` -pub fn replace_order_preserving_variants( +fn replace_order_preserving_variants( mut context: DistributionContext, ) -> Result { context.children = context @@ -1035,10 +1035,7 @@ pub fn replace_order_preserving_variants( if is_sort_preserving_merge(&context.plan) { let child_plan = Arc::clone(&context.children[0].plan); - // It's safe to unwrap because `CoalescePartitionsExec` supports `fetch`. - context.plan = CoalescePartitionsExec::new(child_plan) - .with_fetch(context.plan.fetch()) - .unwrap(); + context.plan = Arc::new(CoalescePartitionsExec::new(child_plan)); return Ok(context); } else if let Some(repartition) = context.plan.as_any().downcast_ref::() diff --git a/datafusion/physical-optimizer/src/enforce_sorting/mod.rs b/datafusion/physical-optimizer/src/enforce_sorting/mod.rs index b606aa85c1e16..20733b65692fc 100644 --- a/datafusion/physical-optimizer/src/enforce_sorting/mod.rs +++ b/datafusion/physical-optimizer/src/enforce_sorting/mod.rs @@ -400,7 +400,6 @@ pub fn parallelize_sorts( ), )) } else if is_coalesce_partitions(&requirements.plan) { - let fetch = requirements.plan.fetch(); // There is an unnecessary `CoalescePartitionsExec` in the plan. // This will handle the recursive `CoalescePartitionsExec` plans. requirements = remove_bottleneck_in_subplan(requirements)?; @@ -409,10 +408,7 @@ pub fn parallelize_sorts( Ok(Transformed::yes( PlanWithCorrespondingCoalescePartitions::new( - // Safe to unwrap, because `CoalescePartitionsExec` has a fetch - CoalescePartitionsExec::new(Arc::clone(&requirements.plan)) - .with_fetch(fetch) - .unwrap(), + Arc::new(CoalescePartitionsExec::new(Arc::clone(&requirements.plan))), false, vec![requirements], ), diff --git a/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs b/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs index 7fe62a146afb9..2c5c0d4d510ec 100644 --- a/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs +++ b/datafusion/physical-optimizer/src/enforce_sorting/replace_with_order_preserving_variants.rs @@ -27,7 +27,7 @@ use crate::utils::{ use datafusion_common::config::ConfigOptions; use datafusion_common::tree_node::Transformed; -use datafusion_common::{internal_err, Result}; +use datafusion_common::Result; use datafusion_physical_expr_common::sort_expr::LexOrdering; use datafusion_physical_plan::coalesce_partitions::CoalescePartitionsExec; use datafusion_physical_plan::execution_plan::EmissionType; @@ -93,7 +93,7 @@ pub fn update_order_preservation_ctx_children_data(opc: &mut OrderPreservationCo /// inside `sort_input` with their order-preserving variants. This will /// generate an alternative plan, which will be accepted or rejected later on /// depending on whether it helps us remove a `SortExec`. -pub fn plan_with_order_preserving_variants( +fn plan_with_order_preserving_variants( mut sort_input: OrderPreservationContext, // Flag indicating that it is desirable to replace `RepartitionExec`s with // `SortPreservingRepartitionExec`s: @@ -138,19 +138,6 @@ pub fn plan_with_order_preserving_variants( } else if is_coalesce_partitions(&sort_input.plan) && is_spm_better { let child = &sort_input.children[0].plan; if let Some(ordering) = child.output_ordering() { - let mut fetch = fetch; - if let Some(coalesce_fetch) = sort_input.plan.fetch() { - if let Some(sort_fetch) = fetch { - if coalesce_fetch < sort_fetch { - return internal_err!( - "CoalescePartitionsExec fetch [{:?}] should be greater than or equal to SortExec fetch [{:?}]", coalesce_fetch, sort_fetch - ); - } - } else { - // If the sort node does not have a fetch, we need to keep the coalesce node's fetch. - fetch = Some(coalesce_fetch); - } - }; // When the input of a `CoalescePartitionsExec` has an ordering, // replace it with a `SortPreservingMergeExec` if appropriate: let spm = SortPreservingMergeExec::new(ordering.clone(), Arc::clone(child)) diff --git a/datafusion/physical-optimizer/src/lib.rs b/datafusion/physical-optimizer/src/lib.rs index 57dac21b6eeed..35503f3b0b5f9 100644 --- a/datafusion/physical-optimizer/src/lib.rs +++ b/datafusion/physical-optimizer/src/lib.rs @@ -36,7 +36,6 @@ pub mod optimizer; pub mod output_requirements; pub mod projection_pushdown; pub mod pruning; -pub mod push_down_filter; pub mod sanity_checker; pub mod topk_aggregation; pub mod update_aggr_exprs; diff --git a/datafusion/physical-optimizer/src/limit_pushdown.rs b/datafusion/physical-optimizer/src/limit_pushdown.rs index 7469c3af9344c..5887cb51a727b 100644 --- a/datafusion/physical-optimizer/src/limit_pushdown.rs +++ b/datafusion/physical-optimizer/src/limit_pushdown.rs @@ -246,7 +246,16 @@ pub fn pushdown_limit_helper( Ok((Transformed::no(pushdown_plan), global_state)) } } else { - global_state.satisfied = true; + // Add fetch or a `LimitExec`: + // If the plan's children have limit and the child's limit < parent's limit, we shouldn't change the global state to true, + // because the children limit will be overridden if the global state is changed. + if !pushdown_plan + .children() + .iter() + .any(|&child| extract_limit(child).is_some()) + { + global_state.satisfied = true; + } pushdown_plan = if let Some(plan_with_fetch) = maybe_fetchable { if global_skip > 0 { add_global_limit(plan_with_fetch, global_skip, Some(global_fetch)) diff --git a/datafusion/physical-optimizer/src/optimizer.rs b/datafusion/physical-optimizer/src/optimizer.rs index d4ff7d6b9e153..bab31150e2508 100644 --- a/datafusion/physical-optimizer/src/optimizer.rs +++ b/datafusion/physical-optimizer/src/optimizer.rs @@ -30,7 +30,6 @@ use crate::limit_pushdown::LimitPushdown; use crate::limited_distinct_aggregation::LimitedDistinctAggregation; use crate::output_requirements::OutputRequirements; use crate::projection_pushdown::ProjectionPushdown; -use crate::push_down_filter::PushdownFilter; use crate::sanity_checker::SanityCheckPlan; use crate::topk_aggregation::TopKAggregation; use crate::update_aggr_exprs::OptimizeAggregateOrder; @@ -122,10 +121,6 @@ impl PhysicalOptimizer { // into an `order by max(x) limit y`. In this case it will copy the limit value down // to the aggregation, allowing it to use only y number of accumulators. Arc::new(TopKAggregation::new()), - // The FilterPushdown rule tries to push down filters as far as it can. - // For example, it will push down filtering from a `FilterExec` to - // a `DataSourceExec`, or from a `TopK`'s current state to a `DataSourceExec`. - Arc::new(PushdownFilter::new()), // The LimitPushdown rule tries to push limits down as far as possible, // replacing operators with fetching variants, or adding limits // past operators that support limit pushdown. diff --git a/datafusion/physical-optimizer/src/pruning.rs b/datafusion/physical-optimizer/src/pruning.rs index 1dd168f181676..b5287f3d33f3c 100644 --- a/datafusion/physical-optimizer/src/pruning.rs +++ b/datafusion/physical-optimizer/src/pruning.rs @@ -41,7 +41,6 @@ use datafusion_common::{Column, DFSchema}; use datafusion_expr_common::operator::Operator; use datafusion_physical_expr::utils::{collect_columns, Guarantee, LiteralGuarantee}; use datafusion_physical_expr::{expressions as phys_expr, PhysicalExprRef}; -use datafusion_physical_expr_common::physical_expr::snapshot_physical_expr; use datafusion_physical_plan::{ColumnarValue, PhysicalExpr}; /// A source of runtime statistical information to [`PruningPredicate`]s. @@ -313,13 +312,13 @@ pub trait PruningStatistics { /// * `true`: there MAY be rows that pass the predicate, **KEEPS** the container /// /// * `NULL`: there MAY be rows that pass the predicate, **KEEPS** the container -/// Note that rewritten predicate can evaluate to NULL when some of -/// the min/max values are not known. *Note that this is different than -/// the SQL filter semantics where `NULL` means the row is filtered -/// out.* +/// Note that rewritten predicate can evaluate to NULL when some of +/// the min/max values are not known. *Note that this is different than +/// the SQL filter semantics where `NULL` means the row is filtered +/// out.* /// /// * `false`: there are no rows that could possibly match the predicate, -/// **PRUNES** the container +/// **PRUNES** the container /// /// For example, given a column `x`, the `x_min`, `x_max`, `x_null_count`, and /// `x_row_count` represent the minimum and maximum values, the null count of @@ -528,9 +527,6 @@ impl PruningPredicate { /// See the struct level documentation on [`PruningPredicate`] for more /// details. pub fn try_new(expr: Arc, schema: SchemaRef) -> Result { - // Get a (simpler) snapshot of the physical expr here to use with `PruningPredicate` - // which does not handle dynamic exprs in general - let expr = snapshot_physical_expr(expr)?; let unhandled_hook = Arc::new(ConstantUnhandledPredicateHook::default()) as _; // build predicate expression once diff --git a/datafusion/physical-optimizer/src/push_down_filter.rs b/datafusion/physical-optimizer/src/push_down_filter.rs deleted file mode 100644 index 80201454d06d4..0000000000000 --- a/datafusion/physical-optimizer/src/push_down_filter.rs +++ /dev/null @@ -1,535 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::sync::Arc; - -use crate::PhysicalOptimizerRule; - -use datafusion_common::tree_node::{Transformed, TreeNode, TreeNodeRecursion}; -use datafusion_common::{config::ConfigOptions, Result}; -use datafusion_physical_expr::conjunction; -use datafusion_physical_plan::filter::FilterExec; -use datafusion_physical_plan::filter_pushdown::{ - FilterDescription, FilterPushdownResult, FilterPushdownSupport, -}; -use datafusion_physical_plan::tree_node::PlanContext; -use datafusion_physical_plan::ExecutionPlan; - -/// Attempts to recursively push given filters from the top of the tree into leafs. -/// -/// # Default Implementation -/// -/// The default implementation in [`ExecutionPlan::try_pushdown_filters`] is a no-op -/// that assumes that: -/// -/// * Parent filters can't be passed onto children. -/// * This node has no filters to contribute. -/// -/// # Example: Push filter into a `DataSourceExec` -/// -/// For example, consider the following plan: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ CoalesceBatchesExec │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ FilterExec │ -/// │ filters = [ id=1] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// └──────────────────────┘ -/// ``` -/// -/// Our goal is to move the `id = 1` filter from the [`FilterExec`] node to the `DataSourceExec` node. -/// -/// If this filter is selective pushing it into the scan can avoid massive -/// amounts of data being read from the source (the projection is `*` so all -/// matching columns are read). -/// -/// The new plan looks like: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ CoalesceBatchesExec │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// │ filters = [ id=1] │ -/// └──────────────────────┘ -/// ``` -/// -/// # Example: Push filters with `ProjectionExec` -/// -/// Let's consider a more complex example involving a [`ProjectionExec`] -/// node in between the [`FilterExec`] and `DataSourceExec` nodes that -/// creates a new column that the filter depends on. -/// -/// ```text -/// ┌──────────────────────┐ -/// │ CoalesceBatchesExec │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ FilterExec │ -/// │ filters = │ -/// │ [cost>50,id=1] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ ProjectionExec │ -/// │ cost = price * 1.2 │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// └──────────────────────┘ -/// ``` -/// -/// We want to push down the filters `[id=1]` to the `DataSourceExec` node, -/// but can't push down `cost>50` because it requires the [`ProjectionExec`] -/// node to be executed first. A simple thing to do would be to split up the -/// filter into two separate filters and push down the first one: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ CoalesceBatchesExec │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ FilterExec │ -/// │ filters = │ -/// │ [cost>50] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ ProjectionExec │ -/// │ cost = price * 1.2 │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// │ filters = [ id=1] │ -/// └──────────────────────┘ -/// ``` -/// -/// We can actually however do better by pushing down `price * 1.2 > 50` -/// instead of `cost > 50`: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ CoalesceBatchesExec │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ ProjectionExec │ -/// │ cost = price * 1.2 │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// │ filters = [id=1, │ -/// │ price * 1.2 > 50] │ -/// └──────────────────────┘ -/// ``` -/// -/// # Example: Push filters within a subtree -/// -/// There are also cases where we may be able to push down filters within a -/// subtree but not the entire tree. A good example of this is aggregation -/// nodes: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ ProjectionExec │ -/// │ projection = * │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ FilterExec │ -/// │ filters = [sum > 10] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌───────────────────────┐ -/// │ AggregateExec │ -/// │ group by = [id] │ -/// │ aggregate = │ -/// │ [sum(price)] │ -/// └───────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ FilterExec │ -/// │ filters = [id=1] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// └──────────────────────┘ -/// ``` -/// -/// The transformation here is to push down the `id=1` filter to the -/// `DataSourceExec` node: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ ProjectionExec │ -/// │ projection = * │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ FilterExec │ -/// │ filters = [sum > 10] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌───────────────────────┐ -/// │ AggregateExec │ -/// │ group by = [id] │ -/// │ aggregate = │ -/// │ [sum(price)] │ -/// └───────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// │ filters = [id=1] │ -/// └──────────────────────┘ -/// ``` -/// -/// The point here is that: -/// 1. We cannot push down `sum > 10` through the [`AggregateExec`] node into the `DataSourceExec` node. -/// Any filters above the [`AggregateExec`] node are not pushed down. -/// This is determined by calling [`ExecutionPlan::try_pushdown_filters`] on the [`AggregateExec`] node. -/// 2. We need to keep recursing into the tree so that we can discover the other [`FilterExec`] node and push -/// down the `id=1` filter. -/// -/// # Example: Push filters through Joins -/// -/// It is also possible to push down filters through joins and filters that -/// originate from joins. For example, a hash join where we build a hash -/// table of the left side and probe the right side (ignoring why we would -/// choose this order, typically it depends on the size of each table, -/// etc.). -/// -/// ```text -/// ┌─────────────────────┐ -/// │ FilterExec │ -/// │ filters = │ -/// │ [d.size > 100] │ -/// └─────────────────────┘ -/// │ -/// │ -/// ┌──────────▼──────────┐ -/// │ │ -/// │ HashJoinExec │ -/// │ [u.dept@hash(d.id)] │ -/// │ │ -/// └─────────────────────┘ -/// │ -/// ┌────────────┴────────────┐ -/// ┌──────────▼──────────┐ ┌──────────▼──────────┐ -/// │ DataSourceExec │ │ DataSourceExec │ -/// │ alias [users as u] │ │ alias [dept as d] │ -/// │ │ │ │ -/// └─────────────────────┘ └─────────────────────┘ -/// ``` -/// -/// There are two pushdowns we can do here: -/// 1. Push down the `d.size > 100` filter through the `HashJoinExec` node to the `DataSourceExec` -/// node for the `departments` table. -/// 2. Push down the hash table state from the `HashJoinExec` node to the `DataSourceExec` node to avoid reading -/// rows from the `users` table that will be eliminated by the join. -/// This can be done via a bloom filter or similar and is not (yet) supported -/// in DataFusion. See . -/// -/// ```text -/// ┌─────────────────────┐ -/// │ │ -/// │ HashJoinExec │ -/// │ [u.dept@hash(d.id)] │ -/// │ │ -/// └─────────────────────┘ -/// │ -/// ┌────────────┴────────────┐ -/// ┌──────────▼──────────┐ ┌──────────▼──────────┐ -/// │ DataSourceExec │ │ DataSourceExec │ -/// │ alias [users as u] │ │ alias [dept as d] │ -/// │ filters = │ │ filters = │ -/// │ [depg@hash(d.id)] │ │ [ d.size > 100] │ -/// └─────────────────────┘ └─────────────────────┘ -/// ``` -/// -/// You may notice in this case that the filter is *dynamic*: the hash table -/// is built _after_ the `departments` table is read and at runtime. We -/// don't have a concrete `InList` filter or similar to push down at -/// optimization time. These sorts of dynamic filters are handled by -/// building a specialized [`PhysicalExpr`] that can be evaluated at runtime -/// and internally maintains a reference to the hash table or other state. -/// -/// To make working with these sorts of dynamic filters more tractable we have the method [`PhysicalExpr::snapshot`] -/// which attempts to simplify a dynamic filter into a "basic" non-dynamic filter. -/// For a join this could mean converting it to an `InList` filter or a min/max filter for example. -/// See `datafusion/physical-plan/src/dynamic_filters.rs` for more details. -/// -/// # Example: Push TopK filters into Scans -/// -/// Another form of dynamic filter is pushing down the state of a `TopK` -/// operator for queries like `SELECT * FROM t ORDER BY id LIMIT 10`: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ TopK │ -/// │ limit = 10 │ -/// │ order by = [id] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// └──────────────────────┘ -/// ``` -/// -/// We can avoid large amounts of data processing by transforming this into: -/// -/// ```text -/// ┌──────────────────────┐ -/// │ TopK │ -/// │ limit = 10 │ -/// │ order by = [id] │ -/// └──────────────────────┘ -/// │ -/// ▼ -/// ┌──────────────────────┐ -/// │ DataSourceExec │ -/// │ projection = * │ -/// │ filters = │ -/// │ [id < @ TopKHeap] │ -/// └──────────────────────┘ -/// ``` -/// -/// Now as we fill our `TopK` heap we can push down the state of the heap to -/// the `DataSourceExec` node to avoid reading files / row groups / pages / -/// rows that could not possibly be in the top 10. -/// -/// This is not yet implemented in DataFusion. See -/// -/// -/// [`PhysicalExpr`]: datafusion_physical_plan::PhysicalExpr -/// [`PhysicalExpr::snapshot`]: datafusion_physical_plan::PhysicalExpr::snapshot -/// [`FilterExec`]: datafusion_physical_plan::filter::FilterExec -/// [`ProjectionExec`]: datafusion_physical_plan::projection::ProjectionExec -/// [`AggregateExec`]: datafusion_physical_plan::aggregates::AggregateExec -#[derive(Debug)] -pub struct PushdownFilter {} - -impl Default for PushdownFilter { - fn default() -> Self { - Self::new() - } -} - -pub type FilterDescriptionContext = PlanContext; - -impl PhysicalOptimizerRule for PushdownFilter { - fn optimize( - &self, - plan: Arc, - config: &ConfigOptions, - ) -> Result> { - let context = FilterDescriptionContext::new_default(plan); - - context - .transform_up(|node| { - if node.plan.as_any().downcast_ref::().is_some() { - let initial_plan = Arc::clone(&node.plan); - let mut accept_updated = false; - let updated_node = node.transform_down(|filter_node| { - Self::try_pushdown(filter_node, config, &mut accept_updated) - }); - - if accept_updated { - updated_node - } else { - Ok(Transformed::no(FilterDescriptionContext::new_default( - initial_plan, - ))) - } - } - // Other filter introducing operators extends here - else { - Ok(Transformed::no(node)) - } - }) - .map(|updated| updated.data.plan) - } - - fn name(&self) -> &str { - "PushdownFilter" - } - - fn schema_check(&self) -> bool { - true // Filter pushdown does not change the schema of the plan - } -} - -impl PushdownFilter { - pub fn new() -> Self { - Self {} - } - - fn try_pushdown( - mut node: FilterDescriptionContext, - config: &ConfigOptions, - accept_updated: &mut bool, - ) -> Result> { - let initial_description = FilterDescription { - filters: node.data.take_description(), - }; - - let FilterPushdownResult { - support, - remaining_description, - } = node - .plan - .try_pushdown_filters(initial_description, config)?; - - match support { - FilterPushdownSupport::Supported { - mut child_descriptions, - op, - revisit, - } => { - if revisit { - // This check handles cases where the current operator is entirely removed - // from the plan and replaced with its child. In such cases, to not skip - // over the new node, we need to explicitly re-apply this pushdown logic - // to the new node. - // - // TODO: If TreeNodeRecursion supports a Revisit mechanism in the future, - // this manual recursion could be removed. - - // If the operator is removed, it should not leave any filters as remaining - debug_assert!(remaining_description.filters.is_empty()); - // Operators having 2 children cannot be removed - debug_assert_eq!(child_descriptions.len(), 1); - debug_assert_eq!(node.children.len(), 1); - - node.plan = op; - node.data = child_descriptions.swap_remove(0); - node.children = node.children.swap_remove(0).children; - Self::try_pushdown(node, config, accept_updated) - } else { - if remaining_description.filters.is_empty() { - // Filter can be pushed down safely - node.plan = op; - if node.children.is_empty() { - *accept_updated = true; - } else { - for (child, descr) in - node.children.iter_mut().zip(child_descriptions) - { - child.data = descr; - } - } - } else { - // Filter cannot be pushed down - node = insert_filter_exec( - node, - child_descriptions, - remaining_description, - )?; - } - Ok(Transformed::yes(node)) - } - } - FilterPushdownSupport::NotSupported => { - if remaining_description.filters.is_empty() { - Ok(Transformed { - data: node, - transformed: false, - tnr: TreeNodeRecursion::Stop, - }) - } else { - node = insert_filter_exec( - node, - vec![FilterDescription::empty(); 1], - remaining_description, - )?; - Ok(Transformed { - data: node, - transformed: true, - tnr: TreeNodeRecursion::Stop, - }) - } - } - } - } -} - -fn insert_filter_exec( - node: FilterDescriptionContext, - mut child_descriptions: Vec, - remaining_description: FilterDescription, -) -> Result { - let mut new_child_node = node; - - // Filter has one child - if !child_descriptions.is_empty() { - debug_assert_eq!(child_descriptions.len(), 1); - new_child_node.data = child_descriptions.swap_remove(0); - } - let new_plan = Arc::new(FilterExec::try_new( - conjunction(remaining_description.filters), - Arc::clone(&new_child_node.plan), - )?); - let new_children = vec![new_child_node]; - let new_data = FilterDescription::empty(); - - Ok(FilterDescriptionContext::new( - new_plan, - new_data, - new_children, - )) -} diff --git a/datafusion/physical-plan/Cargo.toml b/datafusion/physical-plan/Cargo.toml index 5210ee26755c9..1f38e2ed31263 100644 --- a/datafusion/physical-plan/Cargo.toml +++ b/datafusion/physical-plan/Cargo.toml @@ -72,7 +72,6 @@ insta = { workspace = true } rand = { workspace = true } rstest = { workspace = true } rstest_reuse = "0.7.0" -tempfile = "3.19.1" tokio = { workspace = true, features = [ "rt-multi-thread", "fs", @@ -82,7 +81,3 @@ tokio = { workspace = true, features = [ [[bench]] harness = false name = "partial_ordering" - -[[bench]] -harness = false -name = "spill_io" diff --git a/datafusion/physical-plan/benches/spill_io.rs b/datafusion/physical-plan/benches/spill_io.rs deleted file mode 100644 index 3b877671ad583..0000000000000 --- a/datafusion/physical-plan/benches/spill_io.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use arrow::array::{ - Date32Builder, Decimal128Builder, Int32Builder, RecordBatch, StringBuilder, -}; -use arrow::datatypes::{DataType, Field, Schema}; -use criterion::{criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; -use datafusion_execution::runtime_env::RuntimeEnv; -use datafusion_physical_plan::common::collect; -use datafusion_physical_plan::metrics::{ExecutionPlanMetricsSet, SpillMetrics}; -use datafusion_physical_plan::SpillManager; -use std::sync::Arc; -use tokio::runtime::Runtime; - -pub fn create_batch(num_rows: usize, allow_nulls: bool) -> RecordBatch { - let schema = Arc::new(Schema::new(vec![ - Field::new("c0", DataType::Int32, true), - Field::new("c1", DataType::Utf8, true), - Field::new("c2", DataType::Date32, true), - Field::new("c3", DataType::Decimal128(11, 2), true), - ])); - - let mut a = Int32Builder::new(); - let mut b = StringBuilder::new(); - let mut c = Date32Builder::new(); - let mut d = Decimal128Builder::new() - .with_precision_and_scale(11, 2) - .unwrap(); - - for i in 0..num_rows { - a.append_value(i as i32); - c.append_value(i as i32); - d.append_value((i * 1000000) as i128); - if allow_nulls && i % 10 == 0 { - b.append_null(); - } else { - b.append_value(format!("this is string number {i}")); - } - } - - let a = a.finish(); - let b = b.finish(); - let c = c.finish(); - let d = d.finish(); - - RecordBatch::try_new( - schema.clone(), - vec![Arc::new(a), Arc::new(b), Arc::new(c), Arc::new(d)], - ) - .unwrap() -} - -// BENCHMARK: REVALIDATION OVERHEAD COMPARISON -// --------------------------------------------------------- -// To compare performance with/without Arrow IPC validation: -// -// 1. Locate the function `read_spill` -// 2. Modify the `skip_validation` flag: -// - Set to `false` to enable validation -// 3. Rerun `cargo bench --bench spill_io` -fn bench_spill_io(c: &mut Criterion) { - let env = Arc::new(RuntimeEnv::default()); - let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0); - let schema = Arc::new(Schema::new(vec![ - Field::new("c0", DataType::Int32, true), - Field::new("c1", DataType::Utf8, true), - Field::new("c2", DataType::Date32, true), - Field::new("c3", DataType::Decimal128(11, 2), true), - ])); - let spill_manager = SpillManager::new(env, metrics, schema); - - let mut group = c.benchmark_group("spill_io"); - let rt = Runtime::new().unwrap(); - - group.bench_with_input( - BenchmarkId::new("StreamReader/read_100", ""), - &spill_manager, - |b, spill_manager| { - b.iter_batched( - // Setup phase: Create fresh state for each benchmark iteration. - // - generate an ipc file. - // This ensures each iteration starts with clean resources. - || { - let batch = create_batch(8192, true); - spill_manager - .spill_record_batch_and_finish(&vec![batch; 100], "Test") - .unwrap() - .unwrap() - }, - // Benchmark phase: - // - Execute the read operation via SpillManager - // - Wait for the consumer to finish processing - |spill_file| { - rt.block_on(async { - let stream = - spill_manager.read_spill_as_stream(spill_file).unwrap(); - let _ = collect(stream).await.unwrap(); - }) - }, - BatchSize::LargeInput, - ) - }, - ); - group.finish(); -} - -criterion_group!(benches, bench_spill_io); -criterion_main!(benches); diff --git a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs index e9c3c42e632b5..005dcc8da3863 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/multi_group_by/primitive.rs @@ -158,7 +158,7 @@ impl GroupColumn (true, Some(false)) => { self.nulls.append_n(rows.len(), true); self.group_values - .extend(iter::repeat_n(T::default_value(), rows.len())); + .extend(iter::repeat(T::default_value()).take(rows.len())); } (false, _) => { diff --git a/datafusion/physical-plan/src/aggregates/group_values/row.rs b/datafusion/physical-plan/src/aggregates/group_values/row.rs index 75c0e32491abc..63751d4703135 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/row.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/row.rs @@ -202,7 +202,6 @@ impl GroupValues for GroupValuesRows { EmitTo::All => { let output = self.row_converter.convert_rows(&group_values)?; group_values.clear(); - self.map.clear(); output } EmitTo::First(n) => { diff --git a/datafusion/physical-plan/src/aggregates/order/full.rs b/datafusion/physical-plan/src/aggregates/order/full.rs index eb98611f79dfb..218855459b1e2 100644 --- a/datafusion/physical-plan/src/aggregates/order/full.rs +++ b/datafusion/physical-plan/src/aggregates/order/full.rs @@ -92,7 +92,7 @@ impl GroupOrderingFull { Some(EmitTo::First(*current)) } } - State::Complete => Some(EmitTo::All), + State::Complete { .. } => Some(EmitTo::All), } } @@ -106,7 +106,7 @@ impl GroupOrderingFull { assert!(*current >= n); *current -= n; } - State::Complete => panic!("invalid state: complete"), + State::Complete { .. } => panic!("invalid state: complete"), } } @@ -133,7 +133,7 @@ impl GroupOrderingFull { current: max_group_index, } } - State::Complete => { + State::Complete { .. } => { panic!("Saw new group after input was complete"); } }; diff --git a/datafusion/physical-plan/src/aggregates/order/partial.rs b/datafusion/physical-plan/src/aggregates/order/partial.rs index c7a75e5f26404..aff69277a4cef 100644 --- a/datafusion/physical-plan/src/aggregates/order/partial.rs +++ b/datafusion/physical-plan/src/aggregates/order/partial.rs @@ -181,7 +181,7 @@ impl GroupOrderingPartial { assert!(*current_sort >= n); *current_sort -= n; } - State::Complete => panic!("invalid state: complete"), + State::Complete { .. } => panic!("invalid state: complete"), } } diff --git a/datafusion/physical-plan/src/aggregates/row_hash.rs b/datafusion/physical-plan/src/aggregates/row_hash.rs index 232565a04466f..077f18d510339 100644 --- a/datafusion/physical-plan/src/aggregates/row_hash.rs +++ b/datafusion/physical-plan/src/aggregates/row_hash.rs @@ -507,16 +507,6 @@ impl GroupedHashAggregateStream { AggregateMode::Partial, )?; - // Need to update the GROUP BY expressions to point to the correct column after schema change - let merging_group_by_expr = agg_group_by - .expr - .iter() - .enumerate() - .map(|(idx, (_, name))| { - (Arc::new(Column::new(name.as_str(), idx)) as _, name.clone()) - }) - .collect(); - let partial_agg_schema = Arc::new(partial_agg_schema); let spill_expr = group_schema @@ -560,7 +550,7 @@ impl GroupedHashAggregateStream { spill_schema: partial_agg_schema, is_stream_merging: false, merging_aggregate_arguments, - merging_group_by: PhysicalGroupBy::new_single(merging_group_by_expr), + merging_group_by: PhysicalGroupBy::new_single(agg_group_by.expr.clone()), peak_mem_used: MetricBuilder::new(&agg.metrics) .gauge("peak_mem_used", partition), spill_manager, @@ -975,7 +965,7 @@ impl GroupedHashAggregateStream { /// memory. Currently only [`GroupOrdering::None`] is supported for spilling. fn spill_previous_if_necessary(&mut self, batch: &RecordBatch) -> Result<()> { // TODO: support group_ordering for spilling - if !self.group_values.is_empty() + if self.group_values.len() > 0 && batch.num_rows() > 0 && matches!(self.group_ordering, GroupOrdering::None) && !self.spill_state.is_stream_merging diff --git a/datafusion/physical-plan/src/coalesce/mod.rs b/datafusion/physical-plan/src/coalesce/mod.rs index 0eca27f8e40e0..eb4a7d875c95a 100644 --- a/datafusion/physical-plan/src/coalesce/mod.rs +++ b/datafusion/physical-plan/src/coalesce/mod.rs @@ -90,7 +90,7 @@ impl BatchCoalescer { /// # Arguments /// - `schema` - the schema of the output batches /// - `target_batch_size` - the minimum number of rows for each - /// output batch (until limit reached) + /// output batch (until limit reached) /// - `fetch` - the maximum number of rows to fetch, `None` means fetch all rows pub fn new( schema: SchemaRef, @@ -285,7 +285,7 @@ mod tests { fn test_coalesce() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat_n(batch, 10)) + .with_batches(std::iter::repeat(batch).take(10)) // expected output is batches of at least 20 rows (except for the final batch) .with_target_batch_size(21) .with_expected_output_sizes(vec![24, 24, 24, 8]) @@ -296,7 +296,7 @@ mod tests { fn test_coalesce_with_fetch_larger_than_input_size() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat_n(batch, 10)) + .with_batches(std::iter::repeat(batch).take(10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 100 // expected to behave the same as `test_concat_batches` .with_target_batch_size(21) @@ -309,7 +309,7 @@ mod tests { fn test_coalesce_with_fetch_less_than_input_size() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat_n(batch, 10)) + .with_batches(std::iter::repeat(batch).take(10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 50 .with_target_batch_size(21) .with_fetch(Some(50)) @@ -321,7 +321,7 @@ mod tests { fn test_coalesce_with_fetch_less_than_target_and_no_remaining_rows() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat_n(batch, 10)) + .with_batches(std::iter::repeat(batch).take(10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 48 .with_target_batch_size(21) .with_fetch(Some(48)) @@ -333,7 +333,7 @@ mod tests { fn test_coalesce_with_fetch_less_target_batch_size() { let batch = uint32_batch(0..8); Test::new() - .with_batches(std::iter::repeat_n(batch, 10)) + .with_batches(std::iter::repeat(batch).take(10)) // input is 10 batches x 8 rows (80 rows) with fetch limit of 10 .with_target_batch_size(21) .with_fetch(Some(10)) diff --git a/datafusion/physical-plan/src/coalesce_batches.rs b/datafusion/physical-plan/src/coalesce_batches.rs index faab5fdc5eb6c..5244038b9ae27 100644 --- a/datafusion/physical-plan/src/coalesce_batches.rs +++ b/datafusion/physical-plan/src/coalesce_batches.rs @@ -35,10 +35,6 @@ use datafusion_execution::TaskContext; use crate::coalesce::{BatchCoalescer, CoalescerState}; use crate::execution_plan::CardinalityEffect; -use crate::filter_pushdown::{ - filter_pushdown_transparent, FilterDescription, FilterPushdownResult, -}; -use datafusion_common::config::ConfigOptions; use futures::ready; use futures::stream::{Stream, StreamExt}; @@ -216,17 +212,6 @@ impl ExecutionPlan for CoalesceBatchesExec { fn cardinality_effect(&self) -> CardinalityEffect { CardinalityEffect::Equal } - - fn try_pushdown_filters( - &self, - fd: FilterDescription, - _config: &ConfigOptions, - ) -> Result>> { - Ok(filter_pushdown_transparent::>( - Arc::new(self.clone()), - fd, - )) - } } /// Stream for [`CoalesceBatchesExec`]. See [`CoalesceBatchesExec`] for more details. diff --git a/datafusion/physical-plan/src/display.rs b/datafusion/physical-plan/src/display.rs index e247f5ad9d194..f437295a35551 100644 --- a/datafusion/physical-plan/src/display.rs +++ b/datafusion/physical-plan/src/display.rs @@ -657,7 +657,7 @@ impl TreeRenderVisitor<'_, '_> { } } - let halfway_point = extra_height.div_ceil(2); + let halfway_point = (extra_height + 1) / 2; // Render the actual node. for render_y in 0..=extra_height { diff --git a/datafusion/physical-plan/src/execution_plan.rs b/datafusion/physical-plan/src/execution_plan.rs index 2b6eac7be0675..2bc5706ee0e18 100644 --- a/datafusion/physical-plan/src/execution_plan.rs +++ b/datafusion/physical-plan/src/execution_plan.rs @@ -16,9 +16,6 @@ // under the License. pub use crate::display::{DefaultDisplay, DisplayAs, DisplayFormatType, VerboseDisplay}; -use crate::filter_pushdown::{ - filter_pushdown_not_supported, FilterDescription, FilterPushdownResult, -}; pub use crate::metrics::Metric; pub use crate::ordering::InputOrderMode; pub use crate::stream::EmptyRecordBatchStream; @@ -470,41 +467,6 @@ pub trait ExecutionPlan: Debug + DisplayAs + Send + Sync { ) -> Result>> { Ok(None) } - - /// Attempts to recursively push given filters from the top of the tree into leafs. - /// - /// This is used for various optimizations, such as: - /// - /// * Pushing down filters into scans in general to minimize the amount of data that needs to be materialzied. - /// * Pushing down dynamic filters from operators like TopK and Joins into scans. - /// - /// Generally the further down (closer to leaf nodes) that filters can be pushed, the better. - /// - /// Consider the case of a query such as `SELECT * FROM t WHERE a = 1 AND b = 2`. - /// With no filter pushdown the scan needs to read and materialize all the data from `t` and then filter based on `a` and `b`. - /// With filter pushdown into the scan it can first read only `a`, then `b` and keep track of - /// which rows match the filter. - /// Then only for rows that match the filter does it have to materialize the rest of the columns. - /// - /// # Default Implementation - /// - /// The default implementation assumes: - /// * Parent filters can't be passed onto children. - /// * This node has no filters to contribute. - /// - /// # Implementation Notes - /// - /// Most of the actual logic is implemented as a Physical Optimizer rule. - /// See [`PushdownFilter`] for more details. - /// - /// [`PushdownFilter`]: https://docs.rs/datafusion/latest/datafusion/physical_optimizer/filter_pushdown/struct.PushdownFilter.html - fn try_pushdown_filters( - &self, - fd: FilterDescription, - _config: &ConfigOptions, - ) -> Result>> { - Ok(filter_pushdown_not_supported(fd)) - } } /// [`ExecutionPlan`] Invariant Level @@ -557,15 +519,13 @@ pub trait ExecutionPlanProperties { /// If this ExecutionPlan makes no changes to the schema of the rows flowing /// through it or how columns within each row relate to each other, it /// should return the equivalence properties of its input. For - /// example, since [`FilterExec`] may remove rows from its input, but does not + /// example, since `FilterExec` may remove rows from its input, but does not /// otherwise modify them, it preserves its input equivalence properties. /// However, since `ProjectionExec` may calculate derived expressions, it /// needs special handling. /// /// See also [`ExecutionPlan::maintains_input_order`] and [`Self::output_ordering`] /// for related concepts. - /// - /// [`FilterExec`]: crate::filter::FilterExec fn equivalence_properties(&self) -> &EquivalenceProperties; } diff --git a/datafusion/physical-plan/src/filter.rs b/datafusion/physical-plan/src/filter.rs index 95fa67025e90d..a8a9973ea0434 100644 --- a/datafusion/physical-plan/src/filter.rs +++ b/datafusion/physical-plan/src/filter.rs @@ -26,9 +26,6 @@ use super::{ }; use crate::common::can_project; use crate::execution_plan::CardinalityEffect; -use crate::filter_pushdown::{ - FilterDescription, FilterPushdownResult, FilterPushdownSupport, -}; use crate::projection::{ make_with_child, try_embed_projection, update_expr, EmbeddedProjection, ProjectionExec, @@ -42,7 +39,6 @@ use arrow::compute::filter_record_batch; use arrow::datatypes::{DataType, SchemaRef}; use arrow::record_batch::RecordBatch; use datafusion_common::cast::as_boolean_array; -use datafusion_common::config::ConfigOptions; use datafusion_common::stats::Precision; use datafusion_common::{ internal_err, plan_err, project_schema, DataFusionError, Result, ScalarValue, @@ -50,7 +46,7 @@ use datafusion_common::{ use datafusion_execution::TaskContext; use datafusion_expr::Operator; use datafusion_physical_expr::equivalence::ProjectionMapping; -use datafusion_physical_expr::expressions::{BinaryExpr, Column}; +use datafusion_physical_expr::expressions::BinaryExpr; use datafusion_physical_expr::intervals::utils::check_support; use datafusion_physical_expr::utils::collect_columns; use datafusion_physical_expr::{ @@ -437,56 +433,6 @@ impl ExecutionPlan for FilterExec { } try_embed_projection(projection, self) } - - fn try_pushdown_filters( - &self, - mut fd: FilterDescription, - _config: &ConfigOptions, - ) -> Result>> { - // Extend the filter descriptions - fd.filters.push(Arc::clone(&self.predicate)); - - // Extract the information - let child_descriptions = vec![fd]; - let remaining_description = FilterDescription { filters: vec![] }; - let filter_input = Arc::clone(self.input()); - - if let Some(projection_indices) = self.projection.as_ref() { - // Push the filters down, but leave a ProjectionExec behind, instead of the FilterExec - let filter_child_schema = filter_input.schema(); - let proj_exprs = projection_indices - .iter() - .map(|p| { - let field = filter_child_schema.field(*p).clone(); - ( - Arc::new(Column::new(field.name(), *p)) as Arc, - field.name().to_string(), - ) - }) - .collect::>(); - let projection_exec = - Arc::new(ProjectionExec::try_new(proj_exprs, filter_input)?) as _; - - Ok(FilterPushdownResult { - support: FilterPushdownSupport::Supported { - child_descriptions, - op: projection_exec, - revisit: false, - }, - remaining_description, - }) - } else { - // Pull out the FilterExec, and inform the rule as it should be re-run - Ok(FilterPushdownResult { - support: FilterPushdownSupport::Supported { - child_descriptions, - op: filter_input, - revisit: true, - }, - remaining_description, - }) - } - } } impl EmbeddedProjection for FilterExec { diff --git a/datafusion/physical-plan/src/filter_pushdown.rs b/datafusion/physical-plan/src/filter_pushdown.rs deleted file mode 100644 index 38f5aef5923e1..0000000000000 --- a/datafusion/physical-plan/src/filter_pushdown.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -use std::sync::Arc; - -use crate::ExecutionPlan; -use datafusion_physical_expr_common::physical_expr::PhysicalExpr; - -#[derive(Clone, Debug)] -pub struct FilterDescription { - /// Expressions coming from the parent nodes - pub filters: Vec>, -} - -impl Default for FilterDescription { - fn default() -> Self { - Self::empty() - } -} - -impl FilterDescription { - /// Takes the filters out of the struct, leaving an empty vector in its place. - pub fn take_description(&mut self) -> Vec> { - std::mem::take(&mut self.filters) - } - - pub fn empty() -> FilterDescription { - Self { filters: vec![] } - } -} - -#[derive(Debug)] -pub enum FilterPushdownSupport { - Supported { - // Filter predicates which can be pushed down through the operator. - // NOTE that these are not placed into any operator. - child_descriptions: Vec, - // Possibly updated new operator - op: T, - // Whether the node is removed from the plan and the rule should be re-run manually - // on the new node. - // TODO: If TreeNodeRecursion supports Revisit mechanism, this flag can be removed - revisit: bool, - }, - NotSupported, -} - -#[derive(Debug)] -pub struct FilterPushdownResult { - pub support: FilterPushdownSupport, - // Filters which cannot be pushed down through the operator. - // NOTE that caller of try_pushdown_filters() should handle these remanining predicates, - // possibly introducing a FilterExec on top of this operator. - pub remaining_description: FilterDescription, -} - -pub fn filter_pushdown_not_supported( - remaining_description: FilterDescription, -) -> FilterPushdownResult { - FilterPushdownResult { - support: FilterPushdownSupport::NotSupported, - remaining_description, - } -} - -pub fn filter_pushdown_transparent( - plan: Arc, - fd: FilterDescription, -) -> FilterPushdownResult> { - let child_descriptions = vec![fd]; - let remaining_description = FilterDescription::empty(); - - FilterPushdownResult { - support: FilterPushdownSupport::Supported { - child_descriptions, - op: plan, - revisit: false, - }, - remaining_description, - } -} diff --git a/datafusion/physical-plan/src/joins/cross_join.rs b/datafusion/physical-plan/src/joins/cross_join.rs index 8dd1addff15ce..639fae7615af0 100644 --- a/datafusion/physical-plan/src/joins/cross_join.rs +++ b/datafusion/physical-plan/src/joins/cross_join.rs @@ -25,6 +25,7 @@ use super::utils::{ BatchTransformer, BuildProbeJoinMetrics, NoopBatchTransformer, OnceAsync, OnceFut, StatefulStreamResult, }; +use crate::coalesce_partitions::CoalescePartitionsExec; use crate::execution_plan::{boundedness_from_children, EmissionType}; use crate::metrics::{ExecutionPlanMetricsSet, MetricsSet}; use crate::projection::{ @@ -188,11 +189,19 @@ impl CrossJoinExec { /// Asynchronously collect the result of the left child async fn load_left_input( - stream: SendableRecordBatchStream, + left: Arc, + context: Arc, metrics: BuildProbeJoinMetrics, reservation: MemoryReservation, ) -> Result { - let left_schema = stream.schema(); + // merge all left parts into a single stream + let left_schema = left.schema(); + let merge = if left.output_partitioning().partition_count() != 1 { + Arc::new(CoalescePartitionsExec::new(left)) + } else { + left + }; + let stream = merge.execute(0, context)?; // Load all batches and count the rows let (batches, _metrics, reservation) = stream @@ -282,13 +291,6 @@ impl ExecutionPlan for CrossJoinExec { partition: usize, context: Arc, ) -> Result { - if self.left.output_partitioning().partition_count() != 1 { - return internal_err!( - "Invalid CrossJoinExec, the output partition count of the left child must be 1,\ - consider using CoalescePartitionsExec or the EnforceDistribution rule" - ); - } - let stream = self.right.execute(partition, Arc::clone(&context))?; let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics); @@ -301,15 +303,14 @@ impl ExecutionPlan for CrossJoinExec { let enforce_batch_size_in_joins = context.session_config().enforce_batch_size_in_joins(); - let left_fut = self.left_fut.try_once(|| { - let left_stream = self.left.execute(0, context)?; - - Ok(load_left_input( - left_stream, + let left_fut = self.left_fut.once(|| { + load_left_input( + Arc::clone(&self.left), + context, join_metrics.clone(), reservation, - )) - })?; + ) + }); if enforce_batch_size_in_joins { Ok(Box::pin(CrossJoinStream { diff --git a/datafusion/physical-plan/src/joins/hash_join.rs b/datafusion/physical-plan/src/joins/hash_join.rs index e8904db0f3eaf..c2a313edd1564 100644 --- a/datafusion/physical-plan/src/joins/hash_join.rs +++ b/datafusion/physical-plan/src/joins/hash_join.rs @@ -32,7 +32,6 @@ use super::{ utils::{OnceAsync, OnceFut}, PartitionMode, SharedBitmapBuilder, }; -use super::{JoinOn, JoinOnRef}; use crate::execution_plan::{boundedness_from_children, EmissionType}; use crate::projection::{ try_embed_projection, try_pushdown_through_join, EmbeddedProjection, JoinData, @@ -41,6 +40,7 @@ use crate::projection::{ use crate::spill::get_record_batch_memory_size; use crate::ExecutionPlanProperties; use crate::{ + coalesce_partitions::CoalescePartitionsExec, common::can_project, handle_state, hash_utils::create_hashes, @@ -50,7 +50,8 @@ use crate::{ build_batch_from_indices, build_join_schema, check_join_is_valid, estimate_join_statistics, need_produce_result_in_final, symmetric_join_output_partitioning, BuildProbeJoinMetrics, ColumnIndex, - JoinFilter, JoinHashMap, JoinHashMapType, StatefulStreamResult, + JoinFilter, JoinHashMap, JoinHashMapType, JoinOn, JoinOnRef, + StatefulStreamResult, }, metrics::{ExecutionPlanMetricsSet, MetricsSet}, DisplayAs, DisplayFormatType, Distribution, ExecutionPlan, Partitioning, @@ -791,42 +792,34 @@ impl ExecutionPlan for HashJoinExec { ); } - if self.mode == PartitionMode::CollectLeft && left_partitions != 1 { - return internal_err!( - "Invalid HashJoinExec, the output partition count of the left child must be 1 in CollectLeft mode,\ - consider using CoalescePartitionsExec or the EnforceDistribution rule" - ); - } - let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics); let left_fut = match self.mode { - PartitionMode::CollectLeft => self.left_fut.try_once(|| { - let left_stream = self.left.execute(0, Arc::clone(&context))?; - + PartitionMode::CollectLeft => self.left_fut.once(|| { let reservation = MemoryConsumer::new("HashJoinInput").register(context.memory_pool()); - - Ok(collect_left_input( + collect_left_input( + None, self.random_state.clone(), - left_stream, + Arc::clone(&self.left), on_left.clone(), + Arc::clone(&context), join_metrics.clone(), reservation, need_produce_result_in_final(self.join_type), self.right().output_partitioning().partition_count(), - )) - })?, + ) + }), PartitionMode::Partitioned => { - let left_stream = self.left.execute(partition, Arc::clone(&context))?; - let reservation = MemoryConsumer::new(format!("HashJoinInput[{partition}]")) .register(context.memory_pool()); OnceFut::new(collect_left_input( + Some(partition), self.random_state.clone(), - left_stream, + Arc::clone(&self.left), on_left.clone(), + Arc::clone(&context), join_metrics.clone(), reservation, need_produce_result_in_final(self.join_type), @@ -937,22 +930,36 @@ impl ExecutionPlan for HashJoinExec { /// Reads the left (build) side of the input, buffering it in memory, to build a /// hash table (`LeftJoinData`) +#[allow(clippy::too_many_arguments)] async fn collect_left_input( + partition: Option, random_state: RandomState, - left_stream: SendableRecordBatchStream, + left: Arc, on_left: Vec, + context: Arc, metrics: BuildProbeJoinMetrics, reservation: MemoryReservation, with_visited_indices_bitmap: bool, probe_threads_count: usize, ) -> Result { - let schema = left_stream.schema(); + let schema = left.schema(); + + let (left_input, left_input_partition) = if let Some(partition) = partition { + (left, partition) + } else if left.output_partitioning().partition_count() != 1 { + (Arc::new(CoalescePartitionsExec::new(left)) as _, 0) + } else { + (left, 0) + }; + + // Depending on partition argument load single partition or whole left side in memory + let stream = left_input.execute(left_input_partition, Arc::clone(&context))?; // This operation performs 2 steps at once: // 1. creates a [JoinHashMap] of all batches from the stream // 2. stores the batches in a vector. let initial = (Vec::new(), 0, metrics, reservation); - let (batches, num_rows, metrics, mut reservation) = left_stream + let (batches, num_rows, metrics, mut reservation) = stream .try_fold(initial, |mut acc, batch| async { let batch_size = get_record_batch_memory_size(&batch); // Reserve memory for incoming batch @@ -1648,7 +1655,6 @@ impl EmbeddedProjection for HashJoinExec { #[cfg(test)] mod tests { use super::*; - use crate::coalesce_partitions::CoalescePartitionsExec; use crate::test::TestMemoryExec; use crate::{ common, expressions::Column, repartition::RepartitionExec, test::build_table_i32, @@ -2099,7 +2105,6 @@ mod tests { let left = TestMemoryExec::try_new_exec(&[vec![batch1], vec![batch2]], schema, None) .unwrap(); - let left = Arc::new(CoalescePartitionsExec::new(left)); let right = build_table( ("a1", &vec![1, 2, 3]), @@ -2172,7 +2177,6 @@ mod tests { let left = TestMemoryExec::try_new_exec(&[vec![batch1], vec![batch2]], schema, None) .unwrap(); - let left = Arc::new(CoalescePartitionsExec::new(left)); let right = build_table( ("a2", &vec![20, 30, 10]), ("b2", &vec![5, 6, 4]), diff --git a/datafusion/physical-plan/src/joins/mod.rs b/datafusion/physical-plan/src/joins/mod.rs index 1d36db996434e..22a8c0bc798c8 100644 --- a/datafusion/physical-plan/src/joins/mod.rs +++ b/datafusion/physical-plan/src/joins/mod.rs @@ -19,7 +19,6 @@ use arrow::array::BooleanBufferBuilder; pub use cross_join::CrossJoinExec; -use datafusion_physical_expr::PhysicalExprRef; pub use hash_join::HashJoinExec; pub use nested_loop_join::NestedLoopJoinExec; use parking_lot::Mutex; @@ -40,11 +39,6 @@ mod join_hash_map; #[cfg(test)] pub mod test_utils; -/// The on clause of the join, as vector of (left, right) columns. -pub type JoinOn = Vec<(PhysicalExprRef, PhysicalExprRef)>; -/// Reference for JoinOn. -pub type JoinOnRef<'a> = &'a [(PhysicalExprRef, PhysicalExprRef)]; - #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Hash join Partitioning mode pub enum PartitionMode { diff --git a/datafusion/physical-plan/src/joins/nested_loop_join.rs b/datafusion/physical-plan/src/joins/nested_loop_join.rs index b902795950966..cdd2eaeca8997 100644 --- a/datafusion/physical-plan/src/joins/nested_loop_join.rs +++ b/datafusion/physical-plan/src/joins/nested_loop_join.rs @@ -28,6 +28,7 @@ use super::utils::{ need_produce_result_in_final, reorder_output_after_swap, swap_join_projection, BatchSplitter, BatchTransformer, NoopBatchTransformer, StatefulStreamResult, }; +use crate::coalesce_partitions::CoalescePartitionsExec; use crate::common::can_project; use crate::execution_plan::{boundedness_from_children, EmissionType}; use crate::joins::utils::{ @@ -482,13 +483,6 @@ impl ExecutionPlan for NestedLoopJoinExec { partition: usize, context: Arc, ) -> Result { - if self.left.output_partitioning().partition_count() != 1 { - return internal_err!( - "Invalid NestedLoopJoinExec, the output partition count of the left child must be 1,\ - consider using CoalescePartitionsExec or the EnforceDistribution rule" - ); - } - let join_metrics = BuildProbeJoinMetrics::new(partition, &self.metrics); // Initialization reservation for load of inner table @@ -496,17 +490,16 @@ impl ExecutionPlan for NestedLoopJoinExec { MemoryConsumer::new(format!("NestedLoopJoinLoad[{partition}]")) .register(context.memory_pool()); - let inner_table = self.inner_table.try_once(|| { - let stream = self.left.execute(0, Arc::clone(&context))?; - - Ok(collect_left_input( - stream, + let inner_table = self.inner_table.once(|| { + collect_left_input( + Arc::clone(&self.left), + Arc::clone(&context), join_metrics.clone(), load_reservation, need_produce_result_in_final(self.join_type), self.right().output_partitioning().partition_count(), - )) - })?; + ) + }); let batch_size = context.session_config().batch_size(); let enforce_batch_size_in_joins = @@ -617,13 +610,20 @@ impl ExecutionPlan for NestedLoopJoinExec { /// Asynchronously collect input into a single batch, and creates `JoinLeftData` from it async fn collect_left_input( - stream: SendableRecordBatchStream, + input: Arc, + context: Arc, join_metrics: BuildProbeJoinMetrics, reservation: MemoryReservation, with_visited_left_side: bool, probe_threads_count: usize, ) -> Result { - let schema = stream.schema(); + let schema = input.schema(); + let merge = if input.output_partitioning().partition_count() != 1 { + Arc::new(CoalescePartitionsExec::new(input)) + } else { + input + }; + let stream = merge.execute(0, context)?; // Load all batches and count the rows let (batches, metrics, mut reservation) = stream diff --git a/datafusion/physical-plan/src/joins/sort_merge_join.rs b/datafusion/physical-plan/src/joins/sort_merge_join.rs index 89f2e3c911f89..716cff939f663 100644 --- a/datafusion/physical-plan/src/joins/sort_merge_join.rs +++ b/datafusion/physical-plan/src/joins/sort_merge_join.rs @@ -823,65 +823,42 @@ impl BufferedBatch { /// Sort-Merge join stream that consumes streamed and buffered data streams /// and produces joined output stream. struct SortMergeJoinStream { - // ======================================================================== - // PROPERTIES: - // These fields are initialized at the start and remain constant throughout - // the execution. - // ======================================================================== + /// Current state of the stream + pub state: SortMergeJoinState, /// Output schema pub schema: SchemaRef, - /// null == null? - pub null_equals_null: bool, /// Sort options of join columns used to sort streamed and buffered data stream pub sort_options: Vec, - /// optional join filter - pub filter: Option, - /// How the join is performed - pub join_type: JoinType, - /// Target output batch size - pub batch_size: usize, - - // ======================================================================== - // STREAMED FIELDS: - // These fields manage the properties and state of the streamed input. - // ======================================================================== + /// null == null? + pub null_equals_null: bool, /// Input schema of streamed pub streamed_schema: SchemaRef, - /// Streamed data stream - pub streamed: SendableRecordBatchStream, - /// Current processing record batch of streamed - pub streamed_batch: StreamedBatch, - /// (used in outer join) Is current streamed row joined at least once? - pub streamed_joined: bool, - /// State of streamed - pub streamed_state: StreamedState, - /// Join key columns of streamed - pub on_streamed: Vec, - - // ======================================================================== - // BUFFERED FIELDS: - // These fields manage the properties and state of the buffered input. - // ======================================================================== /// Input schema of buffered pub buffered_schema: SchemaRef, + /// Streamed data stream + pub streamed: SendableRecordBatchStream, /// Buffered data stream pub buffered: SendableRecordBatchStream, + /// Current processing record batch of streamed + pub streamed_batch: StreamedBatch, /// Current buffered data pub buffered_data: BufferedData, + /// (used in outer join) Is current streamed row joined at least once? + pub streamed_joined: bool, /// (used in outer join) Is current buffered batches joined at least once? pub buffered_joined: bool, + /// State of streamed + pub streamed_state: StreamedState, /// State of buffered pub buffered_state: BufferedState, + /// The comparison result of current streamed row and buffered batches + pub current_ordering: Ordering, + /// Join key columns of streamed + pub on_streamed: Vec, /// Join key columns of buffered pub on_buffered: Vec, - - // ======================================================================== - // MERGE JOIN STATES: - // These fields track the execution state of merge join and are updated - // during the execution. - // ======================================================================== - /// Current state of the stream - pub state: SortMergeJoinState, + /// optional join filter + pub filter: Option, /// Staging output array builders pub staging_output_record_batches: JoinedRecordBatches, /// Output buffer. Currently used by filtering as it requires double buffering @@ -891,21 +868,18 @@ struct SortMergeJoinStream { /// Increased when we put rows into buffer and decreased after we actually output batches. /// Used to trigger output when sufficient rows are ready pub output_size: usize, - /// The comparison result of current streamed row and buffered batches - pub current_ordering: Ordering, - /// Manages the process of spilling and reading back intermediate data - pub spill_manager: SpillManager, - - // ======================================================================== - // EXECUTION RESOURCES: - // Fields related to managing execution resources and monitoring performance. - // ======================================================================== + /// Target output batch size + pub batch_size: usize, + /// How the join is performed + pub join_type: JoinType, /// Metrics pub join_metrics: SortMergeJoinMetrics, /// Memory reservation pub reservation: MemoryReservation, /// Runtime env pub runtime_env: Arc, + /// Manages the process of spilling and reading back intermediate data + pub spill_manager: SpillManager, /// A unique number for each batch pub streamed_batch_counter: AtomicUsize, } diff --git a/datafusion/physical-plan/src/joins/test_utils.rs b/datafusion/physical-plan/src/joins/test_utils.rs index d38637dae0282..e70007aa651f7 100644 --- a/datafusion/physical-plan/src/joins/test_utils.rs +++ b/datafusion/physical-plan/src/joins/test_utils.rs @@ -444,7 +444,8 @@ pub fn build_sides_record_batches( .collect::>(), )); let ordered_asc_null_first = Arc::new(Int32Array::from_iter({ - std::iter::repeat_n(None, index as usize) + std::iter::repeat(None) + .take(index as usize) .chain(rest_of.clone().map(Some)) .collect::>>() })); @@ -452,12 +453,13 @@ pub fn build_sides_record_batches( rest_of .clone() .map(Some) - .chain(std::iter::repeat_n(None, index as usize)) + .chain(std::iter::repeat(None).take(index as usize)) .collect::>>() })); let ordered_desc_null_first = Arc::new(Int32Array::from_iter({ - std::iter::repeat_n(None, index as usize) + std::iter::repeat(None) + .take(index as usize) .chain(rest_of.rev().map(Some)) .collect::>>() })); diff --git a/datafusion/physical-plan/src/joins/utils.rs b/datafusion/physical-plan/src/joins/utils.rs index 5516f172d5101..f6c720dbb707a 100644 --- a/datafusion/physical-plan/src/joins/utils.rs +++ b/datafusion/physical-plan/src/joins/utils.rs @@ -32,7 +32,6 @@ use crate::{ // compatibility pub use super::join_filter::JoinFilter; pub use super::join_hash_map::{JoinHashMap, JoinHashMapType}; -pub use crate::joins::{JoinOn, JoinOnRef}; use arrow::array::{ builder::UInt64Builder, downcast_array, new_null_array, Array, ArrowPrimitiveType, @@ -63,6 +62,11 @@ use futures::future::{BoxFuture, Shared}; use futures::{ready, FutureExt}; use parking_lot::Mutex; +/// The on clause of the join, as vector of (left, right) columns. +pub type JoinOn = Vec<(PhysicalExprRef, PhysicalExprRef)>; +/// Reference for JoinOn. +pub type JoinOnRef<'a> = &'a [(PhysicalExprRef, PhysicalExprRef)]; + /// Checks whether the schemas "left" and "right" and columns "on" represent a valid join. /// They are valid whenever their columns' intersection equals the set `on` pub fn check_join_is_valid(left: &Schema, right: &Schema, on: JoinOnRef) -> Result<()> { @@ -324,7 +328,7 @@ pub fn build_join_schema( } /// A [`OnceAsync`] runs an `async` closure once, where multiple calls to -/// [`OnceAsync::try_once`] return a [`OnceFut`] that resolves to the result of the +/// [`OnceAsync::once`] return a [`OnceFut`] that resolves to the result of the /// same computation. /// /// This is useful for joins where the results of one child are needed to proceed @@ -337,7 +341,7 @@ pub fn build_join_schema( /// /// Each output partition waits on the same `OnceAsync` before proceeding. pub(crate) struct OnceAsync { - fut: Mutex>>>, + fut: Mutex>>, } impl Default for OnceAsync { @@ -356,22 +360,19 @@ impl Debug for OnceAsync { impl OnceAsync { /// If this is the first call to this function on this object, will invoke - /// `f` to obtain a future and return a [`OnceFut`] referring to this. `f` - /// may fail, in which case its error is returned. + /// `f` to obtain a future and return a [`OnceFut`] referring to this /// /// If this is not the first call, will return a [`OnceFut`] referring - /// to the same future as was returned by the first call - or the same - /// error if the initial call to `f` failed. - pub(crate) fn try_once(&self, f: F) -> Result> + /// to the same future as was returned by the first call + pub(crate) fn once(&self, f: F) -> OnceFut where - F: FnOnce() -> Result, + F: FnOnce() -> Fut, Fut: Future> + Send + 'static, { self.fut .lock() - .get_or_insert_with(|| f().map(OnceFut::new).map_err(Arc::new)) + .get_or_insert_with(|| OnceFut::new(f())) .clone() - .map_err(DataFusionError::Shared) } } diff --git a/datafusion/physical-plan/src/lib.rs b/datafusion/physical-plan/src/lib.rs index a1862554b303e..04fbd06fabcde 100644 --- a/datafusion/physical-plan/src/lib.rs +++ b/datafusion/physical-plan/src/lib.rs @@ -50,7 +50,6 @@ pub use crate::ordering::InputOrderMode; pub use crate::stream::EmptyRecordBatchStream; pub use crate::topk::TopK; pub use crate::visitor::{accept, visit_execution_plan, ExecutionPlanVisitor}; -pub use spill::spill_manager::SpillManager; mod ordering; mod render_tree; @@ -67,7 +66,6 @@ pub mod empty; pub mod execution_plan; pub mod explain; pub mod filter; -pub mod filter_pushdown; pub mod joins; pub mod limit; pub mod memory; diff --git a/datafusion/physical-plan/src/projection.rs b/datafusion/physical-plan/src/projection.rs index 72934c74446eb..1d3e23ea90974 100644 --- a/datafusion/physical-plan/src/projection.rs +++ b/datafusion/physical-plan/src/projection.rs @@ -33,7 +33,7 @@ use super::{ SendableRecordBatchStream, Statistics, }; use crate::execution_plan::CardinalityEffect; -use crate::joins::utils::{ColumnIndex, JoinFilter, JoinOn, JoinOnRef}; +use crate::joins::utils::{ColumnIndex, JoinFilter}; use crate::{ColumnStatistics, DisplayFormatType, ExecutionPlan, PhysicalExpr}; use arrow::datatypes::{Field, Schema, SchemaRef}; @@ -446,6 +446,11 @@ pub fn try_embed_projection( } } +/// The on clause of the join, as vector of (left, right) columns. +pub type JoinOn = Vec<(PhysicalExprRef, PhysicalExprRef)>; +/// Reference for JoinOn. +pub type JoinOnRef<'a> = &'a [(PhysicalExprRef, PhysicalExprRef)]; + pub struct JoinData { pub projected_left_child: ProjectionExec, pub projected_right_child: ProjectionExec, diff --git a/datafusion/physical-plan/src/repartition/mod.rs b/datafusion/physical-plan/src/repartition/mod.rs index c480fc2abaa1a..ebc751201378b 100644 --- a/datafusion/physical-plan/src/repartition/mod.rs +++ b/datafusion/physical-plan/src/repartition/mod.rs @@ -43,7 +43,6 @@ use crate::{DisplayFormatType, ExecutionPlan, Partitioning, PlanProperties, Stat use arrow::array::{PrimitiveArray, RecordBatch, RecordBatchOptions}; use arrow::compute::take_arrays; use arrow::datatypes::{SchemaRef, UInt32Type}; -use datafusion_common::config::ConfigOptions; use datafusion_common::utils::transpose; use datafusion_common::HashMap; use datafusion_common::{not_impl_err, DataFusionError, Result}; @@ -53,9 +52,6 @@ use datafusion_execution::TaskContext; use datafusion_physical_expr::{EquivalenceProperties, PhysicalExpr}; use datafusion_physical_expr_common::sort_expr::LexOrdering; -use crate::filter_pushdown::{ - filter_pushdown_transparent, FilterDescription, FilterPushdownResult, -}; use futures::stream::Stream; use futures::{FutureExt, StreamExt, TryStreamExt}; use log::trace; @@ -512,18 +508,11 @@ impl DisplayAs for RepartitionExec { } DisplayFormatType::TreeRender => { writeln!(f, "partitioning_scheme={}", self.partitioning(),)?; - - let input_partition_count = - self.input.output_partitioning().partition_count(); - let output_partition_count = self.partitioning().partition_count(); - let input_to_output_partition_str = - format!("{} -> {}", input_partition_count, output_partition_count); writeln!( f, - "partition_count(in->out)={}", - input_to_output_partition_str + "output_partition_count={}", + self.input.output_partitioning().partition_count() )?; - if self.preserve_order { writeln!(f, "preserve_order={}", self.preserve_order)?; } @@ -734,17 +723,6 @@ impl ExecutionPlan for RepartitionExec { new_partitioning, )?))) } - - fn try_pushdown_filters( - &self, - fd: FilterDescription, - _config: &ConfigOptions, - ) -> Result>> { - Ok(filter_pushdown_transparent::>( - Arc::new(self.clone()), - fd, - )) - } } impl RepartitionExec { diff --git a/datafusion/physical-plan/src/sorts/cursor.rs b/datafusion/physical-plan/src/sorts/cursor.rs index efb9c0a47bf58..3d3bd81948e03 100644 --- a/datafusion/physical-plan/src/sorts/cursor.rs +++ b/datafusion/physical-plan/src/sorts/cursor.rs @@ -284,7 +284,7 @@ impl CursorArray for GenericByteArray { impl CursorArray for StringViewArray { type Values = StringViewArray; fn values(&self) -> Self { - self.gc() + self.clone() } } diff --git a/datafusion/physical-plan/src/sorts/merge.rs b/datafusion/physical-plan/src/sorts/merge.rs index 2b42457635f7b..1c2b8cd0c91b7 100644 --- a/datafusion/physical-plan/src/sorts/merge.rs +++ b/datafusion/physical-plan/src/sorts/merge.rs @@ -217,8 +217,9 @@ impl SortPreservingMergeStream { // we skip the following block. Until then, this function may be called multiple // times and can return Poll::Pending if any partition returns Poll::Pending. if self.loser_tree.is_empty() { - while let Some(&partition_idx) = self.uninitiated_partitions.front() { - match self.maybe_poll_stream(cx, partition_idx) { + let remaining_partitions = self.uninitiated_partitions.clone(); + for i in remaining_partitions { + match self.maybe_poll_stream(cx, i) { Poll::Ready(Err(e)) => { self.aborted = true; return Poll::Ready(Some(Err(e))); @@ -227,8 +228,10 @@ impl SortPreservingMergeStream { // If a partition returns Poll::Pending, to avoid continuously polling it // and potentially increasing upstream buffer sizes, we move it to the // back of the polling queue. - self.uninitiated_partitions.rotate_left(1); - + if let Some(front) = self.uninitiated_partitions.pop_front() { + // This pop_front can never return `None`. + self.uninitiated_partitions.push_back(front); + } // This function could remain in a pending state, so we manually wake it here. // However, this approach can be investigated further to find a more natural way // to avoid disrupting the runtime scheduler. @@ -238,13 +241,10 @@ impl SortPreservingMergeStream { _ => { // If the polling result is Poll::Ready(Some(batch)) or Poll::Ready(None), // we remove this partition from the queue so it is not polled again. - self.uninitiated_partitions.pop_front(); + self.uninitiated_partitions.retain(|idx| *idx != i); } } } - - // Claim the memory for the uninitiated partitions - self.uninitiated_partitions.shrink_to_fit(); self.init_loser_tree(); } diff --git a/datafusion/physical-plan/src/sorts/sort.rs b/datafusion/physical-plan/src/sorts/sort.rs index 9d0f34cc7f0fd..ed35492041be0 100644 --- a/datafusion/physical-plan/src/sorts/sort.rs +++ b/datafusion/physical-plan/src/sorts/sort.rs @@ -49,10 +49,8 @@ use arrow::array::{ }; use arrow::compute::{concat_batches, lexsort_to_indices, take_arrays, SortColumn}; use arrow::datatypes::{DataType, SchemaRef}; -use arrow::row::{RowConverter, Rows, SortField}; -use datafusion_common::{ - exec_datafusion_err, internal_datafusion_err, internal_err, DataFusionError, Result, -}; +use arrow::row::{RowConverter, SortField}; +use datafusion_common::{internal_datafusion_err, internal_err, Result}; use datafusion_execution::disk_manager::RefCountedTempFile; use datafusion_execution::memory_pool::{MemoryConsumer, MemoryReservation}; use datafusion_execution::runtime_env::RuntimeEnv; @@ -89,9 +87,8 @@ impl ExternalSorterMetrics { /// 1. get a non-empty new batch from input /// /// 2. check with the memory manager there is sufficient space to -/// buffer the batch in memory. -/// -/// 2.1 if memory is sufficient, buffer batch in memory, go to 1. +/// buffer the batch in memory 2.1 if memory sufficient, buffer +/// batch in memory, go to 1. /// /// 2.2 if no more memory is available, sort all buffered batches and /// spill to file. buffer the next batch in memory, go to 1. @@ -206,8 +203,8 @@ struct ExternalSorter { schema: SchemaRef, /// Sort expressions expr: Arc<[PhysicalSortExpr]>, - /// RowConverter corresponding to the sort expressions - sort_keys_row_converter: Arc, + /// If Some, the maximum number of output rows that will be produced + fetch: Option, /// The target number of rows for output batches batch_size: usize, /// If the in size of buffered memory batches is below this size, @@ -219,8 +216,10 @@ struct ExternalSorter { // STATE BUFFERS: // Fields that hold intermediate data during sorting // ======================================================================== - /// Unsorted input batches stored in the memory buffer + /// Potentially unsorted in memory buffer in_mem_batches: Vec, + /// if `Self::in_mem_batches` are sorted + in_mem_batches_sorted: bool, /// During external sorting, in-memory intermediate data will be appended to /// this file incrementally. Once finished, this file will be moved to [`Self::finished_spill_files`]. @@ -261,11 +260,12 @@ impl ExternalSorter { schema: SchemaRef, expr: LexOrdering, batch_size: usize, + fetch: Option, sort_spill_reservation_bytes: usize, sort_in_place_threshold_bytes: usize, metrics: &ExecutionPlanMetricsSet, runtime: Arc, - ) -> Result { + ) -> Self { let metrics = ExternalSorterMetrics::new(metrics, partition_id); let reservation = MemoryConsumer::new(format!("ExternalSorter[{partition_id}]")) .with_can_spill(true) @@ -275,36 +275,21 @@ impl ExternalSorter { MemoryConsumer::new(format!("ExternalSorterMerge[{partition_id}]")) .register(&runtime.memory_pool); - // Construct RowConverter for sort keys - let sort_fields = expr - .iter() - .map(|e| { - let data_type = e - .expr - .data_type(&schema) - .map_err(|e| e.context("Resolving sort expression data type"))?; - Ok(SortField::new_with_options(data_type, e.options)) - }) - .collect::>>()?; - - let converter = RowConverter::new(sort_fields).map_err(|e| { - exec_datafusion_err!("Failed to create RowConverter: {:?}", e) - })?; - let spill_manager = SpillManager::new( Arc::clone(&runtime), metrics.spill_metrics.clone(), Arc::clone(&schema), ); - Ok(Self { + Self { schema, in_mem_batches: vec![], + in_mem_batches_sorted: false, in_progress_spill_file: None, finished_spill_files: vec![], expr: expr.into(), - sort_keys_row_converter: Arc::new(converter), metrics, + fetch, reservation, spill_manager, merge_reservation, @@ -312,7 +297,7 @@ impl ExternalSorter { batch_size, sort_spill_reservation_bytes, sort_in_place_threshold_bytes, - }) + } } /// Appends an unsorted [`RecordBatch`] to `in_mem_batches` @@ -324,10 +309,18 @@ impl ExternalSorter { } self.reserve_memory_for_merge()?; - self.reserve_memory_for_batch_and_maybe_spill(&input) - .await?; + + let size = get_reserved_byte_for_record_batch(&input); + if self.reservation.try_grow(size).is_err() { + self.sort_or_spill_in_mem_batches(false).await?; + // We've already freed more than half of reserved memory, + // so we can grow the reservation again. There's nothing we can do + // if this try_grow fails. + self.reservation.try_grow(size)?; + } self.in_mem_batches.push(input); + self.in_mem_batches_sorted = false; Ok(()) } @@ -357,7 +350,7 @@ impl ExternalSorter { // `in_mem_batches` and the memory limit is almost reached, merging // them with the spilled files at the same time might cause OOM. if !self.in_mem_batches.is_empty() { - self.sort_and_spill_in_mem_batches().await?; + self.sort_or_spill_in_mem_batches(true).await?; } for spill in self.finished_spill_files.drain(..) { @@ -376,7 +369,7 @@ impl ExternalSorter { .with_expressions(expressions.as_ref()) .with_metrics(self.metrics.baseline.clone()) .with_batch_size(self.batch_size) - .with_fetch(None) + .with_fetch(self.fetch) .with_reservation(self.merge_reservation.new_empty()) .build() } else { @@ -404,13 +397,16 @@ impl ExternalSorter { self.metrics.spill_metrics.spill_file_count.value() } - /// Appending globally sorted batches to the in-progress spill file, and clears - /// the `globally_sorted_batches` (also its memory reservation) afterwards. - async fn consume_and_spill_append( - &mut self, - globally_sorted_batches: &mut Vec, - ) -> Result<()> { - if globally_sorted_batches.is_empty() { + /// When calling, all `in_mem_batches` must be sorted (*), and then all of them will + /// be appended to the in-progress spill file. + /// + /// (*) 'Sorted' here means globally sorted for all buffered batches when the + /// memory limit is reached, instead of partially sorted within the batch. + async fn spill_append(&mut self) -> Result<()> { + assert!(self.in_mem_batches_sorted); + + // we could always get a chance to free some memory as long as we are holding some + if self.in_mem_batches.is_empty() { return Ok(()); } @@ -420,25 +416,21 @@ impl ExternalSorter { Some(self.spill_manager.create_in_progress_file("Sorting")?); } - Self::organize_stringview_arrays(globally_sorted_batches)?; + self.organize_stringview_arrays()?; debug!("Spilling sort data of ExternalSorter to disk whilst inserting"); - let batches_to_spill = std::mem::take(globally_sorted_batches); + let batches = std::mem::take(&mut self.in_mem_batches); self.reservation.free(); let in_progress_file = self.in_progress_spill_file.as_mut().ok_or_else(|| { internal_datafusion_err!("In-progress spill file should be initialized") })?; - for batch in batches_to_spill { + for batch in batches { in_progress_file.append_batch(&batch)?; } - if !globally_sorted_batches.is_empty() { - return internal_err!("This function consumes globally_sorted_batches, so it should be empty after taking."); - } - Ok(()) } @@ -457,7 +449,7 @@ impl ExternalSorter { Ok(()) } - /// Reconstruct `globally_sorted_batches` to organize the payload buffers of each + /// Reconstruct `self.in_mem_batches` to organize the payload buffers of each /// `StringViewArray` in sequential order by calling `gc()` on them. /// /// Note this is a workaround until is @@ -486,12 +478,10 @@ impl ExternalSorter { /// /// Then when spilling each batch, the writer has to write all referenced buffers /// repeatedly. - fn organize_stringview_arrays( - globally_sorted_batches: &mut Vec, - ) -> Result<()> { - let mut organized_batches = Vec::with_capacity(globally_sorted_batches.len()); + fn organize_stringview_arrays(&mut self) -> Result<()> { + let mut organized_batches = Vec::with_capacity(self.in_mem_batches.len()); - for batch in globally_sorted_batches.drain(..) { + for batch in self.in_mem_batches.drain(..) { let mut new_columns: Vec> = Vec::with_capacity(batch.num_columns()); @@ -517,40 +507,43 @@ impl ExternalSorter { organized_batches.push(organized_batch); } - *globally_sorted_batches = organized_batches; + self.in_mem_batches = organized_batches; Ok(()) } - /// Sorts the in-memory batches and merges them into a single sorted run, then writes - /// the result to spill files. - async fn sort_and_spill_in_mem_batches(&mut self) -> Result<()> { - if self.in_mem_batches.is_empty() { - return internal_err!( - "in_mem_batches must not be empty when attempting to sort and spill" - ); - } - + /// Sorts the in_mem_batches in place + /// + /// Sorting may have freed memory, especially if fetch is `Some`. If + /// the memory usage has dropped by a factor of 2, then we don't have + /// to spill. Otherwise, we spill to free up memory for inserting + /// more batches. + /// The factor of 2 aims to avoid a degenerate case where the + /// memory required for `fetch` is just under the memory available, + /// causing repeated re-sorting of data + /// + /// # Arguments + /// + /// * `force_spill` - If true, the method will spill the in-memory batches + /// even if the memory usage has not dropped by a factor of 2. Otherwise it will + /// only spill when the memory usage has dropped by the pre-defined factor. + /// + async fn sort_or_spill_in_mem_batches(&mut self, force_spill: bool) -> Result<()> { // Release the memory reserved for merge back to the pool so // there is some left when `in_mem_sort_stream` requests an // allocation. At the end of this function, memory will be // reserved again for the next spill. self.merge_reservation.free(); + let before = self.reservation.size(); + let mut sorted_stream = self.in_mem_sort_stream(self.metrics.baseline.intermediate())?; - // After `in_mem_sort_stream()` is constructed, all `in_mem_batches` is taken - // to construct a globally sorted stream. - if !self.in_mem_batches.is_empty() { - return internal_err!( - "in_mem_batches should be empty after constructing sorted stream" - ); - } - // 'global' here refers to all buffered batches when the memory limit is - // reached. This variable will buffer the sorted batches after - // sort-preserving merge and incrementally append to spill files. - let mut globally_sorted_batches: Vec = vec![]; + // `self.in_mem_batches` is already taken away by the sort_stream, now it is empty. + // We'll gradually collect the sorted stream into self.in_mem_batches, or directly + // write sorted batches to disk when the memory is insufficient. + let mut spilled = false; while let Some(batch) = sorted_stream.next().await { let batch = batch?; let sorted_size = get_reserved_byte_for_record_batch(&batch); @@ -558,11 +551,12 @@ impl ExternalSorter { // Although the reservation is not enough, the batch is // already in memory, so it's okay to combine it with previously // sorted batches, and spill together. - globally_sorted_batches.push(batch); - self.consume_and_spill_append(&mut globally_sorted_batches) - .await?; // reservation is freed in spill() + self.in_mem_batches.push(batch); + self.spill_append().await?; // reservation is freed in spill() + spilled = true; } else { - globally_sorted_batches.push(batch); + self.in_mem_batches.push(batch); + self.in_mem_batches_sorted = true; } } @@ -570,17 +564,18 @@ impl ExternalSorter { // upcoming `self.reserve_memory_for_merge()` may fail due to insufficient memory. drop(sorted_stream); - self.consume_and_spill_append(&mut globally_sorted_batches) - .await?; - self.spill_finish().await?; - - // Sanity check after spilling - let buffers_cleared_property = - self.in_mem_batches.is_empty() && globally_sorted_batches.is_empty(); - if !buffers_cleared_property { - return internal_err!( - "in_mem_batches and globally_sorted_batches should be cleared before" - ); + // Sorting may free up some memory especially when fetch is `Some`. If we have + // not freed more than 50% of the memory, then we have to spill to free up more + // memory for inserting more batches. + if (self.reservation.size() > before / 2) || force_spill { + // We have not freed more than 50% of the memory, so we have to spill to + // free up more memory + self.spill_append().await?; + spilled = true; + } + + if spilled { + self.spill_finish().await?; } // Reserve headroom for next sort/merge @@ -680,8 +675,7 @@ impl ExternalSorter { let batch = concat_batches(&self.schema, &self.in_mem_batches)?; self.in_mem_batches.clear(); self.reservation - .try_resize(get_reserved_byte_for_record_batch(&batch)) - .map_err(Self::err_with_oom_context)?; + .try_resize(get_reserved_byte_for_record_batch(&batch))?; let reservation = self.reservation.take(); return self.sort_batch_stream(batch, metrics, reservation); } @@ -706,7 +700,7 @@ impl ExternalSorter { .with_expressions(expressions.as_ref()) .with_metrics(metrics) .with_batch_size(self.batch_size) - .with_fetch(None) + .with_fetch(self.fetch) .with_reservation(self.merge_reservation.new_empty()) .build() } @@ -727,30 +721,17 @@ impl ExternalSorter { ); let schema = batch.schema(); + let fetch = self.fetch; let expressions: LexOrdering = self.expr.iter().cloned().collect(); - let row_converter = Arc::clone(&self.sort_keys_row_converter); - let stream = futures::stream::once(async move { - let _timer = metrics.elapsed_compute().timer(); - - let sort_columns = expressions - .iter() - .map(|expr| expr.evaluate_to_sort_column(&batch)) - .collect::>>()?; - - let sorted = if is_multi_column_with_lists(&sort_columns) { - // lex_sort_to_indices doesn't support List with more than one column - // https://github.com/apache/arrow-rs/issues/5454 - sort_batch_row_based(&batch, &expressions, row_converter, None)? - } else { - sort_batch(&batch, &expressions, None)? - }; - + let stream = futures::stream::once(futures::future::lazy(move |_| { + let timer = metrics.elapsed_compute().timer(); + let sorted = sort_batch(&batch, &expressions, fetch)?; + timer.done(); metrics.record_output(sorted.num_rows()); drop(batch); drop(reservation); Ok(sorted) - }); - + })); Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream))) } @@ -762,51 +743,12 @@ impl ExternalSorter { if self.runtime.disk_manager.tmp_files_enabled() { let size = self.sort_spill_reservation_bytes; if self.merge_reservation.size() != size { - self.merge_reservation - .try_resize(size) - .map_err(Self::err_with_oom_context)?; + self.merge_reservation.try_resize(size)?; } } Ok(()) } - - /// Reserves memory to be able to accommodate the given batch. - /// If memory is scarce, tries to spill current in-memory batches to disk first. - async fn reserve_memory_for_batch_and_maybe_spill( - &mut self, - input: &RecordBatch, - ) -> Result<()> { - let size = get_reserved_byte_for_record_batch(input); - - match self.reservation.try_grow(size) { - Ok(_) => Ok(()), - Err(e) => { - if self.in_mem_batches.is_empty() { - return Err(Self::err_with_oom_context(e)); - } - - // Spill and try again. - self.sort_and_spill_in_mem_batches().await?; - self.reservation - .try_grow(size) - .map_err(Self::err_with_oom_context) - } - } - } - - /// Wraps the error with a context message suggesting settings to tweak. - /// This is meant to be used with DataFusionError::ResourcesExhausted only. - fn err_with_oom_context(e: DataFusionError) -> DataFusionError { - match e { - DataFusionError::ResourcesExhausted(_) => e.context( - "Not enough memory to continue external sort. \ - Consider increasing the memory limit, or decreasing sort_spill_reservation_bytes" - ), - // This is not an OOM error, so just return it as is. - _ => e, - } - } } /// Estimate how much memory is needed to sort a `RecordBatch`. @@ -834,45 +776,6 @@ impl Debug for ExternalSorter { } } -/// Converts rows into a sorted array of indices based on their order. -/// This function returns the indices that represent the sorted order of the rows. -fn rows_to_indices(rows: Rows, limit: Option) -> Result { - let mut sort: Vec<_> = rows.iter().enumerate().collect(); - sort.sort_unstable_by(|(_, a), (_, b)| a.cmp(b)); - - let mut len = rows.num_rows(); - if let Some(limit) = limit { - len = limit.min(len); - } - let indices = - UInt32Array::from_iter_values(sort.iter().take(len).map(|(i, _)| *i as u32)); - Ok(indices) -} - -/// Sorts a `RecordBatch` by converting its sort columns into Arrow Row Format for faster comparison. -fn sort_batch_row_based( - batch: &RecordBatch, - expressions: &LexOrdering, - row_converter: Arc, - fetch: Option, -) -> Result { - let sort_columns = expressions - .iter() - .map(|expr| expr.evaluate_to_sort_column(batch).map(|col| col.values)) - .collect::>>()?; - let rows = row_converter.convert_columns(&sort_columns)?; - let indices = rows_to_indices(rows, fetch)?; - let columns = take_arrays(batch.columns(), &indices, None)?; - - let options = RecordBatchOptions::new().with_row_count(Some(indices.len())); - - Ok(RecordBatch::try_new_with_options( - batch.schema(), - columns, - &options, - )?) -} - pub fn sort_batch( batch: &RecordBatch, expressions: &LexOrdering, @@ -935,9 +838,7 @@ pub(crate) fn lexsort_to_indices_multi_columns( }, ); - // Note: row converter is reused through `sort_batch_row_based()`, this function - // is not used during normal sort execution, but it's kept temporarily because - // it's inside a public interface `sort_batch()`. + // TODO reuse converter and rows, refer to TopK. let converter = RowConverter::new(fields)?; let rows = converter.convert_columns(&columns)?; let mut sort: Vec<_> = rows.iter().enumerate().collect(); @@ -970,8 +871,6 @@ pub struct SortExec { preserve_partitioning: bool, /// Fetch highest/lowest n results fetch: Option, - /// Normalized common sort prefix between the input and the sort expressions (only used with fetch) - common_sort_prefix: LexOrdering, /// Cache holding plan properties like equivalences, output partitioning etc. cache: PlanProperties, } @@ -981,15 +880,13 @@ impl SortExec { /// sorted output partition. pub fn new(expr: LexOrdering, input: Arc) -> Self { let preserve_partitioning = false; - let (cache, sort_prefix) = - Self::compute_properties(&input, expr.clone(), preserve_partitioning); + let cache = Self::compute_properties(&input, expr.clone(), preserve_partitioning); Self { expr, input, metrics_set: ExecutionPlanMetricsSet::new(), preserve_partitioning, fetch: None, - common_sort_prefix: sort_prefix, cache, } } @@ -1041,7 +938,6 @@ impl SortExec { expr: self.expr.clone(), metrics_set: self.metrics_set.clone(), preserve_partitioning: self.preserve_partitioning, - common_sort_prefix: self.common_sort_prefix.clone(), fetch, cache, } @@ -1075,21 +971,19 @@ impl SortExec { } /// This function creates the cache object that stores the plan properties such as schema, equivalence properties, ordering, partitioning, etc. - /// It also returns the common sort prefix between the input and the sort expressions. fn compute_properties( input: &Arc, sort_exprs: LexOrdering, preserve_partitioning: bool, - ) -> (PlanProperties, LexOrdering) { + ) -> PlanProperties { // Determine execution mode: let requirement = LexRequirement::from(sort_exprs); - - let (sort_prefix, sort_satisfied) = input + let sort_satisfied = input .equivalence_properties() - .extract_common_sort_prefix(&requirement); + .ordering_satisfy_requirement(&requirement); // The emission type depends on whether the input is already sorted: - // - If already fully sorted, we can emit results in the same way as the input + // - If already sorted, we can emit results in the same way as the input // - If not sorted, we must wait until all data is processed to emit results (Final) let emission_type = if sort_satisfied { input.pipeline_behavior() @@ -1125,14 +1019,11 @@ impl SortExec { let output_partitioning = Self::output_partitioning_helper(input, preserve_partitioning); - ( - PlanProperties::new( - eq_properties, - output_partitioning, - emission_type, - boundedness, - ), - LexOrdering::from(sort_prefix), + PlanProperties::new( + eq_properties, + output_partitioning, + emission_type, + boundedness, ) } } @@ -1144,12 +1035,7 @@ impl DisplayAs for SortExec { let preserve_partitioning = self.preserve_partitioning; match self.fetch { Some(fetch) => { - write!(f, "SortExec: TopK(fetch={fetch}), expr=[{}], preserve_partitioning=[{preserve_partitioning}]", self.expr)?; - if !self.common_sort_prefix.is_empty() { - write!(f, ", sort_prefix=[{}]", self.common_sort_prefix) - } else { - Ok(()) - } + write!(f, "SortExec: TopK(fetch={fetch}), expr=[{}], preserve_partitioning=[{preserve_partitioning}]", self.expr) } None => write!(f, "SortExec: expr=[{}], preserve_partitioning=[{preserve_partitioning}]", self.expr), } @@ -1169,10 +1055,7 @@ impl DisplayAs for SortExec { impl ExecutionPlan for SortExec { fn name(&self) -> &'static str { - match self.fetch { - Some(_) => "SortExec(TopK)", - None => "SortExec", - } + "SortExec" } fn as_any(&self) -> &dyn Any { @@ -1225,12 +1108,10 @@ impl ExecutionPlan for SortExec { trace!("End SortExec's input.execute for partition: {}", partition); - let requirement = &LexRequirement::from(self.expr.clone()); - let sort_satisfied = self .input .equivalence_properties() - .ordering_satisfy_requirement(requirement); + .ordering_satisfy_requirement(&LexRequirement::from(self.expr.clone())); match (sort_satisfied, self.fetch.as_ref()) { (true, Some(fetch)) => Ok(Box::pin(LimitStream::new( @@ -1244,7 +1125,6 @@ impl ExecutionPlan for SortExec { let mut topk = TopK::try_new( partition, input.schema(), - self.common_sort_prefix.clone(), self.expr.clone(), *fetch, context.session_config().batch_size(), @@ -1257,9 +1137,6 @@ impl ExecutionPlan for SortExec { while let Some(batch) = input.next().await { let batch = batch?; topk.insert_batch(batch)?; - if topk.finished { - break; - } } topk.emit() }) @@ -1272,11 +1149,12 @@ impl ExecutionPlan for SortExec { input.schema(), self.expr.clone(), context.session_config().batch_size(), + self.fetch, execution_options.sort_spill_reservation_bytes, execution_options.sort_in_place_threshold_bytes, &self.metrics_set, context.runtime_env(), - )?; + ); Ok(Box::pin(RecordBatchStreamAdapter::new( self.schema(), futures::stream::once(async move { @@ -1369,7 +1247,7 @@ mod tests { use arrow::datatypes::*; use datafusion_common::cast::as_primitive_array; use datafusion_common::test_util::batches_to_string; - use datafusion_common::{DataFusionError, Result, ScalarValue}; + use datafusion_common::{Result, ScalarValue}; use datafusion_execution::config::SessionConfig; use datafusion_execution::runtime_env::RuntimeEnvBuilder; use datafusion_execution::RecordBatchStream; @@ -1594,69 +1472,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn test_batch_reservation_error() -> Result<()> { - // Pick a memory limit and sort_spill_reservation that make the first batch reservation fail. - // These values assume that the ExternalSorter will reserve 800 bytes for the first batch. - let expected_batch_reservation = 800; - let merge_reservation: usize = 0; // Set to 0 for simplicity - let memory_limit: usize = expected_batch_reservation + merge_reservation - 1; // Just short of what we need - - let session_config = - SessionConfig::new().with_sort_spill_reservation_bytes(merge_reservation); - let runtime = RuntimeEnvBuilder::new() - .with_memory_limit(memory_limit, 1.0) - .build_arc()?; - let task_ctx = Arc::new( - TaskContext::default() - .with_session_config(session_config) - .with_runtime(runtime), - ); - - let plan = test::scan_partitioned(1); - - // Read the first record batch to assert that our memory limit and sort_spill_reservation - // settings trigger the test scenario. - { - let mut stream = plan.execute(0, Arc::clone(&task_ctx))?; - let first_batch = stream.next().await.unwrap()?; - let batch_reservation = get_reserved_byte_for_record_batch(&first_batch); - - assert_eq!(batch_reservation, expected_batch_reservation); - assert!(memory_limit < (merge_reservation + batch_reservation)); - } - - let sort_exec = Arc::new(SortExec::new( - LexOrdering::new(vec![PhysicalSortExpr { - expr: col("i", &plan.schema())?, - options: SortOptions::default(), - }]), - plan, - )); - - let result = collect( - Arc::clone(&sort_exec) as Arc, - Arc::clone(&task_ctx), - ) - .await; - - let err = result.unwrap_err(); - assert!( - matches!(err, DataFusionError::Context(..)), - "Assertion failed: expected a Context error, but got: {:?}", - err - ); - - // Assert that the context error is wrapping a resources exhausted error. - assert!( - matches!(err.find_root(), DataFusionError::ResourcesExhausted(_)), - "Assertion failed: expected a ResourcesExhausted error, but got: {:?}", - err - ); - - Ok(()) - } - #[tokio::test] async fn test_sort_spill_utf8_strings() -> Result<()> { let session_config = SessionConfig::new() diff --git a/datafusion/physical-plan/src/spill/in_progress_spill_file.rs b/datafusion/physical-plan/src/spill/in_progress_spill_file.rs index 7617e0a22a504..8c1ed77559078 100644 --- a/datafusion/physical-plan/src/spill/in_progress_spill_file.rs +++ b/datafusion/physical-plan/src/spill/in_progress_spill_file.rs @@ -49,12 +49,7 @@ impl InProgressSpillFile { } } - /// Appends a `RecordBatch` to the spill file, initializing the writer if necessary. - /// - /// # Errors - /// - Returns an error if the file is not active (has been finalized) - /// - Returns an error if appending would exceed the disk usage limit configured - /// by `max_temp_directory_size` in `DiskManager` + /// Appends a `RecordBatch` to the file, initializing the writer if necessary. pub fn append_batch(&mut self, batch: &RecordBatch) -> Result<()> { if self.in_progress_file.is_none() { return Err(exec_datafusion_err!( @@ -75,11 +70,6 @@ impl InProgressSpillFile { } if let Some(writer) = &mut self.writer { let (spilled_rows, spilled_bytes) = writer.write(batch)?; - if let Some(in_progress_file) = &mut self.in_progress_file { - in_progress_file.update_disk_usage()?; - } else { - unreachable!() // Already checked inside current function - } // Update metrics self.spill_writer.metrics.spilled_bytes.add(spilled_bytes); diff --git a/datafusion/physical-plan/src/spill/mod.rs b/datafusion/physical-plan/src/spill/mod.rs index 1101616a41060..88bf7953daeb4 100644 --- a/datafusion/physical-plan/src/spill/mod.rs +++ b/datafusion/physical-plan/src/spill/mod.rs @@ -23,161 +23,25 @@ pub(crate) mod spill_manager; use std::fs::File; use std::io::BufReader; use std::path::{Path, PathBuf}; -use std::pin::Pin; use std::ptr::NonNull; -use std::sync::Arc; -use std::task::{Context, Poll}; use arrow::array::ArrayData; use arrow::datatypes::{Schema, SchemaRef}; use arrow::ipc::{reader::StreamReader, writer::StreamWriter}; use arrow::record_batch::RecordBatch; +use tokio::sync::mpsc::Sender; -use datafusion_common::{exec_datafusion_err, DataFusionError, HashSet, Result}; -use datafusion_common_runtime::SpawnedTask; -use datafusion_execution::disk_manager::RefCountedTempFile; -use datafusion_execution::RecordBatchStream; -use futures::{FutureExt as _, Stream}; +use datafusion_common::{exec_datafusion_err, HashSet, Result}; -/// Stream that reads spill files from disk where each batch is read in a spawned blocking task -/// It will read one batch at a time and will not do any buffering, to buffer data use [`crate::common::spawn_buffered`] -/// -/// A simpler solution would be spawning a long-running blocking task for each -/// file read (instead of each batch). This approach does not work because when -/// the number of concurrent reads exceeds the Tokio thread pool limit, -/// deadlocks can occur and block progress. -struct SpillReaderStream { - schema: SchemaRef, - state: SpillReaderStreamState, -} - -/// When we poll for the next batch, we will get back both the batch and the reader, -/// so we can call `next` again. -type NextRecordBatchResult = Result<(StreamReader>, Option)>; - -enum SpillReaderStreamState { - /// Initial state: the stream was not initialized yet - /// and the file was not opened - Uninitialized(RefCountedTempFile), - - /// A read is in progress in a spawned blocking task for which we hold the handle. - ReadInProgress(SpawnedTask), - - /// A read has finished and we wait for being polled again in order to start reading the next batch. - Waiting(StreamReader>), - - /// The stream has finished, successfully or not. - Done, -} - -impl SpillReaderStream { - fn new(schema: SchemaRef, spill_file: RefCountedTempFile) -> Self { - Self { - schema, - state: SpillReaderStreamState::Uninitialized(spill_file), - } - } - - fn poll_next_inner( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - match &mut self.state { - SpillReaderStreamState::Uninitialized(_) => { - // Temporarily replace with `Done` to be able to pass the file to the task. - let SpillReaderStreamState::Uninitialized(spill_file) = - std::mem::replace(&mut self.state, SpillReaderStreamState::Done) - else { - unreachable!() - }; - - let task = SpawnedTask::spawn_blocking(move || { - let file = BufReader::new(File::open(spill_file.path())?); - // SAFETY: DataFusion's spill writer strictly follows Arrow IPC specifications - // with validated schemas and buffers. Skip redundant validation during read - // to speedup read operation. This is safe for DataFusion as input guaranteed to be correct when written. - let mut reader = unsafe { - StreamReader::try_new(file, None)?.with_skip_validation(true) - }; - - let next_batch = reader.next().transpose()?; - - Ok((reader, next_batch)) - }); - - self.state = SpillReaderStreamState::ReadInProgress(task); - - // Poll again immediately so the inner task is polled and the waker is - // registered. - self.poll_next_inner(cx) - } - - SpillReaderStreamState::ReadInProgress(task) => { - let result = futures::ready!(task.poll_unpin(cx)) - .unwrap_or_else(|err| Err(DataFusionError::External(Box::new(err)))); - - match result { - Ok((reader, batch)) => { - match batch { - Some(batch) => { - self.state = SpillReaderStreamState::Waiting(reader); - - Poll::Ready(Some(Ok(batch))) - } - None => { - // Stream is done - self.state = SpillReaderStreamState::Done; - - Poll::Ready(None) - } - } - } - Err(err) => { - self.state = SpillReaderStreamState::Done; - - Poll::Ready(Some(Err(err))) - } - } - } - - SpillReaderStreamState::Waiting(_) => { - // Temporarily replace with `Done` to be able to pass the file to the task. - let SpillReaderStreamState::Waiting(mut reader) = - std::mem::replace(&mut self.state, SpillReaderStreamState::Done) - else { - unreachable!() - }; - - let task = SpawnedTask::spawn_blocking(move || { - let next_batch = reader.next().transpose()?; - - Ok((reader, next_batch)) - }); - - self.state = SpillReaderStreamState::ReadInProgress(task); - - // Poll again immediately so the inner task is polled and the waker is - // registered. - self.poll_next_inner(cx) - } - - SpillReaderStreamState::Done => Poll::Ready(None), - } - } -} - -impl Stream for SpillReaderStream { - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.get_mut().poll_next_inner(cx) - } -} - -impl RecordBatchStream for SpillReaderStream { - fn schema(&self) -> SchemaRef { - Arc::clone(&self.schema) +fn read_spill(sender: Sender>, path: &Path) -> Result<()> { + let file = BufReader::new(File::open(path)?); + let reader = StreamReader::try_new(file, None)?; + for batch in reader { + sender + .blocking_send(batch.map_err(Into::into)) + .map_err(|e| exec_datafusion_err!("{e}"))?; } + Ok(()) } /// Spill the `RecordBatch` to disk as smaller batches @@ -338,7 +202,6 @@ mod tests { use arrow::record_batch::RecordBatch; use datafusion_common::Result; use datafusion_execution::runtime_env::RuntimeEnv; - use futures::StreamExt as _; use std::sync::Arc; @@ -738,42 +601,4 @@ mod tests { Ok(()) } - - #[test] - fn test_reading_more_spills_than_tokio_blocking_threads() -> Result<()> { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .max_blocking_threads(1) - .build() - .unwrap() - .block_on(async { - let batch = build_table_i32( - ("a2", &vec![0, 1, 2]), - ("b2", &vec![3, 4, 5]), - ("c2", &vec![4, 5, 6]), - ); - - let schema = batch.schema(); - - // Construct SpillManager - let env = Arc::new(RuntimeEnv::default()); - let metrics = SpillMetrics::new(&ExecutionPlanMetricsSet::new(), 0); - let spill_manager = SpillManager::new(env, metrics, Arc::clone(&schema)); - let batches: [_; 10] = std::array::from_fn(|_| batch.clone()); - - let spill_file_1 = spill_manager - .spill_record_batch_and_finish(&batches, "Test1")? - .unwrap(); - let spill_file_2 = spill_manager - .spill_record_batch_and_finish(&batches, "Test2")? - .unwrap(); - - let mut stream_1 = spill_manager.read_spill_as_stream(spill_file_1)?; - let mut stream_2 = spill_manager.read_spill_as_stream(spill_file_2)?; - stream_1.next().await; - stream_2.next().await; - - Ok(()) - }) - } } diff --git a/datafusion/physical-plan/src/spill/spill_manager.rs b/datafusion/physical-plan/src/spill/spill_manager.rs index 78cd47a8bad07..4a8e293323f02 100644 --- a/datafusion/physical-plan/src/spill/spill_manager.rs +++ b/datafusion/physical-plan/src/spill/spill_manager.rs @@ -27,9 +27,10 @@ use datafusion_common::Result; use datafusion_execution::disk_manager::RefCountedTempFile; use datafusion_execution::SendableRecordBatchStream; -use crate::{common::spawn_buffered, metrics::SpillMetrics}; +use crate::metrics::SpillMetrics; +use crate::stream::RecordBatchReceiverStream; -use super::{in_progress_spill_file::InProgressSpillFile, SpillReaderStream}; +use super::{in_progress_spill_file::InProgressSpillFile, read_spill}; /// The `SpillManager` is responsible for the following tasks: /// - Reading and writing `RecordBatch`es to raw files based on the provided configurations. @@ -72,10 +73,7 @@ impl SpillManager { /// intended to incrementally write in-memory batches into the same spill file, /// use [`Self::create_in_progress_file`] instead. /// None is returned if no batches are spilled. - /// - /// # Errors - /// - Returns an error if spilling would exceed the disk usage limit configured - /// by `max_temp_directory_size` in `DiskManager` + #[allow(dead_code)] // TODO: remove after change SMJ to use SpillManager pub fn spill_record_batch_and_finish( &self, batches: &[RecordBatch], @@ -92,10 +90,7 @@ impl SpillManager { /// Refer to the documentation for [`Self::spill_record_batch_and_finish`]. This method /// additionally spills the `RecordBatch` into smaller batches, divided by `row_limit`. - /// - /// # Errors - /// - Returns an error if spilling would exceed the disk usage limit configured - /// by `max_temp_directory_size` in `DiskManager` + #[allow(dead_code)] // TODO: remove after change aggregate to use SpillManager pub fn spill_record_batch_by_size( &self, batch: &RecordBatch, @@ -125,11 +120,14 @@ impl SpillManager { &self, spill_file_path: RefCountedTempFile, ) -> Result { - let stream = Box::pin(SpillReaderStream::new( + let mut builder = RecordBatchReceiverStream::builder( Arc::clone(&self.schema), - spill_file_path, - )); + self.batch_read_buffer_capacity, + ); + let sender = builder.tx(); - Ok(spawn_buffered(stream, self.batch_read_buffer_capacity)) + builder.spawn_blocking(move || read_spill(sender, spill_file_path.path())); + + Ok(builder.build()) } } diff --git a/datafusion/physical-plan/src/topk/mod.rs b/datafusion/physical-plan/src/topk/mod.rs index 0b5780b9143f9..85de1eefce2e4 100644 --- a/datafusion/physical-plan/src/topk/mod.rs +++ b/datafusion/physical-plan/src/topk/mod.rs @@ -18,7 +18,7 @@ //! TopK: Combination of Sort / LIMIT use arrow::{ - compute::interleave_record_batch, + compute::interleave, row::{RowConverter, Rows, SortField}, }; use std::mem::size_of; @@ -27,10 +27,10 @@ use std::{cmp::Ordering, collections::BinaryHeap, sync::Arc}; use super::metrics::{BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder}; use crate::spill::get_record_batch_memory_size; use crate::{stream::RecordBatchStreamAdapter, SendableRecordBatchStream}; -use arrow::array::{ArrayRef, RecordBatch}; +use arrow::array::{Array, ArrayRef, RecordBatch}; use arrow::datatypes::SchemaRef; +use datafusion_common::HashMap; use datafusion_common::Result; -use datafusion_common::{internal_datafusion_err, HashMap}; use datafusion_execution::{ memory_pool::{MemoryConsumer, MemoryReservation}, runtime_env::RuntimeEnv, @@ -70,25 +70,6 @@ use datafusion_physical_expr_common::sort_expr::LexOrdering; /// The same answer can be produced by simply keeping track of the top /// K=3 elements, reducing the total amount of required buffer memory. /// -/// # Partial Sort Optimization -/// -/// This implementation additionally optimizes queries where the input is already -/// partially sorted by a common prefix of the requested ordering. Once the top K -/// heap is full, if subsequent rows are guaranteed to be strictly greater (in sort -/// order) on this prefix than the largest row currently stored, the operator -/// safely terminates early. -/// -/// ## Example -/// -/// For input sorted by `(day DESC)`, but not by `timestamp`, a query such as: -/// -/// ```sql -/// SELECT day, timestamp FROM sensor ORDER BY day DESC, timestamp DESC LIMIT 10; -/// ``` -/// -/// can terminate scanning early once sufficient rows from the latest days have been -/// collected, skipping older data. -/// /// # Structure /// /// This operator tracks the top K items using a `TopKHeap`. @@ -109,43 +90,15 @@ pub struct TopK { scratch_rows: Rows, /// stores the top k values and their sort key values, in order heap: TopKHeap, - /// row converter, for common keys between the sort keys and the input ordering - common_sort_prefix_converter: Option, - /// Common sort prefix between the input and the sort expressions to allow early exit optimization - common_sort_prefix: Arc<[PhysicalSortExpr]>, - /// If true, indicates that all rows of subsequent batches are guaranteed - /// to be greater (by byte order, after row conversion) than the top K, - /// which means the top K won't change and the computation can be finished early. - pub(crate) finished: bool, -} - -// Guesstimate for memory allocation: estimated number of bytes used per row in the RowConverter -const ESTIMATED_BYTES_PER_ROW: usize = 20; - -fn build_sort_fields( - ordering: &LexOrdering, - schema: &SchemaRef, -) -> Result> { - ordering - .iter() - .map(|e| { - Ok(SortField::new_with_options( - e.expr.data_type(schema)?, - e.options, - )) - }) - .collect::>() } impl TopK { /// Create a new [`TopK`] that stores the top `k` values, as /// defined by the sort expressions in `expr`. // TODO: make a builder or some other nicer API - #[allow(clippy::too_many_arguments)] pub fn try_new( partition_id: usize, schema: SchemaRef, - common_sort_prefix: LexOrdering, expr: LexOrdering, k: usize, batch_size: usize, @@ -155,34 +108,35 @@ impl TopK { let reservation = MemoryConsumer::new(format!("TopK[{partition_id}]")) .register(&runtime.memory_pool); - let sort_fields: Vec<_> = build_sort_fields(&expr, &schema)?; + let expr: Arc<[PhysicalSortExpr]> = expr.into(); + + let sort_fields: Vec<_> = expr + .iter() + .map(|e| { + Ok(SortField::new_with_options( + e.expr.data_type(&schema)?, + e.options, + )) + }) + .collect::>()?; // TODO there is potential to add special cases for single column sort fields // to improve performance let row_converter = RowConverter::new(sort_fields)?; - let scratch_rows = - row_converter.empty_rows(batch_size, ESTIMATED_BYTES_PER_ROW * batch_size); - - let prefix_row_converter = if common_sort_prefix.is_empty() { - None - } else { - let input_sort_fields: Vec<_> = - build_sort_fields(&common_sort_prefix, &schema)?; - Some(RowConverter::new(input_sort_fields)?) - }; + let scratch_rows = row_converter.empty_rows( + batch_size, + 20 * batch_size, // guesstimate 20 bytes per row + ); Ok(Self { schema: Arc::clone(&schema), metrics: TopKMetrics::new(metrics, partition_id), reservation, batch_size, - expr: Arc::from(expr), + expr, row_converter, scratch_rows, - heap: TopKHeap::new(k, batch_size), - common_sort_prefix_converter: prefix_row_converter, - common_sort_prefix: Arc::from(common_sort_prefix), - finished: false, + heap: TopKHeap::new(k, batch_size, schema), }) } @@ -190,8 +144,7 @@ impl TopK { /// the top k seen so far. pub fn insert_batch(&mut self, batch: RecordBatch) -> Result<()> { // Updates on drop - let baseline = self.metrics.baseline.clone(); - let _timer = baseline.elapsed_compute().timer(); + let _timer = self.metrics.baseline.elapsed_compute().timer(); let sort_keys: Vec = self .expr @@ -210,7 +163,7 @@ impl TopK { // TODO make this algorithmically better?: // Idea: filter out rows >= self.heap.max() early (before passing to `RowConverter`) // this avoids some work and also might be better vectorizable. - let mut batch_entry = self.heap.register_batch(batch.clone()); + let mut batch_entry = self.heap.register_batch(batch); for (index, row) in rows.iter().enumerate() { match self.heap.max() { // heap has k items, and the new row is greater than the @@ -230,87 +183,6 @@ impl TopK { // update memory reservation self.reservation.try_resize(self.size())?; - - // flag the topK as finished if we know that all - // subsequent batches are guaranteed to be greater (by byte order, after row conversion) than the top K, - // which means the top K won't change and the computation can be finished early. - self.attempt_early_completion(&batch)?; - - Ok(()) - } - - /// If input ordering shares a common sort prefix with the TopK, and if the TopK's heap is full, - /// check if the computation can be finished early. - /// This is the case if the last row of the current batch is strictly greater than the max row in the heap, - /// comparing only on the shared prefix columns. - fn attempt_early_completion(&mut self, batch: &RecordBatch) -> Result<()> { - // Early exit if the batch is empty as there is no last row to extract from it. - if batch.num_rows() == 0 { - return Ok(()); - } - - // prefix_row_converter is only `Some` if the input ordering has a common prefix with the TopK, - // so early exit if it is `None`. - let Some(prefix_converter) = &self.common_sort_prefix_converter else { - return Ok(()); - }; - - // Early exit if the heap is not full (`heap.max()` only returns `Some` if the heap is full). - let Some(max_topk_row) = self.heap.max() else { - return Ok(()); - }; - - // Evaluate the prefix for the last row of the current batch. - let last_row_idx = batch.num_rows() - 1; - let mut batch_prefix_scratch = - prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW); // 1 row with capacity ESTIMATED_BYTES_PER_ROW - - self.compute_common_sort_prefix(batch, last_row_idx, &mut batch_prefix_scratch)?; - - // Retrieve the max row from the heap. - let store_entry = self - .heap - .store - .get(max_topk_row.batch_id) - .ok_or(internal_datafusion_err!("Invalid batch id in topK heap"))?; - let max_batch = &store_entry.batch; - let mut heap_prefix_scratch = - prefix_converter.empty_rows(1, ESTIMATED_BYTES_PER_ROW); // 1 row with capacity ESTIMATED_BYTES_PER_ROW - self.compute_common_sort_prefix( - max_batch, - max_topk_row.index, - &mut heap_prefix_scratch, - )?; - - // If the last row's prefix is strictly greater than the max prefix, mark as finished. - if batch_prefix_scratch.row(0).as_ref() > heap_prefix_scratch.row(0).as_ref() { - self.finished = true; - } - - Ok(()) - } - - // Helper function to compute the prefix for a given batch and row index, storing the result in scratch. - fn compute_common_sort_prefix( - &self, - batch: &RecordBatch, - last_row_idx: usize, - scratch: &mut Rows, - ) -> Result<()> { - let last_row: Vec = self - .common_sort_prefix - .iter() - .map(|expr| { - expr.expr - .evaluate(&batch.slice(last_row_idx, 1))? - .into_array(1) - }) - .collect::>()?; - - self.common_sort_prefix_converter - .as_ref() - .unwrap() - .append(scratch, &last_row)?; Ok(()) } @@ -325,9 +197,6 @@ impl TopK { row_converter: _, scratch_rows: _, mut heap, - common_sort_prefix_converter: _, - common_sort_prefix: _, - finished: _, } = self; let _timer = metrics.baseline.elapsed_compute().timer(); // time updated on drop @@ -402,13 +271,13 @@ struct TopKHeap { } impl TopKHeap { - fn new(k: usize, batch_size: usize) -> Self { + fn new(k: usize, batch_size: usize, schema: SchemaRef) -> Self { assert!(k > 0); Self { k, batch_size, inner: BinaryHeap::new(), - store: RecordBatchStore::new(), + store: RecordBatchStore::new(schema), owned_bytes: 0, } } @@ -485,6 +354,8 @@ impl TopKHeap { /// high, as a single [`RecordBatch`], and a sorted vec of the /// current heap's contents pub fn emit_with_state(&mut self) -> Result<(Option, Vec)> { + let schema = Arc::clone(self.store.schema()); + // generate sorted rows let topk_rows = std::mem::take(&mut self.inner).into_sorted_vec(); @@ -499,20 +370,30 @@ impl TopKHeap { .map(|(i, k)| (i, k.index)) .collect(); - let record_batches: Vec<_> = topk_rows - .iter() - .map(|k| { - let entry = self.store.get(k.batch_id).expect("invalid stored batch id"); - &entry.batch + let num_columns = schema.fields().len(); + + // build the output columns one at time, using the + // `interleave` kernel to pick rows from different arrays + let output_columns: Vec<_> = (0..num_columns) + .map(|col| { + let input_arrays: Vec<_> = topk_rows + .iter() + .map(|k| { + let entry = + self.store.get(k.batch_id).expect("invalid stored batch id"); + entry.batch.column(col) as &dyn Array + }) + .collect(); + + // at this point `indices` contains indexes within the + // rows and `input_arrays` contains a reference to the + // relevant Array for that index. `interleave` pulls + // them together into a single new array + Ok(interleave(&input_arrays, &indices)?) }) - .collect(); - - // At this point `indices` contains indexes within the - // rows and `input_arrays` contains a reference to the - // relevant RecordBatch for that index. `interleave_record_batch` pulls - // them together into a single new batch - let new_batch = interleave_record_batch(&record_batches, &indices)?; + .collect::>()?; + let new_batch = RecordBatch::try_new(schema, output_columns)?; Ok((Some(new_batch), topk_rows)) } @@ -667,14 +548,17 @@ struct RecordBatchStore { batches: HashMap, /// total size of all record batches tracked by this store batches_size: usize, + /// schema of the batches + schema: SchemaRef, } impl RecordBatchStore { - fn new() -> Self { + fn new(schema: SchemaRef) -> Self { Self { next_id: 0, batches: HashMap::new(), batches_size: 0, + schema, } } @@ -725,6 +609,11 @@ impl RecordBatchStore { self.batches.is_empty() } + /// return the schema of batches stored + fn schema(&self) -> &SchemaRef { + &self.schema + } + /// remove a use from the specified batch id. If the use count /// reaches zero the batch entry is removed from the store /// @@ -760,10 +649,6 @@ mod tests { use super::*; use arrow::array::{Float64Array, Int32Array, RecordBatch}; use arrow::datatypes::{DataType, Field, Schema}; - use arrow_schema::SortOptions; - use datafusion_common::assert_batches_eq; - use datafusion_physical_expr::expressions::col; - use futures::TryStreamExt; /// This test ensures the size calculation is correct for RecordBatches with multiple columns. #[test] @@ -773,7 +658,7 @@ mod tests { Field::new("ints", DataType::Int32, true), Field::new("float64", DataType::Float64, false), ])); - let mut record_batch_store = RecordBatchStore::new(); + let mut record_batch_store = RecordBatchStore::new(Arc::clone(&schema)); let int_array = Int32Array::from(vec![Some(1), Some(2), Some(3), Some(4), Some(5)]); // 5 * 4 = 20 let float64_array = Float64Array::from(vec![1.0, 2.0, 3.0, 4.0, 5.0]); // 5 * 8 = 40 @@ -796,98 +681,4 @@ mod tests { record_batch_store.unuse(0); assert_eq!(record_batch_store.batches_size, 0); } - - /// This test validates that the `try_finish` method marks the TopK operator as finished - /// when the prefix (on column "a") of the last row in the current batch is strictly greater - /// than the max top‑k row. - /// The full sort expression is defined on both columns ("a", "b"), but the input ordering is only on "a". - #[tokio::test] - async fn test_try_finish_marks_finished_with_prefix() -> Result<()> { - // Create a schema with two columns. - let schema = Arc::new(Schema::new(vec![ - Field::new("a", DataType::Int32, false), - Field::new("b", DataType::Float64, false), - ])); - - // Create sort expressions. - // Full sort: first by "a", then by "b". - let sort_expr_a = PhysicalSortExpr { - expr: col("a", schema.as_ref())?, - options: SortOptions::default(), - }; - let sort_expr_b = PhysicalSortExpr { - expr: col("b", schema.as_ref())?, - options: SortOptions::default(), - }; - - // Input ordering uses only column "a" (a prefix of the full sort). - let input_ordering = LexOrdering::from(vec![sort_expr_a.clone()]); - let full_expr = LexOrdering::from(vec![sort_expr_a, sort_expr_b]); - - // Create a dummy runtime environment and metrics. - let runtime = Arc::new(RuntimeEnv::default()); - let metrics = ExecutionPlanMetricsSet::new(); - - // Create a TopK instance with k = 3 and batch_size = 2. - let mut topk = TopK::try_new( - 0, - Arc::clone(&schema), - input_ordering, - full_expr, - 3, - 2, - runtime, - &metrics, - )?; - - // Create the first batch with two columns: - // Column "a": [1, 1, 2], Column "b": [20.0, 15.0, 30.0]. - let array_a1: ArrayRef = - Arc::new(Int32Array::from(vec![Some(1), Some(1), Some(2)])); - let array_b1: ArrayRef = Arc::new(Float64Array::from(vec![20.0, 15.0, 30.0])); - let batch1 = RecordBatch::try_new(Arc::clone(&schema), vec![array_a1, array_b1])?; - - // Insert the first batch. - // At this point the heap is not yet “finished” because the prefix of the last row of the batch - // is not strictly greater than the prefix of the max top‑k row (both being `2`). - topk.insert_batch(batch1)?; - assert!( - !topk.finished, - "Expected 'finished' to be false after the first batch." - ); - - // Create the second batch with two columns: - // Column "a": [2, 3], Column "b": [10.0, 20.0]. - let array_a2: ArrayRef = Arc::new(Int32Array::from(vec![Some(2), Some(3)])); - let array_b2: ArrayRef = Arc::new(Float64Array::from(vec![10.0, 20.0])); - let batch2 = RecordBatch::try_new(Arc::clone(&schema), vec![array_a2, array_b2])?; - - // Insert the second batch. - // The last row in this batch has a prefix value of `3`, - // which is strictly greater than the max top‑k row (with value `2`), - // so try_finish should mark the TopK as finished. - topk.insert_batch(batch2)?; - assert!( - topk.finished, - "Expected 'finished' to be true after the second batch." - ); - - // Verify the TopK correctly emits the top k rows from both batches - // (the value 10.0 for b is from the second batch). - let results: Vec<_> = topk.emit()?.try_collect().await?; - assert_batches_eq!( - &[ - "+---+------+", - "| a | b |", - "+---+------+", - "| 1 | 15.0 |", - "| 1 | 20.0 |", - "| 2 | 10.0 |", - "+---+------+", - ], - &results - ); - - Ok(()) - } } diff --git a/datafusion/proto-common/proto/datafusion_common.proto b/datafusion/proto-common/proto/datafusion_common.proto index 82f1e91d9c9b4..bbeea5e1ec237 100644 --- a/datafusion/proto-common/proto/datafusion_common.proto +++ b/datafusion/proto-common/proto/datafusion_common.proto @@ -545,10 +545,6 @@ message ParquetOptions { uint64 max_row_group_size = 15; string created_by = 16; - - oneof coerce_int96_opt { - string coerce_int96 = 32; - } } enum JoinSide { diff --git a/datafusion/proto-common/src/common.rs b/datafusion/proto-common/src/common.rs index 9af63e3b07365..61711dcf8e088 100644 --- a/datafusion/proto-common/src/common.rs +++ b/datafusion/proto-common/src/common.rs @@ -17,7 +17,6 @@ use datafusion_common::{internal_datafusion_err, DataFusionError}; -/// Return a `DataFusionError::Internal` with the given message pub fn proto_error>(message: S) -> DataFusionError { internal_datafusion_err!("{}", message.into()) } diff --git a/datafusion/proto-common/src/from_proto/mod.rs b/datafusion/proto-common/src/from_proto/mod.rs index bd969db316872..da43a97899565 100644 --- a/datafusion/proto-common/src/from_proto/mod.rs +++ b/datafusion/proto-common/src/from_proto/mod.rs @@ -984,9 +984,6 @@ impl TryFrom<&protobuf::ParquetOptions> for ParquetOptions { maximum_buffered_record_batches_per_stream: value.maximum_buffered_record_batches_per_stream as usize, schema_force_view_types: value.schema_force_view_types, binary_as_string: value.binary_as_string, - coerce_int96: value.coerce_int96_opt.clone().map(|opt| match opt { - protobuf::parquet_options::CoerceInt96Opt::CoerceInt96(v) => Some(v), - }).unwrap_or(None), skip_arrow_metadata: value.skip_arrow_metadata, }) } diff --git a/datafusion/proto-common/src/generated/pbjson.rs b/datafusion/proto-common/src/generated/pbjson.rs index b44b05e9ca296..b0241fd47a26f 100644 --- a/datafusion/proto-common/src/generated/pbjson.rs +++ b/datafusion/proto-common/src/generated/pbjson.rs @@ -4981,9 +4981,6 @@ impl serde::Serialize for ParquetOptions { if self.bloom_filter_ndv_opt.is_some() { len += 1; } - if self.coerce_int96_opt.is_some() { - len += 1; - } let mut struct_ser = serializer.serialize_struct("datafusion_common.ParquetOptions", len)?; if self.enable_page_index { struct_ser.serialize_field("enablePageIndex", &self.enable_page_index)?; @@ -5139,13 +5136,6 @@ impl serde::Serialize for ParquetOptions { } } } - if let Some(v) = self.coerce_int96_opt.as_ref() { - match v { - parquet_options::CoerceInt96Opt::CoerceInt96(v) => { - struct_ser.serialize_field("coerceInt96", v)?; - } - } - } struct_ser.end() } } @@ -5213,8 +5203,6 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { "bloomFilterFpp", "bloom_filter_ndv", "bloomFilterNdv", - "coerce_int96", - "coerceInt96", ]; #[allow(clippy::enum_variant_names)] @@ -5249,7 +5237,6 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { Encoding, BloomFilterFpp, BloomFilterNdv, - CoerceInt96, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -5301,7 +5288,6 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { "encoding" => Ok(GeneratedField::Encoding), "bloomFilterFpp" | "bloom_filter_fpp" => Ok(GeneratedField::BloomFilterFpp), "bloomFilterNdv" | "bloom_filter_ndv" => Ok(GeneratedField::BloomFilterNdv), - "coerceInt96" | "coerce_int96" => Ok(GeneratedField::CoerceInt96), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -5351,7 +5337,6 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { let mut encoding_opt__ = None; let mut bloom_filter_fpp_opt__ = None; let mut bloom_filter_ndv_opt__ = None; - let mut coerce_int96_opt__ = None; while let Some(k) = map_.next_key()? { match k { GeneratedField::EnablePageIndex => { @@ -5548,12 +5533,6 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { } bloom_filter_ndv_opt__ = map_.next_value::<::std::option::Option<::pbjson::private::NumberDeserialize<_>>>()?.map(|x| parquet_options::BloomFilterNdvOpt::BloomFilterNdv(x.0)); } - GeneratedField::CoerceInt96 => { - if coerce_int96_opt__.is_some() { - return Err(serde::de::Error::duplicate_field("coerceInt96")); - } - coerce_int96_opt__ = map_.next_value::<::std::option::Option<_>>()?.map(parquet_options::CoerceInt96Opt::CoerceInt96); - } } } Ok(ParquetOptions { @@ -5587,7 +5566,6 @@ impl<'de> serde::Deserialize<'de> for ParquetOptions { encoding_opt: encoding_opt__, bloom_filter_fpp_opt: bloom_filter_fpp_opt__, bloom_filter_ndv_opt: bloom_filter_ndv_opt__, - coerce_int96_opt: coerce_int96_opt__, }) } } diff --git a/datafusion/proto-common/src/generated/prost.rs b/datafusion/proto-common/src/generated/prost.rs index e029327d481d1..b6e9bc1379832 100644 --- a/datafusion/proto-common/src/generated/prost.rs +++ b/datafusion/proto-common/src/generated/prost.rs @@ -804,8 +804,6 @@ pub struct ParquetOptions { pub bloom_filter_fpp_opt: ::core::option::Option, #[prost(oneof = "parquet_options::BloomFilterNdvOpt", tags = "22")] pub bloom_filter_ndv_opt: ::core::option::Option, - #[prost(oneof = "parquet_options::CoerceInt96Opt", tags = "32")] - pub coerce_int96_opt: ::core::option::Option, } /// Nested message and enum types in `ParquetOptions`. pub mod parquet_options { @@ -859,11 +857,6 @@ pub mod parquet_options { #[prost(uint64, tag = "22")] BloomFilterNdv(u64), } - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum CoerceInt96Opt { - #[prost(string, tag = "32")] - CoerceInt96(::prost::alloc::string::String), - } } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Precision { diff --git a/datafusion/proto-common/src/to_proto/mod.rs b/datafusion/proto-common/src/to_proto/mod.rs index 28927cad03b4c..decd0cf630388 100644 --- a/datafusion/proto-common/src/to_proto/mod.rs +++ b/datafusion/proto-common/src/to_proto/mod.rs @@ -836,7 +836,6 @@ impl TryFrom<&ParquetOptions> for protobuf::ParquetOptions { schema_force_view_types: value.schema_force_view_types, binary_as_string: value.binary_as_string, skip_arrow_metadata: value.skip_arrow_metadata, - coerce_int96_opt: value.coerce_int96.clone().map(protobuf::parquet_options::CoerceInt96Opt::CoerceInt96), }) } } diff --git a/datafusion/proto/Cargo.toml b/datafusion/proto/Cargo.toml index 92e697ad2d9c1..553fccf7d428e 100644 --- a/datafusion/proto/Cargo.toml +++ b/datafusion/proto/Cargo.toml @@ -55,6 +55,7 @@ pbjson = { workspace = true, optional = true } prost = { workspace = true } serde = { version = "1.0", optional = true } serde_json = { workspace = true, optional = true } + [dev-dependencies] datafusion-functions = { workspace = true, default-features = true } datafusion-functions-aggregate = { workspace = true } diff --git a/datafusion/proto/proto/datafusion.proto b/datafusion/proto/proto/datafusion.proto index 39236da3b9a82..2e028eb291181 100644 --- a/datafusion/proto/proto/datafusion.proto +++ b/datafusion/proto/proto/datafusion.proto @@ -21,7 +21,7 @@ syntax = "proto3"; package datafusion; option java_multiple_files = true; -option java_package = "org.apache.datafusion.protobuf"; +option java_package = "org.apache.arrow.datafusion.protobuf"; option java_outer_classname = "DatafusionProto"; import "datafusion/proto-common/proto/datafusion_common.proto"; @@ -90,7 +90,7 @@ message ListingTableScanNode { ProjectionColumns projection = 4; datafusion_common.Schema schema = 5; repeated LogicalExprNode filters = 6; - repeated PartitionColumn table_partition_cols = 7; + repeated string table_partition_cols = 7; bool collect_stat = 8; uint32 target_partitions = 9; oneof FileFormatType { diff --git a/datafusion/proto/src/generated/datafusion_proto_common.rs b/datafusion/proto/src/generated/datafusion_proto_common.rs index e029327d481d1..b6e9bc1379832 100644 --- a/datafusion/proto/src/generated/datafusion_proto_common.rs +++ b/datafusion/proto/src/generated/datafusion_proto_common.rs @@ -804,8 +804,6 @@ pub struct ParquetOptions { pub bloom_filter_fpp_opt: ::core::option::Option, #[prost(oneof = "parquet_options::BloomFilterNdvOpt", tags = "22")] pub bloom_filter_ndv_opt: ::core::option::Option, - #[prost(oneof = "parquet_options::CoerceInt96Opt", tags = "32")] - pub coerce_int96_opt: ::core::option::Option, } /// Nested message and enum types in `ParquetOptions`. pub mod parquet_options { @@ -859,11 +857,6 @@ pub mod parquet_options { #[prost(uint64, tag = "22")] BloomFilterNdv(u64), } - #[derive(Clone, PartialEq, ::prost::Oneof)] - pub enum CoerceInt96Opt { - #[prost(string, tag = "32")] - CoerceInt96(::prost::alloc::string::String), - } } #[derive(Clone, PartialEq, ::prost::Message)] pub struct Precision { diff --git a/datafusion/proto/src/generated/prost.rs b/datafusion/proto/src/generated/prost.rs index 41c60b22e3bc7..d2165dad48501 100644 --- a/datafusion/proto/src/generated/prost.rs +++ b/datafusion/proto/src/generated/prost.rs @@ -115,8 +115,8 @@ pub struct ListingTableScanNode { pub schema: ::core::option::Option, #[prost(message, repeated, tag = "6")] pub filters: ::prost::alloc::vec::Vec, - #[prost(message, repeated, tag = "7")] - pub table_partition_cols: ::prost::alloc::vec::Vec, + #[prost(string, repeated, tag = "7")] + pub table_partition_cols: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, #[prost(bool, tag = "8")] pub collect_stat: bool, #[prost(uint32, tag = "9")] diff --git a/datafusion/proto/src/logical_plan/file_formats.rs b/datafusion/proto/src/logical_plan/file_formats.rs index 5c33277dc9f74..e22738973284e 100644 --- a/datafusion/proto/src/logical_plan/file_formats.rs +++ b/datafusion/proto/src/logical_plan/file_formats.rs @@ -415,9 +415,6 @@ impl TableParquetOptionsProto { schema_force_view_types: global_options.global.schema_force_view_types, binary_as_string: global_options.global.binary_as_string, skip_arrow_metadata: global_options.global.skip_arrow_metadata, - coerce_int96_opt: global_options.global.coerce_int96.map(|compression| { - parquet_options::CoerceInt96Opt::CoerceInt96(compression) - }), }), column_specific_options: column_specific_options.into_iter().map(|(column_name, options)| { ParquetColumnSpecificOptions { @@ -514,9 +511,6 @@ impl From<&ParquetOptionsProto> for ParquetOptions { schema_force_view_types: proto.schema_force_view_types, binary_as_string: proto.binary_as_string, skip_arrow_metadata: proto.skip_arrow_metadata, - coerce_int96: proto.coerce_int96_opt.as_ref().map(|opt| match opt { - parquet_options::CoerceInt96Opt::CoerceInt96(coerce_int96) => coerce_int96.clone(), - }), } } } diff --git a/datafusion/proto/src/logical_plan/mod.rs b/datafusion/proto/src/logical_plan/mod.rs index a39e6dac37c10..c65569ef1cfbe 100644 --- a/datafusion/proto/src/logical_plan/mod.rs +++ b/datafusion/proto/src/logical_plan/mod.rs @@ -33,7 +33,7 @@ use crate::{ }; use crate::protobuf::{proto_error, ToProtoError}; -use arrow::datatypes::{DataType, Schema, SchemaBuilder, SchemaRef}; +use arrow::datatypes::{DataType, Schema, SchemaRef}; use datafusion::datasource::cte_worktable::CteWorkTable; #[cfg(feature = "avro")] use datafusion::datasource::file_format::avro::AvroFormat; @@ -355,7 +355,10 @@ impl AsLogicalPlan for LogicalPlanNode { .as_ref() .map(|expr| from_proto::parse_expr(expr, ctx, extension_codec)) .transpose()? - .ok_or_else(|| proto_error("expression required"))?; + .ok_or_else(|| { + DataFusionError::Internal("expression required".to_string()) + })?; + // .try_into()?; LogicalPlanBuilder::from(input).filter(expr)?.build() } LogicalPlanType::Window(window) => { @@ -455,25 +458,23 @@ impl AsLogicalPlan for LogicalPlanNode { .map(ListingTableUrl::parse) .collect::, _>>()?; - let partition_columns = scan - .table_partition_cols - .iter() - .map(|col| { - let Some(arrow_type) = col.arrow_type.as_ref() else { - return Err(proto_error( - "Missing Arrow type in partition columns", - )); - }; - let arrow_type = DataType::try_from(arrow_type).map_err(|e| { - proto_error(format!("Received an unknown ArrowType: {}", e)) - })?; - Ok((col.name.clone(), arrow_type)) - }) - .collect::>>()?; - let options = ListingOptions::new(file_format) .with_file_extension(&scan.file_extension) - .with_table_partition_cols(partition_columns) + .with_table_partition_cols( + scan.table_partition_cols + .iter() + .map(|col| { + ( + col.clone(), + schema + .field_with_name(col) + .unwrap() + .data_type() + .clone(), + ) + }) + .collect(), + ) .with_collect_stat(scan.collect_stat) .with_target_partitions(scan.target_partitions as usize) .with_file_sort_order(all_sort_orders); @@ -1045,6 +1046,7 @@ impl AsLogicalPlan for LogicalPlanNode { }) } }; + let schema: protobuf::Schema = schema.as_ref().try_into()?; let filters: Vec = serialize_exprs(filters, extension_codec)?; @@ -1097,21 +1099,6 @@ impl AsLogicalPlan for LogicalPlanNode { let options = listing_table.options(); - let mut builder = SchemaBuilder::from(schema.as_ref()); - for (idx, field) in schema.fields().iter().enumerate().rev() { - if options - .table_partition_cols - .iter() - .any(|(name, _)| name == field.name()) - { - builder.remove(idx); - } - } - - let schema = builder.finish(); - - let schema: protobuf::Schema = (&schema).try_into()?; - let mut exprs_vec: Vec = vec![]; for order in &options.file_sort_order { let expr_vec = SortExprNodeCollection { @@ -1120,24 +1107,6 @@ impl AsLogicalPlan for LogicalPlanNode { exprs_vec.push(expr_vec); } - let partition_columns = options - .table_partition_cols - .iter() - .map(|(name, arrow_type)| { - let arrow_type = protobuf::ArrowType::try_from(arrow_type) - .map_err(|e| { - proto_error(format!( - "Received an unknown ArrowType: {}", - e - )) - })?; - Ok(protobuf::PartitionColumn { - name: name.clone(), - arrow_type: Some(arrow_type), - }) - }) - .collect::>>()?; - Ok(LogicalPlanNode { logical_plan_type: Some(LogicalPlanType::ListingScan( protobuf::ListingTableScanNode { @@ -1145,7 +1114,11 @@ impl AsLogicalPlan for LogicalPlanNode { table_name: Some(table_name.clone().into()), collect_stat: options.collect_stat, file_extension: options.file_extension.clone(), - table_partition_cols: partition_columns, + table_partition_cols: options + .table_partition_cols + .iter() + .map(|x| x.0.clone()) + .collect::>(), paths: listing_table .table_paths() .iter() @@ -1160,7 +1133,6 @@ impl AsLogicalPlan for LogicalPlanNode { )), }) } else if let Some(view_table) = source.downcast_ref::() { - let schema: protobuf::Schema = schema.as_ref().try_into()?; Ok(LogicalPlanNode { logical_plan_type: Some(LogicalPlanType::ViewScan(Box::new( protobuf::ViewTableScanNode { @@ -1195,7 +1167,6 @@ impl AsLogicalPlan for LogicalPlanNode { )), }) } else { - let schema: protobuf::Schema = schema.as_ref().try_into()?; let mut bytes = vec![]; extension_codec .try_encode_table_provider(table_name, provider, &mut bytes) diff --git a/datafusion/proto/src/physical_plan/from_proto.rs b/datafusion/proto/src/physical_plan/from_proto.rs index a886fc2425456..d1141060f9e05 100644 --- a/datafusion/proto/src/physical_plan/from_proto.rs +++ b/datafusion/proto/src/physical_plan/from_proto.rs @@ -67,7 +67,7 @@ impl From<&protobuf::PhysicalColumn> for Column { /// * `proto` - Input proto with physical sort expression node /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_sort_expr( proto: &protobuf::PhysicalSortExprNode, @@ -94,7 +94,7 @@ pub fn parse_physical_sort_expr( /// * `proto` - Input proto with vector of physical sort expression node /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_sort_exprs( proto: &[protobuf::PhysicalSortExprNode], @@ -118,7 +118,7 @@ pub fn parse_physical_sort_exprs( /// * `name` - Name of the window expression. /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_window_expr( proto: &protobuf::PhysicalWindowExprNode, @@ -203,7 +203,7 @@ where /// * `proto` - Input proto with physical expression node /// * `registry` - A registry knows how to build logical expressions out of user-defined function names /// * `input_schema` - The Arrow schema for the input, used for determining expression data types -/// when performing type coercion. +/// when performing type coercion. /// * `codec` - An extension codec used to decode custom UDFs. pub fn parse_physical_expr( proto: &protobuf::PhysicalExprNode, @@ -555,7 +555,7 @@ impl TryFrom<&protobuf::PartitionedFile> for PartitionedFile { object_meta: ObjectMeta { location: Path::from(val.path.as_str()), last_modified: Utc.timestamp_nanos(val.last_modified_ns as i64), - size: val.size, + size: val.size as usize, e_tag: None, version: None, }, @@ -565,11 +565,7 @@ impl TryFrom<&protobuf::PartitionedFile> for PartitionedFile { .map(|v| v.try_into()) .collect::, _>>()?, range: val.range.as_ref().map(|v| v.try_into()).transpose()?, - statistics: val - .statistics - .as_ref() - .map(|v| v.try_into().map(Arc::new)) - .transpose()?, + statistics: val.statistics.as_ref().map(|v| v.try_into()).transpose()?, extensions: None, metadata_size_hint: None, }) diff --git a/datafusion/proto/src/physical_plan/mod.rs b/datafusion/proto/src/physical_plan/mod.rs index 90d071ab23f56..24cc0d5b3b028 100644 --- a/datafusion/proto/src/physical_plan/mod.rs +++ b/datafusion/proto/src/physical_plan/mod.rs @@ -127,1294 +127,763 @@ impl AsExecutionPlan for protobuf::PhysicalPlanNode { )) })?; match plan { - PhysicalPlanType::Explain(explain) => self.try_into_explain_physical_plan( - explain, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::Projection(projection) => self - .try_into_projection_physical_plan( - projection, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::Filter(filter) => self.try_into_filter_physical_plan( - filter, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::CsvScan(scan) => self.try_into_csv_scan_physical_plan( - scan, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::JsonScan(scan) => self.try_into_json_scan_physical_plan( - scan, - registry, - runtime, - extension_codec, - ), - #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] - PhysicalPlanType::ParquetScan(scan) => self - .try_into_parquet_scan_physical_plan( - scan, + PhysicalPlanType::Explain(explain) => Ok(Arc::new(ExplainExec::new( + Arc::new(explain.schema.as_ref().unwrap().try_into()?), + explain + .stringified_plans + .iter() + .map(|plan| plan.into()) + .collect(), + explain.verbose, + ))), + PhysicalPlanType::Projection(projection) => { + let input: Arc = into_physical_plan( + &projection.input, registry, runtime, extension_codec, - ), - #[cfg_attr(not(feature = "avro"), allow(unused_variables))] - PhysicalPlanType::AvroScan(scan) => self.try_into_avro_scan_physical_plan( - scan, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::CoalesceBatches(coalesce_batches) => self - .try_into_coalesce_batches_physical_plan( - coalesce_batches, + )?; + let exprs = projection + .expr + .iter() + .zip(projection.expr_name.iter()) + .map(|(expr, name)| { + Ok(( + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + )?, + name.to_string(), + )) + }) + .collect::, String)>>>()?; + Ok(Arc::new(ProjectionExec::try_new(exprs, input)?)) + } + PhysicalPlanType::Filter(filter) => { + let input: Arc = into_physical_plan( + &filter.input, registry, runtime, extension_codec, - ), - PhysicalPlanType::Merge(merge) => self.try_into_merge_physical_plan( - merge, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::Repartition(repart) => self - .try_into_repartition_physical_plan( - repart, + )?; + let predicate = filter + .expr + .as_ref() + .map(|expr| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + }) + .transpose()? + .ok_or_else(|| { + DataFusionError::Internal( + "filter (FilterExecNode) in PhysicalPlanNode is missing." + .to_owned(), + ) + })?; + let filter_selectivity = filter.default_filter_selectivity.try_into(); + let projection = if !filter.projection.is_empty() { + Some( + filter + .projection + .iter() + .map(|i| *i as usize) + .collect::>(), + ) + } else { + None + }; + let filter = + FilterExec::try_new(predicate, input)?.with_projection(projection)?; + match filter_selectivity { + Ok(filter_selectivity) => Ok(Arc::new( + filter.with_default_selectivity(filter_selectivity)?, + )), + Err(_) => Err(DataFusionError::Internal( + "filter_selectivity in PhysicalPlanNode is invalid ".to_owned(), + )), + } + } + PhysicalPlanType::CsvScan(scan) => { + let escape = if let Some( + protobuf::csv_scan_exec_node::OptionalEscape::Escape(escape), + ) = &scan.optional_escape + { + Some(str_to_byte(escape, "escape")?) + } else { + None + }; + + let comment = if let Some( + protobuf::csv_scan_exec_node::OptionalComment::Comment(comment), + ) = &scan.optional_comment + { + Some(str_to_byte(comment, "comment")?) + } else { + None + }; + + let source = Arc::new( + CsvSource::new( + scan.has_header, + str_to_byte(&scan.delimiter, "delimiter")?, + 0, + ) + .with_escape(escape) + .with_comment(comment), + ); + + let conf = FileScanConfigBuilder::from(parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), registry, - runtime, extension_codec, - ), - PhysicalPlanType::GlobalLimit(limit) => self - .try_into_global_limit_physical_plan( - limit, + source, + )?) + .with_newlines_in_values(scan.newlines_in_values) + .with_file_compression_type(FileCompressionType::UNCOMPRESSED) + .build(); + Ok(DataSourceExec::from_data_source(conf)) + } + PhysicalPlanType::JsonScan(scan) => { + let scan_conf = parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), registry, - runtime, extension_codec, - ), - PhysicalPlanType::LocalLimit(limit) => self - .try_into_local_limit_physical_plan( - limit, + Arc::new(JsonSource::new()), + )?; + Ok(DataSourceExec::from_data_source(scan_conf)) + } + #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] + PhysicalPlanType::ParquetScan(scan) => { + #[cfg(feature = "parquet")] + { + let schema = parse_protobuf_file_scan_schema( + scan.base_conf.as_ref().unwrap(), + )?; + let predicate = scan + .predicate + .as_ref() + .map(|expr| { + parse_physical_expr( + expr, + registry, + schema.as_ref(), + extension_codec, + ) + }) + .transpose()?; + let mut options = TableParquetOptions::default(); + + if let Some(table_options) = scan.parquet_options.as_ref() { + options = table_options.try_into()?; + } + let mut source = ParquetSource::new(options); + + if let Some(predicate) = predicate { + source = source.with_predicate(Arc::clone(&schema), predicate); + } + let base_config = parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), + registry, + extension_codec, + Arc::new(source), + )?; + Ok(DataSourceExec::from_data_source(base_config)) + } + #[cfg(not(feature = "parquet"))] + panic!("Unable to process a Parquet PhysicalPlan when `parquet` feature is not enabled") + } + #[cfg_attr(not(feature = "avro"), allow(unused_variables))] + PhysicalPlanType::AvroScan(scan) => { + #[cfg(feature = "avro")] + { + let conf = parse_protobuf_file_scan_config( + scan.base_conf.as_ref().unwrap(), + registry, + extension_codec, + Arc::new(AvroSource::new()), + )?; + Ok(DataSourceExec::from_data_source(conf)) + } + #[cfg(not(feature = "avro"))] + panic!("Unable to process a Avro PhysicalPlan when `avro` feature is not enabled") + } + PhysicalPlanType::CoalesceBatches(coalesce_batches) => { + let input: Arc = into_physical_plan( + &coalesce_batches.input, registry, runtime, extension_codec, - ), - PhysicalPlanType::Window(window_agg) => self.try_into_window_physical_plan( - window_agg, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::Aggregate(hash_agg) => self - .try_into_aggregate_physical_plan( - hash_agg, + )?; + Ok(Arc::new( + CoalesceBatchesExec::new( + input, + coalesce_batches.target_batch_size as usize, + ) + .with_fetch(coalesce_batches.fetch.map(|f| f as usize)), + )) + } + PhysicalPlanType::Merge(merge) => { + let input: Arc = + into_physical_plan(&merge.input, registry, runtime, extension_codec)?; + Ok(Arc::new(CoalescePartitionsExec::new(input))) + } + PhysicalPlanType::Repartition(repart) => { + let input: Arc = into_physical_plan( + &repart.input, registry, runtime, extension_codec, - ), - PhysicalPlanType::HashJoin(hashjoin) => self - .try_into_hash_join_physical_plan( - hashjoin, + )?; + let partitioning = parse_protobuf_partitioning( + repart.partitioning.as_ref(), registry, - runtime, + input.schema().as_ref(), extension_codec, - ), - PhysicalPlanType::SymmetricHashJoin(sym_join) => self - .try_into_symmetric_hash_join_physical_plan( - sym_join, + )?; + Ok(Arc::new(RepartitionExec::try_new( + input, + partitioning.unwrap(), + )?)) + } + PhysicalPlanType::GlobalLimit(limit) => { + let input: Arc = + into_physical_plan(&limit.input, registry, runtime, extension_codec)?; + let fetch = if limit.fetch >= 0 { + Some(limit.fetch as usize) + } else { + None + }; + Ok(Arc::new(GlobalLimitExec::new( + input, + limit.skip as usize, + fetch, + ))) + } + PhysicalPlanType::LocalLimit(limit) => { + let input: Arc = + into_physical_plan(&limit.input, registry, runtime, extension_codec)?; + Ok(Arc::new(LocalLimitExec::new(input, limit.fetch as usize))) + } + PhysicalPlanType::Window(window_agg) => { + let input: Arc = into_physical_plan( + &window_agg.input, registry, runtime, extension_codec, - ), - PhysicalPlanType::Union(union) => self.try_into_union_physical_plan( - union, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::Interleave(interleave) => self - .try_into_interleave_physical_plan( - interleave, + )?; + let input_schema = input.schema(); + + let physical_window_expr: Vec> = window_agg + .window_expr + .iter() + .map(|window_expr| { + parse_physical_window_expr( + window_expr, + registry, + input_schema.as_ref(), + extension_codec, + ) + }) + .collect::, _>>()?; + + let partition_keys = window_agg + .partition_keys + .iter() + .map(|expr| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + }) + .collect::>>>()?; + + if let Some(input_order_mode) = window_agg.input_order_mode.as_ref() { + let input_order_mode = match input_order_mode { + window_agg_exec_node::InputOrderMode::Linear(_) => { + InputOrderMode::Linear + } + window_agg_exec_node::InputOrderMode::PartiallySorted( + protobuf::PartiallySortedInputOrderMode { columns }, + ) => InputOrderMode::PartiallySorted( + columns.iter().map(|c| *c as usize).collect(), + ), + window_agg_exec_node::InputOrderMode::Sorted(_) => { + InputOrderMode::Sorted + } + }; + + Ok(Arc::new(BoundedWindowAggExec::try_new( + physical_window_expr, + input, + input_order_mode, + !partition_keys.is_empty(), + )?)) + } else { + Ok(Arc::new(WindowAggExec::try_new( + physical_window_expr, + input, + !partition_keys.is_empty(), + )?)) + } + } + PhysicalPlanType::Aggregate(hash_agg) => { + let input: Arc = into_physical_plan( + &hash_agg.input, registry, runtime, extension_codec, - ), - PhysicalPlanType::CrossJoin(crossjoin) => self - .try_into_cross_join_physical_plan( - crossjoin, + )?; + let mode = protobuf::AggregateMode::try_from(hash_agg.mode).map_err( + |_| { + proto_error(format!( + "Received a AggregateNode message with unknown AggregateMode {}", + hash_agg.mode + )) + }, + )?; + let agg_mode: AggregateMode = match mode { + protobuf::AggregateMode::Partial => AggregateMode::Partial, + protobuf::AggregateMode::Final => AggregateMode::Final, + protobuf::AggregateMode::FinalPartitioned => { + AggregateMode::FinalPartitioned + } + protobuf::AggregateMode::Single => AggregateMode::Single, + protobuf::AggregateMode::SinglePartitioned => { + AggregateMode::SinglePartitioned + } + }; + + let num_expr = hash_agg.group_expr.len(); + + let group_expr = hash_agg + .group_expr + .iter() + .zip(hash_agg.group_expr_name.iter()) + .map(|(expr, name)| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + .map(|expr| (expr, name.to_string())) + }) + .collect::, _>>()?; + + let null_expr = hash_agg + .null_expr + .iter() + .zip(hash_agg.group_expr_name.iter()) + .map(|(expr, name)| { + parse_physical_expr( + expr, + registry, + input.schema().as_ref(), + extension_codec, + ) + .map(|expr| (expr, name.to_string())) + }) + .collect::, _>>()?; + + let groups: Vec> = if !hash_agg.groups.is_empty() { + hash_agg + .groups + .chunks(num_expr) + .map(|g| g.to_vec()) + .collect::>>() + } else { + vec![] + }; + + let input_schema = hash_agg.input_schema.as_ref().ok_or_else(|| { + DataFusionError::Internal( + "input_schema in AggregateNode is missing.".to_owned(), + ) + })?; + let physical_schema: SchemaRef = SchemaRef::new(input_schema.try_into()?); + + let physical_filter_expr = hash_agg + .filter_expr + .iter() + .map(|expr| { + expr.expr + .as_ref() + .map(|e| { + parse_physical_expr( + e, + registry, + &physical_schema, + extension_codec, + ) + }) + .transpose() + }) + .collect::, _>>()?; + + let physical_aggr_expr: Vec> = hash_agg + .aggr_expr + .iter() + .zip(hash_agg.aggr_expr_name.iter()) + .map(|(expr, name)| { + let expr_type = expr.expr_type.as_ref().ok_or_else(|| { + proto_error("Unexpected empty aggregate physical expression") + })?; + + match expr_type { + ExprType::AggregateExpr(agg_node) => { + let input_phy_expr: Vec> = agg_node.expr.iter() + .map(|e| parse_physical_expr(e, registry, &physical_schema, extension_codec)).collect::>>()?; + let ordering_req: LexOrdering = agg_node.ordering_req.iter() + .map(|e| parse_physical_sort_expr(e, registry, &physical_schema, extension_codec)) + .collect::>()?; + agg_node.aggregate_function.as_ref().map(|func| { + match func { + AggregateFunction::UserDefinedAggrFunction(udaf_name) => { + let agg_udf = match &agg_node.fun_definition { + Some(buf) => extension_codec.try_decode_udaf(udaf_name, buf)?, + None => registry.udaf(udaf_name)? + }; + + AggregateExprBuilder::new(agg_udf, input_phy_expr) + .schema(Arc::clone(&physical_schema)) + .alias(name) + .with_ignore_nulls(agg_node.ignore_nulls) + .with_distinct(agg_node.distinct) + .order_by(ordering_req) + .build() + .map(Arc::new) + } + } + }).transpose()?.ok_or_else(|| { + proto_error("Invalid AggregateExpr, missing aggregate_function") + }) + } + _ => internal_err!( + "Invalid aggregate expression for AggregateExec" + ), + } + }) + .collect::, _>>()?; + + let limit = hash_agg + .limit + .as_ref() + .map(|lit_value| lit_value.limit as usize); + + let agg = AggregateExec::try_new( + agg_mode, + PhysicalGroupBy::new(group_expr, null_expr, groups), + physical_aggr_expr, + physical_filter_expr, + input, + physical_schema, + )?; + + let agg = agg.with_limit(limit); + + Ok(Arc::new(agg)) + } + PhysicalPlanType::HashJoin(hashjoin) => { + let left: Arc = into_physical_plan( + &hashjoin.left, registry, runtime, extension_codec, - ), - PhysicalPlanType::Empty(empty) => self.try_into_empty_physical_plan( - empty, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::PlaceholderRow(placeholder) => self - .try_into_placeholder_row_physical_plan( - placeholder, + )?; + let right: Arc = into_physical_plan( + &hashjoin.right, registry, runtime, extension_codec, - ), - PhysicalPlanType::Sort(sort) => { - self.try_into_sort_physical_plan(sort, registry, runtime, extension_codec) + )?; + let left_schema = left.schema(); + let right_schema = right.schema(); + let on: Vec<(PhysicalExprRef, PhysicalExprRef)> = hashjoin + .on + .iter() + .map(|col| { + let left = parse_physical_expr( + &col.left.clone().unwrap(), + registry, + left_schema.as_ref(), + extension_codec, + )?; + let right = parse_physical_expr( + &col.right.clone().unwrap(), + registry, + right_schema.as_ref(), + extension_codec, + )?; + Ok((left, right)) + }) + .collect::>()?; + let join_type = protobuf::JoinType::try_from(hashjoin.join_type) + .map_err(|_| { + proto_error(format!( + "Received a HashJoinNode message with unknown JoinType {}", + hashjoin.join_type + )) + })?; + let filter = hashjoin + .filter + .as_ref() + .map(|f| { + let schema = f + .schema + .as_ref() + .ok_or_else(|| proto_error("Missing JoinFilter schema"))? + .try_into()?; + + let expression = parse_physical_expr( + f.expression.as_ref().ok_or_else(|| { + proto_error("Unexpected empty filter expression") + })?, + registry, &schema, + extension_codec, + )?; + let column_indices = f.column_indices + .iter() + .map(|i| { + let side = protobuf::JoinSide::try_from(i.side) + .map_err(|_| proto_error(format!( + "Received a HashJoinNode message with JoinSide in Filter {}", + i.side)) + )?; + + Ok(ColumnIndex { + index: i.index as usize, + side: side.into(), + }) + }) + .collect::>>()?; + + Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; + + let partition_mode = protobuf::PartitionMode::try_from( + hashjoin.partition_mode, + ) + .map_err(|_| { + proto_error(format!( + "Received a HashJoinNode message with unknown PartitionMode {}", + hashjoin.partition_mode + )) + })?; + let partition_mode = match partition_mode { + protobuf::PartitionMode::CollectLeft => PartitionMode::CollectLeft, + protobuf::PartitionMode::Partitioned => PartitionMode::Partitioned, + protobuf::PartitionMode::Auto => PartitionMode::Auto, + }; + let projection = if !hashjoin.projection.is_empty() { + Some( + hashjoin + .projection + .iter() + .map(|i| *i as usize) + .collect::>(), + ) + } else { + None + }; + Ok(Arc::new(HashJoinExec::try_new( + left, + right, + on, + filter, + &join_type.into(), + projection, + partition_mode, + hashjoin.null_equals_null, + )?)) } - PhysicalPlanType::SortPreservingMerge(sort) => self - .try_into_sort_preserving_merge_physical_plan( - sort, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::Extension(extension) => self - .try_into_extension_physical_plan( - extension, + PhysicalPlanType::SymmetricHashJoin(sym_join) => { + let left = into_physical_plan( + &sym_join.left, registry, runtime, extension_codec, - ), - PhysicalPlanType::NestedLoopJoin(join) => self - .try_into_nested_loop_join_physical_plan( - join, + )?; + let right = into_physical_plan( + &sym_join.right, registry, runtime, extension_codec, - ), - PhysicalPlanType::Analyze(analyze) => self.try_into_analyze_physical_plan( - analyze, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::JsonSink(sink) => self.try_into_json_sink_physical_plan( - sink, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::CsvSink(sink) => self.try_into_csv_sink_physical_plan( - sink, - registry, - runtime, - extension_codec, - ), - - #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] - PhysicalPlanType::ParquetSink(sink) => self - .try_into_parquet_sink_physical_plan( - sink, - registry, - runtime, - extension_codec, - ), - PhysicalPlanType::Unnest(unnest) => self.try_into_unnest_physical_plan( - unnest, - registry, - runtime, - extension_codec, - ), - } - } - - fn try_from_physical_plan( - plan: Arc, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result - where - Self: Sized, - { - let plan_clone = Arc::clone(&plan); - let plan = plan.as_any(); - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_explain_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_projection_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_analyze_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_filter_exec( - exec, - extension_codec, - ); - } - - if let Some(limit) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_global_limit_exec( - limit, - extension_codec, - ); - } - - if let Some(limit) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_local_limit_exec( - limit, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_hash_join_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_symmetric_hash_join_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_cross_join_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_aggregate_exec( - exec, - extension_codec, - ); - } - - if let Some(empty) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_empty_exec( - empty, - extension_codec, - ); - } - - if let Some(empty) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_placeholder_row_exec( - empty, - extension_codec, - ); - } - - if let Some(coalesce_batches) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_coalesce_batches_exec( - coalesce_batches, - extension_codec, - ); - } - - if let Some(data_source_exec) = plan.downcast_ref::() { - if let Some(node) = protobuf::PhysicalPlanNode::try_from_data_source_exec( - data_source_exec, - extension_codec, - )? { - return Ok(node); - } - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_coalesce_partitions_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_repartition_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_sort_exec(exec, extension_codec); - } - - if let Some(union) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_union_exec( - union, - extension_codec, - ); - } - - if let Some(interleave) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_interleave_exec( - interleave, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_sort_preserving_merge_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_nested_loop_join_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_window_agg_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_bounded_window_agg_exec( - exec, - extension_codec, - ); - } - - if let Some(exec) = plan.downcast_ref::() { - if let Some(node) = protobuf::PhysicalPlanNode::try_from_data_sink_exec( - exec, - extension_codec, - )? { - return Ok(node); - } - } - - if let Some(exec) = plan.downcast_ref::() { - return protobuf::PhysicalPlanNode::try_from_unnest_exec( - exec, - extension_codec, - ); - } - - let mut buf: Vec = vec![]; - match extension_codec.try_encode(Arc::clone(&plan_clone), &mut buf) { - Ok(_) => { - let inputs: Vec = plan_clone - .children() - .into_iter() - .cloned() - .map(|i| { - protobuf::PhysicalPlanNode::try_from_physical_plan( - i, - extension_codec, - ) - }) - .collect::>()?; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Extension( - protobuf::PhysicalExtensionNode { node: buf, inputs }, - )), - }) - } - Err(e) => internal_err!( - "Unsupported plan and extension codec failed with [{e}]. Plan: {plan_clone:?}" - ), - } - } -} - -impl protobuf::PhysicalPlanNode { - fn try_into_explain_physical_plan( - &self, - explain: &protobuf::ExplainExecNode, - _registry: &dyn FunctionRegistry, - _runtime: &RuntimeEnv, - _extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - Ok(Arc::new(ExplainExec::new( - Arc::new(explain.schema.as_ref().unwrap().try_into()?), - explain - .stringified_plans - .iter() - .map(|plan| plan.into()) - .collect(), - explain.verbose, - ))) - } - - fn try_into_projection_physical_plan( - &self, - projection: &protobuf::ProjectionExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&projection.input, registry, runtime, extension_codec)?; - let exprs = projection - .expr - .iter() - .zip(projection.expr_name.iter()) - .map(|(expr, name)| { - Ok(( - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - )?, - name.to_string(), - )) - }) - .collect::, String)>>>()?; - Ok(Arc::new(ProjectionExec::try_new(exprs, input)?)) - } - - fn try_into_filter_physical_plan( - &self, - filter: &protobuf::FilterExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&filter.input, registry, runtime, extension_codec)?; - let predicate = filter - .expr - .as_ref() - .map(|expr| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - }) - .transpose()? - .ok_or_else(|| { - DataFusionError::Internal( - "filter (FilterExecNode) in PhysicalPlanNode is missing.".to_owned(), - ) - })?; - let filter_selectivity = filter.default_filter_selectivity.try_into(); - let projection = if !filter.projection.is_empty() { - Some( - filter - .projection + )?; + let left_schema = left.schema(); + let right_schema = right.schema(); + let on = sym_join + .on .iter() - .map(|i| *i as usize) - .collect::>(), - ) - } else { - None - }; - let filter = - FilterExec::try_new(predicate, input)?.with_projection(projection)?; - match filter_selectivity { - Ok(filter_selectivity) => Ok(Arc::new( - filter.with_default_selectivity(filter_selectivity)?, - )), - Err(_) => Err(DataFusionError::Internal( - "filter_selectivity in PhysicalPlanNode is invalid ".to_owned(), - )), - } - } - - fn try_into_csv_scan_physical_plan( - &self, - scan: &protobuf::CsvScanExecNode, - registry: &dyn FunctionRegistry, - _runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let escape = - if let Some(protobuf::csv_scan_exec_node::OptionalEscape::Escape(escape)) = - &scan.optional_escape - { - Some(str_to_byte(escape, "escape")?) - } else { - None - }; - - let comment = if let Some( - protobuf::csv_scan_exec_node::OptionalComment::Comment(comment), - ) = &scan.optional_comment - { - Some(str_to_byte(comment, "comment")?) - } else { - None - }; - - let source = Arc::new( - CsvSource::new( - scan.has_header, - str_to_byte(&scan.delimiter, "delimiter")?, - 0, - ) - .with_escape(escape) - .with_comment(comment), - ); - - let conf = FileScanConfigBuilder::from(parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), - registry, - extension_codec, - source, - )?) - .with_newlines_in_values(scan.newlines_in_values) - .with_file_compression_type(FileCompressionType::UNCOMPRESSED) - .build(); - Ok(DataSourceExec::from_data_source(conf)) - } - - fn try_into_json_scan_physical_plan( - &self, - scan: &protobuf::JsonScanExecNode, - registry: &dyn FunctionRegistry, - _runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let scan_conf = parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), - registry, - extension_codec, - Arc::new(JsonSource::new()), - )?; - Ok(DataSourceExec::from_data_source(scan_conf)) - } - - #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] - fn try_into_parquet_scan_physical_plan( - &self, - scan: &protobuf::ParquetScanExecNode, - registry: &dyn FunctionRegistry, - _runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - #[cfg(feature = "parquet")] - { - let schema = - parse_protobuf_file_scan_schema(scan.base_conf.as_ref().unwrap())?; - let predicate = scan - .predicate - .as_ref() - .map(|expr| { - parse_physical_expr(expr, registry, schema.as_ref(), extension_codec) - }) - .transpose()?; - let mut options = TableParquetOptions::default(); - - if let Some(table_options) = scan.parquet_options.as_ref() { - options = table_options.try_into()?; - } - let mut source = ParquetSource::new(options); - - if let Some(predicate) = predicate { - source = source.with_predicate(Arc::clone(&schema), predicate); - } - let base_config = parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), - registry, - extension_codec, - Arc::new(source), - )?; - Ok(DataSourceExec::from_data_source(base_config)) - } - #[cfg(not(feature = "parquet"))] - panic!("Unable to process a Parquet PhysicalPlan when `parquet` feature is not enabled") - } - - #[cfg_attr(not(feature = "avro"), allow(unused_variables))] - fn try_into_avro_scan_physical_plan( - &self, - scan: &protobuf::AvroScanExecNode, - registry: &dyn FunctionRegistry, - _runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - #[cfg(feature = "avro")] - { - let conf = parse_protobuf_file_scan_config( - scan.base_conf.as_ref().unwrap(), - registry, - extension_codec, - Arc::new(AvroSource::new()), - )?; - Ok(DataSourceExec::from_data_source(conf)) - } - #[cfg(not(feature = "avro"))] - panic!("Unable to process a Avro PhysicalPlan when `avro` feature is not enabled") - } - - fn try_into_coalesce_batches_physical_plan( - &self, - coalesce_batches: &protobuf::CoalesceBatchesExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = into_physical_plan( - &coalesce_batches.input, - registry, - runtime, - extension_codec, - )?; - Ok(Arc::new( - CoalesceBatchesExec::new(input, coalesce_batches.target_batch_size as usize) - .with_fetch(coalesce_batches.fetch.map(|f| f as usize)), - )) - } - - fn try_into_merge_physical_plan( - &self, - merge: &protobuf::CoalescePartitionsExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&merge.input, registry, runtime, extension_codec)?; - Ok(Arc::new(CoalescePartitionsExec::new(input))) - } - - fn try_into_repartition_physical_plan( - &self, - repart: &protobuf::RepartitionExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&repart.input, registry, runtime, extension_codec)?; - let partitioning = parse_protobuf_partitioning( - repart.partitioning.as_ref(), - registry, - input.schema().as_ref(), - extension_codec, - )?; - Ok(Arc::new(RepartitionExec::try_new( - input, - partitioning.unwrap(), - )?)) - } - - fn try_into_global_limit_physical_plan( - &self, - limit: &protobuf::GlobalLimitExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&limit.input, registry, runtime, extension_codec)?; - let fetch = if limit.fetch >= 0 { - Some(limit.fetch as usize) - } else { - None - }; - Ok(Arc::new(GlobalLimitExec::new( - input, - limit.skip as usize, - fetch, - ))) - } - - fn try_into_local_limit_physical_plan( - &self, - limit: &protobuf::LocalLimitExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&limit.input, registry, runtime, extension_codec)?; - Ok(Arc::new(LocalLimitExec::new(input, limit.fetch as usize))) - } - - fn try_into_window_physical_plan( - &self, - window_agg: &protobuf::WindowAggExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&window_agg.input, registry, runtime, extension_codec)?; - let input_schema = input.schema(); - - let physical_window_expr: Vec> = window_agg - .window_expr - .iter() - .map(|window_expr| { - parse_physical_window_expr( - window_expr, - registry, - input_schema.as_ref(), - extension_codec, - ) - }) - .collect::, _>>()?; - - let partition_keys = window_agg - .partition_keys - .iter() - .map(|expr| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - }) - .collect::>>>()?; - - if let Some(input_order_mode) = window_agg.input_order_mode.as_ref() { - let input_order_mode = match input_order_mode { - window_agg_exec_node::InputOrderMode::Linear(_) => InputOrderMode::Linear, - window_agg_exec_node::InputOrderMode::PartiallySorted( - protobuf::PartiallySortedInputOrderMode { columns }, - ) => InputOrderMode::PartiallySorted( - columns.iter().map(|c| *c as usize).collect(), - ), - window_agg_exec_node::InputOrderMode::Sorted(_) => InputOrderMode::Sorted, - }; - - Ok(Arc::new(BoundedWindowAggExec::try_new( - physical_window_expr, - input, - input_order_mode, - !partition_keys.is_empty(), - )?)) - } else { - Ok(Arc::new(WindowAggExec::try_new( - physical_window_expr, - input, - !partition_keys.is_empty(), - )?)) - } - } - - fn try_into_aggregate_physical_plan( - &self, - hash_agg: &protobuf::AggregateExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&hash_agg.input, registry, runtime, extension_codec)?; - let mode = protobuf::AggregateMode::try_from(hash_agg.mode).map_err(|_| { - proto_error(format!( - "Received a AggregateNode message with unknown AggregateMode {}", - hash_agg.mode - )) - })?; - let agg_mode: AggregateMode = match mode { - protobuf::AggregateMode::Partial => AggregateMode::Partial, - protobuf::AggregateMode::Final => AggregateMode::Final, - protobuf::AggregateMode::FinalPartitioned => AggregateMode::FinalPartitioned, - protobuf::AggregateMode::Single => AggregateMode::Single, - protobuf::AggregateMode::SinglePartitioned => { - AggregateMode::SinglePartitioned - } - }; - - let num_expr = hash_agg.group_expr.len(); - - let group_expr = hash_agg - .group_expr - .iter() - .zip(hash_agg.group_expr_name.iter()) - .map(|(expr, name)| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - .map(|expr| (expr, name.to_string())) - }) - .collect::, _>>()?; - - let null_expr = hash_agg - .null_expr - .iter() - .zip(hash_agg.group_expr_name.iter()) - .map(|(expr, name)| { - parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - ) - .map(|expr| (expr, name.to_string())) - }) - .collect::, _>>()?; - - let groups: Vec> = if !hash_agg.groups.is_empty() { - hash_agg - .groups - .chunks(num_expr) - .map(|g| g.to_vec()) - .collect::>>() - } else { - vec![] - }; - - let input_schema = hash_agg.input_schema.as_ref().ok_or_else(|| { - DataFusionError::Internal( - "input_schema in AggregateNode is missing.".to_owned(), - ) - })?; - let physical_schema: SchemaRef = SchemaRef::new(input_schema.try_into()?); - - let physical_filter_expr = hash_agg - .filter_expr - .iter() - .map(|expr| { - expr.expr - .as_ref() - .map(|e| { - parse_physical_expr( - e, + .map(|col| { + let left = parse_physical_expr( + &col.left.clone().unwrap(), registry, - &physical_schema, + left_schema.as_ref(), extension_codec, - ) + )?; + let right = parse_physical_expr( + &col.right.clone().unwrap(), + registry, + right_schema.as_ref(), + extension_codec, + )?; + Ok((left, right)) }) - .transpose() - }) - .collect::, _>>()?; - - let physical_aggr_expr: Vec> = hash_agg - .aggr_expr - .iter() - .zip(hash_agg.aggr_expr_name.iter()) - .map(|(expr, name)| { - let expr_type = expr.expr_type.as_ref().ok_or_else(|| { - proto_error("Unexpected empty aggregate physical expression") - })?; - - match expr_type { - ExprType::AggregateExpr(agg_node) => { - let input_phy_expr: Vec> = agg_node - .expr - .iter() - .map(|e| { - parse_physical_expr( - e, - registry, - &physical_schema, - extension_codec, - ) - }) - .collect::>>()?; - let ordering_req: LexOrdering = agg_node - .ordering_req - .iter() - .map(|e| { - parse_physical_sort_expr( - e, - registry, - &physical_schema, - extension_codec, - ) - }) - .collect::>()?; - agg_node - .aggregate_function + .collect::>()?; + let join_type = protobuf::JoinType::try_from(sym_join.join_type) + .map_err(|_| { + proto_error(format!( + "Received a SymmetricHashJoin message with unknown JoinType {}", + sym_join.join_type + )) + })?; + let filter = sym_join + .filter + .as_ref() + .map(|f| { + let schema = f + .schema .as_ref() - .map(|func| match func { - AggregateFunction::UserDefinedAggrFunction(udaf_name) => { - let agg_udf = match &agg_node.fun_definition { - Some(buf) => extension_codec - .try_decode_udaf(udaf_name, buf)?, - None => registry.udaf(udaf_name)?, - }; - - AggregateExprBuilder::new(agg_udf, input_phy_expr) - .schema(Arc::clone(&physical_schema)) - .alias(name) - .with_ignore_nulls(agg_node.ignore_nulls) - .with_distinct(agg_node.distinct) - .order_by(ordering_req) - .build() - .map(Arc::new) - } - }) - .transpose()? - .ok_or_else(|| { - proto_error( - "Invalid AggregateExpr, missing aggregate_function", - ) + .ok_or_else(|| proto_error("Missing JoinFilter schema"))? + .try_into()?; + + let expression = parse_physical_expr( + f.expression.as_ref().ok_or_else(|| { + proto_error("Unexpected empty filter expression") + })?, + registry, &schema, + extension_codec, + )?; + let column_indices = f.column_indices + .iter() + .map(|i| { + let side = protobuf::JoinSide::try_from(i.side) + .map_err(|_| proto_error(format!( + "Received a HashJoinNode message with JoinSide in Filter {}", + i.side)) + )?; + + Ok(ColumnIndex { + index: i.index as usize, + side: side.into(), + }) }) - } - _ => internal_err!("Invalid aggregate expression for AggregateExec"), - } - }) - .collect::, _>>()?; - - let limit = hash_agg - .limit - .as_ref() - .map(|lit_value| lit_value.limit as usize); - - let agg = AggregateExec::try_new( - agg_mode, - PhysicalGroupBy::new(group_expr, null_expr, groups), - physical_aggr_expr, - physical_filter_expr, - input, - physical_schema, - )?; - - let agg = agg.with_limit(limit); - - Ok(Arc::new(agg)) - } + .collect::>()?; - fn try_into_hash_join_physical_plan( - &self, - hashjoin: &protobuf::HashJoinExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let left: Arc = - into_physical_plan(&hashjoin.left, registry, runtime, extension_codec)?; - let right: Arc = - into_physical_plan(&hashjoin.right, registry, runtime, extension_codec)?; - let left_schema = left.schema(); - let right_schema = right.schema(); - let on: Vec<(PhysicalExprRef, PhysicalExprRef)> = hashjoin - .on - .iter() - .map(|col| { - let left = parse_physical_expr( - &col.left.clone().unwrap(), - registry, - left_schema.as_ref(), - extension_codec, - )?; - let right = parse_physical_expr( - &col.right.clone().unwrap(), - registry, - right_schema.as_ref(), - extension_codec, - )?; - Ok((left, right)) - }) - .collect::>()?; - let join_type = - protobuf::JoinType::try_from(hashjoin.join_type).map_err(|_| { - proto_error(format!( - "Received a HashJoinNode message with unknown JoinType {}", - hashjoin.join_type - )) - })?; - let filter = hashjoin - .filter - .as_ref() - .map(|f| { - let schema = f - .schema - .as_ref() - .ok_or_else(|| proto_error("Missing JoinFilter schema"))? - .try_into()?; + Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; - let expression = parse_physical_expr( - f.expression.as_ref().ok_or_else(|| { - proto_error("Unexpected empty filter expression") - })?, - registry, &schema, + let left_sort_exprs = parse_physical_sort_exprs( + &sym_join.left_sort_exprs, + registry, + &left_schema, extension_codec, )?; - let column_indices = f.column_indices - .iter() - .map(|i| { - let side = protobuf::JoinSide::try_from(i.side) - .map_err(|_| proto_error(format!( - "Received a HashJoinNode message with JoinSide in Filter {}", - i.side)) - )?; - - Ok(ColumnIndex { - index: i.index as usize, - side: side.into(), - }) - }) - .collect::>>()?; - - Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; - - let partition_mode = protobuf::PartitionMode::try_from(hashjoin.partition_mode) - .map_err(|_| { - proto_error(format!( - "Received a HashJoinNode message with unknown PartitionMode {}", - hashjoin.partition_mode - )) - })?; - let partition_mode = match partition_mode { - protobuf::PartitionMode::CollectLeft => PartitionMode::CollectLeft, - protobuf::PartitionMode::Partitioned => PartitionMode::Partitioned, - protobuf::PartitionMode::Auto => PartitionMode::Auto, - }; - let projection = if !hashjoin.projection.is_empty() { - Some( - hashjoin - .projection - .iter() - .map(|i| *i as usize) - .collect::>(), - ) - } else { - None - }; - Ok(Arc::new(HashJoinExec::try_new( - left, - right, - on, - filter, - &join_type.into(), - projection, - partition_mode, - hashjoin.null_equals_null, - )?)) - } + let left_sort_exprs = if left_sort_exprs.is_empty() { + None + } else { + Some(left_sort_exprs) + }; - fn try_into_symmetric_hash_join_physical_plan( - &self, - sym_join: &protobuf::SymmetricHashJoinExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let left = - into_physical_plan(&sym_join.left, registry, runtime, extension_codec)?; - let right = - into_physical_plan(&sym_join.right, registry, runtime, extension_codec)?; - let left_schema = left.schema(); - let right_schema = right.schema(); - let on = sym_join - .on - .iter() - .map(|col| { - let left = parse_physical_expr( - &col.left.clone().unwrap(), + let right_sort_exprs = parse_physical_sort_exprs( + &sym_join.right_sort_exprs, registry, - left_schema.as_ref(), + &right_schema, extension_codec, )?; - let right = parse_physical_expr( - &col.right.clone().unwrap(), + let right_sort_exprs = if right_sort_exprs.is_empty() { + None + } else { + Some(right_sort_exprs) + }; + + let partition_mode = + protobuf::StreamPartitionMode::try_from(sym_join.partition_mode).map_err(|_| { + proto_error(format!( + "Received a SymmetricHashJoin message with unknown PartitionMode {}", + sym_join.partition_mode + )) + })?; + let partition_mode = match partition_mode { + protobuf::StreamPartitionMode::SinglePartition => { + StreamJoinPartitionMode::SinglePartition + } + protobuf::StreamPartitionMode::PartitionedExec => { + StreamJoinPartitionMode::Partitioned + } + }; + SymmetricHashJoinExec::try_new( + left, + right, + on, + filter, + &join_type.into(), + sym_join.null_equals_null, + left_sort_exprs, + right_sort_exprs, + partition_mode, + ) + .map(|e| Arc::new(e) as _) + } + PhysicalPlanType::Union(union) => { + let mut inputs: Vec> = vec![]; + for input in &union.inputs { + inputs.push(input.try_into_physical_plan( + registry, + runtime, + extension_codec, + )?); + } + Ok(Arc::new(UnionExec::new(inputs))) + } + PhysicalPlanType::Interleave(interleave) => { + let mut inputs: Vec> = vec![]; + for input in &interleave.inputs { + inputs.push(input.try_into_physical_plan( + registry, + runtime, + extension_codec, + )?); + } + Ok(Arc::new(InterleaveExec::try_new(inputs)?)) + } + PhysicalPlanType::CrossJoin(crossjoin) => { + let left: Arc = into_physical_plan( + &crossjoin.left, registry, - right_schema.as_ref(), + runtime, extension_codec, )?; - Ok((left, right)) - }) - .collect::>()?; - let join_type = - protobuf::JoinType::try_from(sym_join.join_type).map_err(|_| { - proto_error(format!( - "Received a SymmetricHashJoin message with unknown JoinType {}", - sym_join.join_type - )) - })?; - let filter = sym_join - .filter - .as_ref() - .map(|f| { - let schema = f - .schema - .as_ref() - .ok_or_else(|| proto_error("Missing JoinFilter schema"))? - .try_into()?; - - let expression = parse_physical_expr( - f.expression.as_ref().ok_or_else(|| { - proto_error("Unexpected empty filter expression") - })?, - registry, &schema, + let right: Arc = into_physical_plan( + &crossjoin.right, + registry, + runtime, extension_codec, )?; - let column_indices = f.column_indices - .iter() - .map(|i| { - let side = protobuf::JoinSide::try_from(i.side) - .map_err(|_| proto_error(format!( - "Received a HashJoinNode message with JoinSide in Filter {}", - i.side)) - )?; - - Ok(ColumnIndex { - index: i.index as usize, - side: side.into(), - }) - }) - .collect::>()?; - - Ok(JoinFilter::new(expression, column_indices, Arc::new(schema))) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; - - let left_sort_exprs = parse_physical_sort_exprs( - &sym_join.left_sort_exprs, - registry, - &left_schema, - extension_codec, - )?; - let left_sort_exprs = if left_sort_exprs.is_empty() { - None - } else { - Some(left_sort_exprs) - }; - - let right_sort_exprs = parse_physical_sort_exprs( - &sym_join.right_sort_exprs, - registry, - &right_schema, - extension_codec, - )?; - let right_sort_exprs = if right_sort_exprs.is_empty() { - None - } else { - Some(right_sort_exprs) - }; - - let partition_mode = protobuf::StreamPartitionMode::try_from( - sym_join.partition_mode, - ) - .map_err(|_| { - proto_error(format!( - "Received a SymmetricHashJoin message with unknown PartitionMode {}", - sym_join.partition_mode - )) - })?; - let partition_mode = match partition_mode { - protobuf::StreamPartitionMode::SinglePartition => { - StreamJoinPartitionMode::SinglePartition + Ok(Arc::new(CrossJoinExec::new(left, right))) } - protobuf::StreamPartitionMode::PartitionedExec => { - StreamJoinPartitionMode::Partitioned + PhysicalPlanType::Empty(empty) => { + let schema = Arc::new(convert_required!(empty.schema)?); + Ok(Arc::new(EmptyExec::new(schema))) } - }; - SymmetricHashJoinExec::try_new( - left, - right, - on, - filter, - &join_type.into(), - sym_join.null_equals_null, - left_sort_exprs, - right_sort_exprs, - partition_mode, - ) - .map(|e| Arc::new(e) as _) - } - - fn try_into_union_physical_plan( - &self, - union: &protobuf::UnionExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let mut inputs: Vec> = vec![]; - for input in &union.inputs { - inputs.push(input.try_into_physical_plan( - registry, - runtime, - extension_codec, - )?); - } - Ok(Arc::new(UnionExec::new(inputs))) - } - - fn try_into_interleave_physical_plan( - &self, - interleave: &protobuf::InterleaveExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let mut inputs: Vec> = vec![]; - for input in &interleave.inputs { - inputs.push(input.try_into_physical_plan( - registry, - runtime, - extension_codec, - )?); - } - Ok(Arc::new(InterleaveExec::try_new(inputs)?)) - } - - fn try_into_cross_join_physical_plan( - &self, - crossjoin: &protobuf::CrossJoinExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let left: Arc = - into_physical_plan(&crossjoin.left, registry, runtime, extension_codec)?; - let right: Arc = - into_physical_plan(&crossjoin.right, registry, runtime, extension_codec)?; - Ok(Arc::new(CrossJoinExec::new(left, right))) - } - - fn try_into_empty_physical_plan( - &self, - empty: &protobuf::EmptyExecNode, - _registry: &dyn FunctionRegistry, - _runtime: &RuntimeEnv, - _extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let schema = Arc::new(convert_required!(empty.schema)?); - Ok(Arc::new(EmptyExec::new(schema))) - } - - fn try_into_placeholder_row_physical_plan( - &self, - placeholder: &protobuf::PlaceholderRowExecNode, - _registry: &dyn FunctionRegistry, - _runtime: &RuntimeEnv, - _extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let schema = Arc::new(convert_required!(placeholder.schema)?); - Ok(Arc::new(PlaceholderRowExec::new(schema))) - } - - fn try_into_sort_physical_plan( - &self, - sort: &protobuf::SortExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&sort.input, registry, runtime, extension_codec)?; - let exprs = sort + PhysicalPlanType::PlaceholderRow(placeholder) => { + let schema = Arc::new(convert_required!(placeholder.schema)?); + Ok(Arc::new(PlaceholderRowExec::new(schema))) + } + PhysicalPlanType::Sort(sort) => { + let input: Arc = + into_physical_plan(&sort.input, registry, runtime, extension_codec)?; + let exprs = sort .expr .iter() .map(|expr| { @@ -1447,110 +916,90 @@ impl protobuf::PhysicalPlanNode { } }) .collect::>()?; - let fetch = if sort.fetch < 0 { - None - } else { - Some(sort.fetch as usize) - }; - let new_sort = SortExec::new(exprs, input) - .with_fetch(fetch) - .with_preserve_partitioning(sort.preserve_partitioning); - - Ok(Arc::new(new_sort)) - } + let fetch = if sort.fetch < 0 { + None + } else { + Some(sort.fetch as usize) + }; + let new_sort = SortExec::new(exprs, input) + .with_fetch(fetch) + .with_preserve_partitioning(sort.preserve_partitioning); - fn try_into_sort_preserving_merge_physical_plan( - &self, - sort: &protobuf::SortPreservingMergeExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&sort.input, registry, runtime, extension_codec)?; - let exprs = sort - .expr - .iter() - .map(|expr| { - let expr = expr.expr_type.as_ref().ok_or_else(|| { - proto_error(format!( - "physical_plan::from_proto() Unexpected expr {self:?}" - )) - })?; - if let ExprType::Sort(sort_expr) = expr { - let expr = sort_expr - .expr - .as_ref() - .ok_or_else(|| { + Ok(Arc::new(new_sort)) + } + PhysicalPlanType::SortPreservingMerge(sort) => { + let input: Arc = + into_physical_plan(&sort.input, registry, runtime, extension_codec)?; + let exprs = sort + .expr + .iter() + .map(|expr| { + let expr = expr.expr_type.as_ref().ok_or_else(|| { proto_error(format!( - "physical_plan::from_proto() Unexpected sort expr {self:?}" - )) - })? - .as_ref(); - Ok(PhysicalSortExpr { - expr: parse_physical_expr( - expr, - registry, - input.schema().as_ref(), - extension_codec, - )?, - options: SortOptions { - descending: !sort_expr.asc, - nulls_first: sort_expr.nulls_first, - }, + "physical_plan::from_proto() Unexpected expr {self:?}" + )) + })?; + if let ExprType::Sort(sort_expr) = expr { + let expr = sort_expr + .expr + .as_ref() + .ok_or_else(|| { + proto_error(format!( + "physical_plan::from_proto() Unexpected sort expr {self:?}" + )) + })? + .as_ref(); + Ok(PhysicalSortExpr { + expr: parse_physical_expr(expr, registry, input.schema().as_ref(), extension_codec)?, + options: SortOptions { + descending: !sort_expr.asc, + nulls_first: sort_expr.nulls_first, + }, + }) + } else { + internal_err!( + "physical_plan::from_proto() {self:?}" + ) + } }) + .collect::>()?; + let fetch = if sort.fetch < 0 { + None } else { - internal_err!("physical_plan::from_proto() {self:?}") - } - }) - .collect::>()?; - let fetch = if sort.fetch < 0 { - None - } else { - Some(sort.fetch as usize) - }; - Ok(Arc::new( - SortPreservingMergeExec::new(exprs, input).with_fetch(fetch), - )) - } - - fn try_into_extension_physical_plan( - &self, - extension: &protobuf::PhysicalExtensionNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let inputs: Vec> = extension - .inputs - .iter() - .map(|i| i.try_into_physical_plan(registry, runtime, extension_codec)) - .collect::>()?; - - let extension_node = - extension_codec.try_decode(extension.node.as_slice(), &inputs, registry)?; + Some(sort.fetch as usize) + }; + Ok(Arc::new( + SortPreservingMergeExec::new(exprs, input).with_fetch(fetch), + )) + } + PhysicalPlanType::Extension(extension) => { + let inputs: Vec> = extension + .inputs + .iter() + .map(|i| i.try_into_physical_plan(registry, runtime, extension_codec)) + .collect::>()?; - Ok(extension_node) - } + let extension_node = extension_codec.try_decode( + extension.node.as_slice(), + &inputs, + registry, + )?; - fn try_into_nested_loop_join_physical_plan( - &self, - join: &protobuf::NestedLoopJoinExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let left: Arc = - into_physical_plan(&join.left, registry, runtime, extension_codec)?; - let right: Arc = - into_physical_plan(&join.right, registry, runtime, extension_codec)?; - let join_type = protobuf::JoinType::try_from(join.join_type).map_err(|_| { - proto_error(format!( - "Received a NestedLoopJoinExecNode message with unknown JoinType {}", - join.join_type - )) - })?; - let filter = join + Ok(extension_node) + } + PhysicalPlanType::NestedLoopJoin(join) => { + let left: Arc = + into_physical_plan(&join.left, registry, runtime, extension_codec)?; + let right: Arc = + into_physical_plan(&join.right, registry, runtime, extension_codec)?; + let join_type = + protobuf::JoinType::try_from(join.join_type).map_err(|_| { + proto_error(format!( + "Received a NestedLoopJoinExecNode message with unknown JoinType {}", + join.join_type + )) + })?; + let filter = join .filter .as_ref() .map(|f| { @@ -1587,1157 +1036,1121 @@ impl protobuf::PhysicalPlanNode { }) .map_or(Ok(None), |v: Result| v.map(Some))?; - let projection = if !join.projection.is_empty() { - Some( - join.projection - .iter() - .map(|i| *i as usize) - .collect::>(), - ) - } else { - None - }; - - Ok(Arc::new(NestedLoopJoinExec::try_new( - left, - right, - filter, - &join_type.into(), - projection, - )?)) - } + let projection = if !join.projection.is_empty() { + Some( + join.projection + .iter() + .map(|i| *i as usize) + .collect::>(), + ) + } else { + None + }; - fn try_into_analyze_physical_plan( - &self, - analyze: &protobuf::AnalyzeExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: Arc = - into_physical_plan(&analyze.input, registry, runtime, extension_codec)?; - Ok(Arc::new(AnalyzeExec::new( - analyze.verbose, - analyze.show_statistics, - input, - Arc::new(convert_required!(analyze.schema)?), - ))) - } + Ok(Arc::new(NestedLoopJoinExec::try_new( + left, + right, + filter, + &join_type.into(), + projection, + )?)) + } + PhysicalPlanType::Analyze(analyze) => { + let input: Arc = into_physical_plan( + &analyze.input, + registry, + runtime, + extension_codec, + )?; + Ok(Arc::new(AnalyzeExec::new( + analyze.verbose, + analyze.show_statistics, + input, + Arc::new(convert_required!(analyze.schema)?), + ))) + } + PhysicalPlanType::JsonSink(sink) => { + let input = + into_physical_plan(&sink.input, registry, runtime, extension_codec)?; - fn try_into_json_sink_physical_plan( - &self, - sink: &protobuf::JsonSinkExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input = into_physical_plan(&sink.input, registry, runtime, extension_codec)?; - - let data_sink: JsonSink = sink - .sink - .as_ref() - .ok_or_else(|| proto_error("Missing required field in protobuf"))? - .try_into()?; - let sink_schema = input.schema(); - let sort_order = sink - .sort_order - .as_ref() - .map(|collection| { - parse_physical_sort_exprs( - &collection.physical_sort_expr_nodes, + let data_sink: JsonSink = sink + .sink + .as_ref() + .ok_or_else(|| proto_error("Missing required field in protobuf"))? + .try_into()?; + let sink_schema = input.schema(); + let sort_order = sink + .sort_order + .as_ref() + .map(|collection| { + parse_physical_sort_exprs( + &collection.physical_sort_expr_nodes, + registry, + &sink_schema, + extension_codec, + ) + .map(LexRequirement::from) + }) + .transpose()?; + Ok(Arc::new(DataSinkExec::new( + input, + Arc::new(data_sink), + sort_order, + ))) + } + PhysicalPlanType::CsvSink(sink) => { + let input = + into_physical_plan(&sink.input, registry, runtime, extension_codec)?; + + let data_sink: CsvSink = sink + .sink + .as_ref() + .ok_or_else(|| proto_error("Missing required field in protobuf"))? + .try_into()?; + let sink_schema = input.schema(); + let sort_order = sink + .sort_order + .as_ref() + .map(|collection| { + parse_physical_sort_exprs( + &collection.physical_sort_expr_nodes, + registry, + &sink_schema, + extension_codec, + ) + .map(LexRequirement::from) + }) + .transpose()?; + Ok(Arc::new(DataSinkExec::new( + input, + Arc::new(data_sink), + sort_order, + ))) + } + #[cfg_attr(not(feature = "parquet"), allow(unused_variables))] + PhysicalPlanType::ParquetSink(sink) => { + #[cfg(feature = "parquet")] + { + let input = into_physical_plan( + &sink.input, + registry, + runtime, + extension_codec, + )?; + + let data_sink: ParquetSink = sink + .sink + .as_ref() + .ok_or_else(|| proto_error("Missing required field in protobuf"))? + .try_into()?; + let sink_schema = input.schema(); + let sort_order = sink + .sort_order + .as_ref() + .map(|collection| { + parse_physical_sort_exprs( + &collection.physical_sort_expr_nodes, + registry, + &sink_schema, + extension_codec, + ) + .map(LexRequirement::from) + }) + .transpose()?; + Ok(Arc::new(DataSinkExec::new( + input, + Arc::new(data_sink), + sort_order, + ))) + } + #[cfg(not(feature = "parquet"))] + panic!("Trying to use ParquetSink without `parquet` feature enabled"); + } + PhysicalPlanType::Unnest(unnest) => { + let input = into_physical_plan( + &unnest.input, registry, - &sink_schema, + runtime, extension_codec, - ) - .map(LexRequirement::from) - }) - .transpose()?; - Ok(Arc::new(DataSinkExec::new( - input, - Arc::new(data_sink), - sort_order, - ))) - } + )?; - fn try_into_csv_sink_physical_plan( - &self, - sink: &protobuf::CsvSinkExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input = into_physical_plan(&sink.input, registry, runtime, extension_codec)?; - - let data_sink: CsvSink = sink - .sink - .as_ref() - .ok_or_else(|| proto_error("Missing required field in protobuf"))? - .try_into()?; - let sink_schema = input.schema(); - let sort_order = sink - .sort_order - .as_ref() - .map(|collection| { - parse_physical_sort_exprs( - &collection.physical_sort_expr_nodes, - registry, - &sink_schema, - extension_codec, - ) - .map(LexRequirement::from) - }) - .transpose()?; - Ok(Arc::new(DataSinkExec::new( - input, - Arc::new(data_sink), - sort_order, - ))) + Ok(Arc::new(UnnestExec::new( + input, + unnest + .list_type_columns + .iter() + .map(|c| ListUnnest { + index_in_input_schema: c.index_in_input_schema as _, + depth: c.depth as _, + }) + .collect(), + unnest.struct_type_columns.iter().map(|c| *c as _).collect(), + Arc::new(convert_required!(unnest.schema)?), + into_required!(unnest.options)?, + ))) + } + } } - fn try_into_parquet_sink_physical_plan( - &self, - sink: &protobuf::ParquetSinkExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, + fn try_from_physical_plan( + plan: Arc, extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - #[cfg(feature = "parquet")] - { - let input = - into_physical_plan(&sink.input, registry, runtime, extension_codec)?; + ) -> Result + where + Self: Sized, + { + let plan_clone = Arc::clone(&plan); + let plan = plan.as_any(); - let data_sink: ParquetSink = sink - .sink - .as_ref() - .ok_or_else(|| proto_error("Missing required field in protobuf"))? - .try_into()?; - let sink_schema = input.schema(); - let sort_order = sink - .sort_order - .as_ref() - .map(|collection| { - parse_physical_sort_exprs( - &collection.physical_sort_expr_nodes, - registry, - &sink_schema, - extension_codec, - ) - .map(LexRequirement::from) - }) - .transpose()?; - Ok(Arc::new(DataSinkExec::new( - input, - Arc::new(data_sink), - sort_order, - ))) + if let Some(exec) = plan.downcast_ref::() { + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Explain( + protobuf::ExplainExecNode { + schema: Some(exec.schema().as_ref().try_into()?), + stringified_plans: exec + .stringified_plans() + .iter() + .map(|plan| plan.into()) + .collect(), + verbose: exec.verbose(), + }, + )), + }); } - #[cfg(not(feature = "parquet"))] - panic!("Trying to use ParquetSink without `parquet` feature enabled"); - } - - fn try_into_unnest_physical_plan( - &self, - unnest: &protobuf::UnnestExecNode, - registry: &dyn FunctionRegistry, - runtime: &RuntimeEnv, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input = - into_physical_plan(&unnest.input, registry, runtime, extension_codec)?; - Ok(Arc::new(UnnestExec::new( - input, - unnest - .list_type_columns + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + let expr = exec + .expr() .iter() - .map(|c| ListUnnest { - index_in_input_schema: c.index_in_input_schema as _, - depth: c.depth as _, - }) - .collect(), - unnest.struct_type_columns.iter().map(|c| *c as _).collect(), - Arc::new(convert_required!(unnest.schema)?), - into_required!(unnest.options)?, - ))) - } - - fn try_from_explain_exec( - exec: &ExplainExec, - _extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Explain( - protobuf::ExplainExecNode { - schema: Some(exec.schema().as_ref().try_into()?), - stringified_plans: exec - .stringified_plans() - .iter() - .map(|plan| plan.into()) - .collect(), - verbose: exec.verbose(), - }, - )), - }) - } + .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) + .collect::>>()?; + let expr_name = exec.expr().iter().map(|expr| expr.1.clone()).collect(); + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Projection(Box::new( + protobuf::ProjectionExecNode { + input: Some(Box::new(input)), + expr, + expr_name, + }, + ))), + }); + } - fn try_from_projection_exec( - exec: &ProjectionExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - let expr = exec - .expr() - .iter() - .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) - .collect::>>()?; - let expr_name = exec.expr().iter().map(|expr| expr.1.clone()).collect(); - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Projection(Box::new( - protobuf::ProjectionExecNode { - input: Some(Box::new(input)), - expr, - expr_name, - }, - ))), - }) - } + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Analyze(Box::new( + protobuf::AnalyzeExecNode { + verbose: exec.verbose(), + show_statistics: exec.show_statistics(), + input: Some(Box::new(input)), + schema: Some(exec.schema().as_ref().try_into()?), + }, + ))), + }); + } - fn try_from_analyze_exec( - exec: &AnalyzeExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Analyze(Box::new( - protobuf::AnalyzeExecNode { - verbose: exec.verbose(), - show_statistics: exec.show_statistics(), - input: Some(Box::new(input)), - schema: Some(exec.schema().as_ref().try_into()?), - }, - ))), - }) - } + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Filter(Box::new( + protobuf::FilterExecNode { + input: Some(Box::new(input)), + expr: Some(serialize_physical_expr( + exec.predicate(), + extension_codec, + )?), + default_filter_selectivity: exec.default_selectivity() as u32, + projection: exec + .projection() + .as_ref() + .map_or_else(Vec::new, |v| { + v.iter().map(|x| *x as u32).collect::>() + }), + }, + ))), + }); + } - fn try_from_filter_exec( - exec: &FilterExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Filter(Box::new( - protobuf::FilterExecNode { - input: Some(Box::new(input)), - expr: Some(serialize_physical_expr( - exec.predicate(), - extension_codec, - )?), - default_filter_selectivity: exec.default_selectivity() as u32, - projection: exec.projection().as_ref().map_or_else(Vec::new, |v| { - v.iter().map(|x| *x as u32).collect::>() - }), - }, - ))), - }) - } + if let Some(limit) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + limit.input().to_owned(), + extension_codec, + )?; - fn try_from_global_limit_exec( - limit: &GlobalLimitExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - limit.input().to_owned(), - extension_codec, - )?; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::GlobalLimit(Box::new( - protobuf::GlobalLimitExecNode { - input: Some(Box::new(input)), - skip: limit.skip() as u32, - fetch: match limit.fetch() { - Some(n) => n as i64, - _ => -1, // no limit + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::GlobalLimit(Box::new( + protobuf::GlobalLimitExecNode { + input: Some(Box::new(input)), + skip: limit.skip() as u32, + fetch: match limit.fetch() { + Some(n) => n as i64, + _ => -1, // no limit + }, }, - }, - ))), - }) - } + ))), + }); + } - fn try_from_local_limit_exec( - limit: &LocalLimitExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - limit.input().to_owned(), - extension_codec, - )?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::LocalLimit(Box::new( - protobuf::LocalLimitExecNode { - input: Some(Box::new(input)), - fetch: limit.fetch() as u32, - }, - ))), - }) - } + if let Some(limit) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + limit.input().to_owned(), + extension_codec, + )?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::LocalLimit(Box::new( + protobuf::LocalLimitExecNode { + input: Some(Box::new(input)), + fetch: limit.fetch() as u32, + }, + ))), + }); + } - fn try_from_hash_join_exec( - exec: &HashJoinExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; - let on: Vec = exec - .on() - .iter() - .map(|tuple| { - let l = serialize_physical_expr(&tuple.0, extension_codec)?; - let r = serialize_physical_expr(&tuple.1, extension_codec)?; - Ok::<_, DataFusionError>(protobuf::JoinOn { - left: Some(l), - right: Some(r), + if let Some(exec) = plan.downcast_ref::() { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; + let on: Vec = exec + .on() + .iter() + .map(|tuple| { + let l = serialize_physical_expr(&tuple.0, extension_codec)?; + let r = serialize_physical_expr(&tuple.1, extension_codec)?; + Ok::<_, DataFusionError>(protobuf::JoinOn { + left: Some(l), + right: Some(r), + }) }) - }) - .collect::>()?; - let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); - let filter = exec - .filter() - .as_ref() - .map(|f| { - let expression = - serialize_physical_expr(f.expression(), extension_codec)?; - let column_indices = f - .column_indices() - .iter() - .map(|i| { - let side: protobuf::JoinSide = i.side.to_owned().into(); - protobuf::ColumnIndex { - index: i.index as u32, - side: side.into(), - } + .collect::>()?; + let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); + let filter = exec + .filter() + .as_ref() + .map(|f| { + let expression = + serialize_physical_expr(f.expression(), extension_codec)?; + let column_indices = f + .column_indices() + .iter() + .map(|i| { + let side: protobuf::JoinSide = i.side.to_owned().into(); + protobuf::ColumnIndex { + index: i.index as u32, + side: side.into(), + } + }) + .collect(); + let schema = f.schema().as_ref().try_into()?; + Ok(protobuf::JoinFilter { + expression: Some(expression), + column_indices, + schema: Some(schema), }) - .collect(); - let schema = f.schema().as_ref().try_into()?; - Ok(protobuf::JoinFilter { - expression: Some(expression), - column_indices, - schema: Some(schema), }) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; - - let partition_mode = match exec.partition_mode() { - PartitionMode::CollectLeft => protobuf::PartitionMode::CollectLeft, - PartitionMode::Partitioned => protobuf::PartitionMode::Partitioned, - PartitionMode::Auto => protobuf::PartitionMode::Auto, - }; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::HashJoin(Box::new( - protobuf::HashJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - on, - join_type: join_type.into(), - partition_mode: partition_mode.into(), - null_equals_null: exec.null_equals_null(), - filter, - projection: exec.projection.as_ref().map_or_else(Vec::new, |v| { - v.iter().map(|x| *x as u32).collect::>() - }), - }, - ))), - }) - } + .map_or(Ok(None), |v: Result| v.map(Some))?; - fn try_from_symmetric_hash_join_exec( - exec: &SymmetricHashJoinExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; - let on = exec - .on() - .iter() - .map(|tuple| { - let l = serialize_physical_expr(&tuple.0, extension_codec)?; - let r = serialize_physical_expr(&tuple.1, extension_codec)?; - Ok::<_, DataFusionError>(protobuf::JoinOn { - left: Some(l), - right: Some(r), + let partition_mode = match exec.partition_mode() { + PartitionMode::CollectLeft => protobuf::PartitionMode::CollectLeft, + PartitionMode::Partitioned => protobuf::PartitionMode::Partitioned, + PartitionMode::Auto => protobuf::PartitionMode::Auto, + }; + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::HashJoin(Box::new( + protobuf::HashJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + on, + join_type: join_type.into(), + partition_mode: partition_mode.into(), + null_equals_null: exec.null_equals_null(), + filter, + projection: exec.projection.as_ref().map_or_else(Vec::new, |v| { + v.iter().map(|x| *x as u32).collect::>() + }), + }, + ))), + }); + } + + if let Some(exec) = plan.downcast_ref::() { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; + let on = exec + .on() + .iter() + .map(|tuple| { + let l = serialize_physical_expr(&tuple.0, extension_codec)?; + let r = serialize_physical_expr(&tuple.1, extension_codec)?; + Ok::<_, DataFusionError>(protobuf::JoinOn { + left: Some(l), + right: Some(r), + }) }) - }) - .collect::>()?; - let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); - let filter = exec - .filter() - .as_ref() - .map(|f| { - let expression = - serialize_physical_expr(f.expression(), extension_codec)?; - let column_indices = f - .column_indices() - .iter() - .map(|i| { - let side: protobuf::JoinSide = i.side.to_owned().into(); - protobuf::ColumnIndex { - index: i.index as u32, - side: side.into(), - } + .collect::>()?; + let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); + let filter = exec + .filter() + .as_ref() + .map(|f| { + let expression = + serialize_physical_expr(f.expression(), extension_codec)?; + let column_indices = f + .column_indices() + .iter() + .map(|i| { + let side: protobuf::JoinSide = i.side.to_owned().into(); + protobuf::ColumnIndex { + index: i.index as u32, + side: side.into(), + } + }) + .collect(); + let schema = f.schema().as_ref().try_into()?; + Ok(protobuf::JoinFilter { + expression: Some(expression), + column_indices, + schema: Some(schema), }) - .collect(); - let schema = f.schema().as_ref().try_into()?; - Ok(protobuf::JoinFilter { - expression: Some(expression), - column_indices, - schema: Some(schema), }) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; + .map_or(Ok(None), |v: Result| v.map(Some))?; - let partition_mode = match exec.partition_mode() { - StreamJoinPartitionMode::SinglePartition => { - protobuf::StreamPartitionMode::SinglePartition - } - StreamJoinPartitionMode::Partitioned => { - protobuf::StreamPartitionMode::PartitionedExec - } - }; + let partition_mode = match exec.partition_mode() { + StreamJoinPartitionMode::SinglePartition => { + protobuf::StreamPartitionMode::SinglePartition + } + StreamJoinPartitionMode::Partitioned => { + protobuf::StreamPartitionMode::PartitionedExec + } + }; - let left_sort_exprs = exec - .left_sort_exprs() - .map(|exprs| { - exprs - .iter() - .map(|expr| { - Ok(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, + let left_sort_exprs = exec + .left_sort_exprs() + .map(|exprs| { + exprs + .iter() + .map(|expr| { + Ok(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }) }) - }) - .collect::>>() - }) - .transpose()? - .unwrap_or(vec![]); - - let right_sort_exprs = exec - .right_sort_exprs() - .map(|exprs| { - exprs - .iter() - .map(|expr| { - Ok(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, + .collect::>>() + }) + .transpose()? + .unwrap_or(vec![]); + + let right_sort_exprs = exec + .right_sort_exprs() + .map(|exprs| { + exprs + .iter() + .map(|expr| { + Ok(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }) }) - }) - .collect::>>() - }) - .transpose()? - .unwrap_or(vec![]); - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::SymmetricHashJoin(Box::new( - protobuf::SymmetricHashJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - on, - join_type: join_type.into(), - partition_mode: partition_mode.into(), - null_equals_null: exec.null_equals_null(), - left_sort_exprs, - right_sort_exprs, - filter, - }, - ))), - }) - } + .collect::>>() + }) + .transpose()? + .unwrap_or(vec![]); + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::SymmetricHashJoin(Box::new( + protobuf::SymmetricHashJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + on, + join_type: join_type.into(), + partition_mode: partition_mode.into(), + null_equals_null: exec.null_equals_null(), + left_sort_exprs, + right_sort_exprs, + filter, + }, + ))), + }); + } - fn try_from_cross_join_exec( - exec: &CrossJoinExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CrossJoin(Box::new( - protobuf::CrossJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - }, - ))), - }) - } + if let Some(exec) = plan.downcast_ref::() { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CrossJoin(Box::new( + protobuf::CrossJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + }, + ))), + }); + } + if let Some(exec) = plan.downcast_ref::() { + let groups: Vec = exec + .group_expr() + .groups() + .iter() + .flatten() + .copied() + .collect(); - fn try_from_aggregate_exec( - exec: &AggregateExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let groups: Vec = exec - .group_expr() - .groups() - .iter() - .flatten() - .copied() - .collect(); - - let group_names = exec - .group_expr() - .expr() - .iter() - .map(|expr| expr.1.to_owned()) - .collect(); - - let filter = exec - .filter_expr() - .iter() - .map(|expr| serialize_maybe_filter(expr.to_owned(), extension_codec)) - .collect::>>()?; - - let agg = exec - .aggr_expr() - .iter() - .map(|expr| serialize_physical_aggr_expr(expr.to_owned(), extension_codec)) - .collect::>>()?; - - let agg_names = exec - .aggr_expr() - .iter() - .map(|expr| expr.name().to_string()) - .collect::>(); - - let agg_mode = match exec.mode() { - AggregateMode::Partial => protobuf::AggregateMode::Partial, - AggregateMode::Final => protobuf::AggregateMode::Final, - AggregateMode::FinalPartitioned => protobuf::AggregateMode::FinalPartitioned, - AggregateMode::Single => protobuf::AggregateMode::Single, - AggregateMode::SinglePartitioned => { - protobuf::AggregateMode::SinglePartitioned - } - }; - let input_schema = exec.input_schema(); - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - - let null_expr = exec - .group_expr() - .null_expr() - .iter() - .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) - .collect::>>()?; - - let group_expr = exec - .group_expr() - .expr() - .iter() - .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) - .collect::>>()?; - - let limit = exec.limit().map(|value| protobuf::AggLimit { - limit: value as u64, - }); - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Aggregate(Box::new( - protobuf::AggregateExecNode { - group_expr, - group_expr_name: group_names, - aggr_expr: agg, - filter_expr: filter, - aggr_expr_name: agg_names, - mode: agg_mode as i32, - input: Some(Box::new(input)), - input_schema: Some(input_schema.as_ref().try_into()?), - null_expr, - groups, - limit, - }, - ))), - }) - } + let group_names = exec + .group_expr() + .expr() + .iter() + .map(|expr| expr.1.to_owned()) + .collect(); - fn try_from_empty_exec( - empty: &EmptyExec, - _extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let schema = empty.schema().as_ref().try_into()?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Empty(protobuf::EmptyExecNode { - schema: Some(schema), - })), - }) - } + let filter = exec + .filter_expr() + .iter() + .map(|expr| serialize_maybe_filter(expr.to_owned(), extension_codec)) + .collect::>>()?; - fn try_from_placeholder_row_exec( - empty: &PlaceholderRowExec, - _extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let schema = empty.schema().as_ref().try_into()?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::PlaceholderRow( - protobuf::PlaceholderRowExecNode { - schema: Some(schema), - }, - )), - }) - } + let agg = exec + .aggr_expr() + .iter() + .map(|expr| { + serialize_physical_aggr_expr(expr.to_owned(), extension_codec) + }) + .collect::>>()?; - fn try_from_coalesce_batches_exec( - coalesce_batches: &CoalesceBatchesExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - coalesce_batches.input().to_owned(), - extension_codec, - )?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CoalesceBatches(Box::new( - protobuf::CoalesceBatchesExecNode { - input: Some(Box::new(input)), - target_batch_size: coalesce_batches.target_batch_size() as u32, - fetch: coalesce_batches.fetch().map(|n| n as u32), - }, - ))), - }) - } + let agg_names = exec + .aggr_expr() + .iter() + .map(|expr| expr.name().to_string()) + .collect::>(); + + let agg_mode = match exec.mode() { + AggregateMode::Partial => protobuf::AggregateMode::Partial, + AggregateMode::Final => protobuf::AggregateMode::Final, + AggregateMode::FinalPartitioned => { + protobuf::AggregateMode::FinalPartitioned + } + AggregateMode::Single => protobuf::AggregateMode::Single, + AggregateMode::SinglePartitioned => { + protobuf::AggregateMode::SinglePartitioned + } + }; + let input_schema = exec.input_schema(); + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + let null_expr = exec + .group_expr() + .null_expr() + .iter() + .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) + .collect::>>()?; + + let group_expr = exec + .group_expr() + .expr() + .iter() + .map(|expr| serialize_physical_expr(&expr.0, extension_codec)) + .collect::>>()?; + + let limit = exec.limit().map(|value| protobuf::AggLimit { + limit: value as u64, + }); + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Aggregate(Box::new( + protobuf::AggregateExecNode { + group_expr, + group_expr_name: group_names, + aggr_expr: agg, + filter_expr: filter, + aggr_expr_name: agg_names, + mode: agg_mode as i32, + input: Some(Box::new(input)), + input_schema: Some(input_schema.as_ref().try_into()?), + null_expr, + groups, + limit, + }, + ))), + }); + } + + if let Some(empty) = plan.downcast_ref::() { + let schema = empty.schema().as_ref().try_into()?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Empty( + protobuf::EmptyExecNode { + schema: Some(schema), + }, + )), + }); + } + + if let Some(empty) = plan.downcast_ref::() { + let schema = empty.schema().as_ref().try_into()?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::PlaceholderRow( + protobuf::PlaceholderRowExecNode { + schema: Some(schema), + }, + )), + }); + } + + if let Some(coalesce_batches) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + coalesce_batches.input().to_owned(), + extension_codec, + )?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CoalesceBatches(Box::new( + protobuf::CoalesceBatchesExecNode { + input: Some(Box::new(input)), + target_batch_size: coalesce_batches.target_batch_size() as u32, + fetch: coalesce_batches.fetch().map(|n| n as u32), + }, + ))), + }); + } - fn try_from_data_source_exec( - data_source_exec: &DataSourceExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let data_source = data_source_exec.data_source(); - if let Some(maybe_csv) = data_source.as_any().downcast_ref::() { - let source = maybe_csv.file_source(); - if let Some(csv_config) = source.as_any().downcast_ref::() { - return Ok(Some(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CsvScan( - protobuf::CsvScanExecNode { - base_conf: Some(serialize_file_scan_config( - maybe_csv, - extension_codec, - )?), - has_header: csv_config.has_header(), - delimiter: byte_to_string( - csv_config.delimiter(), - "delimiter", - )?, - quote: byte_to_string(csv_config.quote(), "quote")?, - optional_escape: if let Some(escape) = csv_config.escape() { - Some( - protobuf::csv_scan_exec_node::OptionalEscape::Escape( - byte_to_string(escape, "escape")?, - ), - ) - } else { - None - }, - optional_comment: if let Some(comment) = csv_config.comment() - { - Some(protobuf::csv_scan_exec_node::OptionalComment::Comment( + if let Some(data_source_exec) = plan.downcast_ref::() { + let data_source = data_source_exec.data_source(); + if let Some(maybe_csv) = data_source.as_any().downcast_ref::() + { + let source = maybe_csv.file_source(); + if let Some(csv_config) = source.as_any().downcast_ref::() { + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CsvScan( + protobuf::CsvScanExecNode { + base_conf: Some(serialize_file_scan_config( + maybe_csv, + extension_codec, + )?), + has_header: csv_config.has_header(), + delimiter: byte_to_string( + csv_config.delimiter(), + "delimiter", + )?, + quote: byte_to_string(csv_config.quote(), "quote")?, + optional_escape: if let Some(escape) = csv_config.escape() + { + Some( + protobuf::csv_scan_exec_node::OptionalEscape::Escape( + byte_to_string(escape, "escape")?, + ), + ) + } else { + None + }, + optional_comment: if let Some(comment) = + csv_config.comment() + { + Some(protobuf::csv_scan_exec_node::OptionalComment::Comment( byte_to_string(comment, "comment")?, )) - } else { - None + } else { + None + }, + newlines_in_values: maybe_csv.newlines_in_values(), }, - newlines_in_values: maybe_csv.newlines_in_values(), - }, - )), - })); + )), + }); + } } } - if let Some(scan_conf) = data_source.as_any().downcast_ref::() { - let source = scan_conf.file_source(); - if let Some(_json_source) = source.as_any().downcast_ref::() { - return Ok(Some(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::JsonScan( - protobuf::JsonScanExecNode { - base_conf: Some(serialize_file_scan_config( - scan_conf, - extension_codec, - )?), - }, - )), - })); + if let Some(data_source_exec) = plan.downcast_ref::() { + let data_source = data_source_exec.data_source(); + if let Some(scan_conf) = data_source.as_any().downcast_ref::() + { + let source = scan_conf.file_source(); + if let Some(_json_source) = source.as_any().downcast_ref::() { + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::JsonScan( + protobuf::JsonScanExecNode { + base_conf: Some(serialize_file_scan_config( + scan_conf, + extension_codec, + )?), + }, + )), + }); + } } } #[cfg(feature = "parquet")] - if let Some((maybe_parquet, conf)) = - data_source_exec.downcast_to_file_source::() - { - let predicate = conf - .predicate() - .map(|pred| serialize_physical_expr(pred, extension_codec)) - .transpose()?; - return Ok(Some(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::ParquetScan( - protobuf::ParquetScanExecNode { - base_conf: Some(serialize_file_scan_config( - maybe_parquet, - extension_codec, - )?), - predicate, - parquet_options: Some(conf.table_parquet_options().try_into()?), - }, - )), - })); - } - - #[cfg(feature = "avro")] - if let Some(maybe_avro) = data_source.as_any().downcast_ref::() { - let source = maybe_avro.file_source(); - if source.as_any().downcast_ref::().is_some() { - return Ok(Some(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::AvroScan( - protobuf::AvroScanExecNode { + if let Some(exec) = plan.downcast_ref::() { + if let Some((maybe_parquet, conf)) = + exec.downcast_to_file_source::() + { + let predicate = conf + .predicate() + .map(|pred| serialize_physical_expr(pred, extension_codec)) + .transpose()?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::ParquetScan( + protobuf::ParquetScanExecNode { base_conf: Some(serialize_file_scan_config( - maybe_avro, + maybe_parquet, extension_codec, )?), + predicate, + parquet_options: Some( + conf.table_parquet_options().try_into()?, + ), }, )), - })); + }); } } - Ok(None) - } + #[cfg(feature = "avro")] + if let Some(data_source_exec) = plan.downcast_ref::() { + let data_source = data_source_exec.data_source(); + if let Some(maybe_avro) = + data_source.as_any().downcast_ref::() + { + let source = maybe_avro.file_source(); + if source.as_any().downcast_ref::().is_some() { + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::AvroScan( + protobuf::AvroScanExecNode { + base_conf: Some(serialize_file_scan_config( + maybe_avro, + extension_codec, + )?), + }, + )), + }); + } + } + } - fn try_from_coalesce_partitions_exec( - exec: &CoalescePartitionsExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Merge(Box::new( - protobuf::CoalescePartitionsExecNode { - input: Some(Box::new(input)), - }, - ))), - }) - } + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Merge(Box::new( + protobuf::CoalescePartitionsExecNode { + input: Some(Box::new(input)), + }, + ))), + }); + } - fn try_from_repartition_exec( - exec: &RepartitionExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - - let pb_partitioning = - serialize_partitioning(exec.partitioning(), extension_codec)?; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Repartition(Box::new( - protobuf::RepartitionExecNode { - input: Some(Box::new(input)), - partitioning: Some(pb_partitioning), - }, - ))), - }) - } + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; - fn try_from_sort_exec( - exec: &SortExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - let expr = exec - .expr() - .iter() - .map(|expr| { - let sort_expr = Box::new(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }); - Ok(protobuf::PhysicalExprNode { - expr_type: Some(ExprType::Sort(sort_expr)), - }) - }) - .collect::>>()?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Sort(Box::new( - protobuf::SortExecNode { - input: Some(Box::new(input)), - expr, - fetch: match exec.fetch() { - Some(n) => n as i64, - _ => -1, + let pb_partitioning = + serialize_partitioning(exec.partitioning(), extension_codec)?; + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Repartition(Box::new( + protobuf::RepartitionExecNode { + input: Some(Box::new(input)), + partitioning: Some(pb_partitioning), }, - preserve_partitioning: exec.preserve_partitioning(), - }, - ))), - }) - } + ))), + }); + } - fn try_from_union_exec( - union: &UnionExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let mut inputs: Vec = vec![]; - for input in union.inputs() { - inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( - input.to_owned(), + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), extension_codec, - )?); + )?; + let expr = exec + .expr() + .iter() + .map(|expr| { + let sort_expr = Box::new(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }); + Ok(protobuf::PhysicalExprNode { + expr_type: Some(ExprType::Sort(sort_expr)), + }) + }) + .collect::>>()?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Sort(Box::new( + protobuf::SortExecNode { + input: Some(Box::new(input)), + expr, + fetch: match exec.fetch() { + Some(n) => n as i64, + _ => -1, + }, + preserve_partitioning: exec.preserve_partitioning(), + }, + ))), + }); } - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Union(protobuf::UnionExecNode { - inputs, - })), - }) - } - fn try_from_interleave_exec( - interleave: &InterleaveExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let mut inputs: Vec = vec![]; - for input in interleave.inputs() { - inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( - input.to_owned(), - extension_codec, - )?); + if let Some(union) = plan.downcast_ref::() { + let mut inputs: Vec = vec![]; + for input in union.inputs() { + inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( + input.to_owned(), + extension_codec, + )?); + } + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Union( + protobuf::UnionExecNode { inputs }, + )), + }); } - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Interleave( - protobuf::InterleaveExecNode { inputs }, - )), - }) - } - fn try_from_sort_preserving_merge_exec( - exec: &SortPreservingMergeExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - let expr = exec - .expr() - .iter() - .map(|expr| { - let sort_expr = Box::new(protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }); - Ok(protobuf::PhysicalExprNode { - expr_type: Some(ExprType::Sort(sort_expr)), - }) - }) - .collect::>>()?; - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::SortPreservingMerge(Box::new( - protobuf::SortPreservingMergeExecNode { - input: Some(Box::new(input)), - expr, - fetch: exec.fetch().map(|f| f as i64).unwrap_or(-1), - }, - ))), - }) - } + if let Some(interleave) = plan.downcast_ref::() { + let mut inputs: Vec = vec![]; + for input in interleave.inputs() { + inputs.push(protobuf::PhysicalPlanNode::try_from_physical_plan( + input.to_owned(), + extension_codec, + )?); + } + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Interleave( + protobuf::InterleaveExecNode { inputs }, + )), + }); + } - fn try_from_nested_loop_join_exec( - exec: &NestedLoopJoinExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let left = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.left().to_owned(), - extension_codec, - )?; - let right = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.right().to_owned(), - extension_codec, - )?; - - let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); - let filter = exec - .filter() - .as_ref() - .map(|f| { - let expression = - serialize_physical_expr(f.expression(), extension_codec)?; - let column_indices = f - .column_indices() - .iter() - .map(|i| { - let side: protobuf::JoinSide = i.side.to_owned().into(); - protobuf::ColumnIndex { - index: i.index as u32, - side: side.into(), - } + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + let expr = exec + .expr() + .iter() + .map(|expr| { + let sort_expr = Box::new(protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }); + Ok(protobuf::PhysicalExprNode { + expr_type: Some(ExprType::Sort(sort_expr)), }) - .collect(); - let schema = f.schema().as_ref().try_into()?; - Ok(protobuf::JoinFilter { - expression: Some(expression), - column_indices, - schema: Some(schema), }) - }) - .map_or(Ok(None), |v: Result| v.map(Some))?; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::NestedLoopJoin(Box::new( - protobuf::NestedLoopJoinExecNode { - left: Some(Box::new(left)), - right: Some(Box::new(right)), - join_type: join_type.into(), - filter, - projection: exec.projection().map_or_else(Vec::new, |v| { - v.iter().map(|x| *x as u32).collect::>() + .collect::>>()?; + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::SortPreservingMerge( + Box::new(protobuf::SortPreservingMergeExecNode { + input: Some(Box::new(input)), + expr, + fetch: exec.fetch().map(|f| f as i64).unwrap_or(-1), }), - }, - ))), - }) - } + )), + }); + } - fn try_from_window_agg_exec( - exec: &WindowAggExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - - let window_expr = exec - .window_expr() - .iter() - .map(|e| serialize_physical_window_expr(e, extension_codec)) - .collect::>>()?; - - let partition_keys = exec - .partition_keys() - .iter() - .map(|e| serialize_physical_expr(e, extension_codec)) - .collect::>>()?; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Window(Box::new( - protobuf::WindowAggExecNode { - input: Some(Box::new(input)), - window_expr, - partition_keys, - input_order_mode: None, - }, - ))), - }) - } + if let Some(exec) = plan.downcast_ref::() { + let left = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.left().to_owned(), + extension_codec, + )?; + let right = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.right().to_owned(), + extension_codec, + )?; - fn try_from_bounded_window_agg_exec( - exec: &BoundedWindowAggExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - - let window_expr = exec - .window_expr() - .iter() - .map(|e| serialize_physical_window_expr(e, extension_codec)) - .collect::>>()?; - - let partition_keys = exec - .partition_keys() - .iter() - .map(|e| serialize_physical_expr(e, extension_codec)) - .collect::>>()?; - - let input_order_mode = match &exec.input_order_mode { - InputOrderMode::Linear => { - window_agg_exec_node::InputOrderMode::Linear(protobuf::EmptyMessage {}) - } - InputOrderMode::PartiallySorted(columns) => { - window_agg_exec_node::InputOrderMode::PartiallySorted( - protobuf::PartiallySortedInputOrderMode { - columns: columns.iter().map(|c| *c as u64).collect(), + let join_type: protobuf::JoinType = exec.join_type().to_owned().into(); + let filter = exec + .filter() + .as_ref() + .map(|f| { + let expression = + serialize_physical_expr(f.expression(), extension_codec)?; + let column_indices = f + .column_indices() + .iter() + .map(|i| { + let side: protobuf::JoinSide = i.side.to_owned().into(); + protobuf::ColumnIndex { + index: i.index as u32, + side: side.into(), + } + }) + .collect(); + let schema = f.schema().as_ref().try_into()?; + Ok(protobuf::JoinFilter { + expression: Some(expression), + column_indices, + schema: Some(schema), + }) + }) + .map_or(Ok(None), |v: Result| v.map(Some))?; + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::NestedLoopJoin(Box::new( + protobuf::NestedLoopJoinExecNode { + left: Some(Box::new(left)), + right: Some(Box::new(right)), + join_type: join_type.into(), + filter, + projection: exec.projection().map_or_else(Vec::new, |v| { + v.iter().map(|x| *x as u32).collect::>() + }), }, - ) - } - InputOrderMode::Sorted => { - window_agg_exec_node::InputOrderMode::Sorted(protobuf::EmptyMessage {}) - } - }; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Window(Box::new( - protobuf::WindowAggExecNode { - input: Some(Box::new(input)), - window_expr, - partition_keys, - input_order_mode: Some(input_order_mode), - }, - ))), - }) - } + ))), + }); + } - fn try_from_data_sink_exec( - exec: &DataSinkExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result> { - let input: protobuf::PhysicalPlanNode = - protobuf::PhysicalPlanNode::try_from_physical_plan( + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( exec.input().to_owned(), extension_codec, )?; - let sort_order = match exec.sort_order() { - Some(requirements) => { - let expr = requirements - .iter() - .map(|requirement| { - let expr: PhysicalSortExpr = requirement.to_owned().into(); - let sort_expr = protobuf::PhysicalSortExprNode { - expr: Some(Box::new(serialize_physical_expr( - &expr.expr, - extension_codec, - )?)), - asc: !expr.options.descending, - nulls_first: expr.options.nulls_first, - }; - Ok(sort_expr) - }) - .collect::>>()?; - Some(protobuf::PhysicalSortExprNodeCollection { - physical_sort_expr_nodes: expr, - }) - } - None => None, - }; - if let Some(sink) = exec.sink().as_any().downcast_ref::() { - return Ok(Some(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::JsonSink(Box::new( - protobuf::JsonSinkExecNode { + let window_expr = exec + .window_expr() + .iter() + .map(|e| serialize_physical_window_expr(e, extension_codec)) + .collect::>>()?; + + let partition_keys = exec + .partition_keys() + .iter() + .map(|e| serialize_physical_expr(e, extension_codec)) + .collect::>>()?; + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Window(Box::new( + protobuf::WindowAggExecNode { input: Some(Box::new(input)), - sink: Some(sink.try_into()?), - sink_schema: Some(exec.schema().as_ref().try_into()?), - sort_order, + window_expr, + partition_keys, + input_order_mode: None, }, ))), - })); + }); } - if let Some(sink) = exec.sink().as_any().downcast_ref::() { - return Ok(Some(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::CsvSink(Box::new( - protobuf::CsvSinkExecNode { + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + let window_expr = exec + .window_expr() + .iter() + .map(|e| serialize_physical_window_expr(e, extension_codec)) + .collect::>>()?; + + let partition_keys = exec + .partition_keys() + .iter() + .map(|e| serialize_physical_expr(e, extension_codec)) + .collect::>>()?; + + let input_order_mode = match &exec.input_order_mode { + InputOrderMode::Linear => window_agg_exec_node::InputOrderMode::Linear( + protobuf::EmptyMessage {}, + ), + InputOrderMode::PartiallySorted(columns) => { + window_agg_exec_node::InputOrderMode::PartiallySorted( + protobuf::PartiallySortedInputOrderMode { + columns: columns.iter().map(|c| *c as u64).collect(), + }, + ) + } + InputOrderMode::Sorted => window_agg_exec_node::InputOrderMode::Sorted( + protobuf::EmptyMessage {}, + ), + }; + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Window(Box::new( + protobuf::WindowAggExecNode { input: Some(Box::new(input)), - sink: Some(sink.try_into()?), - sink_schema: Some(exec.schema().as_ref().try_into()?), - sort_order, + window_expr, + partition_keys, + input_order_mode: Some(input_order_mode), }, ))), - })); + }); } - #[cfg(feature = "parquet")] - if let Some(sink) = exec.sink().as_any().downcast_ref::() { - return Ok(Some(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::ParquetSink(Box::new( - protobuf::ParquetSinkExecNode { + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + let sort_order = match exec.sort_order() { + Some(requirements) => { + let expr = requirements + .iter() + .map(|requirement| { + let expr: PhysicalSortExpr = requirement.to_owned().into(); + let sort_expr = protobuf::PhysicalSortExprNode { + expr: Some(Box::new(serialize_physical_expr( + &expr.expr, + extension_codec, + )?)), + asc: !expr.options.descending, + nulls_first: expr.options.nulls_first, + }; + Ok(sort_expr) + }) + .collect::>>()?; + Some(protobuf::PhysicalSortExprNodeCollection { + physical_sort_expr_nodes: expr, + }) + } + None => None, + }; + + if let Some(sink) = exec.sink().as_any().downcast_ref::() { + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::JsonSink(Box::new( + protobuf::JsonSinkExecNode { + input: Some(Box::new(input)), + sink: Some(sink.try_into()?), + sink_schema: Some(exec.schema().as_ref().try_into()?), + sort_order, + }, + ))), + }); + } + + if let Some(sink) = exec.sink().as_any().downcast_ref::() { + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::CsvSink(Box::new( + protobuf::CsvSinkExecNode { + input: Some(Box::new(input)), + sink: Some(sink.try_into()?), + sink_schema: Some(exec.schema().as_ref().try_into()?), + sort_order, + }, + ))), + }); + } + + #[cfg(feature = "parquet")] + if let Some(sink) = exec.sink().as_any().downcast_ref::() { + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::ParquetSink(Box::new( + protobuf::ParquetSinkExecNode { + input: Some(Box::new(input)), + sink: Some(sink.try_into()?), + sink_schema: Some(exec.schema().as_ref().try_into()?), + sort_order, + }, + ))), + }); + } + + // If unknown DataSink then let extension handle it + } + + if let Some(exec) = plan.downcast_ref::() { + let input = protobuf::PhysicalPlanNode::try_from_physical_plan( + exec.input().to_owned(), + extension_codec, + )?; + + return Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Unnest(Box::new( + protobuf::UnnestExecNode { input: Some(Box::new(input)), - sink: Some(sink.try_into()?), - sink_schema: Some(exec.schema().as_ref().try_into()?), - sort_order, + schema: Some(exec.schema().try_into()?), + list_type_columns: exec + .list_column_indices() + .iter() + .map(|c| ProtoListUnnest { + index_in_input_schema: c.index_in_input_schema as _, + depth: c.depth as _, + }) + .collect(), + struct_type_columns: exec + .struct_column_indices() + .iter() + .map(|c| *c as _) + .collect(), + options: Some(exec.options().into()), }, ))), - })); + }); } - // If unknown DataSink then let extension handle it - Ok(None) - } + let mut buf: Vec = vec![]; + match extension_codec.try_encode(Arc::clone(&plan_clone), &mut buf) { + Ok(_) => { + let inputs: Vec = plan_clone + .children() + .into_iter() + .cloned() + .map(|i| { + protobuf::PhysicalPlanNode::try_from_physical_plan( + i, + extension_codec, + ) + }) + .collect::>()?; - fn try_from_unnest_exec( - exec: &UnnestExec, - extension_codec: &dyn PhysicalExtensionCodec, - ) -> Result { - let input = protobuf::PhysicalPlanNode::try_from_physical_plan( - exec.input().to_owned(), - extension_codec, - )?; - - Ok(protobuf::PhysicalPlanNode { - physical_plan_type: Some(PhysicalPlanType::Unnest(Box::new( - protobuf::UnnestExecNode { - input: Some(Box::new(input)), - schema: Some(exec.schema().try_into()?), - list_type_columns: exec - .list_column_indices() - .iter() - .map(|c| ProtoListUnnest { - index_in_input_schema: c.index_in_input_schema as _, - depth: c.depth as _, - }) - .collect(), - struct_type_columns: exec - .struct_column_indices() - .iter() - .map(|c| *c as _) - .collect(), - options: Some(exec.options().into()), - }, - ))), - }) + Ok(protobuf::PhysicalPlanNode { + physical_plan_type: Some(PhysicalPlanType::Extension( + protobuf::PhysicalExtensionNode { node: buf, inputs }, + )), + }) + } + Err(e) => internal_err!( + "Unsupported plan and extension codec failed with [{e}]. Plan: {plan_clone:?}" + ), + } } } diff --git a/datafusion/proto/src/physical_plan/to_proto.rs b/datafusion/proto/src/physical_plan/to_proto.rs index d1b1f51ae1075..c196595eeed4a 100644 --- a/datafusion/proto/src/physical_plan/to_proto.rs +++ b/datafusion/proto/src/physical_plan/to_proto.rs @@ -22,7 +22,6 @@ use datafusion::datasource::file_format::parquet::ParquetSink; use datafusion::datasource::physical_plan::FileSink; use datafusion::physical_expr::window::{SlidingAggregateWindowExpr, StandardWindowExpr}; use datafusion::physical_expr::{LexOrdering, PhysicalSortExpr, ScalarFunctionExpr}; -use datafusion::physical_expr_common::physical_expr::snapshot_physical_expr; use datafusion::physical_plan::expressions::{ BinaryExpr, CaseExpr, CastExpr, Column, InListExpr, IsNotNullExpr, IsNullExpr, Literal, NegativeExpr, NotExpr, TryCastExpr, UnKnownColumn, @@ -211,9 +210,6 @@ pub fn serialize_physical_expr( value: &Arc, codec: &dyn PhysicalExtensionCodec, ) -> Result { - // Snapshot the expr in case it has dynamic predicate state so - // it can be serialized - let value = snapshot_physical_expr(Arc::clone(value))?; let expr = value.as_any(); if let Some(expr) = expr.downcast_ref::() { @@ -372,7 +368,7 @@ pub fn serialize_physical_expr( }) } else { let mut buf: Vec = vec![]; - match codec.try_encode_expr(&value, &mut buf) { + match codec.try_encode_expr(value, &mut buf) { Ok(_) => { let inputs: Vec = value .children() @@ -445,7 +441,7 @@ impl TryFrom<&PartitionedFile> for protobuf::PartitionedFile { })? as u64; Ok(protobuf::PartitionedFile { path: pf.object_meta.location.as_ref().to_owned(), - size: pf.object_meta.size, + size: pf.object_meta.size as u64, last_modified_ns, partition_values: pf .partition_values @@ -453,7 +449,7 @@ impl TryFrom<&PartitionedFile> for protobuf::PartitionedFile { .map(|v| v.try_into()) .collect::, _>>()?, range: pf.range.as_ref().map(|r| r.try_into()).transpose()?, - statistics: pf.statistics.as_ref().map(|s| s.as_ref().into()), + statistics: pf.statistics.as_ref().map(|s| s.into()), }) } } @@ -511,7 +507,7 @@ pub fn serialize_file_scan_config( Ok(protobuf::FileScanExecConf { file_groups, - statistics: Some((&conf.file_source.statistics().unwrap()).into()), + statistics: Some((&conf.statistics).into()), limit: conf.limit.map(|l| protobuf::ScanLimit { limit: l as u32 }), projection: conf .projection diff --git a/datafusion/proto/tests/cases/roundtrip_logical_plan.rs b/datafusion/proto/tests/cases/roundtrip_logical_plan.rs index 7ecb2c23a5e13..9fa1f74ae188a 100644 --- a/datafusion/proto/tests/cases/roundtrip_logical_plan.rs +++ b/datafusion/proto/tests/cases/roundtrip_logical_plan.rs @@ -24,10 +24,7 @@ use arrow::datatypes::{ DECIMAL256_MAX_PRECISION, }; use arrow::util::pretty::pretty_format_batches; -use datafusion::datasource::file_format::json::{JsonFormat, JsonFormatFactory}; -use datafusion::datasource::listing::{ - ListingOptions, ListingTable, ListingTableConfig, ListingTableUrl, -}; +use datafusion::datasource::file_format::json::JsonFormatFactory; use datafusion::optimizer::eliminate_nested_union::EliminateNestedUnion; use datafusion::optimizer::Optimizer; use datafusion_common::parsers::CompressionTypeVariant; @@ -973,8 +970,8 @@ async fn roundtrip_expr_api() -> Result<()> { stddev_pop(lit(2.2)), approx_distinct(lit(2)), approx_median(lit(2)), - approx_percentile_cont(lit(2).sort(true, false), lit(0.5), None), - approx_percentile_cont(lit(2).sort(true, false), lit(0.5), Some(lit(50))), + approx_percentile_cont(lit(2), lit(0.5), None), + approx_percentile_cont(lit(2), lit(0.5), Some(lit(50))), approx_percentile_cont_with_weight(lit(2), lit(1), lit(0.5)), grouping(lit(1)), bit_and(lit(2)), @@ -2562,33 +2559,3 @@ async fn roundtrip_union_query() -> Result<()> { ); Ok(()) } - -#[tokio::test] -async fn roundtrip_custom_listing_tables_schema() -> Result<()> { - let ctx = SessionContext::new(); - // Make sure during round-trip, constraint information is preserved - let file_format = JsonFormat::default(); - let table_partition_cols = vec![("part".to_owned(), DataType::Int64)]; - let data = "../core/tests/data/partitioned_table_json"; - let listing_table_url = ListingTableUrl::parse(data)?; - let listing_options = ListingOptions::new(Arc::new(file_format)) - .with_table_partition_cols(table_partition_cols); - - let config = ListingTableConfig::new(listing_table_url) - .with_listing_options(listing_options) - .infer_schema(&ctx.state()) - .await?; - - ctx.register_table("hive_style", Arc::new(ListingTable::try_new(config)?))?; - - let plan = ctx - .sql("SELECT part, value FROM hive_style LIMIT 1") - .await? - .logical_plan() - .clone(); - - let bytes = logical_plan_to_bytes(&plan)?; - let new_plan = logical_plan_from_bytes(&bytes, &ctx)?; - assert_eq!(plan, new_plan); - Ok(()) -} diff --git a/datafusion/proto/tests/cases/roundtrip_physical_plan.rs b/datafusion/proto/tests/cases/roundtrip_physical_plan.rs index 6dddbb5ea0a0e..6356b8b7b0cf4 100644 --- a/datafusion/proto/tests/cases/roundtrip_physical_plan.rs +++ b/datafusion/proto/tests/cases/roundtrip_physical_plan.rs @@ -504,7 +504,7 @@ fn rountrip_aggregate_with_approx_pencentile_cont() -> Result<()> { vec![col("b", &schema)?, lit(0.5)], ) .schema(Arc::clone(&schema)) - .alias("APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY b)") + .alias("APPROX_PERCENTILE_CONT(b, 0.5)") .build() .map(Arc::new)?]; @@ -1676,47 +1676,3 @@ async fn roundtrip_empty_projection() -> Result<()> { let sql = "select 1 from alltypes_plain"; roundtrip_test_sql_with_context(sql, &ctx).await } - -#[tokio::test] -async fn roundtrip_physical_plan_node() { - use datafusion::prelude::*; - use datafusion_proto::physical_plan::{ - AsExecutionPlan, DefaultPhysicalExtensionCodec, - }; - use datafusion_proto::protobuf::PhysicalPlanNode; - - let ctx = SessionContext::new(); - - ctx.register_parquet( - "pt", - &format!( - "{}/alltypes_plain.snappy.parquet", - datafusion_common::test_util::parquet_test_data() - ), - ParquetReadOptions::default(), - ) - .await - .unwrap(); - - let plan = ctx - .sql("select id, string_col, timestamp_col from pt where id > 4 order by string_col") - .await - .unwrap() - .create_physical_plan() - .await - .unwrap(); - - let node: PhysicalPlanNode = - PhysicalPlanNode::try_from_physical_plan(plan, &DefaultPhysicalExtensionCodec {}) - .unwrap(); - - let plan = node - .try_into_physical_plan( - &ctx, - &ctx.runtime_env(), - &DefaultPhysicalExtensionCodec {}, - ) - .unwrap(); - - let _ = plan.execute(0, ctx.task_ctx()).unwrap(); -} diff --git a/datafusion/proto/tests/cases/serialize.rs b/datafusion/proto/tests/cases/serialize.rs index d15e62909f7e4..d1b50105d053d 100644 --- a/datafusion/proto/tests/cases/serialize.rs +++ b/datafusion/proto/tests/cases/serialize.rs @@ -260,7 +260,7 @@ fn test_expression_serialization_roundtrip() { for function in string::functions() { // default to 4 args (though some exprs like substr have error checking) let num_args = 4; - let args: Vec<_> = std::iter::repeat_n(&lit, num_args).cloned().collect(); + let args: Vec<_> = std::iter::repeat(&lit).take(num_args).cloned().collect(); let expr = Expr::ScalarFunction(ScalarFunction::new_udf(function, args)); let extension_codec = DefaultLogicalExtensionCodec {}; diff --git a/datafusion/sql/Cargo.toml b/datafusion/sql/Cargo.toml index b778db46769d0..4435ee0f56cbc 100644 --- a/datafusion/sql/Cargo.toml +++ b/datafusion/sql/Cargo.toml @@ -61,6 +61,5 @@ datafusion-functions-aggregate = { workspace = true } datafusion-functions-nested = { workspace = true } datafusion-functions-window = { workspace = true } env_logger = { workspace = true } -insta = { workspace = true } paste = "^1.0" rstest = { workspace = true } diff --git a/datafusion/sql/src/expr/function.rs b/datafusion/sql/src/expr/function.rs index c0cb5b38ff020..436f4388d8a31 100644 --- a/datafusion/sql/src/expr/function.rs +++ b/datafusion/sql/src/expr/function.rs @@ -74,7 +74,7 @@ fn find_closest_match(candidates: Vec, target: &str) -> Option { }) } -/// Arguments for a function call extracted from the SQL AST +/// Arguments to for a function call extracted from the SQL AST #[derive(Debug)] struct FunctionArgs { /// Function name @@ -91,8 +91,6 @@ struct FunctionArgs { null_treatment: Option, /// DISTINCT distinct: bool, - /// WITHIN GROUP clause, if any - within_group: Vec, } impl FunctionArgs { @@ -117,7 +115,6 @@ impl FunctionArgs { filter, null_treatment, distinct: false, - within_group, }); }; @@ -147,9 +144,6 @@ impl FunctionArgs { } FunctionArgumentClause::OrderBy(oby) => { if order_by.is_some() { - if !within_group.is_empty() { - return plan_err!("ORDER BY clause is only permitted in WITHIN GROUP clause when a WITHIN GROUP is used"); - } return not_impl_err!("Calling {name}: Duplicated ORDER BY clause in function arguments"); } order_by = Some(oby); @@ -182,10 +176,8 @@ impl FunctionArgs { } } - if within_group.len() > 1 { - return not_impl_err!( - "Only a single ordering expression is permitted in a WITHIN GROUP clause" - ); + if !within_group.is_empty() { + return not_impl_err!("WITHIN GROUP is not supported yet: {within_group:?}"); } let order_by = order_by.unwrap_or_default(); @@ -198,7 +190,6 @@ impl FunctionArgs { filter, null_treatment, distinct, - within_group, }) } } @@ -219,14 +210,8 @@ impl SqlToRel<'_, S> { filter, null_treatment, distinct, - within_group, } = function_args; - if over.is_some() && !within_group.is_empty() { - return plan_err!("OVER and WITHIN GROUP clause are can not be used together. \ - OVER is for window function, whereas WITHIN GROUP is for ordered set aggregate function"); - } - // If function is a window function (it has an OVER clause), // it shouldn't have ordering requirement as function argument // required ordering should be defined in OVER clause. @@ -371,49 +356,15 @@ impl SqlToRel<'_, S> { } else { // User defined aggregate functions (UDAF) have precedence in case it has the same name as a scalar built-in function if let Some(fm) = self.context_provider.get_aggregate_meta(&name) { - if fm.is_ordered_set_aggregate() && within_group.is_empty() { - return plan_err!("WITHIN GROUP clause is required when calling ordered set aggregate function({})", fm.name()); - } - - if null_treatment.is_some() && !fm.supports_null_handling_clause() { - return plan_err!( - "[IGNORE | RESPECT] NULLS are not permitted for {}", - fm.name() - ); - } - - let mut args = - self.function_args_to_expr(args, schema, planner_context)?; - - let order_by = if fm.is_ordered_set_aggregate() { - let within_group = self.order_by_to_sort_expr( - within_group, - schema, - planner_context, - false, - None, - )?; - - // add target column expression in within group clause to function arguments - if !within_group.is_empty() { - args = within_group - .iter() - .map(|sort| sort.expr.clone()) - .chain(args) - .collect::>(); - } - (!within_group.is_empty()).then_some(within_group) - } else { - let order_by = self.order_by_to_sort_expr( - order_by, - schema, - planner_context, - true, - None, - )?; - (!order_by.is_empty()).then_some(order_by) - }; - + let order_by = self.order_by_to_sort_expr( + order_by, + schema, + planner_context, + true, + None, + )?; + let order_by = (!order_by.is_empty()).then_some(order_by); + let args = self.function_args_to_expr(args, schema, planner_context)?; let filter: Option> = filter .map(|e| self.sql_expr_to_logical_expr(*e, schema, planner_context)) .transpose()? diff --git a/datafusion/sql/src/expr/value.rs b/datafusion/sql/src/expr/value.rs index be4a45a25750c..d53691ef05d17 100644 --- a/datafusion/sql/src/expr/value.rs +++ b/datafusion/sql/src/expr/value.rs @@ -301,7 +301,7 @@ fn interval_literal(interval_value: SQLExpr, negative: bool) -> Result { fn try_decode_hex_literal(s: &str) -> Option> { let hex_bytes = s.as_bytes(); - let mut decoded_bytes = Vec::with_capacity(hex_bytes.len().div_ceil(2)); + let mut decoded_bytes = Vec::with_capacity((hex_bytes.len() + 1) / 2); let start_idx = hex_bytes.len() % 2; if start_idx > 0 { diff --git a/datafusion/sql/src/parser.rs b/datafusion/sql/src/parser.rs index 27c897f7ad608..822b651eae864 100644 --- a/datafusion/sql/src/parser.rs +++ b/datafusion/sql/src/parser.rs @@ -20,9 +20,9 @@ //! This parser implements DataFusion specific statements such as //! `CREATE EXTERNAL TABLE` -use datafusion_common::config::SqlParserOptions; -use datafusion_common::DataFusionError; -use datafusion_common::{sql_err, Diagnostic, Span}; +use std::collections::VecDeque; +use std::fmt; + use sqlparser::ast::{ExprWithAlias, OrderByOptions}; use sqlparser::tokenizer::TokenWithSpan; use sqlparser::{ @@ -34,8 +34,6 @@ use sqlparser::{ parser::{Parser, ParserError}, tokenizer::{Token, Tokenizer, Word}, }; -use std::collections::VecDeque; -use std::fmt; // Use `Parser::expected` instead, if possible macro_rules! parser_err { @@ -44,7 +42,7 @@ macro_rules! parser_err { }; } -fn parse_file_type(s: &str) -> Result { +fn parse_file_type(s: &str) -> Result { Ok(s.to_uppercase()) } @@ -268,9 +266,11 @@ impl fmt::Display for Statement { } } -fn ensure_not_set(field: &Option, name: &str) -> Result<(), DataFusionError> { +fn ensure_not_set(field: &Option, name: &str) -> Result<(), ParserError> { if field.is_some() { - parser_err!(format!("{name} specified more than once",))? + return Err(ParserError::ParserError(format!( + "{name} specified more than once", + ))); } Ok(()) } @@ -285,7 +285,6 @@ fn ensure_not_set(field: &Option, name: &str) -> Result<(), DataFusionErro /// [`Statement`] for a list of this special syntax pub struct DFParser<'a> { pub parser: Parser<'a>, - options: SqlParserOptions, } /// Same as `sqlparser` @@ -357,28 +356,21 @@ impl<'a> DFParserBuilder<'a> { self } - pub fn build(self) -> Result, DataFusionError> { + pub fn build(self) -> Result, ParserError> { let mut tokenizer = Tokenizer::new(self.dialect, self.sql); - // Convert TokenizerError -> ParserError - let tokens = tokenizer - .tokenize_with_location() - .map_err(ParserError::from)?; + let tokens = tokenizer.tokenize_with_location()?; Ok(DFParser { parser: Parser::new(self.dialect) .with_tokens_with_locations(tokens) .with_recursion_limit(self.recursion_limit), - options: SqlParserOptions { - recursion_limit: self.recursion_limit, - ..Default::default() - }, }) } } impl<'a> DFParser<'a> { #[deprecated(since = "46.0.0", note = "DFParserBuilder")] - pub fn new(sql: &'a str) -> Result { + pub fn new(sql: &'a str) -> Result { DFParserBuilder::new(sql).build() } @@ -386,13 +378,13 @@ impl<'a> DFParser<'a> { pub fn new_with_dialect( sql: &'a str, dialect: &'a dyn Dialect, - ) -> Result { + ) -> Result { DFParserBuilder::new(sql).with_dialect(dialect).build() } /// Parse a sql string into one or [`Statement`]s using the /// [`GenericDialect`]. - pub fn parse_sql(sql: &'a str) -> Result, DataFusionError> { + pub fn parse_sql(sql: &'a str) -> Result, ParserError> { let mut parser = DFParserBuilder::new(sql).build()?; parser.parse_statements() @@ -403,7 +395,7 @@ impl<'a> DFParser<'a> { pub fn parse_sql_with_dialect( sql: &str, dialect: &dyn Dialect, - ) -> Result, DataFusionError> { + ) -> Result, ParserError> { let mut parser = DFParserBuilder::new(sql).with_dialect(dialect).build()?; parser.parse_statements() } @@ -411,14 +403,14 @@ impl<'a> DFParser<'a> { pub fn parse_sql_into_expr_with_dialect( sql: &str, dialect: &dyn Dialect, - ) -> Result { + ) -> Result { let mut parser = DFParserBuilder::new(sql).with_dialect(dialect).build()?; parser.parse_expr() } /// Parse a sql string into one or [`Statement`]s - pub fn parse_statements(&mut self) -> Result, DataFusionError> { + pub fn parse_statements(&mut self) -> Result, ParserError> { let mut stmts = VecDeque::new(); let mut expecting_statement_delimiter = false; loop { @@ -446,26 +438,12 @@ impl<'a> DFParser<'a> { &self, expected: &str, found: TokenWithSpan, - ) -> Result { - let sql_parser_span = found.span; - parser_err!(format!( - "Expected: {expected}, found: {found}{}", - found.span.start - )) - .map_err(|e| { - let e = DataFusionError::from(e); - let span = Span::try_from_sqlparser_span(sql_parser_span); - let diagnostic = Diagnostic::new_error( - format!("Expected: {expected}, found: {found}{}", found.span.start), - span, - ); - - e.with_diagnostic(diagnostic) - }) + ) -> Result { + parser_err!(format!("Expected {expected}, found: {found}")) } /// Parse a new expression - pub fn parse_statement(&mut self) -> Result { + pub fn parse_statement(&mut self) -> Result { match self.parser.peek_token().token { Token::Word(w) => { match w.keyword { @@ -477,7 +455,9 @@ impl<'a> DFParser<'a> { if let Token::Word(w) = self.parser.peek_nth_token(1).token { // use native parser for COPY INTO if w.keyword == Keyword::INTO { - return self.parse_and_handle_statement(); + return Ok(Statement::Statement(Box::from( + self.parser.parse_statement()?, + ))); } } self.parser.next_token(); // COPY @@ -489,49 +469,36 @@ impl<'a> DFParser<'a> { } _ => { // use sqlparser-rs parser - self.parse_and_handle_statement() + Ok(Statement::Statement(Box::from( + self.parser.parse_statement()?, + ))) } } } _ => { // use the native parser - self.parse_and_handle_statement() + Ok(Statement::Statement(Box::from( + self.parser.parse_statement()?, + ))) } } } - pub fn parse_expr(&mut self) -> Result { + pub fn parse_expr(&mut self) -> Result { if let Token::Word(w) = self.parser.peek_token().token { match w.keyword { Keyword::CREATE | Keyword::COPY | Keyword::EXPLAIN => { - return parser_err!("Unsupported command in expression")?; + return parser_err!("Unsupported command in expression"); } _ => {} } } - Ok(self.parser.parse_expr_with_alias()?) - } - - /// Helper method to parse a statement and handle errors consistently, especially for recursion limits - fn parse_and_handle_statement(&mut self) -> Result { - self.parser - .parse_statement() - .map(|stmt| Statement::Statement(Box::from(stmt))) - .map_err(|e| match e { - ParserError::RecursionLimitExceeded => DataFusionError::SQL( - ParserError::RecursionLimitExceeded, - Some(format!( - " (current limit: {})", - self.options.recursion_limit - )), - ), - other => DataFusionError::SQL(other, None), - }) + self.parser.parse_expr_with_alias() } /// Parse a SQL `COPY TO` statement - pub fn parse_copy(&mut self) -> Result { + pub fn parse_copy(&mut self) -> Result { // parse as a query let source = if self.parser.consume_token(&Token::LParen) { let query = self.parser.parse_query()?; @@ -574,7 +541,7 @@ impl<'a> DFParser<'a> { Keyword::WITH => { self.parser.expect_keyword(Keyword::HEADER)?; self.parser.expect_keyword(Keyword::ROW)?; - return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS ('format.has_header' 'true')")?; + return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS ('format.has_header' 'true')"); } Keyword::PARTITIONED => { self.parser.expect_keyword(Keyword::BY)?; @@ -594,13 +561,17 @@ impl<'a> DFParser<'a> { if token == Token::EOF || token == Token::SemiColon { break; } else { - return self.expected("end of statement or ;", token)?; + return Err(ParserError::ParserError(format!( + "Unexpected token {token}" + ))); } } } let Some(target) = builder.target else { - return parser_err!("Missing TO clause in COPY statement")?; + return Err(ParserError::ParserError( + "Missing TO clause in COPY statement".into(), + )); }; Ok(Statement::CopyTo(CopyToStatement { @@ -618,7 +589,7 @@ impl<'a> DFParser<'a> { /// because it allows keywords as well as other non words /// /// [`parse_literal_string`]: sqlparser::parser::Parser::parse_literal_string - pub fn parse_option_key(&mut self) -> Result { + pub fn parse_option_key(&mut self) -> Result { let next_token = self.parser.next_token(); match next_token.token { Token::Word(Word { value, .. }) => { @@ -631,7 +602,7 @@ impl<'a> DFParser<'a> { // Unquoted namespaced keys have to conform to the syntax // "[\.]*". If we have a key that breaks this // pattern, error out: - return self.expected("key name", next_token); + return self.parser.expected("key name", next_token); } } Ok(parts.join(".")) @@ -639,7 +610,7 @@ impl<'a> DFParser<'a> { Token::SingleQuotedString(s) => Ok(s), Token::DoubleQuotedString(s) => Ok(s), Token::EscapedStringLiteral(s) => Ok(s), - _ => self.expected("key name", next_token), + _ => self.parser.expected("key name", next_token), } } @@ -649,7 +620,7 @@ impl<'a> DFParser<'a> { /// word or keyword in this location. /// /// [`parse_value`]: sqlparser::parser::Parser::parse_value - pub fn parse_option_value(&mut self) -> Result { + pub fn parse_option_value(&mut self) -> Result { let next_token = self.parser.next_token(); match next_token.token { // e.g. things like "snappy" or "gzip" that may be keywords @@ -658,12 +629,12 @@ impl<'a> DFParser<'a> { Token::DoubleQuotedString(s) => Ok(Value::DoubleQuotedString(s)), Token::EscapedStringLiteral(s) => Ok(Value::EscapedStringLiteral(s)), Token::Number(n, l) => Ok(Value::Number(n, l)), - _ => self.expected("string or numeric value", next_token), + _ => self.parser.expected("string or numeric value", next_token), } } /// Parse a SQL `EXPLAIN` - pub fn parse_explain(&mut self) -> Result { + pub fn parse_explain(&mut self) -> Result { let analyze = self.parser.parse_keyword(Keyword::ANALYZE); let verbose = self.parser.parse_keyword(Keyword::VERBOSE); let format = self.parse_explain_format()?; @@ -678,7 +649,7 @@ impl<'a> DFParser<'a> { })) } - pub fn parse_explain_format(&mut self) -> Result, DataFusionError> { + pub fn parse_explain_format(&mut self) -> Result, ParserError> { if !self.parser.parse_keyword(Keyword::FORMAT) { return Ok(None); } @@ -688,13 +659,15 @@ impl<'a> DFParser<'a> { Token::Word(w) => Ok(w.value), Token::SingleQuotedString(w) => Ok(w), Token::DoubleQuotedString(w) => Ok(w), - _ => self.expected("an explain format such as TREE", next_token), + _ => self + .parser + .expected("an explain format such as TREE", next_token), }?; Ok(Some(format)) } /// Parse a SQL `CREATE` statement handling `CREATE EXTERNAL TABLE` - pub fn parse_create(&mut self) -> Result { + pub fn parse_create(&mut self) -> Result { if self.parser.parse_keyword(Keyword::EXTERNAL) { self.parse_create_external_table(false) } else if self.parser.parse_keyword(Keyword::UNBOUNDED) { @@ -705,7 +678,7 @@ impl<'a> DFParser<'a> { } } - fn parse_partitions(&mut self) -> Result, DataFusionError> { + fn parse_partitions(&mut self) -> Result, ParserError> { let mut partitions: Vec = vec![]; if !self.parser.consume_token(&Token::LParen) || self.parser.consume_token(&Token::RParen) @@ -735,7 +708,7 @@ impl<'a> DFParser<'a> { } /// Parse the ordering clause of a `CREATE EXTERNAL TABLE` SQL statement - pub fn parse_order_by_exprs(&mut self) -> Result, DataFusionError> { + pub fn parse_order_by_exprs(&mut self) -> Result, ParserError> { let mut values = vec![]; self.parser.expect_token(&Token::LParen)?; loop { @@ -748,7 +721,7 @@ impl<'a> DFParser<'a> { } /// Parse an ORDER BY sub-expression optionally followed by ASC or DESC. - pub fn parse_order_by_expr(&mut self) -> Result { + pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parser.parse_expr()?; let asc = if self.parser.parse_keyword(Keyword::ASC) { @@ -780,7 +753,7 @@ impl<'a> DFParser<'a> { // This is a copy of the equivalent implementation in sqlparser. fn parse_columns( &mut self, - ) -> Result<(Vec, Vec), DataFusionError> { + ) -> Result<(Vec, Vec), ParserError> { let mut columns = vec![]; let mut constraints = vec![]; if !self.parser.consume_token(&Token::LParen) @@ -816,7 +789,7 @@ impl<'a> DFParser<'a> { Ok((columns, constraints)) } - fn parse_column_def(&mut self) -> Result { + fn parse_column_def(&mut self) -> Result { let name = self.parser.parse_identifier()?; let data_type = self.parser.parse_data_type()?; let mut options = vec![]; @@ -847,7 +820,7 @@ impl<'a> DFParser<'a> { fn parse_create_external_table( &mut self, unbounded: bool, - ) -> Result { + ) -> Result { let temporary = self .parser .parse_one_of_keywords(&[Keyword::TEMP, Keyword::TEMPORARY]) @@ -895,15 +868,15 @@ impl<'a> DFParser<'a> { } else { self.parser.expect_keyword(Keyword::HEADER)?; self.parser.expect_keyword(Keyword::ROW)?; - return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS (format.has_header true)")?; + return parser_err!("WITH HEADER ROW clause is no longer in use. Please use the OPTIONS clause with 'format.has_header' set appropriately, e.g., OPTIONS (format.has_header true)"); } } Keyword::DELIMITER => { - return parser_err!("DELIMITER clause is no longer in use. Please use the OPTIONS clause with 'format.delimiter' set appropriately, e.g., OPTIONS (format.delimiter ',')")?; + return parser_err!("DELIMITER clause is no longer in use. Please use the OPTIONS clause with 'format.delimiter' set appropriately, e.g., OPTIONS (format.delimiter ',')"); } Keyword::COMPRESSION => { self.parser.expect_keyword(Keyword::TYPE)?; - return parser_err!("COMPRESSION TYPE clause is no longer in use. Please use the OPTIONS clause with 'format.compression' set appropriately, e.g., OPTIONS (format.compression gzip)")?; + return parser_err!("COMPRESSION TYPE clause is no longer in use. Please use the OPTIONS clause with 'format.compression' set appropriately, e.g., OPTIONS (format.compression gzip)"); } Keyword::PARTITIONED => { self.parser.expect_keyword(Keyword::BY)?; @@ -926,7 +899,7 @@ impl<'a> DFParser<'a> { columns.extend(cols); if !cons.is_empty() { - return sql_err!(ParserError::ParserError( + return Err(ParserError::ParserError( "Constraints on Partition Columns are not supported" .to_string(), )); @@ -946,19 +919,21 @@ impl<'a> DFParser<'a> { if token == Token::EOF || token == Token::SemiColon { break; } else { - return self.expected("end of statement or ;", token)?; + return Err(ParserError::ParserError(format!( + "Unexpected token {token}" + ))); } } } // Validations: location and file_type are required if builder.file_type.is_none() { - return sql_err!(ParserError::ParserError( + return Err(ParserError::ParserError( "Missing STORED AS clause in CREATE EXTERNAL TABLE statement".into(), )); } if builder.location.is_none() { - return sql_err!(ParserError::ParserError( + return Err(ParserError::ParserError( "Missing LOCATION clause in CREATE EXTERNAL TABLE statement".into(), )); } @@ -980,7 +955,7 @@ impl<'a> DFParser<'a> { } /// Parses the set of valid formats - fn parse_file_format(&mut self) -> Result { + fn parse_file_format(&mut self) -> Result { let token = self.parser.next_token(); match &token.token { Token::Word(w) => parse_file_type(&w.value), @@ -992,7 +967,7 @@ impl<'a> DFParser<'a> { /// /// This method supports keywords as key names as well as multiple /// value types such as Numbers as well as Strings. - fn parse_value_options(&mut self) -> Result, DataFusionError> { + fn parse_value_options(&mut self) -> Result, ParserError> { let mut options = vec![]; self.parser.expect_token(&Token::LParen)?; @@ -1024,7 +999,7 @@ mod tests { use sqlparser::dialect::SnowflakeDialect; use sqlparser::tokenizer::Span; - fn expect_parse_ok(sql: &str, expected: Statement) -> Result<(), DataFusionError> { + fn expect_parse_ok(sql: &str, expected: Statement) -> Result<(), ParserError> { let statements = DFParser::parse_sql(sql)?; assert_eq!( statements.len(), @@ -1066,7 +1041,7 @@ mod tests { } #[test] - fn create_external_table() -> Result<(), DataFusionError> { + fn create_external_table() -> Result<(), ParserError> { // positive case let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV LOCATION 'foo.csv'"; let display = None; @@ -1287,13 +1262,13 @@ mod tests { "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV PARTITIONED BY (p1 int, c1) LOCATION 'foo.csv'"; expect_parse_error( sql, - "SQL error: ParserError(\"Expected: a data type name, found: ) at Line: 1, Column: 73\")", + "sql parser error: Expected: a data type name, found: )", ); // negative case: mixed column defs and column names in `PARTITIONED BY` clause let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV PARTITIONED BY (c1, p1 int) LOCATION 'foo.csv'"; - expect_parse_error(sql, "SQL error: ParserError(\"Expected: ',' or ')' after partition definition, found: int at Line: 1, Column: 70\")"); + expect_parse_error(sql, "sql parser error: Expected ',' or ')' after partition definition, found: int"); // positive case: additional options (one entry) can be specified let sql = @@ -1539,7 +1514,7 @@ mod tests { } #[test] - fn copy_to_table_to_table() -> Result<(), DataFusionError> { + fn copy_to_table_to_table() -> Result<(), ParserError> { // positive case let sql = "COPY foo TO bar STORED AS CSV"; let expected = Statement::CopyTo(CopyToStatement { @@ -1555,7 +1530,7 @@ mod tests { } #[test] - fn skip_copy_into_snowflake() -> Result<(), DataFusionError> { + fn skip_copy_into_snowflake() -> Result<(), ParserError> { let sql = "COPY INTO foo FROM @~/staged FILE_FORMAT = (FORMAT_NAME = 'mycsv');"; let dialect = Box::new(SnowflakeDialect); let statements = DFParser::parse_sql_with_dialect(sql, dialect.as_ref())?; @@ -1572,7 +1547,7 @@ mod tests { } #[test] - fn explain_copy_to_table_to_table() -> Result<(), DataFusionError> { + fn explain_copy_to_table_to_table() -> Result<(), ParserError> { let cases = vec![ ("EXPLAIN COPY foo TO bar STORED AS PARQUET", false, false), ( @@ -1613,7 +1588,7 @@ mod tests { } #[test] - fn copy_to_query_to_table() -> Result<(), DataFusionError> { + fn copy_to_query_to_table() -> Result<(), ParserError> { let statement = verified_stmt("SELECT 1"); // unwrap the various layers @@ -1646,7 +1621,7 @@ mod tests { } #[test] - fn copy_to_options() -> Result<(), DataFusionError> { + fn copy_to_options() -> Result<(), ParserError> { let sql = "COPY foo TO bar STORED AS CSV OPTIONS ('row_group_size' '55')"; let expected = Statement::CopyTo(CopyToStatement { source: object_name("foo"), @@ -1663,7 +1638,7 @@ mod tests { } #[test] - fn copy_to_partitioned_by() -> Result<(), DataFusionError> { + fn copy_to_partitioned_by() -> Result<(), ParserError> { let sql = "COPY foo TO bar STORED AS CSV PARTITIONED BY (a) OPTIONS ('row_group_size' '55')"; let expected = Statement::CopyTo(CopyToStatement { source: object_name("foo"), @@ -1680,7 +1655,7 @@ mod tests { } #[test] - fn copy_to_multi_options() -> Result<(), DataFusionError> { + fn copy_to_multi_options() -> Result<(), ParserError> { // order of options is preserved let sql = "COPY foo TO bar STORED AS parquet OPTIONS ('format.row_group_size' 55, 'format.compression' snappy, 'execution.keep_partition_by_columns' true)"; @@ -1779,7 +1754,7 @@ mod tests { assert_contains!( err.to_string(), - "SQL error: RecursionLimitExceeded (current limit: 1)" + "sql parser error: recursion limit exceeded" ); } } diff --git a/datafusion/sql/src/planner.rs b/datafusion/sql/src/planner.rs index 3325c98aa74b6..180017ee9c191 100644 --- a/datafusion/sql/src/planner.rs +++ b/datafusion/sql/src/planner.rs @@ -331,7 +331,7 @@ impl PlannerContext { /// /// Key interfaces are: /// * [`Self::sql_statement_to_plan`]: Convert a statement -/// (e.g. `SELECT ...`) into a [`LogicalPlan`] +/// (e.g. `SELECT ...`) into a [`LogicalPlan`] /// * [`Self::sql_to_expr`]: Convert an expression (e.g. `1 + 2`) into an [`Expr`] pub struct SqlToRel<'a, S: ContextProvider> { pub(crate) context_provider: &'a S, diff --git a/datafusion/sql/src/select.rs b/datafusion/sql/src/select.rs index 33994b60b7357..2a2d0b3b3eb8b 100644 --- a/datafusion/sql/src/select.rs +++ b/datafusion/sql/src/select.rs @@ -16,7 +16,6 @@ // under the License. use std::collections::HashSet; -use std::ops::ControlFlow; use std::sync::Arc; use crate::planner::{ContextProvider, PlannerContext, SqlToRel}; @@ -46,8 +45,8 @@ use datafusion_expr::{ use indexmap::IndexMap; use sqlparser::ast::{ - visit_expressions_mut, Distinct, Expr as SQLExpr, GroupByExpr, NamedWindowExpr, - OrderBy, SelectItemQualifiedWildcardKind, WildcardAdditionalOptions, WindowType, + Distinct, Expr as SQLExpr, GroupByExpr, NamedWindowExpr, OrderBy, + SelectItemQualifiedWildcardKind, WildcardAdditionalOptions, WindowType, }; use sqlparser::ast::{NamedWindowDefinition, Select, SelectItem, TableWithJoins}; @@ -85,7 +84,7 @@ impl SqlToRel<'_, S> { // Handle named windows before processing the projection expression check_conflicting_windows(&select.named_window)?; - self.match_window_definitions(&mut select.projection, &select.named_window)?; + match_window_definitions(&mut select.projection, &select.named_window)?; // Process the SELECT expressions let select_exprs = self.prepare_select_exprs( &base_plan, @@ -759,11 +758,11 @@ impl SqlToRel<'_, S> { /// # Arguments /// /// * `input` - The input plan that will be aggregated. The grouping, aggregate, and - /// "having" expressions must all be resolvable from this plan. + /// "having" expressions must all be resolvable from this plan. /// * `select_exprs` - The projection expressions from the SELECT clause. /// * `having_expr_opt` - Optional HAVING clause. /// * `group_by_exprs` - Grouping expressions from the GROUP BY clause. These can be column - /// references or more complex expressions. + /// references or more complex expressions. /// * `aggr_exprs` - Aggregate expressions, such as `SUM(a)` or `COUNT(1)`. /// /// # Return @@ -772,9 +771,9 @@ impl SqlToRel<'_, S> { /// /// * `plan` - A [LogicalPlan::Aggregate] plan for the newly created aggregate. /// * `select_exprs_post_aggr` - The projection expressions rewritten to reference columns from - /// the aggregate + /// the aggregate /// * `having_expr_post_aggr` - The "having" expression rewritten to reference a column from - /// the aggregate + /// the aggregate fn aggregate( &self, input: &LogicalPlan, @@ -868,61 +867,6 @@ impl SqlToRel<'_, S> { Ok((plan, select_exprs_post_aggr, having_expr_post_aggr)) } - - // If the projection is done over a named window, that window - // name must be defined. Otherwise, it gives an error. - fn match_window_definitions( - &self, - projection: &mut [SelectItem], - named_windows: &[NamedWindowDefinition], - ) -> Result<()> { - let named_windows: Vec<(&NamedWindowDefinition, String)> = named_windows - .iter() - .map(|w| (w, self.ident_normalizer.normalize(w.0.clone()))) - .collect(); - for proj in projection.iter_mut() { - if let SelectItem::ExprWithAlias { expr, alias: _ } - | SelectItem::UnnamedExpr(expr) = proj - { - let mut err = None; - visit_expressions_mut(expr, |expr| { - if let SQLExpr::Function(f) = expr { - if let Some(WindowType::NamedWindow(ident)) = &f.over { - let normalized_ident = - self.ident_normalizer.normalize(ident.clone()); - for ( - NamedWindowDefinition(_, window_expr), - normalized_window_ident, - ) in named_windows.iter() - { - if normalized_ident.eq(normalized_window_ident) { - f.over = Some(match window_expr { - NamedWindowExpr::NamedWindow(ident) => { - WindowType::NamedWindow(ident.clone()) - } - NamedWindowExpr::WindowSpec(spec) => { - WindowType::WindowSpec(spec.clone()) - } - }) - } - } - // All named windows must be defined with a WindowSpec. - if let Some(WindowType::NamedWindow(ident)) = &f.over { - err = - Some(plan_err!("The window {ident} is not defined!")); - return ControlFlow::Break(()); - } - } - } - ControlFlow::Continue(()) - }); - if let Some(err) = err { - return err; - } - } - } - Ok(()) - } } // If there are any multiple-defined windows, we raise an error. @@ -939,3 +883,39 @@ fn check_conflicting_windows(window_defs: &[NamedWindowDefinition]) -> Result<() } Ok(()) } + +// If the projection is done over a named window, that window +// name must be defined. Otherwise, it gives an error. +fn match_window_definitions( + projection: &mut [SelectItem], + named_windows: &[NamedWindowDefinition], +) -> Result<()> { + for proj in projection.iter_mut() { + if let SelectItem::ExprWithAlias { + expr: SQLExpr::Function(f), + alias: _, + } + | SelectItem::UnnamedExpr(SQLExpr::Function(f)) = proj + { + for NamedWindowDefinition(window_ident, window_expr) in named_windows.iter() { + if let Some(WindowType::NamedWindow(ident)) = &f.over { + if ident.eq(window_ident) { + f.over = Some(match window_expr { + NamedWindowExpr::NamedWindow(ident) => { + WindowType::NamedWindow(ident.clone()) + } + NamedWindowExpr::WindowSpec(spec) => { + WindowType::WindowSpec(spec.clone()) + } + }) + } + } + } + // All named windows must be defined with a WindowSpec. + if let Some(WindowType::NamedWindow(ident)) = &f.over { + return plan_err!("The window {ident} is not defined!"); + } + } + } + Ok(()) +} diff --git a/datafusion/sql/src/statement.rs b/datafusion/sql/src/statement.rs index 1f1c235fee6f4..fc6cb0d32feff 100644 --- a/datafusion/sql/src/statement.rs +++ b/datafusion/sql/src/statement.rs @@ -1034,7 +1034,7 @@ impl SqlToRel<'_, S> { TransactionMode::AccessMode(_) => None, TransactionMode::IsolationLevel(level) => Some(level), }) - .next_back() + .last() .copied() .unwrap_or(ast::TransactionIsolationLevel::Serializable); let access_mode: ast::TransactionAccessMode = modes @@ -1043,7 +1043,7 @@ impl SqlToRel<'_, S> { TransactionMode::AccessMode(mode) => Some(mode), TransactionMode::IsolationLevel(_) => None, }) - .next_back() + .last() .copied() .unwrap_or(ast::TransactionAccessMode::ReadWrite); let isolation_level = match isolation_level { @@ -1340,7 +1340,11 @@ impl SqlToRel<'_, S> { let options_map = self.parse_options_map(statement.options, true)?; let maybe_file_type = if let Some(stored_as) = &statement.stored_as { - self.context_provider.get_file_type(stored_as).ok() + if let Ok(ext_file_type) = self.context_provider.get_file_type(stored_as) { + Some(ext_file_type) + } else { + None + } } else { None }; @@ -1543,7 +1547,7 @@ impl SqlToRel<'_, S> { } /// Convert each [TableConstraint] to corresponding [Constraint] - pub fn new_constraint_from_table_constraints( + fn new_constraint_from_table_constraints( &self, constraints: &[TableConstraint], df_schema: &DFSchemaRef, diff --git a/datafusion/sql/src/unparser/ast.rs b/datafusion/sql/src/unparser/ast.rs index 117971af762a1..6fcc203637cc3 100644 --- a/datafusion/sql/src/unparser/ast.rs +++ b/datafusion/sql/src/unparser/ast.rs @@ -32,8 +32,6 @@ pub struct QueryBuilder { fetch: Option, locks: Vec, for_clause: Option, - // If true, we need to unparse LogicalPlan::Union as a SQL `UNION` rather than a `UNION ALL`. - distinct_union: bool, } #[allow(dead_code)] @@ -77,13 +75,6 @@ impl QueryBuilder { self.for_clause = value; self } - pub fn distinct_union(&mut self) -> &mut Self { - self.distinct_union = true; - self - } - pub fn is_distinct_union(&self) -> bool { - self.distinct_union - } pub fn build(&self) -> Result { let order_by = self .order_by_kind @@ -121,7 +112,6 @@ impl QueryBuilder { fetch: Default::default(), locks: Default::default(), for_clause: Default::default(), - distinct_union: false, } } } @@ -165,11 +155,6 @@ impl SelectBuilder { self.projection = value; self } - pub fn pop_projections(&mut self) -> Vec { - let ret = self.projection.clone(); - self.projection.clear(); - ret - } pub fn already_projected(&self) -> bool { !self.projection.is_empty() } diff --git a/datafusion/sql/src/unparser/dialect.rs b/datafusion/sql/src/unparser/dialect.rs index a7bde967f2fa4..05914b98f55f0 100644 --- a/datafusion/sql/src/unparser/dialect.rs +++ b/datafusion/sql/src/unparser/dialect.rs @@ -17,10 +17,7 @@ use std::{collections::HashMap, sync::Arc}; -use super::{ - utils::character_length_to_sql, utils::date_part_to_sql, - utils::sqlite_date_trunc_to_sql, utils::sqlite_from_unixtime_to_sql, Unparser, -}; +use super::{utils::character_length_to_sql, utils::date_part_to_sql, Unparser}; use arrow::datatypes::TimeUnit; use datafusion_common::Result; use datafusion_expr::Expr; @@ -493,8 +490,6 @@ impl Dialect for SqliteDialect { "character_length" => { character_length_to_sql(unparser, self.character_length_style(), args) } - "from_unixtime" => sqlite_from_unixtime_to_sql(unparser, args), - "date_trunc" => sqlite_date_trunc_to_sql(unparser, args), _ => Ok(None), } } diff --git a/datafusion/sql/src/unparser/expr.rs b/datafusion/sql/src/unparser/expr.rs index bf3e7880bea8f..064adde55bdfd 100644 --- a/datafusion/sql/src/unparser/expr.rs +++ b/datafusion/sql/src/unparser/expr.rs @@ -293,7 +293,6 @@ impl Unparser<'_> { distinct, args, filter, - order_by, .. } = &agg.params; @@ -302,16 +301,6 @@ impl Unparser<'_> { Some(filter) => Some(Box::new(self.expr_to_sql_inner(filter)?)), None => None, }; - let within_group = if agg.func.is_ordered_set_aggregate() { - order_by - .as_ref() - .unwrap_or(&Vec::new()) - .iter() - .map(|sort_expr| self.sort_to_sql(sort_expr)) - .collect::>>()? - } else { - Vec::new() - }; Ok(ast::Expr::Function(Function { name: ObjectName::from(vec![Ident { value: func_name.to_string(), @@ -327,7 +316,7 @@ impl Unparser<'_> { filter, null_treatment: None, over: None, - within_group, + within_group: vec![], parameters: ast::FunctionArguments::None, uses_odbc_syntax: false, })) @@ -1700,7 +1689,6 @@ mod tests { use std::ops::{Add, Sub}; use std::{any::Any, sync::Arc, vec}; - use crate::unparser::dialect::SqliteDialect; use arrow::array::{LargeListArray, ListArray}; use arrow::datatypes::{DataType::Int8, Field, Int32Type, Schema, TimeUnit}; use ast::ObjectName; @@ -1713,7 +1701,6 @@ mod tests { ScalarUDFImpl, Signature, Volatility, WindowFrame, WindowFunctionDefinition, }; use datafusion_expr::{interval_month_day_nano_lit, ExprFunctionExt}; - use datafusion_functions::datetime::from_unixtime::FromUnixtimeFunc; use datafusion_functions::expr_fn::{get_field, named_struct}; use datafusion_functions_aggregate::count::count_udaf; use datafusion_functions_aggregate::expr_fn::sum; @@ -1725,7 +1712,7 @@ mod tests { use crate::unparser::dialect::{ CharacterLengthStyle, CustomDialect, CustomDialectBuilder, DateFieldExtractStyle, - DefaultDialect, Dialect, DuckDBDialect, PostgreSqlDialect, ScalarFnToSqlHandler, + Dialect, DuckDBDialect, PostgreSqlDialect, ScalarFnToSqlHandler, }; use super::*; @@ -2884,115 +2871,6 @@ mod tests { Ok(()) } - #[test] - fn test_from_unixtime() -> Result<()> { - let default_dialect: Arc = Arc::new(DefaultDialect {}); - let sqlite_dialect: Arc = Arc::new(SqliteDialect {}); - - for (dialect, expected) in [ - (default_dialect, "from_unixtime(date_col)"), - (sqlite_dialect, "datetime(`date_col`, 'unixepoch')"), - ] { - let unparser = Unparser::new(dialect.as_ref()); - let expr = Expr::ScalarFunction(ScalarFunction { - func: Arc::new(ScalarUDF::from(FromUnixtimeFunc::new())), - args: vec![col("date_col")], - }); - - let ast = unparser.expr_to_sql(&expr)?; - - let actual = ast.to_string(); - let expected = expected.to_string(); - - assert_eq!(actual, expected); - } - Ok(()) - } - - #[test] - fn test_date_trunc() -> Result<()> { - let default_dialect: Arc = Arc::new(DefaultDialect {}); - let sqlite_dialect: Arc = Arc::new(SqliteDialect {}); - - for (dialect, precision, expected) in [ - ( - Arc::clone(&default_dialect), - "YEAR", - "date_trunc('YEAR', date_col)", - ), - ( - Arc::clone(&sqlite_dialect), - "YEAR", - "strftime('%Y', `date_col`)", - ), - ( - Arc::clone(&default_dialect), - "MONTH", - "date_trunc('MONTH', date_col)", - ), - ( - Arc::clone(&sqlite_dialect), - "MONTH", - "strftime('%Y-%m', `date_col`)", - ), - ( - Arc::clone(&default_dialect), - "DAY", - "date_trunc('DAY', date_col)", - ), - ( - Arc::clone(&sqlite_dialect), - "DAY", - "strftime('%Y-%m-%d', `date_col`)", - ), - ( - Arc::clone(&default_dialect), - "HOUR", - "date_trunc('HOUR', date_col)", - ), - ( - Arc::clone(&sqlite_dialect), - "HOUR", - "strftime('%Y-%m-%d %H', `date_col`)", - ), - ( - Arc::clone(&default_dialect), - "MINUTE", - "date_trunc('MINUTE', date_col)", - ), - ( - Arc::clone(&sqlite_dialect), - "MINUTE", - "strftime('%Y-%m-%d %H:%M', `date_col`)", - ), - (default_dialect, "SECOND", "date_trunc('SECOND', date_col)"), - ( - sqlite_dialect, - "SECOND", - "strftime('%Y-%m-%d %H:%M:%S', `date_col`)", - ), - ] { - let unparser = Unparser::new(dialect.as_ref()); - let expr = Expr::ScalarFunction(ScalarFunction { - func: Arc::new(ScalarUDF::from( - datafusion_functions::datetime::date_trunc::DateTruncFunc::new(), - )), - args: vec![ - Expr::Literal(ScalarValue::Utf8(Some(precision.to_string()))), - col("date_col"), - ], - }); - - let ast = unparser.expr_to_sql(&expr)?; - - let actual = ast.to_string(); - let expected = expected.to_string(); - - assert_eq!(actual, expected); - } - Ok(()) - } - #[test] fn test_dictionary_to_sql() -> Result<()> { let dialect = CustomDialectBuilder::new().build(); diff --git a/datafusion/sql/src/unparser/mod.rs b/datafusion/sql/src/unparser/mod.rs index 05b472dc92a93..f90efd103b0f5 100644 --- a/datafusion/sql/src/unparser/mod.rs +++ b/datafusion/sql/src/unparser/mod.rs @@ -118,9 +118,9 @@ impl<'a> Unparser<'a> { /// The child unparsers are called iteratively. /// There are two methods in [`Unparser`] will be called: /// - `extension_to_statement`: This method is called when the custom logical node is a custom statement. - /// If multiple child unparsers return a non-None value, the last unparsing result will be returned. + /// If multiple child unparsers return a non-None value, the last unparsing result will be returned. /// - `extension_to_sql`: This method is called when the custom logical node is part of a statement. - /// If multiple child unparsers are registered for the same custom logical node, all of them will be called in order. + /// If multiple child unparsers are registered for the same custom logical node, all of them will be called in order. pub fn with_extension_unparsers( mut self, extension_unparsers: Vec>, diff --git a/datafusion/sql/src/unparser/plan.rs b/datafusion/sql/src/unparser/plan.rs index b849ca45d299c..a6d89638ff41d 100644 --- a/datafusion/sql/src/unparser/plan.rs +++ b/datafusion/sql/src/unparser/plan.rs @@ -545,23 +545,6 @@ impl Unparser<'_> { false, ); } - - // If this distinct is the parent of a Union and we're in a query context, - // then we need to unparse as a `UNION` rather than a `UNION ALL`. - if let Distinct::All(input) = distinct { - if matches!(input.as_ref(), LogicalPlan::Union(_)) { - if let Some(query_mut) = query.as_mut() { - query_mut.distinct_union(); - return self.select_to_sql_recursively( - input.as_ref(), - query, - select, - relation, - ); - } - } - } - let (select_distinct, input) = match distinct { Distinct::All(input) => (ast::Distinct::Distinct, input.as_ref()), Distinct::On(on) => { @@ -599,10 +582,6 @@ impl Unparser<'_> { } _ => (&join.left, &join.right), }; - // If there's an outer projection plan, it will already set up the projection. - // In that case, we don't need to worry about setting up the projection here. - // The outer projection plan will handle projecting the correct columns. - let already_projected = select.already_projected(); let left_plan = match try_transform_to_simple_table_scan_with_filters(left_plan)? { @@ -620,13 +599,6 @@ impl Unparser<'_> { relation, )?; - let left_projection: Option> = if !already_projected - { - Some(select.pop_projections()) - } else { - None - }; - let right_plan = match try_transform_to_simple_table_scan_with_filters(right_plan)? { Some((plan, filters)) => { @@ -685,13 +657,6 @@ impl Unparser<'_> { &mut right_relation, )?; - let right_projection: Option> = if !already_projected - { - Some(select.pop_projections()) - } else { - None - }; - match join.join_type { JoinType::LeftSemi | JoinType::LeftAnti @@ -737,9 +702,6 @@ impl Unparser<'_> { } else { select.selection(Some(exists_expr)); } - if let Some(projection) = left_projection { - select.projection(projection); - } } JoinType::Inner | JoinType::Left @@ -757,21 +719,6 @@ impl Unparser<'_> { let mut from = select.pop_from().unwrap(); from.push_join(ast_join); select.push_from(from); - if !already_projected { - let Some(left_projection) = left_projection else { - return internal_err!("Left projection is missing"); - }; - - let Some(right_projection) = right_projection else { - return internal_err!("Right projection is missing"); - }; - - let projection = left_projection - .into_iter() - .chain(right_projection.into_iter()) - .collect(); - select.projection(projection); - } } }; @@ -846,15 +793,6 @@ impl Unparser<'_> { return internal_err!("UNION operator requires at least 2 inputs"); } - let set_quantifier = - if query.as_ref().is_some_and(|q| q.is_distinct_union()) { - // Setting the SetQuantifier to None will unparse as a `UNION` - // rather than a `UNION ALL`. - ast::SetQuantifier::None - } else { - ast::SetQuantifier::All - }; - // Build the union expression tree bottom-up by reversing the order // note that we are also swapping left and right inputs because of the rev let union_expr = input_exprs @@ -862,7 +800,7 @@ impl Unparser<'_> { .rev() .reduce(|a, b| SetExpr::SetOperation { op: ast::SetOperator::Union, - set_quantifier, + set_quantifier: ast::SetQuantifier::All, left: Box::new(b), right: Box::new(a), }) @@ -962,9 +900,9 @@ impl Unparser<'_> { /// Try to find the placeholder column name generated by `RecursiveUnnestRewriter`. /// /// - If the column is a placeholder column match the pattern `Expr::Alias(Expr::Column("__unnest_placeholder(...)"))`, - /// it means it is a scalar column, return [UnnestInputType::Scalar]. + /// it means it is a scalar column, return [UnnestInputType::Scalar]. /// - If the column is a placeholder column match the pattern `Expr::Alias(Expr::Column("__unnest_placeholder(outer_ref(...)))")`, - /// it means it is an outer reference column, return [UnnestInputType::OuterReference]. + /// it means it is an outer reference column, return [UnnestInputType::OuterReference]. /// - If the column is not a placeholder column, return [None]. /// /// `outer_ref` is the display result of [Expr::OuterReferenceColumn] diff --git a/datafusion/sql/src/unparser/utils.rs b/datafusion/sql/src/unparser/utils.rs index 37f0a77972007..75038ccc43145 100644 --- a/datafusion/sql/src/unparser/utils.rs +++ b/datafusion/sql/src/unparser/utils.rs @@ -385,7 +385,7 @@ pub(crate) fn try_transform_to_simple_table_scan_with_filters( let mut builder = LogicalPlanBuilder::scan( table_scan.table_name.clone(), Arc::clone(&table_scan.source), - table_scan.projection.clone(), + None, )?; if let Some(alias) = table_alias.take() { @@ -500,72 +500,3 @@ pub(crate) fn character_length_to_sql( character_length_args, )?)) } - -/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc. -/// This remaps `from_unixtime` to `datetime(expr, 'unixepoch')`, expecting the input to be in seconds. -/// It supports no other arguments, so if any are supplied it will return an error. -/// -/// # Errors -/// -/// - If the number of arguments is not 1 - the column or expression to convert. -/// - If the scalar function cannot be converted to SQL. -pub(crate) fn sqlite_from_unixtime_to_sql( - unparser: &Unparser, - from_unixtime_args: &[Expr], -) -> Result> { - if from_unixtime_args.len() != 1 { - return internal_err!( - "from_unixtime for SQLite expects 1 argument, found {}", - from_unixtime_args.len() - ); - } - - Ok(Some(unparser.scalar_function_to_sql( - "datetime", - &[ - from_unixtime_args[0].clone(), - Expr::Literal(ScalarValue::Utf8(Some("unixepoch".to_string()))), - ], - )?)) -} - -/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc. -/// This uses the `strftime` function to format the timestamp as a string depending on the truncation unit. -/// -/// # Errors -/// -/// - If the number of arguments is not 2 - truncation unit and the column or expression to convert. -/// - If the scalar function cannot be converted to SQL. -pub(crate) fn sqlite_date_trunc_to_sql( - unparser: &Unparser, - date_trunc_args: &[Expr], -) -> Result> { - if date_trunc_args.len() != 2 { - return internal_err!( - "date_trunc for SQLite expects 2 arguments, found {}", - date_trunc_args.len() - ); - } - - if let Expr::Literal(ScalarValue::Utf8(Some(unit))) = &date_trunc_args[0] { - let format = match unit.to_lowercase().as_str() { - "year" => "%Y", - "month" => "%Y-%m", - "day" => "%Y-%m-%d", - "hour" => "%Y-%m-%d %H", - "minute" => "%Y-%m-%d %H:%M", - "second" => "%Y-%m-%d %H:%M:%S", - _ => return Ok(None), - }; - - return Ok(Some(unparser.scalar_function_to_sql( - "strftime", - &[ - Expr::Literal(ScalarValue::Utf8(Some(format.to_string()))), - date_trunc_args[1].clone(), - ], - )?)); - } - - Ok(None) -} diff --git a/datafusion/sql/tests/cases/diagnostic.rs b/datafusion/sql/tests/cases/diagnostic.rs index e2e4ada9036b7..ebb21e9cdef53 100644 --- a/datafusion/sql/tests/cases/diagnostic.rs +++ b/datafusion/sql/tests/cases/diagnostic.rs @@ -16,21 +16,19 @@ // under the License. use datafusion_functions::string; -use insta::assert_snapshot; use std::{collections::HashMap, sync::Arc}; use datafusion_common::{Diagnostic, Location, Result, Span}; -use datafusion_sql::{ - parser::{DFParser, DFParserBuilder}, - planner::{ParserOptions, SqlToRel}, -}; +use datafusion_sql::planner::{ParserOptions, SqlToRel}; use regex::Regex; +use sqlparser::{dialect::GenericDialect, parser::Parser}; use crate::{MockContextProvider, MockSessionState}; fn do_query(sql: &'static str) -> Diagnostic { - let statement = DFParserBuilder::new(sql) - .build() + let dialect = GenericDialect {}; + let statement = Parser::new(&dialect) + .try_with_sql(sql) .expect("unable to create parser") .parse_statement() .expect("unable to parse query"); @@ -42,7 +40,7 @@ fn do_query(sql: &'static str) -> Diagnostic { .with_scalar_function(Arc::new(string::concat().as_ref().clone())); let context = MockContextProvider { state }; let sql_to_rel = SqlToRel::new_with_options(&context, options); - match sql_to_rel.statement_to_plan(statement) { + match sql_to_rel.sql_statement_to_plan(statement) { Ok(_) => panic!("expected error"), Err(err) => match err.diagnostic() { Some(diag) => diag.clone(), @@ -138,7 +136,7 @@ fn test_table_not_found() -> Result<()> { let query = "SELECT * FROM /*a*/personx/*a*/"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"table 'personx' not found"); + assert_eq!(diag.message, "table 'personx' not found"); assert_eq!(diag.span, Some(spans["a"])); Ok(()) } @@ -148,7 +146,7 @@ fn test_unqualified_column_not_found() -> Result<()> { let query = "SELECT /*a*/first_namex/*a*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"column 'first_namex' not found"); + assert_eq!(diag.message, "column 'first_namex' not found"); assert_eq!(diag.span, Some(spans["a"])); Ok(()) } @@ -158,7 +156,7 @@ fn test_qualified_column_not_found() -> Result<()> { let query = "SELECT /*a*/person.first_namex/*a*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"column 'first_namex' not found in 'person'"); + assert_eq!(diag.message, "column 'first_namex' not found in 'person'"); assert_eq!(diag.span, Some(spans["a"])); Ok(()) } @@ -168,11 +166,14 @@ fn test_union_wrong_number_of_columns() -> Result<()> { let query = "/*whole+left*/SELECT first_name FROM person/*left*/ UNION ALL /*right*/SELECT first_name, last_name FROM person/*right+whole*/"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"UNION queries have different number of columns"); + assert_eq!( + diag.message, + "UNION queries have different number of columns" + ); assert_eq!(diag.span, Some(spans["whole"])); - assert_snapshot!(diag.notes[0].message, @"this side has 1 fields"); + assert_eq!(diag.notes[0].message, "this side has 1 fields"); assert_eq!(diag.notes[0].span, Some(spans["left"])); - assert_snapshot!(diag.notes[1].message, @"this side has 2 fields"); + assert_eq!(diag.notes[1].message, "this side has 2 fields"); assert_eq!(diag.notes[1].span, Some(spans["right"])); Ok(()) } @@ -182,9 +183,15 @@ fn test_missing_non_aggregate_in_group_by() -> Result<()> { let query = "SELECT id, /*a*/first_name/*a*/ FROM person GROUP BY id"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"'person.first_name' must appear in GROUP BY clause because it's not an aggregate expression"); + assert_eq!( + diag.message, + "'person.first_name' must appear in GROUP BY clause because it's not an aggregate expression" + ); assert_eq!(diag.span, Some(spans["a"])); - assert_snapshot!(diag.helps[0].message, @"Either add 'person.first_name' to GROUP BY clause, or use an aggregare function like ANY_VALUE(person.first_name)"); + assert_eq!( + diag.helps[0].message, + "Either add 'person.first_name' to GROUP BY clause, or use an aggregare function like ANY_VALUE(person.first_name)" + ); Ok(()) } @@ -193,10 +200,10 @@ fn test_ambiguous_reference() -> Result<()> { let query = "SELECT /*a*/first_name/*a*/ FROM person a, person b"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"column 'first_name' is ambiguous"); + assert_eq!(diag.message, "column 'first_name' is ambiguous"); assert_eq!(diag.span, Some(spans["a"])); - assert_snapshot!(diag.notes[0].message, @"possible column a.first_name"); - assert_snapshot!(diag.notes[1].message, @"possible column b.first_name"); + assert_eq!(diag.notes[0].message, "possible column a.first_name"); + assert_eq!(diag.notes[1].message, "possible column b.first_name"); Ok(()) } @@ -206,11 +213,11 @@ fn test_incompatible_types_binary_arithmetic() -> Result<()> { "SELECT /*whole+left*/id/*left*/ + /*right*/first_name/*right+whole*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"expressions have incompatible types"); + assert_eq!(diag.message, "expressions have incompatible types"); assert_eq!(diag.span, Some(spans["whole"])); - assert_snapshot!(diag.notes[0].message, @"has type UInt32"); + assert_eq!(diag.notes[0].message, "has type UInt32"); assert_eq!(diag.notes[0].span, Some(spans["left"])); - assert_snapshot!(diag.notes[1].message, @"has type Utf8"); + assert_eq!(diag.notes[1].message, "has type Utf8"); assert_eq!(diag.notes[1].span, Some(spans["right"])); Ok(()) } @@ -220,7 +227,7 @@ fn test_field_not_found_suggestion() -> Result<()> { let query = "SELECT /*whole*/first_na/*whole*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"column 'first_na' not found"); + assert_eq!(diag.message, "column 'first_na' not found"); assert_eq!(diag.span, Some(spans["whole"])); assert_eq!(diag.notes.len(), 1); @@ -236,7 +243,7 @@ fn test_field_not_found_suggestion() -> Result<()> { }) .collect(); suggested_fields.sort(); - assert_snapshot!(suggested_fields[0], @"person.first_name"); + assert_eq!(suggested_fields[0], "person.first_name"); Ok(()) } @@ -246,7 +253,7 @@ fn test_ambiguous_column_suggestion() -> Result<()> { let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"column 'id' is ambiguous"); + assert_eq!(diag.message, "column 'id' is ambiguous"); assert_eq!(diag.span, Some(spans["whole"])); assert_eq!(diag.notes.len(), 2); @@ -274,8 +281,8 @@ fn test_invalid_function() -> Result<()> { let query = "SELECT /*whole*/concat_not_exist/*whole*/()"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"Invalid function 'concat_not_exist'"); - assert_snapshot!(diag.notes[0].message, @"Possible function 'concat'"); + assert_eq!(diag.message, "Invalid function 'concat_not_exist'"); + assert_eq!(diag.notes[0].message, "Possible function 'concat'"); assert_eq!(diag.span, Some(spans["whole"])); Ok(()) } @@ -285,7 +292,10 @@ fn test_scalar_subquery_multiple_columns() -> Result<(), Box Result<(), Box> let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"Too many columns! The subquery should only return one column"); + assert_eq!( + diag.message, + "Too many columns! The subquery should only return one column" + ); let expected_span = Some(Span { start: spans["id"].start, @@ -347,10 +360,16 @@ fn test_unary_op_plus_with_column() -> Result<()> { let query = "SELECT +/*whole*/first_name/*whole*/ FROM person"; let spans = get_spans(query); let diag = do_query(query); - assert_snapshot!(diag.message, @"+ cannot be used with Utf8"); + assert_eq!(diag.message, "+ cannot be used with Utf8"); assert_eq!(diag.span, Some(spans["whole"])); - assert_snapshot!(diag.notes[0].message, @"+ can only be used with numbers, intervals, and timestamps"); - assert_snapshot!(diag.helps[0].message, @"perhaps you need to cast person.first_name"); + assert_eq!( + diag.notes[0].message, + "+ can only be used with numbers, intervals, and timestamps" + ); + assert_eq!( + diag.helps[0].message, + "perhaps you need to cast person.first_name" + ); Ok(()) } @@ -360,32 +379,16 @@ fn test_unary_op_plus_with_non_column() -> Result<()> { let query = "SELECT +'a'"; let diag = do_query(query); assert_eq!(diag.message, "+ cannot be used with Utf8"); - assert_snapshot!(diag.notes[0].message, @"+ can only be used with numbers, intervals, and timestamps"); + assert_eq!( + diag.notes[0].message, + "+ can only be used with numbers, intervals, and timestamps" + ); assert_eq!(diag.notes[0].span, None); - assert_snapshot!(diag.helps[0].message, @r#"perhaps you need to cast Utf8("a")"#); + assert_eq!( + diag.helps[0].message, + "perhaps you need to cast Utf8(\"a\")" + ); assert_eq!(diag.helps[0].span, None); assert_eq!(diag.span, None); Ok(()) } - -#[test] -fn test_syntax_error() -> Result<()> { - // create a table with a column of type varchar - let query = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV PARTITIONED BY (c1, p1 /*int*/int/*int*/) LOCATION 'foo.csv'"; - let spans = get_spans(query); - match DFParser::parse_sql(query) { - Ok(_) => panic!("expected error"), - Err(err) => match err.diagnostic() { - Some(diag) => { - let diag = diag.clone(); - assert_snapshot!(diag.message, @"Expected: ',' or ')' after partition definition, found: int at Line: 1, Column: 77"); - println!("{:?}", spans); - assert_eq!(diag.span, Some(spans["int"])); - Ok(()) - } - None => { - panic!("expected diagnostic") - } - }, - } -} diff --git a/datafusion/sql/tests/cases/plan_to_sql.rs b/datafusion/sql/tests/cases/plan_to_sql.rs index a458d72d282f1..b7185c2d503df 100644 --- a/datafusion/sql/tests/cases/plan_to_sql.rs +++ b/datafusion/sql/tests/cases/plan_to_sql.rs @@ -17,8 +17,7 @@ use arrow::datatypes::{DataType, Field, Schema}; use datafusion_common::{ - assert_contains, Column, DFSchema, DFSchemaRef, DataFusionError, Result, - TableReference, + assert_contains, Column, DFSchema, DFSchemaRef, Result, TableReference, }; use datafusion_expr::test::function_stub::{ count_udaf, max_udaf, min_udaf, sum, sum_udaf, @@ -39,7 +38,6 @@ use datafusion_sql::unparser::dialect::{ PostgreSqlDialect as UnparserPostgreSqlDialect, SqliteDialect, }; use datafusion_sql::unparser::{expr_to_sql, plan_to_sql, Unparser}; -use insta::assert_snapshot; use sqlparser::ast::Statement; use std::hash::Hash; use std::ops::Add; @@ -64,44 +62,46 @@ use sqlparser::dialect::{Dialect, GenericDialect, MySqlDialect}; use sqlparser::parser::Parser; #[test] -fn test_roundtrip_expr_1() { - let expr = roundtrip_expr(TableReference::bare("person"), "age > 35").unwrap(); - assert_snapshot!(expr, @r#"(age > 35)"#); -} - -#[test] -fn test_roundtrip_expr_2() { - let expr = roundtrip_expr(TableReference::bare("person"), "id = '10'").unwrap(); - assert_snapshot!(expr, @r#"(id = '10')"#); -} - -#[test] -fn test_roundtrip_expr_3() { - let expr = - roundtrip_expr(TableReference::bare("person"), "CAST(id AS VARCHAR)").unwrap(); - assert_snapshot!(expr, @r#"CAST(id AS VARCHAR)"#); -} +fn roundtrip_expr() { + let tests: Vec<(TableReference, &str, &str)> = vec![ + (TableReference::bare("person"), "age > 35", r#"(age > 35)"#), + ( + TableReference::bare("person"), + "id = '10'", + r#"(id = '10')"#, + ), + ( + TableReference::bare("person"), + "CAST(id AS VARCHAR)", + r#"CAST(id AS VARCHAR)"#, + ), + ( + TableReference::bare("person"), + "sum((age * 2))", + r#"sum((age * 2))"#, + ), + ]; -#[test] -fn test_roundtrip_expr_4() { - let expr = roundtrip_expr(TableReference::bare("person"), "sum((age * 2))").unwrap(); - assert_snapshot!(expr, @r#"sum((age * 2))"#); -} + let roundtrip = |table, sql: &str| -> Result { + let dialect = GenericDialect {}; + let sql_expr = Parser::new(&dialect).try_with_sql(sql)?.parse_expr()?; + let state = MockSessionState::default().with_aggregate_function(sum_udaf()); + let context = MockContextProvider { state }; + let schema = context.get_table_source(table)?.schema(); + let df_schema = DFSchema::try_from(schema.as_ref().clone())?; + let sql_to_rel = SqlToRel::new(&context); + let expr = + sql_to_rel.sql_to_expr(sql_expr, &df_schema, &mut PlannerContext::new())?; -fn roundtrip_expr(table: TableReference, sql: &str) -> Result { - let dialect = GenericDialect {}; - let sql_expr = Parser::new(&dialect).try_with_sql(sql)?.parse_expr()?; - let state = MockSessionState::default().with_aggregate_function(sum_udaf()); - let context = MockContextProvider { state }; - let schema = context.get_table_source(table)?.schema(); - let df_schema = DFSchema::try_from(schema.as_ref().clone())?; - let sql_to_rel = SqlToRel::new(&context); - let expr = - sql_to_rel.sql_to_expr(sql_expr, &df_schema, &mut PlannerContext::new())?; + let ast = expr_to_sql(&expr)?; - let ast = expr_to_sql(&expr)?; + Ok(ast.to_string()) + }; - Ok(ast.to_string()) + for (table, query, expected) in tests { + let actual = roundtrip(table, query).unwrap(); + assert_eq!(actual, expected); + } } #[test] @@ -170,13 +170,6 @@ fn roundtrip_statement() -> Result<()> { UNION ALL SELECT j3_string AS col1, j3_id AS id FROM j3 ) AS subquery GROUP BY col1, id ORDER BY col1 ASC, id ASC"#, - r#"SELECT col1, id FROM ( - SELECT j1_string AS col1, j1_id AS id FROM j1 - UNION - SELECT j2_string AS col1, j2_id AS id FROM j2 - UNION - SELECT j3_string AS col1, j3_id AS id FROM j3 - ) AS subquery ORDER BY col1 ASC, id ASC"#, "SELECT id, count(*) over (PARTITION BY first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), last_name, sum(id) over (PARTITION BY first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING), first_name from person", @@ -243,6 +236,10 @@ fn roundtrip_statement() -> Result<()> { let roundtrip_statement = plan_to_sql(&plan)?; + let actual = &roundtrip_statement.to_string(); + println!("roundtrip sql: {actual}"); + println!("plan {}", plan.display_indent()); + let plan_roundtrip = sql_to_rel .sql_statement_to_plan(roundtrip_statement.clone()) .unwrap(); @@ -278,185 +275,109 @@ fn roundtrip_crossjoin() -> Result<()> { let plan_roundtrip = sql_to_rel .sql_statement_to_plan(roundtrip_statement) .unwrap(); - assert_snapshot!( - plan_roundtrip, - @r" - Projection: j1.j1_id, j2.j2_string - Cross Join: - TableScan: j1 - TableScan: j2 - " - ); - - Ok(()) -} - -#[macro_export] -macro_rules! roundtrip_statement_with_dialect_helper { - ( - sql: $sql:expr, - parser_dialect: $parser_dialect:expr, - unparser_dialect: $unparser_dialect:expr, - expected: @ $expected:literal $(,)? - ) => {{ - let statement = Parser::new(&$parser_dialect) - .try_with_sql($sql)? - .parse_statement()?; - - let state = MockSessionState::default() - .with_aggregate_function(max_udaf()) - .with_aggregate_function(min_udaf()) - .with_expr_planner(Arc::new(CoreFunctionPlanner::default())) - .with_expr_planner(Arc::new(NestedFunctionPlanner)); - - let context = MockContextProvider { state }; - let sql_to_rel = SqlToRel::new(&context); - let plan = sql_to_rel - .sql_statement_to_plan(statement) - .unwrap_or_else(|e| panic!("Failed to parse sql: {}\n{e}", $sql)); - - let unparser = Unparser::new(&$unparser_dialect); - let roundtrip_statement = unparser.plan_to_sql(&plan)?; - - let actual = &roundtrip_statement.to_string(); - insta::assert_snapshot!(actual, @ $expected); - }}; -} - -#[test] -fn roundtrip_statement_with_dialect_1() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", - parser_dialect: MySqlDialect {}, - unparser_dialect: UnparserMySqlDialect {}, - // top projection sort gets derived into a subquery - // for MySQL, this subquery needs an alias - expected: @"SELECT `j1_min` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, min(`ta`.`j1_id`) FROM `j1` AS `ta` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_2() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - // top projection sort still gets derived into a subquery in default dialect - // except for the default dialect, the subquery is left non-aliased - expected: @"SELECT j1_min FROM (SELECT min(ta.j1_id) AS j1_min, min(ta.j1_id) FROM j1 AS ta ORDER BY min(ta.j1_id) ASC NULLS LAST) LIMIT 10", - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_3() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "select min(ta.j1_id) as j1_min, max(tb.j1_max) from j1 ta, (select distinct max(ta.j1_id) as j1_max from j1 ta order by max(ta.j1_id)) tb order by min(ta.j1_id) limit 10;", - parser_dialect: MySqlDialect {}, - unparser_dialect: UnparserMySqlDialect {}, - expected: @"SELECT `j1_min`, `max(tb.j1_max)` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, max(`tb`.`j1_max`), min(`ta`.`j1_id`) FROM `j1` AS `ta` CROSS JOIN (SELECT `j1_max` FROM (SELECT DISTINCT max(`ta`.`j1_id`) AS `j1_max` FROM `j1` AS `ta`) AS `derived_distinct`) AS `tb` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", - ); - Ok(()) -} -#[test] -fn roundtrip_statement_with_dialect_4() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "select j1_id from (select 1 as j1_id);", - parser_dialect: MySqlDialect {}, - unparser_dialect: UnparserMySqlDialect {}, - expected: @"SELECT `j1_id` FROM (SELECT 1 AS `j1_id`) AS `derived_projection`", - ); - Ok(()) -} + let expected = "Projection: j1.j1_id, j2.j2_string\ + \n Cross Join: \ + \n TableScan: j1\ + \n TableScan: j2"; -#[test] -fn roundtrip_statement_with_dialect_5() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "select j1_id from (select j1_id from j1 limit 10);", - parser_dialect: MySqlDialect {}, - unparser_dialect: UnparserMySqlDialect {}, - expected: @"SELECT `j1`.`j1_id` FROM (SELECT `j1`.`j1_id` FROM `j1` LIMIT 10) AS `derived_limit`", - ); - Ok(()) -} + assert_eq!(plan_roundtrip.to_string(), expected); -#[test] -fn roundtrip_statement_with_dialect_6() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", - parser_dialect: MySqlDialect {}, - unparser_dialect: UnparserMySqlDialect {}, - expected: @"SELECT `ta`.`j1_id` FROM `j1` AS `ta` ORDER BY `ta`.`j1_id` ASC LIMIT 10", - ); Ok(()) } #[test] -fn roundtrip_statement_with_dialect_7() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT ta.j1_id FROM j1 AS ta ORDER BY ta.j1_id ASC NULLS LAST LIMIT 10"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_8() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT j1_id FROM j1 +fn roundtrip_statement_with_dialect() -> Result<()> { + struct TestStatementWithDialect { + sql: &'static str, + expected: &'static str, + parser_dialect: Box, + unparser_dialect: Box, + } + let tests: Vec = vec![ + TestStatementWithDialect { + sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", + expected: + // top projection sort gets derived into a subquery + // for MySQL, this subquery needs an alias + "SELECT `j1_min` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, min(`ta`.`j1_id`) FROM `j1` AS `ta` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", + parser_dialect: Box::new(MySqlDialect {}), + unparser_dialect: Box::new(UnparserMySqlDialect {}), + }, + TestStatementWithDialect { + sql: "select min(ta.j1_id) as j1_min from j1 ta order by min(ta.j1_id) limit 10;", + expected: + // top projection sort still gets derived into a subquery in default dialect + // except for the default dialect, the subquery is left non-aliased + "SELECT j1_min FROM (SELECT min(ta.j1_id) AS j1_min, min(ta.j1_id) FROM j1 AS ta ORDER BY min(ta.j1_id) ASC NULLS LAST) LIMIT 10", + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "select min(ta.j1_id) as j1_min, max(tb.j1_max) from j1 ta, (select distinct max(ta.j1_id) as j1_max from j1 ta order by max(ta.j1_id)) tb order by min(ta.j1_id) limit 10;", + expected: + "SELECT `j1_min`, `max(tb.j1_max)` FROM (SELECT min(`ta`.`j1_id`) AS `j1_min`, max(`tb`.`j1_max`), min(`ta`.`j1_id`) FROM `j1` AS `ta` CROSS JOIN (SELECT `j1_max` FROM (SELECT DISTINCT max(`ta`.`j1_id`) AS `j1_max` FROM `j1` AS `ta`) AS `derived_distinct`) AS `tb` ORDER BY min(`ta`.`j1_id`) ASC) AS `derived_sort` LIMIT 10", + parser_dialect: Box::new(MySqlDialect {}), + unparser_dialect: Box::new(UnparserMySqlDialect {}), + }, + TestStatementWithDialect { + sql: "select j1_id from (select 1 as j1_id);", + expected: + "SELECT `j1_id` FROM (SELECT 1 AS `j1_id`) AS `derived_projection`", + parser_dialect: Box::new(MySqlDialect {}), + unparser_dialect: Box::new(UnparserMySqlDialect {}), + }, + TestStatementWithDialect { + sql: "select j1_id from (select j1_id from j1 limit 10);", + expected: + "SELECT `j1`.`j1_id` FROM (SELECT `j1`.`j1_id` FROM `j1` LIMIT 10) AS `derived_limit`", + parser_dialect: Box::new(MySqlDialect {}), + unparser_dialect: Box::new(UnparserMySqlDialect {}), + }, + TestStatementWithDialect { + sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", + expected: + "SELECT `ta`.`j1_id` FROM `j1` AS `ta` ORDER BY `ta`.`j1_id` ASC LIMIT 10", + parser_dialect: Box::new(MySqlDialect {}), + unparser_dialect: Box::new(UnparserMySqlDialect {}), + }, + TestStatementWithDialect { + sql: "select ta.j1_id from j1 ta order by j1_id limit 10;", + expected: r#"SELECT ta.j1_id FROM j1 AS ta ORDER BY ta.j1_id ASC NULLS LAST LIMIT 10"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT j1_id FROM j1 UNION ALL SELECT tb.j2_id as j1_id FROM j2 tb ORDER BY j1_id LIMIT 10;", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT j1.j1_id FROM j1 UNION ALL SELECT tb.j2_id AS j1_id FROM j2 AS tb ORDER BY j1_id ASC NULLS LAST LIMIT 10"#, - ); - Ok(()) -} - -// Test query with derived tables that put distinct,sort,limit on the wrong level -#[test] -fn roundtrip_statement_with_dialect_9() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT j1_string from j1 order by j1_id", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT j1.j1_string FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_10() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT j1_string AS a from j1 order by j1_id", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT j1.j1_string AS a FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_11() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT j1_string from j1 join j2 on j1.j1_id = j2.j2_id order by j1_id", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT j1.j1_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id ASC NULLS LAST"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_12() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: " + expected: r#"SELECT j1.j1_id FROM j1 UNION ALL SELECT tb.j2_id AS j1_id FROM j2 AS tb ORDER BY j1_id ASC NULLS LAST LIMIT 10"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + // Test query with derived tables that put distinct,sort,limit on the wrong level + TestStatementWithDialect { + sql: "SELECT j1_string from j1 order by j1_id", + expected: r#"SELECT j1.j1_string FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT j1_string AS a from j1 order by j1_id", + expected: r#"SELECT j1.j1_string AS a FROM j1 ORDER BY j1.j1_id ASC NULLS LAST"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT j1_string from j1 join j2 on j1.j1_id = j2.j2_id order by j1_id", + expected: r#"SELECT j1.j1_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id ASC NULLS LAST"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: " SELECT j1_string, j2_string @@ -476,18 +397,13 @@ fn roundtrip_statement_with_dialect_12() -> Result<(), DataFusionError> { ) abc ORDER BY abc.j2_string", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT DISTINCT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, - ); - Ok(()) -} - -// more tests around subquery/derived table roundtrip -#[test] -fn roundtrip_statement_with_dialect_13() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT string_count FROM ( + expected: r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT DISTINCT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + // more tests around subquery/derived table roundtrip + TestStatementWithDialect { + sql: "SELECT string_count FROM ( SELECT j1_id, min(j2_string) @@ -498,17 +414,12 @@ fn roundtrip_statement_with_dialect_13() -> Result<(), DataFusionError> { j1_id ) AS agg (id, string_count) ", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT agg.string_count FROM (SELECT j1.j1_id, min(j2.j2_string) FROM j1 LEFT OUTER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id) AS agg (id, string_count)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_14() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: " + expected: r#"SELECT agg.string_count FROM (SELECT j1.j1_id, min(j2.j2_string) FROM j1 LEFT OUTER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id) AS agg (id, string_count)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: " SELECT j1_string, j2_string @@ -532,18 +443,13 @@ fn roundtrip_statement_with_dialect_14() -> Result<(), DataFusionError> { ) abc ORDER BY abc.j2_string", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id, j1.j1_string, j2.j2_string ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, - ); - Ok(()) -} - -// Test query that order by columns are not in select columns -#[test] -fn roundtrip_statement_with_dialect_15() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: " + expected: r#"SELECT abc.j1_string, abc.j2_string FROM (SELECT j1.j1_id, j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) GROUP BY j1.j1_id, j1.j1_string, j2.j2_string ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + // Test query that order by columns are not in select columns + TestStatementWithDialect { + sql: " SELECT j1_string FROM @@ -562,364 +468,221 @@ fn roundtrip_statement_with_dialect_15() -> Result<(), DataFusionError> { ) abc ORDER BY j2_string", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT abc.j1_string FROM (SELECT j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_16() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT id FROM (SELECT j1_id from j1) AS c (id)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_17() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT id FROM (SELECT j1_id as id from j1) AS c", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT c.id FROM (SELECT j1.j1_id AS id FROM j1) AS c"#, - ); - Ok(()) -} - -// Test query that has calculation in derived table with columns -#[test] -fn roundtrip_statement_with_dialect_18() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT id FROM (SELECT j1_id + 1 * 3 from j1) AS c (id)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT c.id FROM (SELECT (j1.j1_id + (1 * 3)) FROM j1) AS c (id)"#, - ); - Ok(()) -} - -// Test query that has limit/distinct/order in derived table with columns -#[test] -fn roundtrip_statement_with_dialect_19() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT id FROM (SELECT distinct (j1_id + 1 * 3) FROM j1 LIMIT 1) AS c (id)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT c.id FROM (SELECT DISTINCT (j1.j1_id + (1 * 3)) FROM j1 LIMIT 1) AS c (id)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_20() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT id FROM (SELECT j1_id + 1 FROM j1 ORDER BY j1_id DESC LIMIT 1) AS c (id)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT c.id FROM (SELECT (j1.j1_id + 1) FROM j1 ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 1) AS c (id)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_21() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT id FROM (SELECT CAST((CAST(j1_id as BIGINT) + 1) as int) * 10 FROM j1 LIMIT 1) AS c (id)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT c.id FROM (SELECT (CAST((CAST(j1.j1_id AS BIGINT) + 1) AS INTEGER) * 10) FROM j1 LIMIT 1) AS c (id)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_22() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT id FROM (SELECT CAST(j1_id as BIGINT) + 1 FROM j1 ORDER BY j1_id LIMIT 1) AS c (id)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT c.id FROM (SELECT (CAST(j1.j1_id AS BIGINT) + 1) FROM j1 ORDER BY j1.j1_id ASC NULLS LAST LIMIT 1) AS c (id)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_23() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT temp_j.id2 FROM (SELECT j1.j1_id, j1.j1_string FROM j1) AS temp_j (id2, string2)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_24() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", - parser_dialect: GenericDialect {}, - unparser_dialect: SqliteDialect {}, - expected: @r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2`, `j1`.`j1_string` AS `string2` FROM `j1`) AS `temp_j`"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_25() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM (SELECT j1_id + 1 FROM j1) AS temp_j(id2)", - parser_dialect: GenericDialect {}, - unparser_dialect: SqliteDialect {}, - expected: @r#"SELECT `temp_j`.`id2` FROM (SELECT (`j1`.`j1_id` + 1) AS `id2` FROM `j1`) AS `temp_j`"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_26() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM (SELECT j1_id FROM j1 LIMIT 1) AS temp_j(id2)", - parser_dialect: GenericDialect {}, - unparser_dialect: SqliteDialect {}, - expected: @r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2` FROM `j1` LIMIT 1) AS `temp_j`"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_27() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3])", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))" FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))")"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_28() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT t1.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS t1 (c1)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_29() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]), j1", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))", j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") CROSS JOIN j1"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_30() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_31() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT u.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) UNION ALL SELECT u.c1 FROM (SELECT UNNEST([4, 5, 6]) AS "UNNEST(make_array(Int64(4),Int64(5),Int64(6)))") AS u (c1)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_32() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3])", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))) FROM UNNEST([1, 2, 3])"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_33() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT u.array_col, u.struct_col, "UNNEST(outer_ref(u.array_col))" FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))")"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_34() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT t1.c1 FROM UNNEST([1, 2, 3]) AS t1 (c1)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_35() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]), j1", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))), j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) CROSS JOIN j1"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_36() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, - ); - Ok(()) -} - -#[test] -fn roundtrip_statement_with_dialect_37() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT u.c1 FROM UNNEST([1, 2, 3]) AS u (c1) UNION ALL SELECT u.c1 FROM UNNEST([4, 5, 6]) AS u (c1)"#, - ); - Ok(()) -} + expected: r#"SELECT abc.j1_string FROM (SELECT j1.j1_string, j2.j2_string FROM j1 INNER JOIN j2 ON (j1.j1_id = j2.j2_id) ORDER BY j1.j1_id DESC NULLS FIRST, j2.j2_id DESC NULLS FIRST LIMIT 10) AS abc ORDER BY abc.j2_string ASC NULLS LAST"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT id FROM (SELECT j1_id from j1) AS c (id)", + expected: r#"SELECT c.id FROM (SELECT j1.j1_id FROM j1) AS c (id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT id FROM (SELECT j1_id as id from j1) AS c", + expected: r#"SELECT c.id FROM (SELECT j1.j1_id AS id FROM j1) AS c"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + // Test query that has calculation in derived table with columns + TestStatementWithDialect { + sql: "SELECT id FROM (SELECT j1_id + 1 * 3 from j1) AS c (id)", + expected: r#"SELECT c.id FROM (SELECT (j1.j1_id + (1 * 3)) FROM j1) AS c (id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + // Test query that has limit/distinct/order in derived table with columns + TestStatementWithDialect { + sql: "SELECT id FROM (SELECT distinct (j1_id + 1 * 3) FROM j1 LIMIT 1) AS c (id)", + expected: r#"SELECT c.id FROM (SELECT DISTINCT (j1.j1_id + (1 * 3)) FROM j1 LIMIT 1) AS c (id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT id FROM (SELECT j1_id + 1 FROM j1 ORDER BY j1_id DESC LIMIT 1) AS c (id)", + expected: r#"SELECT c.id FROM (SELECT (j1.j1_id + 1) FROM j1 ORDER BY j1.j1_id DESC NULLS FIRST LIMIT 1) AS c (id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT id FROM (SELECT CAST((CAST(j1_id as BIGINT) + 1) as int) * 10 FROM j1 LIMIT 1) AS c (id)", + expected: r#"SELECT c.id FROM (SELECT (CAST((CAST(j1.j1_id AS BIGINT) + 1) AS INTEGER) * 10) FROM j1 LIMIT 1) AS c (id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT id FROM (SELECT CAST(j1_id as BIGINT) + 1 FROM j1 ORDER BY j1_id LIMIT 1) AS c (id)", + expected: r#"SELECT c.id FROM (SELECT (CAST(j1.j1_id AS BIGINT) + 1) FROM j1 ORDER BY j1.j1_id ASC NULLS LAST LIMIT 1) AS c (id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", + expected: r#"SELECT temp_j.id2 FROM (SELECT j1.j1_id, j1.j1_string FROM j1) AS temp_j (id2, string2)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT temp_j.id2 FROM (SELECT j1_id, j1_string FROM j1) AS temp_j(id2, string2)", + expected: r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2`, `j1`.`j1_string` AS `string2` FROM `j1`) AS `temp_j`"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(SqliteDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM (SELECT j1_id + 1 FROM j1) AS temp_j(id2)", + expected: r#"SELECT `temp_j`.`id2` FROM (SELECT (`j1`.`j1_id` + 1) AS `id2` FROM `j1`) AS `temp_j`"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(SqliteDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM (SELECT j1_id FROM j1 LIMIT 1) AS temp_j(id2)", + expected: r#"SELECT `temp_j`.`id2` FROM (SELECT `j1`.`j1_id` AS `id2` FROM `j1` LIMIT 1) AS `temp_j`"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(SqliteDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3])", + expected: r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))" FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))")"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", + expected: r#"SELECT t1.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS t1 (c1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]), j1", + expected: r#"SELECT "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))", j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") CROSS JOIN j1"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", + expected: r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", + expected: r#"SELECT u.c1 FROM (SELECT UNNEST([1, 2, 3]) AS "UNNEST(make_array(Int64(1),Int64(2),Int64(3)))") AS u (c1) UNION ALL SELECT u.c1 FROM (SELECT UNNEST([4, 5, 6]) AS "UNNEST(make_array(Int64(4),Int64(5),Int64(6)))") AS u (c1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3])", + expected: r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))) FROM UNNEST([1, 2, 3])"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", + expected: r#"SELECT t1.c1 FROM UNNEST([1, 2, 3]) AS t1 (c1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]) AS t1 (c1)", + expected: r#"SELECT t1.c1 FROM UNNEST([1, 2, 3]) AS t1 (c1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]), j1", + expected: r#"SELECT UNNEST(make_array(Int64(1),Int64(2),Int64(3))), j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) CROSS JOIN j1"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) JOIN j1 ON u.c1 = j1.j1_id", + expected: r#"SELECT u.c1, j1.j1_id, j1.j1_string FROM UNNEST([1, 2, 3]) AS u (c1) INNER JOIN j1 ON (u.c1 = j1.j1_id)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM UNNEST([1,2,3]) u(c1) UNION ALL SELECT * FROM UNNEST([4,5,6]) u(c1)", + expected: r#"SELECT u.c1 FROM UNNEST([1, 2, 3]) AS u (c1) UNION ALL SELECT u.c1 FROM UNNEST([4, 5, 6]) AS u (c1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT UNNEST([1,2,3])", + expected: r#"SELECT * FROM UNNEST([1, 2, 3])"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT UNNEST([1,2,3]) as c1", + expected: r#"SELECT UNNEST([1, 2, 3]) AS c1"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT UNNEST([1,2,3]), 1", + expected: r#"SELECT UNNEST([1, 2, 3]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3))), Int64(1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", + expected: r#"SELECT u.array_col, u.struct_col, UNNEST(outer_ref(u.array_col)) FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", + expected: r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col) AS t1 (c1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT unnest([1, 2, 3, 4]) from unnest([1, 2, 3]);", + expected: r#"SELECT UNNEST([1, 2, 3, 4]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3),Int64(4))) FROM UNNEST([1, 2, 3])"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(CustomDialectBuilder::default().with_unnest_as_table_factor(true).build()), + }, + TestStatementWithDialect { + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", + expected: r#"SELECT u.array_col, u.struct_col, "UNNEST(outer_ref(u.array_col))" FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))")"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + TestStatementWithDialect { + sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", + expected: r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))") AS t1 (c1)"#, + parser_dialect: Box::new(GenericDialect {}), + unparser_dialect: Box::new(UnparserDefaultDialect {}), + }, + ]; -#[test] -fn roundtrip_statement_with_dialect_38() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT UNNEST([1,2,3])", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT * FROM UNNEST([1, 2, 3])"#, - ); - Ok(()) -} + for query in tests { + let statement = Parser::new(&*query.parser_dialect) + .try_with_sql(query.sql)? + .parse_statement()?; -#[test] -fn roundtrip_statement_with_dialect_39() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT UNNEST([1,2,3]) as c1", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT UNNEST([1, 2, 3]) AS c1"#, - ); - Ok(()) -} + let state = MockSessionState::default() + .with_aggregate_function(max_udaf()) + .with_aggregate_function(min_udaf()) + .with_expr_planner(Arc::new(CoreFunctionPlanner::default())) + .with_expr_planner(Arc::new(NestedFunctionPlanner)); -#[test] -fn roundtrip_statement_with_dialect_40() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT UNNEST([1,2,3]), 1", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT UNNEST([1, 2, 3]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3))), Int64(1)"#, - ); - Ok(()) -} + let context = MockContextProvider { state }; + let sql_to_rel = SqlToRel::new(&context); + let plan = sql_to_rel + .sql_statement_to_plan(statement) + .unwrap_or_else(|e| panic!("Failed to parse sql: {}\n{e}", query.sql)); -#[test] -fn roundtrip_statement_with_dialect_41() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col)", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT u.array_col, u.struct_col, UNNEST(outer_ref(u.array_col)) FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col)"#, - ); - Ok(()) -} + let unparser = Unparser::new(&*query.unparser_dialect); + let roundtrip_statement = unparser.plan_to_sql(&plan)?; -#[test] -fn roundtrip_statement_with_dialect_42() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN UNNEST(u.array_col) AS t1 (c1)"#, - ); - Ok(()) -} + let actual = &roundtrip_statement.to_string(); + println!("roundtrip sql: {actual}"); + println!("plan {}", plan.display_indent()); -#[test] -fn roundtrip_statement_with_dialect_43() -> Result<(), DataFusionError> { - let unparser = CustomDialectBuilder::default() - .with_unnest_as_table_factor(true) - .build(); - roundtrip_statement_with_dialect_helper!( - sql: "SELECT unnest([1, 2, 3, 4]) from unnest([1, 2, 3]);", - parser_dialect: GenericDialect {}, - unparser_dialect: unparser, - expected: @r#"SELECT UNNEST([1, 2, 3, 4]) AS UNNEST(make_array(Int64(1),Int64(2),Int64(3),Int64(4))) FROM UNNEST([1, 2, 3])"#, - ); - Ok(()) -} + assert_eq!(query.expected, actual); + } -#[test] -fn roundtrip_statement_with_dialect_45() -> Result<(), DataFusionError> { - roundtrip_statement_with_dialect_helper!( - sql: "SELECT * FROM unnest_table u, UNNEST(u.array_col) AS t1 (c1)", - parser_dialect: GenericDialect {}, - unparser_dialect: UnparserDefaultDialect {}, - expected: @r#"SELECT u.array_col, u.struct_col, t1.c1 FROM unnest_table AS u CROSS JOIN LATERAL (SELECT UNNEST(u.array_col) AS "UNNEST(outer_ref(u.array_col))") AS t1 (c1)"#, - ); Ok(()) } @@ -937,14 +700,13 @@ fn test_unnest_logical_plan() -> Result<()> { }; let sql_to_rel = SqlToRel::new(&context); let plan = sql_to_rel.sql_statement_to_plan(statement).unwrap(); - assert_snapshot!( - plan, - @r#" + let expected = r#" Projection: __unnest_placeholder(unnest_table.struct_col).field1, __unnest_placeholder(unnest_table.struct_col).field2, __unnest_placeholder(unnest_table.array_col,depth=1) AS UNNEST(unnest_table.array_col), unnest_table.struct_col, unnest_table.array_col Unnest: lists[__unnest_placeholder(unnest_table.array_col)|depth=1] structs[__unnest_placeholder(unnest_table.struct_col)] Projection: unnest_table.struct_col AS __unnest_placeholder(unnest_table.struct_col), unnest_table.array_col AS __unnest_placeholder(unnest_table.array_col), unnest_table.struct_col, unnest_table.array_col - TableScan: unnest_table"# - ); + TableScan: unnest_table"#.trim_start(); + + assert_eq!(plan.to_string(), expected); Ok(()) } @@ -964,248 +726,121 @@ fn test_aggregation_without_projection() -> Result<()> { let unparser = Unparser::default(); let statement = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - statement, - @r#"SELECT sum(users.age), users."name" FROM users GROUP BY users."name""# + + let actual = &statement.to_string(); + + assert_eq!( + actual, + r#"SELECT sum(users.age), users."name" FROM users GROUP BY users."name""# ); Ok(()) } -/// return a schema with two string columns: "id" and "value" -fn test_schema() -> Schema { - Schema::new(vec![ - Field::new("id", DataType::Utf8, false), - Field::new("value", DataType::Utf8, false), - ]) -} - #[test] -fn test_table_references_in_plan_to_sql_1() { - let table_name = "catalog.schema.table"; - let schema = test_schema(); - let sql = table_references_in_plan_helper( - table_name, - schema, - vec![col("id"), col("value")], - &DefaultDialect {}, - ); - assert_snapshot!( - sql, - @r#"SELECT "table".id, "table"."value" FROM "catalog"."schema"."table""# - ); -} +fn test_table_references_in_plan_to_sql() { + fn test(table_name: &str, expected_sql: &str, dialect: &impl UnparserDialect) { + let schema = Schema::new(vec![ + Field::new("id", DataType::Utf8, false), + Field::new("value", DataType::Utf8, false), + ]); + let plan = table_scan(Some(table_name), &schema, None) + .unwrap() + .project(vec![col("id"), col("value")]) + .unwrap() + .build() + .unwrap(); -#[test] -fn test_table_references_in_plan_to_sql_2() { - let table_name = "schema.table"; - let schema = test_schema(); - let sql = table_references_in_plan_helper( - table_name, - schema, - vec![col("id"), col("value")], + let unparser = Unparser::new(dialect); + let sql = unparser.plan_to_sql(&plan).unwrap(); + + assert_eq!(sql.to_string(), expected_sql) + } + + test( + "catalog.schema.table", + r#"SELECT "table".id, "table"."value" FROM "catalog"."schema"."table""#, &DefaultDialect {}, ); - assert_snapshot!( - sql, - @r#"SELECT "table".id, "table"."value" FROM "schema"."table""# - ); -} - -#[test] -fn test_table_references_in_plan_to_sql_3() { - let table_name = "table"; - let schema = test_schema(); - let sql = table_references_in_plan_helper( - table_name, - schema, - vec![col("id"), col("value")], + test( + "schema.table", + r#"SELECT "table".id, "table"."value" FROM "schema"."table""#, &DefaultDialect {}, ); - assert_snapshot!( - sql, - @r#"SELECT "table".id, "table"."value" FROM "table""# + test( + "table", + r#"SELECT "table".id, "table"."value" FROM "table""#, + &DefaultDialect {}, ); -} -#[test] -fn test_table_references_in_plan_to_sql_4() { - let table_name = "catalog.schema.table"; - let schema = test_schema(); let custom_dialect = CustomDialectBuilder::default() .with_full_qualified_col(true) .with_identifier_quote_style('"') .build(); - let sql = table_references_in_plan_helper( - table_name, - schema, - vec![col("id"), col("value")], + test( + "catalog.schema.table", + r#"SELECT "catalog"."schema"."table"."id", "catalog"."schema"."table"."value" FROM "catalog"."schema"."table""#, &custom_dialect, ); - assert_snapshot!( - sql, - @r#"SELECT "catalog"."schema"."table"."id", "catalog"."schema"."table"."value" FROM "catalog"."schema"."table""# - ); -} - -#[test] -fn test_table_references_in_plan_to_sql_5() { - let table_name = "schema.table"; - let schema = test_schema(); - let custom_dialect = CustomDialectBuilder::default() - .with_full_qualified_col(true) - .with_identifier_quote_style('"') - .build(); - - let sql = table_references_in_plan_helper( - table_name, - schema, - vec![col("id"), col("value")], + test( + "schema.table", + r#"SELECT "schema"."table"."id", "schema"."table"."value" FROM "schema"."table""#, &custom_dialect, ); - assert_snapshot!( - sql, - @r#"SELECT "schema"."table"."id", "schema"."table"."value" FROM "schema"."table""# - ); -} - -#[test] -fn test_table_references_in_plan_to_sql_6() { - let table_name = "table"; - let schema = test_schema(); - let custom_dialect = CustomDialectBuilder::default() - .with_full_qualified_col(true) - .with_identifier_quote_style('"') - .build(); - - let sql = table_references_in_plan_helper( - table_name, - schema, - vec![col("id"), col("value")], + test( + "table", + r#"SELECT "table"."id", "table"."value" FROM "table""#, &custom_dialect, ); - assert_snapshot!( - sql, - @r#"SELECT "table"."id", "table"."value" FROM "table""# - ); -} - -fn table_references_in_plan_helper( - table_name: &str, - table_schema: Schema, - expr: impl IntoIterator>, - dialect: &impl UnparserDialect, -) -> Statement { - let plan = table_scan(Some(table_name), &table_schema, None) - .unwrap() - .project(expr) - .unwrap() - .build() - .unwrap(); - let unparser = Unparser::new(dialect); - unparser.plan_to_sql(&plan).unwrap() } #[test] -fn test_table_scan_with_none_projection_in_plan_to_sql_1() { - let schema = test_schema(); - let table_name = "catalog.schema.table"; - let plan = table_scan_with_empty_projection_and_none_projection_helper( - table_name, schema, None, - ); - let sql = plan_to_sql(&plan).unwrap(); - assert_snapshot!( - sql, - @r#"SELECT * FROM "catalog"."schema"."table""# - ); -} +fn test_table_scan_with_none_projection_in_plan_to_sql() { + fn test(table_name: &str, expected_sql: &str) { + let schema = Schema::new(vec![ + Field::new("id", DataType::Utf8, false), + Field::new("value", DataType::Utf8, false), + ]); -#[test] -fn test_table_scan_with_none_projection_in_plan_to_sql_2() { - let schema = test_schema(); - let table_name = "schema.table"; - let plan = table_scan_with_empty_projection_and_none_projection_helper( - table_name, schema, None, - ); - let sql = plan_to_sql(&plan).unwrap(); - assert_snapshot!( - sql, - @r#"SELECT * FROM "schema"."table""# - ); -} + let plan = table_scan(Some(table_name), &schema, None) + .unwrap() + .build() + .unwrap(); + let sql = plan_to_sql(&plan).unwrap(); + assert_eq!(sql.to_string(), expected_sql) + } -#[test] -fn test_table_scan_with_none_projection_in_plan_to_sql_3() { - let schema = test_schema(); - let table_name = "table"; - let plan = table_scan_with_empty_projection_and_none_projection_helper( - table_name, schema, None, - ); - let sql = plan_to_sql(&plan).unwrap(); - assert_snapshot!( - sql, - @r#"SELECT * FROM "table""# + test( + "catalog.schema.table", + r#"SELECT * FROM "catalog"."schema"."table""#, ); + test("schema.table", r#"SELECT * FROM "schema"."table""#); + test("table", r#"SELECT * FROM "table""#); } #[test] -fn test_table_scan_with_empty_projection_in_plan_to_sql_1() { - let schema = test_schema(); - let table_name = "catalog.schema.table"; - let plan = table_scan_with_empty_projection_and_none_projection_helper( - table_name, - schema, - Some(vec![]), - ); - let sql = plan_to_sql(&plan).unwrap(); - assert_snapshot!( - sql, - @r#"SELECT 1 FROM "catalog"."schema"."table""# - ); -} +fn test_table_scan_with_empty_projection_in_plan_to_sql() { + fn test(table_name: &str, expected_sql: &str) { + let schema = Schema::new(vec![ + Field::new("id", DataType::Utf8, false), + Field::new("value", DataType::Utf8, false), + ]); -#[test] -fn test_table_scan_with_empty_projection_in_plan_to_sql_2() { - let schema = test_schema(); - let table_name = "schema.table"; - let plan = table_scan_with_empty_projection_and_none_projection_helper( - table_name, - schema, - Some(vec![]), - ); - let sql = plan_to_sql(&plan).unwrap(); - assert_snapshot!( - sql, - @r#"SELECT 1 FROM "schema"."table""# - ); -} + let plan = table_scan(Some(table_name), &schema, Some(vec![])) + .unwrap() + .build() + .unwrap(); + let sql = plan_to_sql(&plan).unwrap(); + assert_eq!(sql.to_string(), expected_sql) + } -#[test] -fn test_table_scan_with_empty_projection_in_plan_to_sql_3() { - let schema = test_schema(); - let table_name = "table"; - let plan = table_scan_with_empty_projection_and_none_projection_helper( - table_name, - schema, - Some(vec![]), + test( + "catalog.schema.table", + r#"SELECT 1 FROM "catalog"."schema"."table""#, ); - let sql = plan_to_sql(&plan).unwrap(); - assert_snapshot!( - sql, - @r#"SELECT 1 FROM "table""# - ); -} - -fn table_scan_with_empty_projection_and_none_projection_helper( - table_name: &str, - table_schema: Schema, - projection: Option>, -) -> LogicalPlan { - table_scan(Some(table_name), &table_schema, projection) - .unwrap() - .build() - .unwrap() + test("schema.table", r#"SELECT 1 FROM "schema"."table""#); + test("table", r#"SELECT 1 FROM "table""#); } #[test] @@ -1285,12 +920,12 @@ fn test_pretty_roundtrip() -> Result<()> { Ok(()) } -fn generate_round_trip_statement(dialect: D, sql: &str) -> Statement +fn sql_round_trip(dialect: D, query: &str, expect: &str) where D: Dialect, { let statement = Parser::new(&dialect) - .try_with_sql(sql) + .try_with_sql(query) .unwrap() .parse_statement() .unwrap(); @@ -1307,7 +942,8 @@ where let sql_to_rel = SqlToRel::new(&context); let plan = sql_to_rel.sql_statement_to_plan(statement).unwrap(); - plan_to_sql(&plan).unwrap() + let roundtrip_statement = plan_to_sql(&plan).unwrap(); + assert_eq!(roundtrip_statement.to_string(), expect); } #[test] @@ -1322,10 +958,7 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let sql = plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @"SELECT * FROM (SELECT t1.id FROM t1) AS a" - ); + assert_eq!(sql.to_string(), "SELECT * FROM (SELECT t1.id FROM t1) AS a"); let plan = table_scan(Some("t1"), &schema, None)? .project(vec![col("id")])? @@ -1333,10 +966,7 @@ fn test_table_scan_alias() -> Result<()> { .build()?; let sql = plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @"SELECT * FROM (SELECT t1.id FROM t1) AS a" - ); + assert_eq!(sql.to_string(), "SELECT * FROM (SELECT t1.id FROM t1) AS a"); let plan = table_scan(Some("t1"), &schema, None)? .filter(col("id").gt(lit(5)))? @@ -1344,9 +974,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let sql = plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT * FROM (SELECT t1.id FROM t1 WHERE (t1.id > 5)) AS a"# + assert_eq!( + sql.to_string(), + "SELECT * FROM (SELECT t1.id FROM t1 WHERE (t1.id > 5)) AS a" ); let table_scan_with_two_filter = table_scan_with_filters( @@ -1359,9 +989,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let table_scan_with_two_filter = plan_to_sql(&table_scan_with_two_filter)?; - assert_snapshot!( - table_scan_with_two_filter, - @r#"SELECT a.id FROM t1 AS a WHERE ((a.id > 1) AND (a.age < 2))"# + assert_eq!( + table_scan_with_two_filter.to_string(), + "SELECT a.id FROM t1 AS a WHERE ((a.id > 1) AND (a.age < 2))" ); let table_scan_with_fetch = @@ -1370,9 +1000,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let table_scan_with_fetch = plan_to_sql(&table_scan_with_fetch)?; - assert_snapshot!( - table_scan_with_fetch, - @r#"SELECT a.id FROM (SELECT * FROM t1 LIMIT 10) AS a"# + assert_eq!( + table_scan_with_fetch.to_string(), + "SELECT a.id FROM (SELECT * FROM t1 LIMIT 10) AS a" ); let table_scan_with_pushdown_all = table_scan_with_filter_and_fetch( @@ -1386,9 +1016,9 @@ fn test_table_scan_alias() -> Result<()> { .alias("a")? .build()?; let table_scan_with_pushdown_all = plan_to_sql(&table_scan_with_pushdown_all)?; - assert_snapshot!( - table_scan_with_pushdown_all, - @r#"SELECT a.id FROM (SELECT a.id, a.age FROM t1 AS a WHERE (a.id > 1) LIMIT 10) AS a"# + assert_eq!( + table_scan_with_pushdown_all.to_string(), + "SELECT a.id FROM (SELECT a.id, a.age FROM t1 AS a WHERE (a.id > 1) LIMIT 10) AS a" ); Ok(()) } @@ -1402,24 +1032,18 @@ fn test_table_scan_pushdown() -> Result<()> { let scan_with_projection = table_scan(Some("t1"), &schema, Some(vec![0, 1]))?.build()?; let scan_with_projection = plan_to_sql(&scan_with_projection)?; - assert_snapshot!( - scan_with_projection, - @r#"SELECT t1.id, t1.age FROM t1"# + assert_eq!( + scan_with_projection.to_string(), + "SELECT t1.id, t1.age FROM t1" ); let scan_with_projection = table_scan(Some("t1"), &schema, Some(vec![1]))?.build()?; let scan_with_projection = plan_to_sql(&scan_with_projection)?; - assert_snapshot!( - scan_with_projection, - @r#"SELECT t1.age FROM t1"# - ); + assert_eq!(scan_with_projection.to_string(), "SELECT t1.age FROM t1"); let scan_with_no_projection = table_scan(Some("t1"), &schema, None)?.build()?; let scan_with_no_projection = plan_to_sql(&scan_with_no_projection)?; - assert_snapshot!( - scan_with_no_projection, - @r#"SELECT * FROM t1"# - ); + assert_eq!(scan_with_no_projection.to_string(), "SELECT * FROM t1"); let table_scan_with_projection_alias = table_scan(Some("t1"), &schema, Some(vec![0, 1]))? @@ -1427,9 +1051,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_alias = plan_to_sql(&table_scan_with_projection_alias)?; - assert_snapshot!( - table_scan_with_projection_alias, - @r#"SELECT ta.id, ta.age FROM t1 AS ta"# + assert_eq!( + table_scan_with_projection_alias.to_string(), + "SELECT ta.id, ta.age FROM t1 AS ta" ); let table_scan_with_projection_alias = @@ -1438,9 +1062,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_alias = plan_to_sql(&table_scan_with_projection_alias)?; - assert_snapshot!( - table_scan_with_projection_alias, - @r#"SELECT ta.age FROM t1 AS ta"# + assert_eq!( + table_scan_with_projection_alias.to_string(), + "SELECT ta.age FROM t1 AS ta" ); let table_scan_with_no_projection_alias = table_scan(Some("t1"), &schema, None)? @@ -1448,9 +1072,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_no_projection_alias = plan_to_sql(&table_scan_with_no_projection_alias)?; - assert_snapshot!( - table_scan_with_no_projection_alias, - @r#"SELECT * FROM t1 AS ta"# + assert_eq!( + table_scan_with_no_projection_alias.to_string(), + "SELECT * FROM t1 AS ta" ); let query_from_table_scan_with_projection = LogicalPlanBuilder::from( @@ -1460,9 +1084,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let query_from_table_scan_with_projection = plan_to_sql(&query_from_table_scan_with_projection)?; - assert_snapshot!( - query_from_table_scan_with_projection, - @r#"SELECT t1.id, t1.age FROM t1"# + assert_eq!( + query_from_table_scan_with_projection.to_string(), + "SELECT t1.id, t1.age FROM t1" ); let query_from_table_scan_with_two_projections = LogicalPlanBuilder::from( @@ -1473,9 +1097,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let query_from_table_scan_with_two_projections = plan_to_sql(&query_from_table_scan_with_two_projections)?; - assert_snapshot!( - query_from_table_scan_with_two_projections, - @r#"SELECT t1.id, t1.age FROM (SELECT t1.id, t1.age FROM t1)"# + assert_eq!( + query_from_table_scan_with_two_projections.to_string(), + "SELECT t1.id, t1.age FROM (SELECT t1.id, t1.age FROM t1)" ); let table_scan_with_filter = table_scan_with_filters( @@ -1486,9 +1110,9 @@ fn test_table_scan_pushdown() -> Result<()> { )? .build()?; let table_scan_with_filter = plan_to_sql(&table_scan_with_filter)?; - assert_snapshot!( - table_scan_with_filter, - @r#"SELECT * FROM t1 WHERE (t1.id > t1.age)"# + assert_eq!( + table_scan_with_filter.to_string(), + "SELECT * FROM t1 WHERE (t1.id > t1.age)" ); let table_scan_with_two_filter = table_scan_with_filters( @@ -1499,9 +1123,9 @@ fn test_table_scan_pushdown() -> Result<()> { )? .build()?; let table_scan_with_two_filter = plan_to_sql(&table_scan_with_two_filter)?; - assert_snapshot!( - table_scan_with_two_filter, - @r#"SELECT * FROM t1 WHERE ((t1.id > 1) AND (t1.age < 2))"# + assert_eq!( + table_scan_with_two_filter.to_string(), + "SELECT * FROM t1 WHERE ((t1.id > 1) AND (t1.age < 2))" ); let table_scan_with_filter_alias = table_scan_with_filters( @@ -1513,9 +1137,9 @@ fn test_table_scan_pushdown() -> Result<()> { .alias("ta")? .build()?; let table_scan_with_filter_alias = plan_to_sql(&table_scan_with_filter_alias)?; - assert_snapshot!( - table_scan_with_filter_alias, - @r#"SELECT * FROM t1 AS ta WHERE (ta.id > ta.age)"# + assert_eq!( + table_scan_with_filter_alias.to_string(), + "SELECT * FROM t1 AS ta WHERE (ta.id > ta.age)" ); let table_scan_with_projection_and_filter = table_scan_with_filters( @@ -1527,9 +1151,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_and_filter = plan_to_sql(&table_scan_with_projection_and_filter)?; - assert_snapshot!( - table_scan_with_projection_and_filter, - @r#"SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age)"# + assert_eq!( + table_scan_with_projection_and_filter.to_string(), + "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age)" ); let table_scan_with_projection_and_filter = table_scan_with_filters( @@ -1541,18 +1165,18 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_and_filter = plan_to_sql(&table_scan_with_projection_and_filter)?; - assert_snapshot!( - table_scan_with_projection_and_filter, - @r#"SELECT t1.age FROM t1 WHERE (t1.id > t1.age)"# + assert_eq!( + table_scan_with_projection_and_filter.to_string(), + "SELECT t1.age FROM t1 WHERE (t1.id > t1.age)" ); let table_scan_with_inline_fetch = table_scan_with_filter_and_fetch(Some("t1"), &schema, None, vec![], Some(10))? .build()?; let table_scan_with_inline_fetch = plan_to_sql(&table_scan_with_inline_fetch)?; - assert_snapshot!( - table_scan_with_inline_fetch, - @r#"SELECT * FROM t1 LIMIT 10"# + assert_eq!( + table_scan_with_inline_fetch.to_string(), + "SELECT * FROM t1 LIMIT 10" ); let table_scan_with_projection_and_inline_fetch = table_scan_with_filter_and_fetch( @@ -1565,9 +1189,9 @@ fn test_table_scan_pushdown() -> Result<()> { .build()?; let table_scan_with_projection_and_inline_fetch = plan_to_sql(&table_scan_with_projection_and_inline_fetch)?; - assert_snapshot!( - table_scan_with_projection_and_inline_fetch, - @r#"SELECT t1.id, t1.age FROM t1 LIMIT 10"# + assert_eq!( + table_scan_with_projection_and_inline_fetch.to_string(), + "SELECT t1.id, t1.age FROM t1 LIMIT 10" ); let table_scan_with_all = table_scan_with_filter_and_fetch( @@ -1579,9 +1203,9 @@ fn test_table_scan_pushdown() -> Result<()> { )? .build()?; let table_scan_with_all = plan_to_sql(&table_scan_with_all)?; - assert_snapshot!( - table_scan_with_all, - @r#"SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age) LIMIT 10"# + assert_eq!( + table_scan_with_all.to_string(), + "SELECT t1.id, t1.age FROM t1 WHERE (t1.id > t1.age) LIMIT 10" ); let table_scan_with_additional_filter = table_scan_with_filters( @@ -1593,9 +1217,9 @@ fn test_table_scan_pushdown() -> Result<()> { .filter(col("id").eq(lit(5)))? .build()?; let table_scan_with_filter = plan_to_sql(&table_scan_with_additional_filter)?; - assert_snapshot!( - table_scan_with_filter, - @r#"SELECT * FROM t1 WHERE (t1.id = 5) AND (t1.id > t1.age)"# + assert_eq!( + table_scan_with_filter.to_string(), + "SELECT * FROM t1 WHERE (t1.id = 5) AND (t1.id > t1.age)" ); Ok(()) @@ -1614,9 +1238,9 @@ fn test_sort_with_push_down_fetch() -> Result<()> { .build()?; let sql = plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT t1.id, t1.age FROM t1 ORDER BY t1.age ASC NULLS FIRST LIMIT 10"# + assert_eq!( + format!("{}", sql), + "SELECT t1.id, t1.age FROM t1 ORDER BY t1.age ASC NULLS FIRST LIMIT 10" ); Ok(()) } @@ -1660,10 +1284,10 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_with_filter)?; - assert_snapshot!( - sql, - @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND ("left"."name" LIKE 'some_name' AND (age > 10)))"# - ); + + let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND ("left"."name" LIKE 'some_name' AND (age > 10)))"#; + + assert_eq!(sql.to_string(), expected_sql); let join_plan_no_filter = LogicalPlanBuilder::from(left_plan.clone()) .join( @@ -1675,10 +1299,10 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_no_filter)?; - assert_snapshot!( - sql, - @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND ("left"."name" LIKE 'some_name' AND (age > 10))"# - ); + + let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND ("left"."name" LIKE 'some_name' AND (age > 10))"#; + + assert_eq!(sql.to_string(), expected_sql); let right_plan_with_filter = table_scan_with_filters( Some("right_table"), @@ -1700,10 +1324,10 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_multiple_filters)?; - assert_snapshot!( - sql, - @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table."name" = 'before_join_filter_val')) AND (age > 10))) WHERE ("left"."name" = 'after_join_filter_val')"# - ); + + let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table."name" = 'before_join_filter_val')) AND (age > 10))) WHERE ("left"."name" = 'after_join_filter_val')"#; + + assert_eq!(sql.to_string(), expected_sql); let right_plan_with_filter_schema = table_scan_with_filters( Some("right_table"), @@ -1730,153 +1354,114 @@ fn test_join_with_table_scan_filters() -> Result<()> { .build()?; let sql = plan_to_sql(&join_plan_duplicated_filter)?; - assert_snapshot!( - sql, - @r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table.age > 10)) AND (right_table.age < 11)))"# - ); + + let expected_sql = r#"SELECT * FROM left_table AS "left" INNER JOIN right_table ON "left".id = right_table.id AND (("left".id > 5) AND (("left"."name" LIKE 'some_name' AND (right_table.age > 10)) AND (right_table.age < 11)))"#; + + assert_eq!(sql.to_string(), expected_sql); Ok(()) } #[test] fn test_interval_lhs_eq() { - let statement = generate_round_trip_statement( + sql_round_trip( GenericDialect {}, "select interval '2 seconds' = interval '2 seconds'", + "SELECT (INTERVAL '2.000000000 SECS' = INTERVAL '2.000000000 SECS')", ); - assert_snapshot!( - statement, - @r#"SELECT (INTERVAL '2.000000000 SECS' = INTERVAL '2.000000000 SECS')"# - ) } #[test] fn test_interval_lhs_lt() { - let statement = generate_round_trip_statement( + sql_round_trip( GenericDialect {}, "select interval '2 seconds' < interval '2 seconds'", + "SELECT (INTERVAL '2.000000000 SECS' < INTERVAL '2.000000000 SECS')", ); - assert_snapshot!( - statement, - @r#"SELECT (INTERVAL '2.000000000 SECS' < INTERVAL '2.000000000 SECS')"# - ) } #[test] fn test_without_offset() { - let statement = generate_round_trip_statement(MySqlDialect {}, "select 1"); - assert_snapshot!( - statement, - @r#"SELECT 1"# - ) + sql_round_trip(MySqlDialect {}, "select 1", "SELECT 1"); } #[test] fn test_with_offset0() { - let statement = generate_round_trip_statement(MySqlDialect {}, "select 1 offset 0"); - assert_snapshot!( - statement, - @r#"SELECT 1 OFFSET 0"# - ) + sql_round_trip(MySqlDialect {}, "select 1 offset 0", "SELECT 1 OFFSET 0"); } #[test] fn test_with_offset95() { - let statement = generate_round_trip_statement(MySqlDialect {}, "select 1 offset 95"); - assert_snapshot!( - statement, - @r#"SELECT 1 OFFSET 95"# - ) + sql_round_trip(MySqlDialect {}, "select 1 offset 95", "SELECT 1 OFFSET 95"); } #[test] -fn test_order_by_to_sql_1() { +fn test_order_by_to_sql() { // order by aggregation function - let statement = generate_round_trip_statement( + sql_round_trip( GenericDialect {}, r#"SELECT id, first_name, SUM(id) FROM person GROUP BY id, first_name ORDER BY SUM(id) ASC, first_name DESC, id, first_name LIMIT 10"#, + r#"SELECT person.id, person.first_name, sum(person.id) FROM person GROUP BY person.id, person.first_name ORDER BY sum(person.id) ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"#, ); - assert_snapshot!( - statement, - @r#"SELECT person.id, person.first_name, sum(person.id) FROM person GROUP BY person.id, person.first_name ORDER BY sum(person.id) ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"# - ); -} -#[test] -fn test_order_by_to_sql_2() { // order by aggregation function alias - let statement = generate_round_trip_statement( + sql_round_trip( GenericDialect {}, r#"SELECT id, first_name, SUM(id) as total_sum FROM person GROUP BY id, first_name ORDER BY total_sum ASC, first_name DESC, id, first_name LIMIT 10"#, + r#"SELECT person.id, person.first_name, sum(person.id) AS total_sum FROM person GROUP BY person.id, person.first_name ORDER BY total_sum ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"#, ); - assert_snapshot!( - statement, - @r#"SELECT person.id, person.first_name, sum(person.id) AS total_sum FROM person GROUP BY person.id, person.first_name ORDER BY total_sum ASC NULLS LAST, person.first_name DESC NULLS FIRST, person.id ASC NULLS LAST, person.first_name ASC NULLS LAST LIMIT 10"# - ); -} -#[test] -fn test_order_by_to_sql_3() { - let statement = generate_round_trip_statement( + // order by scalar function from projection + sql_round_trip( GenericDialect {}, r#"SELECT id, first_name, substr(first_name,0,5) FROM person ORDER BY id, substr(first_name,0,5)"#, - ); - assert_snapshot!( - statement, - @r#"SELECT person.id, person.first_name, substr(person.first_name, 0, 5) FROM person ORDER BY person.id ASC NULLS LAST, substr(person.first_name, 0, 5) ASC NULLS LAST"# + r#"SELECT person.id, person.first_name, substr(person.first_name, 0, 5) FROM person ORDER BY person.id ASC NULLS LAST, substr(person.first_name, 0, 5) ASC NULLS LAST"#, ); } #[test] fn test_aggregation_to_sql() { - let sql = r#"SELECT id, first_name, + sql_round_trip( + GenericDialect {}, + r#"SELECT id, first_name, SUM(id) AS total_sum, SUM(id) OVER (PARTITION BY first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) AS moving_sum, MAX(SUM(id)) OVER (PARTITION BY first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_total, rank() OVER (PARTITION BY grouping(id) + grouping(age), CASE WHEN grouping(age) = 0 THEN id END ORDER BY sum(id) DESC) AS rank_within_parent_1, rank() OVER (PARTITION BY grouping(age) + grouping(id), CASE WHEN (CAST(grouping(age) AS BIGINT) = 0) THEN id END ORDER BY sum(id) DESC) AS rank_within_parent_2 FROM person - GROUP BY id, first_name"#; - let statement = generate_round_trip_statement(GenericDialect {}, sql); - assert_snapshot!( - statement, - @"SELECT person.id, person.first_name, sum(person.id) AS total_sum, sum(person.id) OVER (PARTITION BY person.first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) AS moving_sum, max(sum(person.id)) OVER (PARTITION BY person.first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_total, rank() OVER (PARTITION BY (grouping(person.id) + grouping(person.age)), CASE WHEN (grouping(person.age) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_1, rank() OVER (PARTITION BY (grouping(person.age) + grouping(person.id)), CASE WHEN (CAST(grouping(person.age) AS BIGINT) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_2 FROM person GROUP BY person.id, person.first_name", + GROUP BY id, first_name;"#, + r#"SELECT person.id, person.first_name, +sum(person.id) AS total_sum, sum(person.id) OVER (PARTITION BY person.first_name ROWS BETWEEN 5 PRECEDING AND 2 FOLLOWING) AS moving_sum, +max(sum(person.id)) OVER (PARTITION BY person.first_name ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS max_total, +rank() OVER (PARTITION BY (grouping(person.id) + grouping(person.age)), CASE WHEN (grouping(person.age) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_1, +rank() OVER (PARTITION BY (grouping(person.age) + grouping(person.id)), CASE WHEN (CAST(grouping(person.age) AS BIGINT) = 0) THEN person.id END ORDER BY sum(person.id) DESC NULLS FIRST RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS rank_within_parent_2 +FROM person +GROUP BY person.id, person.first_name"#.replace("\n", " ").as_str(), ); } #[test] -fn test_unnest_to_sql_1() { - let statement = generate_round_trip_statement( +fn test_unnest_to_sql() { + sql_round_trip( GenericDialect {}, r#"SELECT unnest(array_col) as u1, struct_col, array_col FROM unnest_table WHERE array_col != NULL ORDER BY struct_col, array_col"#, + r#"SELECT UNNEST(unnest_table.array_col) AS u1, unnest_table.struct_col, unnest_table.array_col FROM unnest_table WHERE (unnest_table.array_col <> NULL) ORDER BY unnest_table.struct_col ASC NULLS LAST, unnest_table.array_col ASC NULLS LAST"#, ); - assert_snapshot!( - statement, - @r#"SELECT UNNEST(unnest_table.array_col) AS u1, unnest_table.struct_col, unnest_table.array_col FROM unnest_table WHERE (unnest_table.array_col <> NULL) ORDER BY unnest_table.struct_col ASC NULLS LAST, unnest_table.array_col ASC NULLS LAST"# - ); -} -#[test] -fn test_unnest_to_sql_2() { - let statement = generate_round_trip_statement( + sql_round_trip( GenericDialect {}, r#"SELECT unnest(make_array(1, 2, 2, 5, NULL)) as u1"#, - ); - assert_snapshot!( - statement, - @r#"SELECT UNNEST([1, 2, 2, 5, NULL]) AS u1"# + r#"SELECT UNNEST([1, 2, 2, 5, NULL]) AS u1"#, ); } #[test] fn test_join_with_no_conditions() { - let statement = generate_round_trip_statement( + sql_round_trip( GenericDialect {}, "SELECT j1.j1_id, j1.j1_string FROM j1 CROSS JOIN j2", - ); - assert_snapshot!( - statement, - @r#"SELECT j1.j1_id, j1.j1_string FROM j1 CROSS JOIN j2"# + "SELECT j1.j1_id, j1.j1_string FROM j1 CROSS JOIN j2", ); } @@ -1977,10 +1562,8 @@ fn test_unparse_extension_to_statement() -> Result<()> { Arc::new(UnusedUnparser {}), ]); let sql = unparser.plan_to_sql(&extension)?; - assert_snapshot!( - sql, - @r#"SELECT j1.j1_id, j1.j1_string FROM j1"# - ); + let expected = "SELECT j1.j1_id, j1.j1_string FROM j1"; + assert_eq!(sql.to_string(), expected); if let Some(err) = plan_to_sql(&extension).err() { assert_contains!( @@ -2042,10 +1625,9 @@ fn test_unparse_extension_to_sql() -> Result<()> { Arc::new(UnusedUnparser {}), ]); let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT j1.j1_id AS user_id FROM (SELECT j1.j1_id, j1.j1_string FROM j1)"# - ); + let expected = + "SELECT j1.j1_id AS user_id FROM (SELECT j1.j1_id, j1.j1_string FROM j1)"; + assert_eq!(sql.to_string(), expected); if let Some(err) = plan_to_sql(&plan).err() { assert_contains!( @@ -2083,10 +1665,10 @@ fn test_unparse_optimized_multi_union() -> Result<()> { ], schema: dfschema.clone(), }); - assert_snapshot!( - unparser.plan_to_sql(&plan)?, - @r#"SELECT 1 AS x, 'a' AS y UNION ALL SELECT 1 AS x, 'b' AS y UNION ALL SELECT 2 AS x, 'a' AS y UNION ALL SELECT 2 AS x, 'c' AS y"# - ); + + let sql = "SELECT 1 AS x, 'a' AS y UNION ALL SELECT 1 AS x, 'b' AS y UNION ALL SELECT 2 AS x, 'a' AS y UNION ALL SELECT 2 AS x, 'c' AS y"; + + assert_eq!(unparser.plan_to_sql(&plan)?.to_string(), sql); let plan = LogicalPlan::Union(Union { inputs: vec![project( @@ -2164,10 +1746,8 @@ fn test_unparse_subquery_alias_with_table_pushdown() -> Result<()> { let unparser = Unparser::default(); let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT customer_view.c_custkey, customer_view.c_name, customer_view.custkey_plus FROM (SELECT customer.c_custkey, (CAST(customer.c_custkey AS BIGINT) + 1) AS custkey_plus, customer.c_name FROM (SELECT customer.c_custkey, customer.c_name FROM customer AS customer) AS customer) AS customer_view"# - ); + let expected = "SELECT customer_view.c_custkey, customer_view.c_name, customer_view.custkey_plus FROM (SELECT customer.c_custkey, (CAST(customer.c_custkey AS BIGINT) + 1) AS custkey_plus, customer.c_name FROM (SELECT customer.c_custkey, customer.c_name FROM customer AS customer) AS customer) AS customer_view"; + assert_eq!(sql.to_string(), expected); Ok(()) } @@ -2198,10 +1778,7 @@ fn test_unparse_left_anti_join() -> Result<()> { let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t1"."d" FROM "t1" WHERE NOT EXISTS (SELECT 1 FROM "t2" AS "__correlated_sq_1" WHERE ("t1"."c" = "__correlated_sq_1"."c"))"# - ); + assert_eq!("SELECT \"t1\".\"d\" FROM \"t1\" WHERE NOT EXISTS (SELECT 1 FROM \"t2\" AS \"__correlated_sq_1\" WHERE (\"t1\".\"c\" = \"__correlated_sq_1\".\"c\"))", sql.to_string()); Ok(()) } @@ -2232,10 +1809,7 @@ fn test_unparse_left_semi_join() -> Result<()> { let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t1"."d" FROM "t1" WHERE EXISTS (SELECT 1 FROM "t2" AS "__correlated_sq_1" WHERE ("t1"."c" = "__correlated_sq_1"."c"))"# - ); + assert_eq!("SELECT \"t1\".\"d\" FROM \"t1\" WHERE EXISTS (SELECT 1 FROM \"t2\" AS \"__correlated_sq_1\" WHERE (\"t1\".\"c\" = \"__correlated_sq_1\".\"c\"))", sql.to_string()); Ok(()) } @@ -2267,10 +1841,7 @@ fn test_unparse_left_mark_join() -> Result<()> { let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t1"."d" FROM "t1" WHERE (EXISTS (SELECT 1 FROM "t2" AS "__correlated_sq_1" WHERE ("t1"."c" = "__correlated_sq_1"."c")) OR ("t1"."d" < 0))"# - ); + assert_eq!("SELECT \"t1\".\"d\" FROM \"t1\" WHERE (EXISTS (SELECT 1 FROM \"t2\" AS \"__correlated_sq_1\" WHERE (\"t1\".\"c\" = \"__correlated_sq_1\".\"c\")) OR (\"t1\".\"d\" < 0))", sql.to_string()); Ok(()) } @@ -2305,10 +1876,7 @@ fn test_unparse_right_semi_join() -> Result<()> { .build()?; let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t2"."c", "t2"."d" FROM "t2" WHERE ("t2"."c" <= 1) AND EXISTS (SELECT 1 FROM "t1" WHERE ("t1"."c" = "t2"."c"))"# - ); + assert_eq!("SELECT \"t2\".\"c\", \"t2\".\"d\" FROM \"t2\" WHERE (\"t2\".\"c\" <= 1) AND EXISTS (SELECT 1 FROM \"t1\" WHERE (\"t1\".\"c\" = \"t2\".\"c\"))", sql.to_string()); Ok(()) } @@ -2343,92 +1911,6 @@ fn test_unparse_right_anti_join() -> Result<()> { .build()?; let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t2"."c", "t2"."d" FROM "t2" WHERE ("t2"."c" <= 1) AND NOT EXISTS (SELECT 1 FROM "t1" WHERE ("t1"."c" = "t2"."c"))"# - ); - Ok(()) -} - -#[test] -fn test_unparse_cross_join_with_table_scan_projection() -> Result<()> { - let schema = Schema::new(vec![ - Field::new("k", DataType::Int32, false), - Field::new("v", DataType::Int32, false), - ]); - // Cross Join: - // SubqueryAlias: t1 - // TableScan: test projection=[v] - // SubqueryAlias: t2 - // TableScan: test projection=[v] - let table_scan1 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; - let table_scan2 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; - let plan = LogicalPlanBuilder::from(subquery_alias(table_scan1, "t1")?) - .cross_join(subquery_alias(table_scan2, "t2")?)? - .build()?; - let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); - let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t1"."v", "t2"."v" FROM "test" AS "t1" CROSS JOIN "test" AS "t2""# - ); - Ok(()) -} - -#[test] -fn test_unparse_inner_join_with_table_scan_projection() -> Result<()> { - let schema = Schema::new(vec![ - Field::new("k", DataType::Int32, false), - Field::new("v", DataType::Int32, false), - ]); - // Inner Join: - // SubqueryAlias: t1 - // TableScan: test projection=[v] - // SubqueryAlias: t2 - // TableScan: test projection=[v] - let table_scan1 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; - let table_scan2 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; - let plan = LogicalPlanBuilder::from(subquery_alias(table_scan1, "t1")?) - .join_on( - subquery_alias(table_scan2, "t2")?, - datafusion_expr::JoinType::Inner, - vec![col("t1.v").eq(col("t2.v"))], - )? - .build()?; - let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); - let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t1"."v", "t2"."v" FROM "test" AS "t1" INNER JOIN "test" AS "t2" ON ("t1"."v" = "t2"."v")"# - ); - Ok(()) -} - -#[test] -fn test_unparse_left_semi_join_with_table_scan_projection() -> Result<()> { - let schema = Schema::new(vec![ - Field::new("k", DataType::Int32, false), - Field::new("v", DataType::Int32, false), - ]); - // LeftSemi Join: - // SubqueryAlias: t1 - // TableScan: test projection=[v] - // SubqueryAlias: t2 - // TableScan: test projection=[v] - let table_scan1 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; - let table_scan2 = table_scan(Some("test"), &schema, Some(vec![1]))?.build()?; - let plan = LogicalPlanBuilder::from(subquery_alias(table_scan1, "t1")?) - .join_on( - subquery_alias(table_scan2, "t2")?, - datafusion_expr::JoinType::LeftSemi, - vec![col("t1.v").eq(col("t2.v"))], - )? - .build()?; - let unparser = Unparser::new(&UnparserPostgreSqlDialect {}); - let sql = unparser.plan_to_sql(&plan)?; - assert_snapshot!( - sql, - @r#"SELECT "t1"."v" FROM "test" AS "t1" WHERE EXISTS (SELECT 1 FROM "test" AS "t2" WHERE ("t1"."v" = "t2"."v"))"# - ); + assert_eq!("SELECT \"t2\".\"c\", \"t2\".\"d\" FROM \"t2\" WHERE (\"t2\".\"c\" <= 1) AND NOT EXISTS (SELECT 1 FROM \"t1\" WHERE (\"t1\".\"c\" = \"t2\".\"c\"))", sql.to_string()); Ok(()) } diff --git a/datafusion/sql/tests/sql_integration.rs b/datafusion/sql/tests/sql_integration.rs index 2804a1de06064..866c08ed0257e 100644 --- a/datafusion/sql/tests/sql_integration.rs +++ b/datafusion/sql/tests/sql_integration.rs @@ -48,7 +48,6 @@ use datafusion_functions_aggregate::{ use datafusion_functions_aggregate::{average::avg_udaf, grouping::grouping_udaf}; use datafusion_functions_nested::make_array::make_array_udf; use datafusion_functions_window::rank::rank_udwf; -use insta::{allow_duplicates, assert_snapshot}; use rstest::rstest; use sqlparser::dialect::{Dialect, GenericDialect, HiveDialect, MySqlDialect}; @@ -56,508 +55,317 @@ mod cases; mod common; #[test] -fn parse_decimals_1() { - let sql = "SELECT 1"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Int64(1) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_2() { - let sql = "SELECT 001"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Int64(1) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_3() { - let sql = "SELECT 0.1"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Decimal128(Some(1),1,1) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_4() { - let sql = "SELECT 0.01"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Decimal128(Some(1),2,2) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_5() { - let sql = "SELECT 1.0"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Decimal128(Some(10),2,1) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_6() { - let sql = "SELECT 10.01"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Decimal128(Some(1001),4,2) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_7() { - let sql = "SELECT 10000000000000000000.00"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Decimal128(Some(1000000000000000000000),22,2) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_8() { - let sql = "SELECT 18446744073709551615"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: UInt64(18446744073709551615) - EmptyRelation - "# - ); -} - -#[test] -fn parse_decimals_9() { - let sql = "SELECT 18446744073709551616"; - let options = parse_decimals_parser_options(); - let plan = logical_plan_with_options(sql, options).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Decimal128(Some(18446744073709551616),20,0) - EmptyRelation - "# - ); -} - -#[test] -fn parse_ident_normalization_1() { - let sql = "SELECT CHARACTER_LENGTH('str')"; - let parser_option = ident_normalization_parser_options_no_ident_normalization(); - let plan = logical_plan_with_options(sql, parser_option).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: character_length(Utf8("str")) - EmptyRelation - "# - ); -} - -#[test] -fn parse_ident_normalization_2() { - let sql = "SELECT CONCAT('Hello', 'World')"; - let parser_option = ident_normalization_parser_options_no_ident_normalization(); - let plan = logical_plan_with_options(sql, parser_option).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: concat(Utf8("Hello"), Utf8("World")) - EmptyRelation - "# - ); -} - -#[test] -fn parse_ident_normalization_3() { - let sql = "SELECT age FROM person"; - let parser_option = ident_normalization_parser_options_ident_normalization(); - let plan = logical_plan_with_options(sql, parser_option).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.age - TableScan: person - "# - ); -} - -#[test] -fn parse_ident_normalization_4() { - let sql = "SELECT AGE FROM PERSON"; - let parser_option = ident_normalization_parser_options_ident_normalization(); - let plan = logical_plan_with_options(sql, parser_option).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.age - TableScan: person - "# - ); -} - -#[test] -fn parse_ident_normalization_5() { - let sql = "SELECT AGE FROM PERSON"; - let parser_option = ident_normalization_parser_options_no_ident_normalization(); - let plan = logical_plan_with_options(sql, parser_option) - .unwrap_err() - .strip_backtrace(); - assert_snapshot!( - plan, - @r#" - Error during planning: No table named: PERSON found - "# - ); +fn parse_decimals() { + let test_data = [ + ("1", "Int64(1)"), + ("001", "Int64(1)"), + ("0.1", "Decimal128(Some(1),1,1)"), + ("0.01", "Decimal128(Some(1),2,2)"), + ("1.0", "Decimal128(Some(10),2,1)"), + ("10.01", "Decimal128(Some(1001),4,2)"), + ( + "10000000000000000000.00", + "Decimal128(Some(1000000000000000000000),22,2)", + ), + ("18446744073709551615", "UInt64(18446744073709551615)"), + ( + "18446744073709551616", + "Decimal128(Some(18446744073709551616),20,0)", + ), + ]; + for (a, b) in test_data { + let sql = format!("SELECT {a}"); + let expected = format!("Projection: {b}\n EmptyRelation"); + quick_test_with_options( + &sql, + &expected, + ParserOptions { + parse_float_as_decimal: true, + enable_ident_normalization: false, + support_varchar_with_length: false, + map_varchar_to_utf8view: false, + enable_options_value_normalization: false, + collect_spans: false, + }, + ); + } } #[test] -fn parse_ident_normalization_6() { - let sql = "SELECT Id FROM UPPERCASE_test"; - let parser_option = ident_normalization_parser_options_no_ident_normalization(); - let plan = logical_plan_with_options(sql, parser_option).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: UPPERCASE_test.Id - TableScan: UPPERCASE_test - "# - ); -} +fn parse_ident_normalization() { + let test_data = [ + ( + "SELECT CHARACTER_LENGTH('str')", + "Ok(Projection: character_length(Utf8(\"str\"))\n EmptyRelation)", + false, + ), + ( + "SELECT CONCAT('Hello', 'World')", + "Ok(Projection: concat(Utf8(\"Hello\"), Utf8(\"World\"))\n EmptyRelation)", + false, + ), + ( + "SELECT age FROM person", + "Ok(Projection: person.age\n TableScan: person)", + true, + ), + ( + "SELECT AGE FROM PERSON", + "Ok(Projection: person.age\n TableScan: person)", + true, + ), + ( + "SELECT AGE FROM PERSON", + "Error during planning: No table named: PERSON found", + false, + ), + ( + "SELECT Id FROM UPPERCASE_test", + "Ok(Projection: UPPERCASE_test.Id\ + \n TableScan: UPPERCASE_test)", + false, + ), + ( + "SELECT \"Id\", lower FROM \"UPPERCASE_test\"", + "Ok(Projection: UPPERCASE_test.Id, UPPERCASE_test.lower\ + \n TableScan: UPPERCASE_test)", + true, + ), + ]; -#[test] -fn parse_ident_normalization_7() { - let sql = r#"SELECT "Id", lower FROM "UPPERCASE_test""#; - let parser_option = ident_normalization_parser_options_ident_normalization(); - let plan = logical_plan_with_options(sql, parser_option).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: UPPERCASE_test.Id, UPPERCASE_test.lower - TableScan: UPPERCASE_test - "# - ); + for (sql, expected, enable_ident_normalization) in test_data { + let plan = logical_plan_with_options( + sql, + ParserOptions { + parse_float_as_decimal: false, + enable_ident_normalization, + support_varchar_with_length: false, + map_varchar_to_utf8view: false, + enable_options_value_normalization: false, + collect_spans: false, + }, + ); + if plan.is_ok() { + let plan = plan.unwrap(); + assert_eq!(expected, format!("Ok({plan})")); + } else { + assert_eq!(expected, plan.unwrap_err().strip_backtrace()); + } + } } #[test] fn select_no_relation() { - let plan = logical_plan("SELECT 1").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: Int64(1) - EmptyRelation - "# + quick_test( + "SELECT 1", + "Projection: Int64(1)\ + \n EmptyRelation", ); } #[test] fn test_real_f32() { - let plan = logical_plan("SELECT CAST(1.1 AS REAL)").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: CAST(Float64(1.1) AS Float32) - EmptyRelation - "# + quick_test( + "SELECT CAST(1.1 AS REAL)", + "Projection: CAST(Float64(1.1) AS Float32)\ + \n EmptyRelation", ); } #[test] fn test_int_decimal_default() { - let plan = logical_plan("SELECT CAST(10 AS DECIMAL)").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: CAST(Int64(10) AS Decimal128(38, 10)) - EmptyRelation - "# + quick_test( + "SELECT CAST(10 AS DECIMAL)", + "Projection: CAST(Int64(10) AS Decimal128(38, 10))\ + \n EmptyRelation", ); } #[test] fn test_int_decimal_no_scale() { - let plan = logical_plan("SELECT CAST(10 AS DECIMAL(5))").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: CAST(Int64(10) AS Decimal128(5, 0)) - EmptyRelation - "# + quick_test( + "SELECT CAST(10 AS DECIMAL(5))", + "Projection: CAST(Int64(10) AS Decimal128(5, 0))\ + \n EmptyRelation", ); } #[test] fn test_tinyint() { - let plan = logical_plan("SELECT CAST(6 AS TINYINT)").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: CAST(Int64(6) AS Int8) - EmptyRelation - "# + quick_test( + "SELECT CAST(6 AS TINYINT)", + "Projection: CAST(Int64(6) AS Int8)\ + \n EmptyRelation", ); } #[test] fn cast_from_subquery() { - let plan = logical_plan("SELECT CAST (a AS FLOAT) FROM (SELECT 1 AS a)").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: CAST(a AS Float32) - Projection: Int64(1) AS a - EmptyRelation - "# + quick_test( + "SELECT CAST (a AS FLOAT) FROM (SELECT 1 AS a)", + "Projection: CAST(a AS Float32)\ + \n Projection: Int64(1) AS a\ + \n EmptyRelation", ); } #[test] fn try_cast_from_aggregation() { - let plan = logical_plan("SELECT TRY_CAST(sum(age) AS FLOAT) FROM person").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: TRY_CAST(sum(person.age) AS Float32) - Aggregate: groupBy=[[]], aggr=[[sum(person.age)]] - TableScan: person - "# + quick_test( + "SELECT TRY_CAST(sum(age) AS FLOAT) FROM person", + "Projection: TRY_CAST(sum(person.age) AS Float32)\ + \n Aggregate: groupBy=[[]], aggr=[[sum(person.age)]]\ + \n TableScan: person", ); } #[test] fn cast_to_invalid_decimal_type_precision_0() { // precision == 0 - let sql = "SELECT CAST(10 AS DECIMAL(0))"; - let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r"Error during planning: Decimal(precision = 0, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`." - ); + { + let sql = "SELECT CAST(10 AS DECIMAL(0))"; + let err = logical_plan(sql).expect_err("query should have failed"); + assert_eq!( + "Error during planning: Decimal(precision = 0, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`.", + err.strip_backtrace() + ); + } } #[test] fn cast_to_invalid_decimal_type_precision_gt_38() { // precision > 38 - let sql = "SELECT CAST(10 AS DECIMAL(39))"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: CAST(Int64(10) AS Decimal256(39, 0)) - EmptyRelation - "# - ); + { + let sql = "SELECT CAST(10 AS DECIMAL(39))"; + let plan = "Projection: CAST(Int64(10) AS Decimal256(39, 0))\n EmptyRelation"; + quick_test(sql, plan); + } } #[test] fn cast_to_invalid_decimal_type_precision_gt_76() { // precision > 76 - let sql = "SELECT CAST(10 AS DECIMAL(79))"; - let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r"Error during planning: Decimal(precision = 79, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`." - ); + { + let sql = "SELECT CAST(10 AS DECIMAL(79))"; + let err = logical_plan(sql).expect_err("query should have failed"); + assert_eq!( + "Error during planning: Decimal(precision = 79, scale = 0) should satisfy `0 < precision <= 76`, and `scale <= precision`.", + err.strip_backtrace() + ); + } } #[test] fn cast_to_invalid_decimal_type_precision_lt_scale() { // precision < scale - let sql = "SELECT CAST(10 AS DECIMAL(5, 10))"; - let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r"Error during planning: Decimal(precision = 5, scale = 10) should satisfy `0 < precision <= 76`, and `scale <= precision`." - ); + { + let sql = "SELECT CAST(10 AS DECIMAL(5, 10))"; + let err = logical_plan(sql).expect_err("query should have failed"); + assert_eq!( + "Error during planning: Decimal(precision = 5, scale = 10) should satisfy `0 < precision <= 76`, and `scale <= precision`.", + err.strip_backtrace() + ); + } } #[test] fn plan_create_table_with_pk() { let sql = "create table person (id int, name string, primary key(id))"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] - EmptyRelation - "# - ); + let plan = r#" +CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] + EmptyRelation + "# + .trim(); + quick_test(sql, plan); let sql = "create table person (id int primary key, name string)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] - EmptyRelation - "# - ); + let plan = r#" +CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0])] + EmptyRelation + "# + .trim(); + quick_test(sql, plan); let sql = "create table person (id int, name string unique not null, primary key(id))"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), Unique([1])] - EmptyRelation - "# - ); + let plan = r#" +CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), Unique([1])] + EmptyRelation + "# + .trim(); + quick_test(sql, plan); let sql = "create table person (id int, name varchar, primary key(name, id));"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([1, 0])] - EmptyRelation - "# - ); + let plan = r#" +CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([1, 0])] + EmptyRelation + "# + .trim(); + quick_test(sql, plan); } #[test] fn plan_create_table_with_multi_pk() { let sql = "create table person (id int, name string primary key, primary key(id))"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), PrimaryKey([1])] - EmptyRelation - "# - ); + let plan = r#" +CreateMemoryTable: Bare { table: "person" } constraints=[PrimaryKey([0]), PrimaryKey([1])] + EmptyRelation + "# + .trim(); + quick_test(sql, plan); } #[test] fn plan_create_table_with_unique() { let sql = "create table person (id int unique, name string)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } constraints=[Unique([0])] - EmptyRelation - "# - ); + let plan = "CreateMemoryTable: Bare { table: \"person\" } constraints=[Unique([0])]\n EmptyRelation"; + quick_test(sql, plan); } #[test] fn plan_create_table_no_pk() { let sql = "create table person (id int, name string)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } - EmptyRelation - "# - ); + let plan = r#" +CreateMemoryTable: Bare { table: "person" } + EmptyRelation + "# + .trim(); + quick_test(sql, plan); } #[test] fn plan_create_table_check_constraint() { let sql = "create table person (id int, name string, unique(id))"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateMemoryTable: Bare { table: "person" } constraints=[Unique([0])] - EmptyRelation - "# - ); + let plan = "CreateMemoryTable: Bare { table: \"person\" } constraints=[Unique([0])]\n EmptyRelation"; + quick_test(sql, plan); } #[test] fn plan_start_transaction() { let sql = "start transaction"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionStart: ReadWrite Serializable - "# - ); + let plan = "TransactionStart: ReadWrite Serializable"; + quick_test(sql, plan); } #[test] fn plan_start_transaction_isolation() { let sql = "start transaction isolation level read committed"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionStart: ReadWrite ReadCommitted - "# - ); + let plan = "TransactionStart: ReadWrite ReadCommitted"; + quick_test(sql, plan); } #[test] fn plan_start_transaction_read_only() { let sql = "start transaction read only"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionStart: ReadOnly Serializable - "# - ); + let plan = "TransactionStart: ReadOnly Serializable"; + quick_test(sql, plan); } #[test] fn plan_start_transaction_fully_qualified() { let sql = "start transaction isolation level read committed read only"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionStart: ReadOnly ReadCommitted - "# - ); + let plan = "TransactionStart: ReadOnly ReadCommitted"; + quick_test(sql, plan); } #[test] @@ -567,131 +375,95 @@ isolation level read committed read only isolation level repeatable read "#; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionStart: ReadOnly RepeatableRead - "# - ); + let plan = "TransactionStart: ReadOnly RepeatableRead"; + quick_test(sql, plan); } #[test] fn plan_commit_transaction() { let sql = "commit transaction"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionEnd: Commit chain:=false - "# - ); + let plan = "TransactionEnd: Commit chain:=false"; + quick_test(sql, plan); } #[test] fn plan_commit_transaction_chained() { let sql = "commit transaction and chain"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionEnd: Commit chain:=true - "# - ); + let plan = "TransactionEnd: Commit chain:=true"; + quick_test(sql, plan); } #[test] fn plan_rollback_transaction() { let sql = "rollback transaction"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionEnd: Rollback chain:=false - "# - ); + let plan = "TransactionEnd: Rollback chain:=false"; + quick_test(sql, plan); } #[test] fn plan_rollback_transaction_chained() { let sql = "rollback transaction and chain"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - TransactionEnd: Rollback chain:=true - "# - ); + let plan = "TransactionEnd: Rollback chain:=true"; + quick_test(sql, plan); } #[test] fn plan_copy_to() { let sql = "COPY test_decimal to 'output.csv' STORED AS CSV"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CopyTo: format=csv output_url=output.csv options: () - TableScan: test_decimal - "# - ); + let plan = r#" +CopyTo: format=csv output_url=output.csv options: () + TableScan: test_decimal + "# + .trim(); + quick_test(sql, plan); } #[test] fn plan_explain_copy_to() { let sql = "EXPLAIN COPY test_decimal to 'output.csv'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Explain - CopyTo: format=csv output_url=output.csv options: () - TableScan: test_decimal - "# - ); + let plan = r#" +Explain + CopyTo: format=csv output_url=output.csv options: () + TableScan: test_decimal + "# + .trim(); + quick_test(sql, plan); } #[test] fn plan_explain_copy_to_format() { let sql = "EXPLAIN COPY test_decimal to 'output.tbl' STORED AS CSV"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Explain - CopyTo: format=csv output_url=output.tbl options: () - TableScan: test_decimal - "# - ); + let plan = r#" +Explain + CopyTo: format=csv output_url=output.tbl options: () + TableScan: test_decimal + "# + .trim(); + quick_test(sql, plan); } #[test] fn plan_insert() { let sql = "insert into person (id, first_name, last_name) values (1, 'Alan', 'Turing')"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Dml: op=[Insert Into] table=[person] - Projection: column1 AS id, column2 AS first_name, column3 AS last_name, CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀 - Values: (CAST(Int64(1) AS UInt32), Utf8("Alan"), Utf8("Turing")) - "# - ); + let plan = "Dml: op=[Insert Into] table=[person]\ + \n Projection: column1 AS id, column2 AS first_name, column3 AS last_name, \ + CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, \ + CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀\ + \n Values: (CAST(Int64(1) AS UInt32), Utf8(\"Alan\"), Utf8(\"Turing\"))"; + quick_test(sql, plan); } #[test] fn plan_insert_no_target_columns() { let sql = "INSERT INTO test_decimal VALUES (1, 2), (3, 4)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Dml: op=[Insert Into] table=[test_decimal] - Projection: column1 AS id, column2 AS price - Values: (CAST(Int64(1) AS Int32), CAST(Int64(2) AS Decimal128(10, 2))), (CAST(Int64(3) AS Int32), CAST(Int64(4) AS Decimal128(10, 2))) - "# - ); + let plan = r#" +Dml: op=[Insert Into] table=[test_decimal] + Projection: column1 AS id, column2 AS price + Values: (CAST(Int64(1) AS Int32), CAST(Int64(2) AS Decimal128(10, 2))), (CAST(Int64(3) AS Int32), CAST(Int64(4) AS Decimal128(10, 2))) + "# + .trim(); + quick_test(sql, plan); } #[rstest] @@ -729,16 +501,14 @@ fn test_insert_schema_errors(#[case] sql: &str, #[case] error: &str) { #[test] fn plan_update() { let sql = "update person set last_name='Kay' where id=1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Dml: op=[Update] table=[person] - Projection: person.id AS id, person.first_name AS first_name, Utf8("Kay") AS last_name, person.age AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 - Filter: person.id = Int64(1) - TableScan: person - "# - ); + let plan = r#" +Dml: op=[Update] table=[person] + Projection: person.id AS id, person.first_name AS first_name, Utf8("Kay") AS last_name, person.age AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 + Filter: person.id = Int64(1) + TableScan: person + "# + .trim(); + quick_test(sql, plan); } #[rstest] @@ -756,30 +526,26 @@ fn update_column_does_not_exist(#[case] sql: &str) { #[test] fn plan_delete() { let sql = "delete from person where id=1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Dml: op=[Delete] table=[person] - Filter: id = Int64(1) - TableScan: person - "# - ); + let plan = r#" +Dml: op=[Delete] table=[person] + Filter: id = Int64(1) + TableScan: person + "# + .trim(); + quick_test(sql, plan); } #[test] fn plan_delete_quoted_identifier_case_sensitive() { let sql = "DELETE FROM \"SomeCatalog\".\"SomeSchema\".\"UPPERCASE_test\" WHERE \"Id\" = 1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Dml: op=[Delete] table=[SomeCatalog.SomeSchema.UPPERCASE_test] - Filter: Id = Int64(1) - TableScan: SomeCatalog.SomeSchema.UPPERCASE_test - "# - ); + let plan = r#" +Dml: op=[Delete] table=[SomeCatalog.SomeSchema.UPPERCASE_test] + Filter: Id = Int64(1) + TableScan: SomeCatalog.SomeSchema.UPPERCASE_test + "# + .trim(); + quick_test(sql, plan); } #[test] @@ -793,24 +559,18 @@ fn select_column_does_not_exist() { fn select_repeated_column() { let sql = "SELECT age, age FROM person"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Projections require unique expression names but the expression "person.age" at position 0 and "person.age" at position 1 have the same name. Consider aliasing ("AS") one of them. - "# + assert_eq!( + "Error during planning: Projections require unique expression names but the expression \"person.age\" at position 0 and \"person.age\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", + err.strip_backtrace() ); } #[test] fn select_scalar_func_with_literal_no_relation() { - let plan = logical_plan("SELECT sqrt(9)").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: sqrt(Int64(9)) - EmptyRelation - "# + quick_test( + "SELECT sqrt(9)", + "Projection: sqrt(Int64(9))\ + \n EmptyRelation", ); } @@ -818,15 +578,10 @@ fn select_scalar_func_with_literal_no_relation() { fn select_simple_filter() { let sql = "SELECT id, first_name, last_name \ FROM person WHERE state = 'CO'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.id, person.first_name, person.last_name - Filter: person.state = Utf8("CO") - TableScan: person - "# - ); + let expected = "Projection: person.id, person.first_name, person.last_name\ + \n Filter: person.state = Utf8(\"CO\")\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -847,58 +602,40 @@ fn select_filter_cannot_use_alias() { fn select_neg_filter() { let sql = "SELECT id, first_name, last_name \ FROM person WHERE NOT state"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.id, person.first_name, person.last_name - Filter: NOT person.state - TableScan: person - "# - ); + let expected = "Projection: person.id, person.first_name, person.last_name\ + \n Filter: NOT person.state\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_compound_filter() { let sql = "SELECT id, first_name, last_name \ FROM person WHERE state = 'CO' AND age >= 21 AND age <= 65"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.id, person.first_name, person.last_name - Filter: person.state = Utf8("CO") AND person.age >= Int64(21) AND person.age <= Int64(65) - TableScan: person - "# - ); + let expected = "Projection: person.id, person.first_name, person.last_name\ + \n Filter: person.state = Utf8(\"CO\") AND person.age >= Int64(21) AND person.age <= Int64(65)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn test_timestamp_filter() { let sql = "SELECT state FROM person WHERE birth_date < CAST (158412331400600000 as timestamp)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state - Filter: person.birth_date < CAST(CAST(Int64(158412331400600000) AS Timestamp(Second, None)) AS Timestamp(Nanosecond, None)) - TableScan: person - "# - ); + let expected = "Projection: person.state\ + \n Filter: person.birth_date < CAST(CAST(Int64(158412331400600000) AS Timestamp(Second, None)) AS Timestamp(Nanosecond, None))\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn test_date_filter() { let sql = "SELECT state FROM person WHERE birth_date < CAST ('2020-01-01' as date)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state - Filter: person.birth_date < CAST(Utf8("2020-01-01") AS Date32) - TableScan: person - "# - ); + + let expected = "Projection: person.state\ + \n Filter: person.birth_date < CAST(Utf8(\"2020-01-01\") AS Date32)\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] @@ -911,43 +648,35 @@ fn select_all_boolean_operators() { AND age >= 21 \ AND age < 65 \ AND age <= 65"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.age, person.first_name, person.last_name - Filter: person.age = Int64(21) AND person.age != Int64(21) AND person.age > Int64(21) AND person.age >= Int64(21) AND person.age < Int64(65) AND person.age <= Int64(65) - TableScan: person - "# - ); + let expected = "Projection: person.age, person.first_name, person.last_name\ + \n Filter: person.age = Int64(21) \ + AND person.age != Int64(21) \ + AND person.age > Int64(21) \ + AND person.age >= Int64(21) \ + AND person.age < Int64(65) \ + AND person.age <= Int64(65)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_between() { let sql = "SELECT state FROM person WHERE age BETWEEN 21 AND 65"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state - Filter: person.age BETWEEN Int64(21) AND Int64(65) - TableScan: person - "# - ); + let expected = "Projection: person.state\ + \n Filter: person.age BETWEEN Int64(21) AND Int64(65)\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] fn select_between_negated() { let sql = "SELECT state FROM person WHERE age NOT BETWEEN 21 AND 65"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state - Filter: person.age NOT BETWEEN Int64(21) AND Int64(65) - TableScan: person - "# - ); + let expected = "Projection: person.state\ + \n Filter: person.age NOT BETWEEN Int64(21) AND Int64(65)\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] @@ -960,18 +689,13 @@ fn select_nested() { FROM person ) AS a ) AS b"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: b.fn2, b.last_name - SubqueryAlias: b - Projection: a.fn1 AS fn2, a.last_name, a.birth_date - SubqueryAlias: a - Projection: person.first_name AS fn1, person.last_name, person.birth_date, person.age - TableScan: person - "# - ); + let expected = "Projection: b.fn2, b.last_name\ + \n SubqueryAlias: b\ + \n Projection: a.fn1 AS fn2, a.last_name, a.birth_date\ + \n SubqueryAlias: a\ + \n Projection: person.first_name AS fn1, person.last_name, person.birth_date, person.age\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -983,34 +707,27 @@ fn select_nested_with_filters() { WHERE age > 20 ) AS a WHERE fn1 = 'X' AND age < 30"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: a.fn1, a.age - Filter: a.fn1 = Utf8("X") AND a.age < Int64(30) - SubqueryAlias: a - Projection: person.first_name AS fn1, person.age - Filter: person.age > Int64(20) - TableScan: person - "# - ); + + let expected = "Projection: a.fn1, a.age\ + \n Filter: a.fn1 = Utf8(\"X\") AND a.age < Int64(30)\ + \n SubqueryAlias: a\ + \n Projection: person.first_name AS fn1, person.age\ + \n Filter: person.age > Int64(20)\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] fn table_with_column_alias() { let sql = "SELECT a, b, c FROM lineitem l (a, b, c)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: l.a, l.b, l.c - SubqueryAlias: l - Projection: lineitem.l_item_id AS a, lineitem.l_description AS b, lineitem.price AS c - TableScan: lineitem - "# - ); + let expected = "Projection: l.a, l.b, l.c\ + \n SubqueryAlias: l\ + \n Projection: lineitem.l_item_id AS a, lineitem.l_description AS b, lineitem.price AS c\ + \n TableScan: lineitem"; + + quick_test(sql, expected); } #[test] @@ -1018,10 +735,9 @@ fn table_with_column_alias_number_cols() { let sql = "SELECT a, b, c FROM lineitem l (a, b)"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r"Error during planning: Source table contains 3 columns but only 2 names given as column alias" + assert_eq!( + "Error during planning: Source table contains 3 columns but only 2 names given as column alias", + err.strip_backtrace() ); } @@ -1029,10 +745,9 @@ fn table_with_column_alias_number_cols() { fn select_with_ambiguous_column() { let sql = "SELECT id FROM person a, person b"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r"Schema error: Ambiguous reference to unqualified field id" + assert_eq!( + "Schema error: Ambiguous reference to unqualified field id", + err.strip_backtrace() ); } @@ -1040,52 +755,37 @@ fn select_with_ambiguous_column() { fn join_with_ambiguous_column() { // This is legal. let sql = "SELECT id FROM person a join person b using(id)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: a.id - Inner Join: Using a.id = b.id - SubqueryAlias: a - TableScan: person - SubqueryAlias: b - TableScan: person - "# - ); + let expected = "Projection: a.id\ + \n Inner Join: Using a.id = b.id\ + \n SubqueryAlias: a\ + \n TableScan: person\ + \n SubqueryAlias: b\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn natural_left_join() { let sql = "SELECT l_item_id FROM lineitem a NATURAL LEFT JOIN lineitem b"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: a.l_item_id - Left Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price - SubqueryAlias: a - TableScan: lineitem - SubqueryAlias: b - TableScan: lineitem - "# - ); + let expected = "Projection: a.l_item_id\ + \n Left Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price\ + \n SubqueryAlias: a\ + \n TableScan: lineitem\ + \n SubqueryAlias: b\ + \n TableScan: lineitem"; + quick_test(sql, expected); } #[test] fn natural_right_join() { let sql = "SELECT l_item_id FROM lineitem a NATURAL RIGHT JOIN lineitem b"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: a.l_item_id - Right Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price - SubqueryAlias: a - TableScan: lineitem - SubqueryAlias: b - TableScan: lineitem - "# - ); + let expected = "Projection: a.l_item_id\ + \n Right Join: Using a.l_item_id = b.l_item_id, a.l_description = b.l_description, a.price = b.price\ + \n SubqueryAlias: a\ + \n TableScan: lineitem\ + \n SubqueryAlias: b\ + \n TableScan: lineitem"; + quick_test(sql, expected); } #[test] @@ -1094,11 +794,10 @@ fn select_with_having() { FROM person HAVING age > 100 AND age < 200"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r"Error during planning: HAVING clause references: person.age > Int64(100) AND person.age < Int64(200) must appear in the GROUP BY clause or be used in an aggregate function" - ); + assert_eq!( + "Error during planning: HAVING clause references: person.age > Int64(100) AND person.age < Int64(200) must appear in the GROUP BY clause or be used in an aggregate function", + err.strip_backtrace() + ); } #[test] @@ -1107,13 +806,10 @@ fn select_with_having_referencing_column_not_in_select() { FROM person HAVING first_name = 'M'"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: HAVING clause references: person.first_name = Utf8("M") must appear in the GROUP BY clause or be used in an aggregate function - "# - ); + assert_eq!( + "Error during planning: HAVING clause references: person.first_name = Utf8(\"M\") must appear in the GROUP BY clause or be used in an aggregate function", + err.strip_backtrace() + ); } #[test] @@ -1123,13 +819,10 @@ fn select_with_having_refers_to_invalid_column() { GROUP BY id HAVING first_name = 'M'"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.first_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.id, max(person.age)" appears in the SELECT clause satisfies this requirement - "# - ); + assert_eq!( + "Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.first_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.id, max(person.age)\" appears in the SELECT clause satisfies this requirement", + err.strip_backtrace() + ); } #[test] @@ -1138,13 +831,10 @@ fn select_with_having_referencing_column_nested_in_select_expression() { FROM person HAVING age > 100"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: HAVING clause references: person.age > Int64(100) must appear in the GROUP BY clause or be used in an aggregate function - "# - ); + assert_eq!( + "Error during planning: HAVING clause references: person.age > Int64(100) must appear in the GROUP BY clause or be used in an aggregate function", + err.strip_backtrace() + ); } #[test] @@ -1153,11 +843,10 @@ fn select_with_having_with_aggregate_not_in_select() { FROM person HAVING MAX(age) > 100"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#"Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.first_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "max(person.age)" appears in the SELECT clause satisfies this requirement"# - ); + assert_eq!( + "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.first_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"max(person.age)\" appears in the SELECT clause satisfies this requirement", + err.strip_backtrace() + ); } #[test] @@ -1165,16 +854,11 @@ fn select_aggregate_with_having_that_reuses_aggregate() { let sql = "SELECT MAX(age) FROM person HAVING MAX(age) < 30"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: max(person.age) - Filter: max(person.age) < Int64(30) - Aggregate: groupBy=[[]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: max(person.age)\ + \n Filter: max(person.age) < Int64(30)\ + \n Aggregate: groupBy=[[]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1182,16 +866,11 @@ fn select_aggregate_with_having_with_aggregate_not_in_select() { let sql = "SELECT max(age) FROM person HAVING max(first_name) > 'M'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: max(person.age) - Filter: max(person.first_name) > Utf8("M") - Aggregate: groupBy=[[]], aggr=[[max(person.age), max(person.first_name)]] - TableScan: person - "# - ); + let expected = "Projection: max(person.age)\ + \n Filter: max(person.first_name) > Utf8(\"M\")\ + \n Aggregate: groupBy=[[]], aggr=[[max(person.age), max(person.first_name)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1200,12 +879,9 @@ fn select_aggregate_with_having_referencing_column_not_in_select() { FROM person HAVING first_name = 'M'"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.first_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "count(*)" appears in the SELECT clause satisfies this requirement - "# + assert_eq!( + "Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.first_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"count(*)\" appears in the SELECT clause satisfies this requirement", + err.strip_backtrace() ); } @@ -1215,16 +891,11 @@ fn select_aggregate_aliased_with_having_referencing_aggregate_by_its_alias() { FROM person HAVING max_age < 30"; // FIXME: add test for having in execution - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: max(person.age) AS max_age - Filter: max(person.age) < Int64(30) - Aggregate: groupBy=[[]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: max(person.age) AS max_age\ + \n Filter: max(person.age) < Int64(30)\ + \n Aggregate: groupBy=[[]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1232,16 +903,11 @@ fn select_aggregate_aliased_with_having_that_reuses_aggregate_but_not_by_its_ali let sql = "SELECT max(age) as max_age FROM person HAVING max(age) < 30"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: max(person.age) AS max_age - Filter: max(person.age) < Int64(30) - Aggregate: groupBy=[[]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: max(person.age) AS max_age\ + \n Filter: max(person.age) < Int64(30)\ + \n Aggregate: groupBy=[[]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1250,16 +916,11 @@ fn select_aggregate_with_group_by_with_having() { FROM person GROUP BY first_name HAVING first_name = 'M'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: person.first_name = Utf8("M") - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: person.first_name = Utf8(\"M\")\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1269,17 +930,12 @@ fn select_aggregate_with_group_by_with_having_and_where() { WHERE id > 5 GROUP BY first_name HAVING MAX(age) < 100"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: max(person.age) < Int64(100) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - Filter: person.id > Int64(5) - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: max(person.age) < Int64(100)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n Filter: person.id > Int64(5)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1289,17 +945,12 @@ fn select_aggregate_with_group_by_with_having_and_where_filtering_on_aggregate_c WHERE id > 5 AND age > 18 GROUP BY first_name HAVING MAX(age) < 100"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: max(person.age) < Int64(100) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - Filter: person.id > Int64(5) AND person.age > Int64(18) - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: max(person.age) < Int64(100)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n Filter: person.id > Int64(5) AND person.age > Int64(18)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1308,16 +959,11 @@ fn select_aggregate_with_group_by_with_having_using_column_by_alias() { FROM person GROUP BY first_name HAVING MAX(age) > 2 AND fn = 'M'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name AS fn, max(person.age) - Filter: max(person.age) > Int64(2) AND person.first_name = Utf8("M") - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name AS fn, max(person.age)\ + \n Filter: max(person.age) > Int64(2) AND person.first_name = Utf8(\"M\")\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1327,16 +973,11 @@ fn select_aggregate_with_group_by_with_having_using_columns_with_and_without_the FROM person GROUP BY first_name HAVING MAX(age) > 2 AND max_age < 5 AND first_name = 'M' AND fn = 'N'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name AS fn, max(person.age) AS max_age - Filter: max(person.age) > Int64(2) AND max(person.age) < Int64(5) AND person.first_name = Utf8("M") AND person.first_name = Utf8("N") - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name AS fn, max(person.age) AS max_age\ + \n Filter: max(person.age) > Int64(2) AND max(person.age) < Int64(5) AND person.first_name = Utf8(\"M\") AND person.first_name = Utf8(\"N\")\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1345,16 +986,11 @@ fn select_aggregate_with_group_by_with_having_that_reuses_aggregate() { FROM person GROUP BY first_name HAVING MAX(age) > 100"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: max(person.age) > Int64(100) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: max(person.age) > Int64(100)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1363,13 +999,10 @@ fn select_aggregate_with_group_by_with_having_referencing_column_not_in_group_by FROM person GROUP BY first_name HAVING MAX(age) > 10 AND last_name = 'M'"; - let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.last_name" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.first_name, max(person.age)" appears in the SELECT clause satisfies this requirement - "# + let err = logical_plan(sql).expect_err("query should have failed"); + assert_eq!( + "Error during planning: Column in HAVING must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.last_name\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.first_name, max(person.age)\" appears in the SELECT clause satisfies this requirement", + err.strip_backtrace() ); } @@ -1379,16 +1012,11 @@ fn select_aggregate_with_group_by_with_having_that_reuses_aggregate_multiple_tim FROM person GROUP BY first_name HAVING MAX(age) > 100 AND MAX(age) < 200"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: max(person.age) > Int64(100) AND max(person.age) < Int64(200) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: max(person.age) > Int64(100) AND max(person.age) < Int64(200)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1397,16 +1025,11 @@ fn select_aggregate_with_group_by_with_having_using_aggregate_not_in_select() { FROM person GROUP BY first_name HAVING MAX(age) > 100 AND MIN(id) < 50"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: max(person.age) > Int64(100) AND min(person.id) < Int64(50) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: max(person.age) > Int64(100) AND min(person.id) < Int64(50)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1416,16 +1039,11 @@ fn select_aggregate_aliased_with_group_by_with_having_referencing_aggregate_by_i FROM person GROUP BY first_name HAVING max_age > 100"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) AS max_age - Filter: max(person.age) > Int64(100) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age) AS max_age\ + \n Filter: max(person.age) > Int64(100)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1435,16 +1053,11 @@ fn select_aggregate_compound_aliased_with_group_by_with_having_referencing_compo FROM person GROUP BY first_name HAVING max_age_plus_one > 100"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) + Int64(1) AS max_age_plus_one - Filter: max(person.age) + Int64(1) > Int64(100) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age) + Int64(1) AS max_age_plus_one\ + \n Filter: max(person.age) + Int64(1) > Int64(100)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1454,16 +1067,11 @@ fn select_aggregate_with_group_by_with_having_using_derived_column_aggregate_not FROM person GROUP BY first_name HAVING MAX(age) > 100 AND MIN(id - 2) < 50"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: max(person.age) > Int64(100) AND min(person.id - Int64(2)) < Int64(50) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id - Int64(2))]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: max(person.age) > Int64(100) AND min(person.id - Int64(2)) < Int64(50)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), min(person.id - Int64(2))]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -1472,67 +1080,46 @@ fn select_aggregate_with_group_by_with_having_using_count_star_not_in_select() { FROM person GROUP BY first_name HAVING MAX(age) > 100 AND count(*) < 50"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.first_name, max(person.age) - Filter: max(person.age) > Int64(100) AND count(*) < Int64(50) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), count(*)]] - TableScan: person - "# - ); + let expected = "Projection: person.first_name, max(person.age)\ + \n Filter: max(person.age) > Int64(100) AND count(*) < Int64(50)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.age), count(*)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_binary_expr() { let sql = "SELECT age + salary from person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.age + person.salary - TableScan: person - "# - ); + let expected = "Projection: person.age + person.salary\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_binary_expr_nested() { let sql = "SELECT (age + salary)/2 from person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: (person.age + person.salary) / Int64(2) - TableScan: person - "# - ); + let expected = "Projection: (person.age + person.salary) / Int64(2)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_simple_aggregate() { - let plan = logical_plan("SELECT MIN(age) FROM person").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: min(person.age) - Aggregate: groupBy=[[]], aggr=[[min(person.age)]] - TableScan: person - "# + quick_test( + "SELECT MIN(age) FROM person", + "Projection: min(person.age)\ + \n Aggregate: groupBy=[[]], aggr=[[min(person.age)]]\ + \n TableScan: person", ); } #[test] fn test_sum_aggregate() { - let plan = logical_plan("SELECT sum(age) from person").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: sum(person.age) - Aggregate: groupBy=[[]], aggr=[[sum(person.age)]] - TableScan: person - "# + quick_test( + "SELECT sum(age) from person", + "Projection: sum(person.age)\ + \n Aggregate: groupBy=[[]], aggr=[[sum(person.age)]]\ + \n TableScan: person", ); } @@ -1547,97 +1134,70 @@ fn select_simple_aggregate_column_does_not_exist() { fn select_simple_aggregate_repeated_aggregate() { let sql = "SELECT MIN(age), MIN(age) FROM person"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Projections require unique expression names but the expression "min(person.age)" at position 0 and "min(person.age)" at position 1 have the same name. Consider aliasing ("AS") one of them. - "# + assert_eq!( + "Error during planning: Projections require unique expression names but the expression \"min(person.age)\" at position 0 and \"min(person.age)\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", + err.strip_backtrace() ); } #[test] fn select_simple_aggregate_repeated_aggregate_with_single_alias() { - let plan = logical_plan("SELECT MIN(age), MIN(age) AS a FROM person").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: min(person.age), min(person.age) AS a - Aggregate: groupBy=[[]], aggr=[[min(person.age)]] - TableScan: person - "# + quick_test( + "SELECT MIN(age), MIN(age) AS a FROM person", + "Projection: min(person.age), min(person.age) AS a\ + \n Aggregate: groupBy=[[]], aggr=[[min(person.age)]]\ + \n TableScan: person", ); } #[test] fn select_simple_aggregate_repeated_aggregate_with_unique_aliases() { - let plan = logical_plan("SELECT MIN(age) AS a, MIN(age) AS b FROM person").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: min(person.age) AS a, min(person.age) AS b - Aggregate: groupBy=[[]], aggr=[[min(person.age)]] - TableScan: person - "# + quick_test( + "SELECT MIN(age) AS a, MIN(age) AS b FROM person", + "Projection: min(person.age) AS a, min(person.age) AS b\ + \n Aggregate: groupBy=[[]], aggr=[[min(person.age)]]\ + \n TableScan: person", ); } #[test] fn select_from_typed_string_values() { - let plan = logical_plan( - "SELECT col1, col2 FROM (VALUES (TIMESTAMP '2021-06-10 17:01:00Z', DATE '2004-04-09')) as t (col1, col2)", - ).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: t.col1, t.col2 - SubqueryAlias: t - Projection: column1 AS col1, column2 AS col2 - Values: (CAST(Utf8("2021-06-10 17:01:00Z") AS Timestamp(Nanosecond, None)), CAST(Utf8("2004-04-09") AS Date32)) - "# - ); + quick_test( + "SELECT col1, col2 FROM (VALUES (TIMESTAMP '2021-06-10 17:01:00Z', DATE '2004-04-09')) as t (col1, col2)", + "Projection: t.col1, t.col2\ + \n SubqueryAlias: t\ + \n Projection: column1 AS col1, column2 AS col2\ + \n Values: (CAST(Utf8(\"2021-06-10 17:01:00Z\") AS Timestamp(Nanosecond, None)), CAST(Utf8(\"2004-04-09\") AS Date32))", + ); } #[test] fn select_simple_aggregate_repeated_aggregate_with_repeated_aliases() { let sql = "SELECT MIN(age) AS a, MIN(age) AS a FROM person"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Projections require unique expression names but the expression "min(person.age) AS a" at position 0 and "min(person.age) AS a" at position 1 have the same name. Consider aliasing ("AS") one of them. - "# + assert_eq!( + "Error during planning: Projections require unique expression names but the expression \"min(person.age) AS a\" at position 0 and \"min(person.age) AS a\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", + err.strip_backtrace() ); } #[test] fn select_simple_aggregate_with_groupby() { - let plan = - logical_plan("SELECT state, MIN(age), MAX(age) FROM person GROUP BY state") - .unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state, min(person.age), max(person.age) - Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]] - TableScan: person - "# + quick_test( + "SELECT state, MIN(age), MAX(age) FROM person GROUP BY state", + "Projection: person.state, min(person.age), max(person.age)\ + \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]]\ + \n TableScan: person", ); } #[test] fn select_simple_aggregate_with_groupby_with_aliases() { - let plan = - logical_plan("SELECT state AS a, MIN(age) AS b FROM person GROUP BY state") - .unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state AS a, min(person.age) AS b - Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] - TableScan: person - "# + quick_test( + "SELECT state AS a, MIN(age) AS b FROM person GROUP BY state", + "Projection: person.state AS a, min(person.age) AS b\ + \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ + \n TableScan: person", ); } @@ -1645,26 +1205,19 @@ fn select_simple_aggregate_with_groupby_with_aliases() { fn select_simple_aggregate_with_groupby_with_aliases_repeated() { let sql = "SELECT state AS a, MIN(age) AS a FROM person GROUP BY state"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Projections require unique expression names but the expression "person.state AS a" at position 0 and "min(person.age) AS a" at position 1 have the same name. Consider aliasing ("AS") one of them. - "# + assert_eq!( + "Error during planning: Projections require unique expression names but the expression \"person.state AS a\" at position 0 and \"min(person.age) AS a\" at position 1 have the same name. Consider aliasing (\"AS\") one of them.", + err.strip_backtrace() ); } #[test] fn select_simple_aggregate_with_groupby_column_unselected() { - let plan = - logical_plan("SELECT MIN(age), MAX(age) FROM person GROUP BY state").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: min(person.age), max(person.age) - Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]] - TableScan: person - "# + quick_test( + "SELECT MIN(age), MAX(age) FROM person GROUP BY state", + "Projection: min(person.age), max(person.age)\ + \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age), max(person.age)]]\ + \n TableScan: person", ); } @@ -1672,13 +1225,11 @@ fn select_simple_aggregate_with_groupby_column_unselected() { fn select_simple_aggregate_with_groupby_and_column_in_group_by_does_not_exist() { let sql = "SELECT sum(age) FROM person GROUP BY doesnotexist"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Schema error: No field named doesnotexist. Valid fields are "sum(person.age)", person.id, person.first_name, person.last_name, person.age, person.state, person.salary, person.birth_date, person."😀". - "# - ); + let expected = "Schema error: No field named doesnotexist. \ + Valid fields are \"sum(person.age)\", \ + person.id, person.first_name, person.last_name, person.age, person.state, \ + person.salary, person.birth_date, person.\"😀\"."; + assert_eq!(err.strip_backtrace(), expected); } #[test] @@ -1692,50 +1243,35 @@ fn select_simple_aggregate_with_groupby_and_column_in_aggregate_does_not_exist() fn select_interval_out_of_range() { let sql = "SELECT INTERVAL '100000000000000000 day'"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( + assert_eq!( + "Arrow error: Invalid argument error: Unable to represent 100000000000000000 days in a signed 32-bit integer", err.strip_backtrace(), - @r#" - Arrow error: Invalid argument error: Unable to represent 100000000000000000 days in a signed 32-bit integer - "# ); } #[test] fn select_simple_aggregate_with_groupby_and_column_is_in_aggregate_and_groupby() { - let plan = - logical_plan("SELECT MAX(first_name) FROM person GROUP BY first_name").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: max(person.first_name) - Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.first_name)]] - TableScan: person - "# + quick_test( + "SELECT MAX(first_name) FROM person GROUP BY first_name", + "Projection: max(person.first_name)\ + \n Aggregate: groupBy=[[person.first_name]], aggr=[[max(person.first_name)]]\ + \n TableScan: person", ); } #[test] fn select_simple_aggregate_with_groupby_can_use_positions() { - let plan = logical_plan("SELECT state, age AS b, count(1) FROM person GROUP BY 1, 2") - .unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state, person.age AS b, count(Int64(1)) - Aggregate: groupBy=[[person.state, person.age]], aggr=[[count(Int64(1))]] - TableScan: person - "# + quick_test( + "SELECT state, age AS b, count(1) FROM person GROUP BY 1, 2", + "Projection: person.state, person.age AS b, count(Int64(1))\ + \n Aggregate: groupBy=[[person.state, person.age]], aggr=[[count(Int64(1))]]\ + \n TableScan: person", ); - let plan = logical_plan("SELECT state, age AS b, count(1) FROM person GROUP BY 2, 1") - .unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state, person.age AS b, count(Int64(1)) - Aggregate: groupBy=[[person.age, person.state]], aggr=[[count(Int64(1))]] - TableScan: person - "# + quick_test( + "SELECT state, age AS b, count(1) FROM person GROUP BY 2, 1", + "Projection: person.state, person.age AS b, count(Int64(1))\ + \n Aggregate: groupBy=[[person.age, person.state]], aggr=[[count(Int64(1))]]\ + \n TableScan: person", ); } @@ -1743,36 +1279,26 @@ fn select_simple_aggregate_with_groupby_can_use_positions() { fn select_simple_aggregate_with_groupby_position_out_of_range() { let sql = "SELECT state, MIN(age) FROM person GROUP BY 0"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Cannot find column with position 0 in SELECT clause. Valid columns: 1 to 2 - "# + assert_eq!( + "Error during planning: Cannot find column with position 0 in SELECT clause. Valid columns: 1 to 2", + err.strip_backtrace() ); let sql2 = "SELECT state, MIN(age) FROM person GROUP BY 5"; let err2 = logical_plan(sql2).expect_err("query should have failed"); - - assert_snapshot!( - err2.strip_backtrace(), - @r#" - Error during planning: Cannot find column with position 5 in SELECT clause. Valid columns: 1 to 2 - "# + assert_eq!( + "Error during planning: Cannot find column with position 5 in SELECT clause. Valid columns: 1 to 2", + err2.strip_backtrace() ); } #[test] fn select_simple_aggregate_with_groupby_can_use_alias() { - let plan = - logical_plan("SELECT state AS a, MIN(age) AS b FROM person GROUP BY a").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state AS a, min(person.age) AS b - Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] - TableScan: person - "# + quick_test( + "SELECT state AS a, MIN(age) AS b FROM person GROUP BY a", + "Projection: person.state AS a, min(person.age) AS b\ + \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ + \n TableScan: person", ); } @@ -1780,83 +1306,56 @@ fn select_simple_aggregate_with_groupby_can_use_alias() { fn select_simple_aggregate_with_groupby_aggregate_repeated() { let sql = "SELECT state, MIN(age), MIN(age) FROM person GROUP BY state"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Projections require unique expression names but the expression "min(person.age)" at position 1 and "min(person.age)" at position 2 have the same name. Consider aliasing ("AS") one of them. - "# + assert_eq!( + "Error during planning: Projections require unique expression names but the expression \"min(person.age)\" at position 1 and \"min(person.age)\" at position 2 have the same name. Consider aliasing (\"AS\") one of them.", + err.strip_backtrace() ); } #[test] fn select_simple_aggregate_with_groupby_aggregate_repeated_and_one_has_alias() { - let plan = - logical_plan("SELECT state, MIN(age), MIN(age) AS ma FROM person GROUP BY state") - .unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state, min(person.age), min(person.age) AS ma - Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] - TableScan: person - "# - ); + quick_test( + "SELECT state, MIN(age), MIN(age) AS ma FROM person GROUP BY state", + "Projection: person.state, min(person.age), min(person.age) AS ma\ + \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ + \n TableScan: person", + ) } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_unselected() { - let plan = - logical_plan("SELECT MIN(first_name) FROM person GROUP BY age + 1").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: min(person.first_name) - Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] - TableScan: person - "# + quick_test( + "SELECT MIN(first_name) FROM person GROUP BY age + 1", + "Projection: min(person.first_name)\ + \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ + \n TableScan: person", ); } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_selected_and_resolvable() { - let plan = - logical_plan("SELECT age + 1, MIN(first_name) FROM person GROUP BY age + 1") - .unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.age + Int64(1), min(person.first_name) - Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] - TableScan: person - "# + quick_test( + "SELECT age + 1, MIN(first_name) FROM person GROUP BY age + 1", + "Projection: person.age + Int64(1), min(person.first_name)\ + \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ + \n TableScan: person", ); - let plan = - logical_plan("SELECT MIN(first_name), age + 1 FROM person GROUP BY age + 1") - .unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: min(person.first_name), person.age + Int64(1) - Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] - TableScan: person - "# + quick_test( + "SELECT MIN(first_name), age + 1 FROM person GROUP BY age + 1", + "Projection: min(person.first_name), person.age + Int64(1)\ + \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ + \n TableScan: person", ); } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_nested_and_resolvable() { - let plan = logical_plan( - "SELECT ((age + 1) / 2) * (age + 1), MIN(first_name) FROM person GROUP BY age + 1" - ).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.age + Int64(1) / Int64(2) * person.age + Int64(1), min(person.first_name) - Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]] - TableScan: person - "# - ); + quick_test( + "SELECT ((age + 1) / 2) * (age + 1), MIN(first_name) FROM person GROUP BY age + 1", + "Projection: person.age + Int64(1) / Int64(2) * person.age + Int64(1), min(person.first_name)\ + \n Aggregate: groupBy=[[person.age + Int64(1)]], aggr=[[min(person.first_name)]]\ + \n TableScan: person", + ); } #[test] @@ -1865,192 +1364,131 @@ fn select_simple_aggregate_with_groupby_non_column_expression_nested_and_not_res // The query should fail, because age + 9 is not in the group by. let sql = "SELECT ((age + 1) / 2) * (age + 9), MIN(first_name) FROM person GROUP BY age + 1"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.age" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.age + Int64(1), min(person.first_name)" appears in the SELECT clause satisfies this requirement - "# - ); + assert_eq!( + "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.age\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.age + Int64(1), min(person.first_name)\" appears in the SELECT clause satisfies this requirement", + err.strip_backtrace() + ); } #[test] fn select_simple_aggregate_with_groupby_non_column_expression_and_its_column_selected() { let sql = "SELECT age, MIN(first_name) FROM person GROUP BY age + 1"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "person.age" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "person.age + Int64(1), min(person.first_name)" appears in the SELECT clause satisfies this requirement - "# - ); + assert_eq!( + "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"person.age\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"person.age + Int64(1), min(person.first_name)\" appears in the SELECT clause satisfies this requirement", + err.strip_backtrace() + ); } #[test] fn select_simple_aggregate_nested_in_binary_expr_with_groupby() { - let plan = - logical_plan("SELECT state, MIN(age) < 10 FROM person GROUP BY state").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state, min(person.age) < Int64(10) - Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]] - TableScan: person - "# + quick_test( + "SELECT state, MIN(age) < 10 FROM person GROUP BY state", + "Projection: person.state, min(person.age) < Int64(10)\ + \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age)]]\ + \n TableScan: person", ); } #[test] fn select_simple_aggregate_and_nested_groupby_column() { - let plan = - logical_plan("SELECT MAX(first_name), age + 1 FROM person GROUP BY age").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: max(person.first_name), person.age + Int64(1) - Aggregate: groupBy=[[person.age]], aggr=[[max(person.first_name)]] - TableScan: person - "# + quick_test( + "SELECT age + 1, MAX(first_name) FROM person GROUP BY age", + "Projection: person.age + Int64(1), max(person.first_name)\ + \n Aggregate: groupBy=[[person.age]], aggr=[[max(person.first_name)]]\ + \n TableScan: person", ); } #[test] fn select_aggregate_compounded_with_groupby_column() { - let plan = logical_plan("SELECT age + MIN(salary) FROM person GROUP BY age").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.age + min(person.salary) - Aggregate: groupBy=[[person.age]], aggr=[[min(person.salary)]] - TableScan: person - "# + quick_test( + "SELECT age + MIN(salary) FROM person GROUP BY age", + "Projection: person.age + min(person.salary)\ + \n Aggregate: groupBy=[[person.age]], aggr=[[min(person.salary)]]\ + \n TableScan: person", ); } #[test] fn select_aggregate_with_non_column_inner_expression_with_groupby() { - let plan = - logical_plan("SELECT state, MIN(age + 1) FROM person GROUP BY state").unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.state, min(person.age + Int64(1)) - Aggregate: groupBy=[[person.state]], aggr=[[min(person.age + Int64(1))]] - TableScan: person - "# + quick_test( + "SELECT state, MIN(age + 1) FROM person GROUP BY state", + "Projection: person.state, min(person.age + Int64(1))\ + \n Aggregate: groupBy=[[person.state]], aggr=[[min(person.age + Int64(1))]]\ + \n TableScan: person", ); } #[test] fn select_count_one() { let sql = "SELECT count(1) FROM person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: count(Int64(1)) - Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] - TableScan: person -"# - ); + let expected = "Projection: count(Int64(1))\ + \n Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_count_column() { let sql = "SELECT count(id) FROM person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: count(person.id) - Aggregate: groupBy=[[]], aggr=[[count(person.id)]] - TableScan: person -"# - ); + let expected = "Projection: count(person.id)\ + \n Aggregate: groupBy=[[]], aggr=[[count(person.id)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_approx_median() { let sql = "SELECT approx_median(age) FROM person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: approx_median(person.age) - Aggregate: groupBy=[[]], aggr=[[approx_median(person.age)]] - TableScan: person -"# - ); + let expected = "Projection: approx_median(person.age)\ + \n Aggregate: groupBy=[[]], aggr=[[approx_median(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_scalar_func() { let sql = "SELECT sqrt(age) FROM person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: sqrt(person.age) - TableScan: person -"# - ); + let expected = "Projection: sqrt(person.age)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_aliased_scalar_func() { let sql = "SELECT sqrt(person.age) AS square_people FROM person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: sqrt(person.age) AS square_people - TableScan: person -"# - ); + let expected = "Projection: sqrt(person.age) AS square_people\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_where_nullif_division() { let sql = "SELECT c3/(c4+c5) \ FROM aggregate_test_100 WHERE c3/nullif(c4+c5, 0) > 0.1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: aggregate_test_100.c3 / (aggregate_test_100.c4 + aggregate_test_100.c5) - Filter: aggregate_test_100.c3 / nullif(aggregate_test_100.c4 + aggregate_test_100.c5, Int64(0)) > Float64(0.1) - TableScan: aggregate_test_100 -"# - ); + let expected = "Projection: aggregate_test_100.c3 / (aggregate_test_100.c4 + aggregate_test_100.c5)\ + \n Filter: aggregate_test_100.c3 / nullif(aggregate_test_100.c4 + aggregate_test_100.c5, Int64(0)) > Float64(0.1)\ + \n TableScan: aggregate_test_100"; + quick_test(sql, expected); } #[test] fn select_where_with_negative_operator() { let sql = "SELECT c3 FROM aggregate_test_100 WHERE c3 > -0.1 AND -c4 > 0"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: aggregate_test_100.c3 - Filter: aggregate_test_100.c3 > Float64(-0.1) AND (- aggregate_test_100.c4) > Int64(0) - TableScan: aggregate_test_100 -"# - ); + let expected = "Projection: aggregate_test_100.c3\ + \n Filter: aggregate_test_100.c3 > Float64(-0.1) AND (- aggregate_test_100.c4) > Int64(0)\ + \n TableScan: aggregate_test_100"; + quick_test(sql, expected); } #[test] fn select_where_with_positive_operator() { let sql = "SELECT c3 FROM aggregate_test_100 WHERE c3 > +0.1 AND +c4 > 0"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: aggregate_test_100.c3 - Filter: aggregate_test_100.c3 > Float64(0.1) AND aggregate_test_100.c4 > Int64(0) - TableScan: aggregate_test_100 -"# - ); + let expected = "Projection: aggregate_test_100.c3\ + \n Filter: aggregate_test_100.c3 > Float64(0.1) AND aggregate_test_100.c4 > Int64(0)\ + \n TableScan: aggregate_test_100"; + quick_test(sql, expected); } #[test] @@ -2058,43 +1496,30 @@ fn select_where_compound_identifiers() { let sql = "SELECT aggregate_test_100.c3 \ FROM public.aggregate_test_100 \ WHERE aggregate_test_100.c3 > 0.1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: public.aggregate_test_100.c3 - Filter: public.aggregate_test_100.c3 > Float64(0.1) - TableScan: public.aggregate_test_100 -"# - ); + let expected = "Projection: public.aggregate_test_100.c3\ + \n Filter: public.aggregate_test_100.c3 > Float64(0.1)\ + \n TableScan: public.aggregate_test_100"; + quick_test(sql, expected); } #[test] fn select_order_by_index() { let sql = "SELECT id FROM person ORDER BY 1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: person.id ASC NULLS LAST - Projection: person.id - TableScan: person -"# - ); + let expected = "Sort: person.id ASC NULLS LAST\ + \n Projection: person.id\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] fn select_order_by_multiple_index() { let sql = "SELECT id, state, age FROM person ORDER BY 1, 3"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: person.id ASC NULLS LAST, person.age ASC NULLS LAST - Projection: person.id, person.state, person.age - TableScan: person -"# - ); + let expected = "Sort: person.id ASC NULLS LAST, person.age ASC NULLS LAST\ + \n Projection: person.id, person.state, person.age\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] @@ -2103,12 +1528,9 @@ fn select_order_by_index_of_0() { let err = logical_plan(sql) .expect_err("query should have failed") .strip_backtrace(); - - assert_snapshot!( - err, - @r#" - Error during planning: Order by index starts at 1 for column indexes - "# + assert_eq!( + "Error during planning: Order by index starts at 1 for column indexes", + err ); } @@ -2118,243 +1540,162 @@ fn select_order_by_index_oob() { let err = logical_plan(sql) .expect_err("query should have failed") .strip_backtrace(); - - assert_snapshot!( - err, - @r#" - Error during planning: Order by column out of bounds, specified: 2, max: 1 - "# + assert_eq!( + "Error during planning: Order by column out of bounds, specified: 2, max: 1", + err ); } #[test] -fn select_with_order_by() { +fn select_order_by() { let sql = "SELECT id FROM person ORDER BY id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: person.id ASC NULLS LAST - Projection: person.id - TableScan: person -"# - ); + let expected = "Sort: person.id ASC NULLS LAST\ + \n Projection: person.id\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_order_by_desc() { let sql = "SELECT id FROM person ORDER BY id DESC"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: person.id DESC NULLS FIRST - Projection: person.id - TableScan: person -"# - ); + let expected = "Sort: person.id DESC NULLS FIRST\ + \n Projection: person.id\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_order_by_nulls_last() { - let plan = logical_plan("SELECT id FROM person ORDER BY id DESC NULLS LAST").unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: person.id DESC NULLS LAST - Projection: person.id - TableScan: person -"# + quick_test( + "SELECT id FROM person ORDER BY id DESC NULLS LAST", + "Sort: person.id DESC NULLS LAST\ + \n Projection: person.id\ + \n TableScan: person", ); - let plan = logical_plan("SELECT id FROM person ORDER BY id NULLS LAST").unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: person.id ASC NULLS LAST - Projection: person.id - TableScan: person -"# + quick_test( + "SELECT id FROM person ORDER BY id NULLS LAST", + "Sort: person.id ASC NULLS LAST\ + \n Projection: person.id\ + \n TableScan: person", ); } #[test] fn select_group_by() { let sql = "SELECT state FROM person GROUP BY state"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.state - Aggregate: groupBy=[[person.state]], aggr=[[]] - TableScan: person -"# - ); + let expected = "Projection: person.state\ + \n Aggregate: groupBy=[[person.state]], aggr=[[]]\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] fn select_group_by_columns_not_in_select() { let sql = "SELECT MAX(age) FROM person GROUP BY state"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: max(person.age) - Aggregate: groupBy=[[person.state]], aggr=[[max(person.age)]] - TableScan: person -"# - ); + let expected = "Projection: max(person.age)\ + \n Aggregate: groupBy=[[person.state]], aggr=[[max(person.age)]]\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] fn select_group_by_count_star() { let sql = "SELECT state, count(*) FROM person GROUP BY state"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.state, count(*) - Aggregate: groupBy=[[person.state]], aggr=[[count(*)]] - TableScan: person -"# - ); + let expected = "Projection: person.state, count(*)\ + \n Aggregate: groupBy=[[person.state]], aggr=[[count(*)]]\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] fn select_group_by_needs_projection() { let sql = "SELECT count(state), state FROM person GROUP BY state"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: count(person.state), person.state - Aggregate: groupBy=[[person.state]], aggr=[[count(person.state)]] - TableScan: person - "# - ); + let expected = "\ + Projection: count(person.state), person.state\ + \n Aggregate: groupBy=[[person.state]], aggr=[[count(person.state)]]\ + \n TableScan: person"; + + quick_test(sql, expected); } #[test] fn select_7480_1() { let sql = "SELECT c1, MIN(c12) FROM aggregate_test_100 GROUP BY c1, c13"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: aggregate_test_100.c1, min(aggregate_test_100.c12) - Aggregate: groupBy=[[aggregate_test_100.c1, aggregate_test_100.c13]], aggr=[[min(aggregate_test_100.c12)]] - TableScan: aggregate_test_100 -"# - ); + let expected = "Projection: aggregate_test_100.c1, min(aggregate_test_100.c12)\ + \n Aggregate: groupBy=[[aggregate_test_100.c1, aggregate_test_100.c13]], aggr=[[min(aggregate_test_100.c12)]]\ + \n TableScan: aggregate_test_100"; + quick_test(sql, expected); } #[test] fn select_7480_2() { let sql = "SELECT c1, c13, MIN(c12) FROM aggregate_test_100 GROUP BY c1"; let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column "aggregate_test_100.c13" must appear in the GROUP BY clause or must be part of an aggregate function, currently only "aggregate_test_100.c1, min(aggregate_test_100.c12)" appears in the SELECT clause satisfies this requirement - "# + assert_eq!( + "Error during planning: Column in SELECT must be in GROUP BY or an aggregate function: While expanding wildcard, column \"aggregate_test_100.c13\" must appear in the GROUP BY clause or must be part of an aggregate function, currently only \"aggregate_test_100.c1, min(aggregate_test_100.c12)\" appears in the SELECT clause satisfies this requirement", + err.strip_backtrace() ); } #[test] fn create_external_table_csv() { let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV LOCATION 'foo.csv'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Bare { table: "t" } -"# - ); + let expected = "CreateExternalTable: Bare { table: \"t\" }"; + quick_test(sql, expected); } #[test] fn create_external_table_with_pk() { let sql = "CREATE EXTERNAL TABLE t(c1 int, primary key(c1)) STORED AS CSV LOCATION 'foo.csv'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Bare { table: "t" } constraints=[PrimaryKey([0])] - "# - ); + let expected = + "CreateExternalTable: Bare { table: \"t\" } constraints=[PrimaryKey([0])]"; + quick_test(sql, expected); } #[test] fn create_external_table_wih_schema() { let sql = "CREATE EXTERNAL TABLE staging.foo STORED AS CSV LOCATION 'foo.csv'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Partial { schema: "staging", table: "foo" } -"# - ); + let expected = "CreateExternalTable: Partial { schema: \"staging\", table: \"foo\" }"; + quick_test(sql, expected); } #[test] fn create_schema_with_quoted_name() { let sql = "CREATE SCHEMA \"quoted_schema_name\""; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateCatalogSchema: "quoted_schema_name" -"# - ); + let expected = "CreateCatalogSchema: \"quoted_schema_name\""; + quick_test(sql, expected); } #[test] fn create_schema_with_quoted_unnormalized_name() { let sql = "CREATE SCHEMA \"Foo\""; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateCatalogSchema: "Foo" -"# - ); + let expected = "CreateCatalogSchema: \"Foo\""; + quick_test(sql, expected); } #[test] fn create_schema_with_unquoted_normalized_name() { let sql = "CREATE SCHEMA Foo"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateCatalogSchema: "foo" -"# - ); + let expected = "CreateCatalogSchema: \"foo\""; + quick_test(sql, expected); } #[test] fn create_external_table_custom() { let sql = "CREATE EXTERNAL TABLE dt STORED AS DELTATABLE LOCATION 's3://bucket/schema/table';"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Bare { table: "dt" } -"# - ); + let expected = r#"CreateExternalTable: Bare { table: "dt" }"#; + quick_test(sql, expected); } #[test] fn create_external_table_csv_no_schema() { let sql = "CREATE EXTERNAL TABLE t STORED AS CSV LOCATION 'foo.csv'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Bare { table: "t" } -"# - ); + let expected = "CreateExternalTable: Bare { table: \"t\" }"; + quick_test(sql, expected); } #[test] @@ -2367,18 +1708,9 @@ fn create_external_table_with_compression_type() { "CREATE EXTERNAL TABLE t(c1 int) STORED AS JSON LOCATION 'foo.json.bz2' OPTIONS ('format.compression' 'bzip2')", "CREATE EXTERNAL TABLE t(c1 int) STORED AS NONSTANDARD LOCATION 'foo.unk' OPTIONS ('format.compression' 'gzip')", ]; - - allow_duplicates! { - for sql in sqls { - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - CreateExternalTable: Bare { table: "t" } - "# - ); - } - + for sql in sqls { + let expected = "CreateExternalTable: Bare { table: \"t\" }"; + quick_test(sql, expected); } // negative case @@ -2390,66 +1722,41 @@ fn create_external_table_with_compression_type() { "CREATE EXTERNAL TABLE t STORED AS ARROW LOCATION 'foo.arrow' OPTIONS ('format.compression' 'gzip')", "CREATE EXTERNAL TABLE t STORED AS ARROW LOCATION 'foo.arrow' OPTIONS ('format.compression' 'bzip2')", ]; - - allow_duplicates! { - for sql in sqls { - let err = logical_plan(sql).expect_err("query should have failed"); - - assert_snapshot!( - err.strip_backtrace(), - @r#" - Error during planning: File compression type cannot be set for PARQUET, AVRO, or ARROW files. - "# - ); - - } + for sql in sqls { + let err = logical_plan(sql).expect_err("query should have failed"); + assert_eq!( + "Error during planning: File compression type cannot be set for PARQUET, AVRO, or ARROW files.", + err.strip_backtrace() + ); } } #[test] fn create_external_table_parquet() { let sql = "CREATE EXTERNAL TABLE t(c1 int) STORED AS PARQUET LOCATION 'foo.parquet'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Bare { table: "t" } -"# - ); + let expected = "CreateExternalTable: Bare { table: \"t\" }"; + quick_test(sql, expected); } #[test] fn create_external_table_parquet_sort_order() { let sql = "create external table foo(a varchar, b varchar, c timestamp) stored as parquet location '/tmp/foo' with order (c)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Bare { table: "foo" } -"# - ); + let expected = "CreateExternalTable: Bare { table: \"foo\" }"; + quick_test(sql, expected); } #[test] fn create_external_table_parquet_no_schema() { let sql = "CREATE EXTERNAL TABLE t STORED AS PARQUET LOCATION 'foo.parquet'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#"CreateExternalTable: Bare { table: "t" }"# - ); + let expected = "CreateExternalTable: Bare { table: \"t\" }"; + quick_test(sql, expected); } #[test] fn create_external_table_parquet_no_schema_sort_order() { let sql = "CREATE EXTERNAL TABLE t STORED AS PARQUET LOCATION 'foo.parquet' WITH ORDER (id)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -CreateExternalTable: Bare { table: "t" } -"# - ); + let expected = "CreateExternalTable: Bare { table: \"t\" }"; + quick_test(sql, expected); } #[test] @@ -2458,16 +1765,11 @@ fn equijoin_explicit_syntax() { FROM person \ JOIN orders \ ON id = customer_id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id = orders.customer_id\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -2476,16 +1778,12 @@ fn equijoin_with_condition() { FROM person \ JOIN orders \ ON id = customer_id AND order_id > 1 "; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1)\ + \n TableScan: person\ + \n TableScan: orders"; + + quick_test(sql, expected); } #[test] @@ -2494,16 +1792,11 @@ fn left_equijoin_with_conditions() { FROM person \ LEFT JOIN orders \ ON id = customer_id AND order_id > 1 AND age < 30"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Left Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1) AND person.age < Int64(30) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, orders.order_id\ + \n Left Join: Filter: person.id = orders.customer_id AND orders.order_id > Int64(1) AND person.age < Int64(30)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -2512,16 +1805,12 @@ fn right_equijoin_with_conditions() { FROM person \ RIGHT JOIN orders \ ON id = customer_id AND id > 1 AND order_id < 100"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Right Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Right Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -2530,16 +1819,11 @@ fn full_equijoin_with_conditions() { FROM person \ FULL JOIN orders \ ON id = customer_id AND id > 1 AND order_id < 100"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Full Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, orders.order_id\ + \n Full Join: Filter: person.id = orders.customer_id AND person.id > Int64(1) AND orders.order_id < Int64(100)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -2548,16 +1832,11 @@ fn join_with_table_name() { FROM person \ JOIN orders \ ON person.id = orders.customer_id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id = orders.customer_id\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -2566,17 +1845,12 @@ fn join_with_using() { FROM person \ JOIN person as person2 \ USING (id)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.first_name, person.id - Inner Join: Using person.id = person2.id - TableScan: person - SubqueryAlias: person2 - TableScan: person -"# - ); + let expected = "Projection: person.first_name, person.id\ + \n Inner Join: Using person.id = person2.id\ + \n TableScan: person\ + \n SubqueryAlias: person2\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -2585,225 +1859,166 @@ fn equijoin_explicit_syntax_3_tables() { FROM person \ JOIN orders ON id = customer_id \ JOIN lineitem ON o_item_id = l_item_id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id, lineitem.l_description - Inner Join: Filter: orders.o_item_id = lineitem.l_item_id - Inner Join: Filter: person.id = orders.customer_id - TableScan: person - TableScan: orders - TableScan: lineitem -"# - ); + let expected = "Projection: person.id, orders.order_id, lineitem.l_description\ + \n Inner Join: Filter: orders.o_item_id = lineitem.l_item_id\ + \n Inner Join: Filter: person.id = orders.customer_id\ + \n TableScan: person\ + \n TableScan: orders\ + \n TableScan: lineitem"; + quick_test(sql, expected); } #[test] fn boolean_literal_in_condition_expression() { let sql = "SELECT order_id \ - FROM orders \ - WHERE delivered = false OR delivered = true"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id - Filter: orders.delivered = Boolean(false) OR orders.delivered = Boolean(true) - TableScan: orders -"# - ); + FROM orders \ + WHERE delivered = false OR delivered = true"; + let expected = "Projection: orders.order_id\ + \n Filter: orders.delivered = Boolean(false) OR orders.delivered = Boolean(true)\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn union() { let sql = "SELECT order_id from orders UNION SELECT order_id FROM orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Distinct: - Union - Projection: orders.order_id - TableScan: orders - Projection: orders.order_id - TableScan: orders -"# - ); + let expected = "\ + Distinct:\ + \n Union\ + \n Projection: orders.order_id\ + \n TableScan: orders\ + \n Projection: orders.order_id\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn union_by_name_different_columns() { let sql = "SELECT order_id from orders UNION BY NAME SELECT order_id, 1 FROM orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Distinct: - Union - Projection: order_id, NULL AS Int64(1) - Projection: orders.order_id - TableScan: orders - Projection: order_id, Int64(1) - Projection: orders.order_id, Int64(1) - TableScan: orders -"# - ); + let expected = "\ + Distinct:\ + \n Union\ + \n Projection: order_id, NULL AS Int64(1)\ + \n Projection: orders.order_id\ + \n TableScan: orders\ + \n Projection: order_id, Int64(1)\ + \n Projection: orders.order_id, Int64(1)\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn union_by_name_same_column_names() { let sql = "SELECT order_id from orders UNION SELECT order_id FROM orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Distinct: - Union - Projection: orders.order_id - TableScan: orders - Projection: orders.order_id - TableScan: orders -"# - ); + let expected = "\ + Distinct:\ + \n Union\ + \n Projection: orders.order_id\ + \n TableScan: orders\ + \n Projection: orders.order_id\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn union_all() { let sql = "SELECT order_id from orders UNION ALL SELECT order_id FROM orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Union - Projection: orders.order_id - TableScan: orders - Projection: orders.order_id - TableScan: orders -"# - ); + let expected = "Union\ + \n Projection: orders.order_id\ + \n TableScan: orders\ + \n Projection: orders.order_id\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn union_all_by_name_different_columns() { let sql = "SELECT order_id from orders UNION ALL BY NAME SELECT order_id, 1 FROM orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Union - Projection: order_id, NULL AS Int64(1) - Projection: orders.order_id - TableScan: orders - Projection: order_id, Int64(1) - Projection: orders.order_id, Int64(1) - TableScan: orders -"# - ); + let expected = "\ + Union\ + \n Projection: order_id, NULL AS Int64(1)\ + \n Projection: orders.order_id\ + \n TableScan: orders\ + \n Projection: order_id, Int64(1)\ + \n Projection: orders.order_id, Int64(1)\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn union_all_by_name_same_column_names() { let sql = "SELECT order_id from orders UNION ALL BY NAME SELECT order_id FROM orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Union - Projection: order_id - Projection: orders.order_id - TableScan: orders - Projection: order_id - Projection: orders.order_id - TableScan: orders -"# - ); + let expected = "\ + Union\ + \n Projection: order_id\ + \n Projection: orders.order_id\ + \n TableScan: orders\ + \n Projection: order_id\ + \n Projection: orders.order_id\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn empty_over() { let sql = "SELECT order_id, MAX(order_id) OVER () from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ + \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn empty_over_with_alias() { let sql = "SELECT order_id oid, MAX(order_id) OVER () max_oid from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid - WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid\ + \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn empty_over_dup_with_alias() { let sql = "SELECT order_id oid, MAX(order_id) OVER () max_oid, MAX(order_id) OVER () max_oid_dup from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid_dup - WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING AS max_oid_dup\ + \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn empty_over_dup_with_different_sort() { let sql = "SELECT order_id oid, MAX(order_id) OVER (), MAX(order_id) OVER (ORDER BY order_id) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - WindowAggr: windowExpr=[[max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id AS oid, max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.order_id) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n WindowAggr: windowExpr=[[max(orders.order_id) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn empty_over_plus() { let sql = "SELECT order_id, MAX(qty * 1.1) OVER () from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - WindowAggr: windowExpr=[[max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ + \n WindowAggr: windowExpr=[[max(orders.qty * Float64(1.1)) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn empty_over_multiple() { let sql = "SELECT order_id, MAX(qty) OVER (), min(qty) over (), avg(qty) OVER () from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - WindowAggr: windowExpr=[[max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ + \n WindowAggr: windowExpr=[[max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, avg(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -2818,15 +2033,11 @@ Projection: orders.order_id, max(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AN #[test] fn over_partition_by() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ + \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -2844,61 +2055,45 @@ Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS #[test] fn over_order_by() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn over_order_by_with_window_frame_double_end() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id ROWS BETWEEN 3 PRECEDING and 3 FOLLOWING), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn over_order_by_with_window_frame_single_end() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id ROWS 3 PRECEDING), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn over_order_by_with_window_frame_single_end_groups() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id GROUPS 3 PRECEDING), MIN(qty) OVER (ORDER BY order_id DESC) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] GROUPS BETWEEN 3 PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -2916,16 +2111,12 @@ Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS #[test] fn over_order_by_two_sort_keys() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id), MIN(qty) OVER (ORDER BY (order_id + 1)) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id + Int64(1) ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -2944,17 +2135,13 @@ Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS #[test] fn over_order_by_sort_keys_sorting() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY qty, order_id), sum(qty) OVER (), MIN(qty) OVER (ORDER BY order_id, qty) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -2971,17 +2158,13 @@ Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST #[test] fn over_order_by_sort_keys_sorting_prefix_compacting() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY order_id), sum(qty) OVER (), MIN(qty) OVER (ORDER BY order_id, qty) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -3003,18 +2186,14 @@ Projection: orders.order_id, max(orders.qty) ORDER BY [orders.order_id ASC NULLS #[test] fn over_order_by_sort_keys_sorting_global_order_compacting() { let sql = "SELECT order_id, MAX(qty) OVER (ORDER BY qty, order_id), sum(qty) OVER (), MIN(qty) OVER (ORDER BY order_id, qty) from orders ORDER BY order_id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: orders.order_id ASC NULLS LAST - Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Sort: orders.order_id ASC NULLS LAST\ + \n Projection: orders.order_id, max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING, min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[sum(orders.qty) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n WindowAggr: windowExpr=[[max(orders.qty) ORDER BY [orders.qty ASC NULLS LAST, orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) ORDER BY [orders.order_id ASC NULLS LAST, orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -3030,15 +2209,11 @@ Sort: orders.order_id ASC NULLS LAST fn over_partition_by_order_by() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id ORDER BY qty) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -3054,15 +2229,11 @@ Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDE fn over_partition_by_order_by_no_dup() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id, qty ORDER BY qty) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -3081,16 +2252,12 @@ Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orde fn over_partition_by_order_by_mix_up() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id, qty ORDER BY qty), MIN(qty) OVER (PARTITION BY qty ORDER BY order_id) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.qty] ORDER BY [orders.order_id ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } /// psql result @@ -3108,121 +2275,90 @@ Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id, orde fn over_partition_by_order_by_mix_up_prefix() { let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY order_id ORDER BY qty), MIN(qty) OVER (PARTITION BY order_id, qty ORDER BY price) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW - WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\ + \n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ORDER BY [orders.qty ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n WindowAggr: windowExpr=[[min(orders.qty) PARTITION BY [orders.order_id, orders.qty] ORDER BY [orders.price ASC NULLS LAST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn approx_median_window() { let sql = "SELECT order_id, APPROX_MEDIAN(qty) OVER(PARTITION BY order_id) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - WindowAggr: windowExpr=[[approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let expected = "\ + Projection: orders.order_id, approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\ + \n WindowAggr: windowExpr=[[approx_median(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn select_typed_date_string() { let sql = "SELECT date '2020-12-10' AS date"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: CAST(Utf8("2020-12-10") AS Date32) AS date - EmptyRelation -"# - ); + let expected = "Projection: CAST(Utf8(\"2020-12-10\") AS Date32) AS date\ + \n EmptyRelation"; + quick_test(sql, expected); } #[test] fn select_typed_time_string() { let sql = "SELECT TIME '08:09:10.123' AS time"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: CAST(Utf8("08:09:10.123") AS Time64(Nanosecond)) AS time - EmptyRelation -"# - ); + let expected = + "Projection: CAST(Utf8(\"08:09:10.123\") AS Time64(Nanosecond)) AS time\ + \n EmptyRelation"; + quick_test(sql, expected); } #[test] fn select_multibyte_column() { let sql = r#"SELECT "😀" FROM person"#; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.😀 - TableScan: person -"# - ); + let expected = "Projection: person.😀\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn select_groupby_orderby() { // ensure that references are correctly resolved in the order by clause // see https://github.com/apache/datafusion/issues/4854 + let sql = r#"SELECT + avg(age) AS "value", + date_trunc('month', birth_date) AS "birth_date" + FROM person GROUP BY birth_date ORDER BY birth_date; +"#; + // expect that this is not an ambiguous reference + let expected = + "Sort: birth_date ASC NULLS LAST\ + \n Projection: avg(person.age) AS value, date_trunc(Utf8(\"month\"), person.birth_date) AS birth_date\ + \n Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]]\ + \n TableScan: person"; + quick_test(sql, expected); + + // Use fully qualified `person.birth_date` as argument to date_trunc, plan should be the same + let sql = r#"SELECT + avg(age) AS "value", + date_trunc('month', person.birth_date) AS "birth_date" + FROM person GROUP BY birth_date ORDER BY birth_date; +"#; + quick_test(sql, expected); - let sqls = vec![ - r#" - SELECT - avg(age) AS "value", - date_trunc('month', birth_date) AS "birth_date" - FROM person GROUP BY birth_date ORDER BY birth_date; - "#, - // Use fully qualified `person.birth_date` as argument to date_trunc, plan should be the same - r#" - SELECT - avg(age) AS "value", - date_trunc('month', person.birth_date) AS "birth_date" - FROM person GROUP BY birth_date ORDER BY birth_date; - "#, - // Use fully qualified `person.birth_date` as group by, plan should be the same - r#" - SELECT - avg(age) AS "value", - date_trunc('month', birth_date) AS "birth_date" - FROM person GROUP BY person.birth_date ORDER BY birth_date; - "#, - // Use fully qualified `person.birth_date` in both group and date_trunc, plan should be the same - r#" - SELECT - avg(age) AS "value", - date_trunc('month', person.birth_date) AS "birth_date" - FROM person GROUP BY person.birth_date ORDER BY birth_date; - "#, - ]; - for sql in sqls { - let plan = logical_plan(sql).unwrap(); - allow_duplicates! { - assert_snapshot!( - plan, - // expect that this is not an ambiguous reference - @r#" - Sort: birth_date ASC NULLS LAST - Projection: avg(person.age) AS value, date_trunc(Utf8("month"), person.birth_date) AS birth_date - Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]] - TableScan: person - "# - ); - } - } + // Use fully qualified `person.birth_date` as group by, plan should be the same + let sql = r#"SELECT + avg(age) AS "value", + date_trunc('month', birth_date) AS "birth_date" + FROM person GROUP BY person.birth_date ORDER BY birth_date; +"#; + quick_test(sql, expected); + + // Use fully qualified `person.birth_date` in both group and date_trunc, plan should be the same + let sql = r#"SELECT + avg(age) AS "value", + date_trunc('month', person.birth_date) AS "birth_date" + FROM person GROUP BY person.birth_date ORDER BY birth_date; +"#; + quick_test(sql, expected); // Use columnized `avg(age)` in the order by let sql = r#"SELECT @@ -3231,16 +2367,13 @@ fn select_groupby_orderby() { FROM person GROUP BY person.birth_date ORDER BY avg(age) + avg(age); "#; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: avg(person.age) + avg(person.age) ASC NULLS LAST - Projection: avg(person.age) + avg(person.age), date_trunc(Utf8("month"), person.birth_date) AS birth_date - Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]] - TableScan: person -"# - ); + let expected = + "Sort: avg(person.age) + avg(person.age) ASC NULLS LAST\ + \n Projection: avg(person.age) + avg(person.age), date_trunc(Utf8(\"month\"), person.birth_date) AS birth_date\ + \n Aggregate: groupBy=[[person.birth_date]], aggr=[[avg(person.age)]]\ + \n TableScan: person"; + + quick_test(sql, expected); } fn logical_plan(sql: &str) -> Result { @@ -3355,149 +2488,114 @@ impl ScalarUDFImpl for DummyUDF { } } -fn parse_decimals_parser_options() -> ParserOptions { - ParserOptions { - parse_float_as_decimal: true, - enable_ident_normalization: false, - support_varchar_with_length: false, - map_varchar_to_utf8view: false, - enable_options_value_normalization: false, - collect_spans: false, - } +/// Create logical plan, write with formatter, compare to expected output +fn quick_test(sql: &str, expected: &str) { + quick_test_with_options(sql, expected, ParserOptions::default()) } -fn ident_normalization_parser_options_no_ident_normalization() -> ParserOptions { - ParserOptions { - parse_float_as_decimal: true, - enable_ident_normalization: false, - support_varchar_with_length: false, - map_varchar_to_utf8view: false, - enable_options_value_normalization: false, - collect_spans: false, - } +fn quick_test_with_options(sql: &str, expected: &str, options: ParserOptions) { + let plan = logical_plan_with_options(sql, options).unwrap(); + assert_eq!(format!("{plan}"), expected); } -fn ident_normalization_parser_options_ident_normalization() -> ParserOptions { - ParserOptions { - parse_float_as_decimal: true, - enable_ident_normalization: true, - support_varchar_with_length: false, - map_varchar_to_utf8view: false, - enable_options_value_normalization: false, - collect_spans: false, +fn prepare_stmt_quick_test( + sql: &str, + expected_plan: &str, + expected_data_types: &str, +) -> LogicalPlan { + let plan = logical_plan(sql).unwrap(); + + let assert_plan = plan.clone(); + // verify plan + assert_eq!(format!("{assert_plan}"), expected_plan); + + // verify data types + if let LogicalPlan::Statement(Statement::Prepare(Prepare { data_types, .. })) = + assert_plan + { + let dt = format!("{data_types:?}"); + assert_eq!(dt, expected_data_types); } + + plan } -fn generate_prepare_stmt_and_data_types(sql: &str) -> (LogicalPlan, String) { - let plan = logical_plan(sql).unwrap(); - let data_types = match &plan { - LogicalPlan::Statement(Statement::Prepare(Prepare { data_types, .. })) => { - format!("{data_types:?}") - } - _ => panic!("Expected a Prepare statement"), - }; - (plan, data_types) +fn prepare_stmt_replace_params_quick_test( + plan: LogicalPlan, + param_values: impl Into, + expected_plan: &str, +) -> LogicalPlan { + // replace params + let plan = plan.with_param_values(param_values).unwrap(); + assert_eq!(format!("{plan}"), expected_plan); + + plan } #[test] fn select_partially_qualified_column() { - let sql = "SELECT person.first_name FROM public.person"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: public.person.first_name - TableScan: public.person -"# - ); + let sql = r#"SELECT person.first_name FROM public.person"#; + let expected = "Projection: public.person.first_name\ + \n TableScan: public.person"; + quick_test(sql, expected); } #[test] fn cross_join_not_to_inner_join() { let sql = "select person.id from person, orders, lineitem where person.id = person.age;"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id - Filter: person.id = person.age - Cross Join: - Cross Join: - TableScan: person - TableScan: orders - TableScan: lineitem -"# - ); + let expected = "Projection: person.id\ + \n Filter: person.id = person.age\ + \n Cross Join: \ + \n Cross Join: \ + \n TableScan: person\ + \n TableScan: orders\ + \n TableScan: lineitem"; + quick_test(sql, expected); } #[test] fn join_with_aliases() { let sql = "select peeps.id, folks.first_name from person as peeps join person as folks on peeps.id = folks.id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: peeps.id, folks.first_name - Inner Join: Filter: peeps.id = folks.id - SubqueryAlias: peeps - TableScan: person - SubqueryAlias: folks - TableScan: person -"# - ); + let expected = "Projection: peeps.id, folks.first_name\ + \n Inner Join: Filter: peeps.id = folks.id\ + \n SubqueryAlias: peeps\ + \n TableScan: person\ + \n SubqueryAlias: folks\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn negative_interval_plus_interval_in_projection() { let sql = "select -interval '2 days' + interval '5 days';"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") - EmptyRelation -"# - ); + let expected = + "Projection: IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\")\n EmptyRelation"; + quick_test(sql, expected); } #[test] fn complex_interval_expression_in_projection() { let sql = "select -interval '2 days' + interval '5 days'+ (-interval '3 days' + interval '5 days');"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: -3, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") - EmptyRelation -"# - ); + let expected = + "Projection: IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: -2, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: -3, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\")\n EmptyRelation"; + quick_test(sql, expected); } #[test] fn negative_sum_intervals_in_projection() { let sql = "select -((interval '2 days' + interval '5 days') + -(interval '4 days' + interval '7 days'));"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: (- IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 2, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") + (- IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 4, nanoseconds: 0 }") + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 7, nanoseconds: 0 }"))) - EmptyRelation -"# - ); + let expected = + "Projection: (- IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 2, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\") + (- IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 4, nanoseconds: 0 }\") + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 7, nanoseconds: 0 }\")))\n EmptyRelation"; + quick_test(sql, expected); } #[test] fn date_plus_interval_in_projection() { let sql = "select t_date32 + interval '5 days' FROM test"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: test.t_date32 + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }") - TableScan: test -"# - ); + let expected = + "Projection: test.t_date32 + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 5, nanoseconds: 0 }\")\n TableScan: test"; + quick_test(sql, expected); } #[test] @@ -3506,15 +2604,11 @@ fn date_plus_interval_in_filter() { WHERE t_date64 \ BETWEEN cast('1999-12-31' as date) \ AND cast('1999-12-31' as date) + interval '30 days'"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: test.t_date64 - Filter: test.t_date64 BETWEEN CAST(Utf8("1999-12-31") AS Date32) AND CAST(Utf8("1999-12-31") AS Date32) + IntervalMonthDayNano("IntervalMonthDayNano { months: 0, days: 30, nanoseconds: 0 }") - TableScan: test -"# - ); + let expected = + "Projection: test.t_date64\ + \n Filter: test.t_date64 BETWEEN CAST(Utf8(\"1999-12-31\") AS Date32) AND CAST(Utf8(\"1999-12-31\") AS Date32) + IntervalMonthDayNano(\"IntervalMonthDayNano { months: 0, days: 30, nanoseconds: 0 }\")\ + \n TableScan: test"; + quick_test(sql, expected); } #[test] @@ -3523,20 +2617,16 @@ fn exists_subquery() { (SELECT first_name FROM person \ WHERE last_name = p.last_name \ AND state = p.state)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: p.id - Filter: EXISTS () - Subquery: - Projection: person.first_name - Filter: person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state) - TableScan: person - SubqueryAlias: p - TableScan: person -"# - ); + + let expected = "Projection: p.id\ + \n Filter: EXISTS ()\ + \n Subquery:\ + \n Projection: person.first_name\ + \n Filter: person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state)\ + \n TableScan: person\ + \n SubqueryAlias: p\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -3548,84 +2638,68 @@ fn exists_subquery_schema_outer_schema_overlap() { WHERE person.id = p2.id \ AND person.last_name = p.last_name \ AND person.state = p.state)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id - Filter: person.id = p.id AND EXISTS () - Subquery: - Projection: person.first_name - Filter: person.id = p2.id AND person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state) - Cross Join: - TableScan: person - SubqueryAlias: p2 - TableScan: person - Cross Join: - TableScan: person - SubqueryAlias: p - TableScan: person -"# - ); + + let expected = "Projection: person.id\ + \n Filter: person.id = p.id AND EXISTS ()\ + \n Subquery:\ + \n Projection: person.first_name\ + \n Filter: person.id = p2.id AND person.last_name = outer_ref(p.last_name) AND person.state = outer_ref(p.state)\ + \n Cross Join: \ + \n TableScan: person\ + \n SubqueryAlias: p2\ + \n TableScan: person\ + \n Cross Join: \ + \n TableScan: person\ + \n SubqueryAlias: p\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn in_subquery_uncorrelated() { let sql = "SELECT id FROM person p WHERE id IN \ (SELECT id FROM person)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: p.id - Filter: p.id IN () - Subquery: - Projection: person.id - TableScan: person - SubqueryAlias: p - TableScan: person -"# - ); + + let expected = "Projection: p.id\ + \n Filter: p.id IN ()\ + \n Subquery:\ + \n Projection: person.id\ + \n TableScan: person\ + \n SubqueryAlias: p\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn not_in_subquery_correlated() { let sql = "SELECT id FROM person p WHERE id NOT IN \ (SELECT id FROM person WHERE last_name = p.last_name AND state = 'CO')"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: p.id - Filter: p.id NOT IN () - Subquery: - Projection: person.id - Filter: person.last_name = outer_ref(p.last_name) AND person.state = Utf8("CO") - TableScan: person - SubqueryAlias: p - TableScan: person -"# - ); + + let expected = "Projection: p.id\ + \n Filter: p.id NOT IN ()\ + \n Subquery:\ + \n Projection: person.id\ + \n Filter: person.last_name = outer_ref(p.last_name) AND person.state = Utf8(\"CO\")\ + \n TableScan: person\ + \n SubqueryAlias: p\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn scalar_subquery() { let sql = "SELECT p.id, (SELECT MAX(id) FROM person WHERE last_name = p.last_name) FROM person p"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: p.id, () - Subquery: - Projection: max(person.id) - Aggregate: groupBy=[[]], aggr=[[max(person.id)]] - Filter: person.last_name = outer_ref(p.last_name) - TableScan: person - SubqueryAlias: p - TableScan: person -"# - ); + + let expected = "Projection: p.id, ()\ + \n Subquery:\ + \n Projection: max(person.id)\ + \n Aggregate: groupBy=[[]], aggr=[[max(person.id)]]\ + \n Filter: person.last_name = outer_ref(p.last_name)\ + \n TableScan: person\ + \n SubqueryAlias: p\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -3637,54 +2711,41 @@ fn scalar_subquery_reference_outer_field() { FROM j1, j3 \ WHERE j2_id = j1_id \ AND j1_id = j3_id)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: j1.j1_string, j2.j2_string - Filter: j1.j1_id = j2.j2_id - Int64(1) AND j2.j2_id < () - Subquery: - Projection: count(*) - Aggregate: groupBy=[[]], aggr=[[count(*)]] - Filter: outer_ref(j2.j2_id) = j1.j1_id AND j1.j1_id = j3.j3_id - Cross Join: - TableScan: j1 - TableScan: j3 - Cross Join: - TableScan: j1 - TableScan: j2 -"# - ); + + let expected = "Projection: j1.j1_string, j2.j2_string\ + \n Filter: j1.j1_id = j2.j2_id - Int64(1) AND j2.j2_id < ()\ + \n Subquery:\ + \n Projection: count(*)\ + \n Aggregate: groupBy=[[]], aggr=[[count(*)]]\ + \n Filter: outer_ref(j2.j2_id) = j1.j1_id AND j1.j1_id = j3.j3_id\ + \n Cross Join: \ + \n TableScan: j1\ + \n TableScan: j3\ + \n Cross Join: \ + \n TableScan: j1\ + \n TableScan: j2"; + + quick_test(sql, expected); } #[test] fn aggregate_with_rollup() { let sql = "SELECT id, state, age, count(*) FROM person GROUP BY id, ROLLUP (state, age)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.state, person.age, count(*) - Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[count(*)]] - TableScan: person -"# - ); + let expected = "Projection: person.id, person.state, person.age, count(*)\ + \n Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[count(*)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn aggregate_with_rollup_with_grouping() { let sql = "SELECT id, state, age, grouping(state), grouping(age), grouping(state) + grouping(age), count(*) \ FROM person GROUP BY id, ROLLUP (state, age)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.state, person.age, grouping(person.state), grouping(person.age), grouping(person.state) + grouping(person.age), count(*) - Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[grouping(person.state), grouping(person.age), count(*)]] - TableScan: person -"# - ); + let expected = "Projection: person.id, person.state, person.age, grouping(person.state), grouping(person.age), grouping(person.state) + grouping(person.age), count(*)\ + \n Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.state, person.age))]], aggr=[[grouping(person.state), grouping(person.age), count(*)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -3702,58 +2763,38 @@ fn rank_partition_grouping() { from person group by rollup(state, last_name)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: sum(person.age) AS total_sum, person.state, person.last_name, grouping(person.state) + grouping(person.last_name) AS x, rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW AS the_rank - WindowAggr: windowExpr=[[rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] - Aggregate: groupBy=[[ROLLUP (person.state, person.last_name)]], aggr=[[sum(person.age), grouping(person.state), grouping(person.last_name)]] - TableScan: person -"# - ); + let expected = "Projection: sum(person.age) AS total_sum, person.state, person.last_name, grouping(person.state) + grouping(person.last_name) AS x, rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW AS the_rank\ + \n WindowAggr: windowExpr=[[rank() PARTITION BY [grouping(person.state) + grouping(person.last_name), CASE WHEN grouping(person.last_name) = Int64(0) THEN person.state END] ORDER BY [sum(person.age) DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]]\ + \n Aggregate: groupBy=[[ROLLUP (person.state, person.last_name)]], aggr=[[sum(person.age), grouping(person.state), grouping(person.last_name)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn aggregate_with_cube() { let sql = "SELECT id, state, age, count(*) FROM person GROUP BY id, CUBE (state, age)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.state, person.age, count(*) - Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.age), (person.id, person.state, person.age))]], aggr=[[count(*)]] - TableScan: person -"# - ); + let expected = "Projection: person.id, person.state, person.age, count(*)\ + \n Aggregate: groupBy=[[GROUPING SETS ((person.id), (person.id, person.state), (person.id, person.age), (person.id, person.state, person.age))]], aggr=[[count(*)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn round_decimal() { let sql = "SELECT round(price/3, 2) FROM test_decimal"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: round(test_decimal.price / Int64(3), Int64(2)) - TableScan: test_decimal -"# - ); + let expected = "Projection: round(test_decimal.price / Int64(3), Int64(2))\ + \n TableScan: test_decimal"; + quick_test(sql, expected); } #[test] fn aggregate_with_grouping_sets() { let sql = "SELECT id, state, age, count(*) FROM person GROUP BY id, GROUPING SETS ((state), (state, age), (id, state))"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.state, person.age, count(*) - Aggregate: groupBy=[[GROUPING SETS ((person.id, person.state), (person.id, person.state, person.age), (person.id, person.id, person.state))]], aggr=[[count(*)]] - TableScan: person -"# - ); + let expected = "Projection: person.id, person.state, person.age, count(*)\ + \n Aggregate: groupBy=[[GROUPING SETS ((person.id, person.state), (person.id, person.state, person.age), (person.id, person.id, person.state))]], aggr=[[count(*)]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -3761,16 +2802,11 @@ fn join_on_disjunction_condition() { let sql = "SELECT id, order_id \ FROM person \ JOIN orders ON id = customer_id OR person.age > 30"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id OR person.age > Int64(30) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id = orders.customer_id OR person.age > Int64(30)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -3778,16 +2814,11 @@ fn join_on_complex_condition() { let sql = "SELECT id, order_id \ FROM person \ JOIN orders ON id = customer_id AND (person.age > 30 OR person.last_name = 'X')"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id AND (person.age > Int64(30) OR person.last_name = Utf8("X")) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id = orders.customer_id AND (person.age > Int64(30) OR person.last_name = Utf8(\"X\"))\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -3795,16 +2826,11 @@ fn hive_aggregate_with_filter() -> Result<()> { let dialect = &HiveDialect {}; let sql = "SELECT sum(age) FILTER (WHERE age > 4) FROM person"; let plan = logical_plan_with_dialect(sql, dialect)?; - - assert_snapshot!( - plan, - @r##" - Projection: sum(person.age) FILTER (WHERE person.age > Int64(4)) - Aggregate: groupBy=[[]], aggr=[[sum(person.age) FILTER (WHERE person.age > Int64(4))]] - TableScan: person - "## - ); - + let expected = "Projection: sum(person.age) FILTER (WHERE person.age > Int64(4))\ + \n Aggregate: groupBy=[[]], aggr=[[sum(person.age) FILTER (WHERE person.age > Int64(4))]]\ + \n TableScan: person" + .to_string(); + assert_eq!(plan.display_indent().to_string(), expected); Ok(()) } @@ -3815,130 +2841,84 @@ fn order_by_unaliased_name() { // SchemaError(FieldNotFound { qualifier: Some("p"), name: "state", valid_fields: ["z", "q"] }) let sql = "select p.state z, sum(age) q from person p group by p.state order by p.state"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: z, q - Sort: p.state ASC NULLS LAST - Projection: p.state AS z, sum(p.age) AS q, p.state - Aggregate: groupBy=[[p.state]], aggr=[[sum(p.age)]] - SubqueryAlias: p - TableScan: person -"# - ); + let expected = "Projection: z, q\ + \n Sort: p.state ASC NULLS LAST\ + \n Projection: p.state AS z, sum(p.age) AS q, p.state\ + \n Aggregate: groupBy=[[p.state]], aggr=[[sum(p.age)]]\ + \n SubqueryAlias: p\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn order_by_ambiguous_name() { let sql = "select * from person a join person b using (id) order by age"; - let err = logical_plan(sql).unwrap_err().strip_backtrace(); + let expected = "Schema error: Ambiguous reference to unqualified field age"; - assert_snapshot!( - err, - @r###" - Schema error: Ambiguous reference to unqualified field age - "### - ); + let err = logical_plan(sql).unwrap_err(); + assert_eq!(err.strip_backtrace(), expected); } #[test] fn group_by_ambiguous_name() { let sql = "select max(id) from person a join person b using (id) group by age"; - let err = logical_plan(sql).unwrap_err().strip_backtrace(); + let expected = "Schema error: Ambiguous reference to unqualified field age"; - assert_snapshot!( - err, - @r###" - Schema error: Ambiguous reference to unqualified field age - "### - ); + let err = logical_plan(sql).unwrap_err(); + assert_eq!(err.strip_backtrace(), expected); } #[test] fn test_zero_offset_with_limit() { let sql = "select id from person where person.id > 100 LIMIT 5 OFFSET 0;"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Limit: skip=0, fetch=5 - Projection: person.id - Filter: person.id > Int64(100) - TableScan: person -"# - ); + let expected = "Limit: skip=0, fetch=5\ + \n Projection: person.id\ + \n Filter: person.id > Int64(100)\ + \n TableScan: person"; + quick_test(sql, expected); + // Flip the order of LIMIT and OFFSET in the query. Plan should remain the same. let sql = "SELECT id FROM person WHERE person.id > 100 OFFSET 0 LIMIT 5;"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Limit: skip=0, fetch=5 - Projection: person.id - Filter: person.id > Int64(100) - TableScan: person -"# - ); + quick_test(sql, expected); } #[test] fn test_offset_no_limit() { let sql = "SELECT id FROM person WHERE person.id > 100 OFFSET 5;"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Limit: skip=5, fetch=None - Projection: person.id - Filter: person.id > Int64(100) - TableScan: person -"# - ); + let expected = "Limit: skip=5, fetch=None\ + \n Projection: person.id\ + \n Filter: person.id > Int64(100)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn test_offset_after_limit() { let sql = "select id from person where person.id > 100 LIMIT 5 OFFSET 3;"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Limit: skip=3, fetch=5 - Projection: person.id - Filter: person.id > Int64(100) - TableScan: person -"# - ); + let expected = "Limit: skip=3, fetch=5\ + \n Projection: person.id\ + \n Filter: person.id > Int64(100)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn test_offset_before_limit() { let sql = "select id from person where person.id > 100 OFFSET 3 LIMIT 5;"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Limit: skip=3, fetch=5 - Projection: person.id - Filter: person.id > Int64(100) - TableScan: person -"# - ); + let expected = "Limit: skip=3, fetch=5\ + \n Projection: person.id\ + \n Filter: person.id > Int64(100)\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn test_distribute_by() { let sql = "select id from person distribute by state"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Repartition: DistributeBy(person.state) - Projection: person.id - TableScan: person -"# - ); + let expected = "Repartition: DistributeBy(person.state)\ + \n Projection: person.id\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -3966,16 +2946,12 @@ fn test_constant_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id = 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = Int64(10) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id = Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -3984,16 +2960,13 @@ fn test_right_left_expr_eq_join() { FROM person \ INNER JOIN orders \ ON orders.customer_id * 2 = person.id + 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + + quick_test(sql, expected); } #[test] @@ -4002,16 +2975,12 @@ fn test_single_column_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id + 10 = orders.customer_id * 2"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id + Int64(10) = orders.customer_id * Int64(2) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id + Int64(10) = orders.customer_id * Int64(2)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4020,16 +2989,12 @@ fn test_multiple_column_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id + person.age + 10 = orders.customer_id * 2 - orders.price"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id * Int64(2) - orders.price - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id * Int64(2) - orders.price\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4038,16 +3003,12 @@ fn test_left_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id + person.age + 10 = orders.customer_id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id + person.age + Int64(10) = orders.customer_id\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4056,16 +3017,12 @@ fn test_right_expr_eq_join() { FROM person \ INNER JOIN orders \ ON person.id = orders.customer_id * 2 - orders.price"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id * Int64(2) - orders.price - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Inner Join: Filter: person.id = orders.customer_id * Int64(2) - orders.price\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4074,58 +3031,38 @@ fn test_noneq_with_filter_join() { let sql = "SELECT person.id, person.first_name \ FROM person INNER JOIN orders \ ON person.age > 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.first_name - Inner Join: Filter: person.age > Int64(10) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, person.first_name\ + \n Inner Join: Filter: person.age > Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); // left join let sql = "SELECT person.id, person.first_name \ FROM person LEFT JOIN orders \ ON person.age > 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.first_name - Left Join: Filter: person.age > Int64(10) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, person.first_name\ + \n Left Join: Filter: person.age > Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); // right join let sql = "SELECT person.id, person.first_name \ FROM person RIGHT JOIN orders \ ON person.age > 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.first_name - Right Join: Filter: person.age > Int64(10) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, person.first_name\ + \n Right Join: Filter: person.age > Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); // full join let sql = "SELECT person.id, person.first_name \ FROM person FULL JOIN orders \ ON person.age > 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.first_name - Full Join: Filter: person.age > Int64(10) - TableScan: person - TableScan: orders -"# - ); + let expected = "Projection: person.id, person.first_name\ + \n Full Join: Filter: person.age > Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4136,16 +3073,12 @@ fn test_one_side_constant_full_join() { FROM person \ FULL OUTER JOIN orders \ ON person.id = 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, orders.order_id - Full Join: Filter: person.id = Int64(10) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, orders.order_id\ + \n Full Join: Filter: person.id = Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4154,48 +3087,34 @@ fn test_select_join_key_inner_join() { FROM person INNER JOIN orders ON orders.customer_id * 2 = person.id + 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.customer_id * Int64(2), person.id + Int64(10) - Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: orders.customer_id * Int64(2), person.id + Int64(10)\ + \n Inner Join: Filter: orders.customer_id * Int64(2) = person.id + Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] fn test_select_order_by() { let sql = "SELECT '1' from person order by id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: Utf8("1") - Sort: person.id ASC NULLS LAST - Projection: Utf8("1"), person.id - TableScan: person -"# - ); + + let expected = "Projection: Utf8(\"1\")\n Sort: person.id ASC NULLS LAST\n Projection: Utf8(\"1\"), person.id\n TableScan: person"; + quick_test(sql, expected); } #[test] fn test_select_distinct_order_by() { let sql = "SELECT distinct '1' from person order by id"; + let expected = + "Error during planning: For SELECT DISTINCT, ORDER BY expressions person.id must appear in select list"; + // It should return error. let result = logical_plan(sql); assert!(result.is_err()); - let err = result.err().unwrap().strip_backtrace(); - - assert_snapshot!( - err, - @r###" - Error during planning: For SELECT DISTINCT, ORDER BY expressions person.id must appear in select list - "### - ); + let err = result.err().unwrap(); + assert_eq!(err.strip_backtrace(), expected); } #[rstest] @@ -4229,16 +3148,11 @@ fn test_select_unsupported_syntax_errors(#[case] sql: &str, #[case] error: &str) fn select_order_by_with_cast() { let sql = "SELECT first_name AS first_name FROM (SELECT first_name AS first_name FROM person) ORDER BY CAST(first_name as INT)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Sort: CAST(person.first_name AS Int32) ASC NULLS LAST - Projection: person.first_name - Projection: person.first_name - TableScan: person -"# - ); + let expected = "Sort: CAST(person.first_name AS Int32) ASC NULLS LAST\ + \n Projection: person.first_name\ + \n Projection: person.first_name\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -4259,16 +3173,12 @@ fn test_duplicated_left_join_key_inner_join() { FROM person INNER JOIN orders ON person.id * 2 = orders.customer_id + 10 and person.id * 2 = orders.order_id"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.age - Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id * Int64(2) = orders.order_id - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, person.age\ + \n Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id * Int64(2) = orders.order_id\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4278,16 +3188,12 @@ fn test_duplicated_right_join_key_inner_join() { FROM person INNER JOIN orders ON person.id * 2 = orders.customer_id + 10 and person.id = orders.customer_id + 10"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.age - Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id = orders.customer_id + Int64(10) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, person.age\ + \n Inner Join: Filter: person.id * Int64(2) = orders.customer_id + Int64(10) AND person.id = orders.customer_id + Int64(10)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -4297,17 +3203,13 @@ fn test_ambiguous_column_references_in_on_join() { INNER JOIN person as p2 ON id = 1"; + let expected = "Schema error: Ambiguous reference to unqualified field id"; + // It should return error. let result = logical_plan(sql); assert!(result.is_err()); - let err = result.err().unwrap().strip_backtrace(); - - assert_snapshot!( - err, - @r###" - Schema error: Ambiguous reference to unqualified field id - "### - ); + let err = result.err().unwrap(); + assert_eq!(err.strip_backtrace(), expected); } #[test] @@ -4316,18 +3218,14 @@ fn test_ambiguous_column_references_with_in_using_join() { from person as p1 INNER JOIN person as p2 using(id)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: p1.id, p1.age, p2.id - Inner Join: Using p1.id = p2.id - SubqueryAlias: p1 - TableScan: person - SubqueryAlias: p2 - TableScan: person -"# - ); + + let expected = "Projection: p1.id, p1.age, p2.id\ + \n Inner Join: Using p1.id = p2.id\ + \n SubqueryAlias: p1\ + \n TableScan: person\ + \n SubqueryAlias: p2\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] @@ -4335,12 +3233,9 @@ fn test_prepare_statement_to_plan_panic_param_format() { // param is not number following the $ sign // panic due to error returned from the parser let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = $foo"; - - assert_snapshot!( + assert_eq!( logical_plan(sql).unwrap_err().strip_backtrace(), - @r###" - Error during planning: Invalid placeholder, not a number: $foo - "### + "Error during planning: Invalid placeholder, not a number: $foo" ); } @@ -4349,12 +3244,9 @@ fn test_prepare_statement_to_plan_panic_param_zero() { // param is zero following the $ sign // panic due to error returned from the parser let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = $0"; - - assert_snapshot!( + assert_eq!( logical_plan(sql).unwrap_err().strip_backtrace(), - @r###" - Error during planning: Invalid placeholder, zero is not a valid index: $0 - "### + "Error during planning: Invalid placeholder, zero is not a valid index: $0" ); } @@ -4372,12 +3264,8 @@ fn test_prepare_statement_to_plan_panic_prepare_wrong_syntax() { #[test] fn test_prepare_statement_to_plan_panic_no_relation_and_constant_param() { let sql = "PREPARE my_plan(INT) AS SELECT id + $1"; - - let plan = logical_plan(sql).unwrap_err().strip_backtrace(); - assert_snapshot!( - plan, - @r"Schema error: No field named id." - ); + let expected = "Schema error: No field named id."; + assert_eq!(logical_plan(sql).unwrap_err().strip_backtrace(), expected); } #[test] @@ -4419,58 +3307,46 @@ fn test_prepare_statement_to_plan_panic_is_param() { fn test_prepare_statement_to_plan_no_param() { // no embedded parameter but still declare it let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = 10"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int32] - Projection: person.id, person.age - Filter: person.age = Int64(10) - TableScan: person - "# - ); - assert_snapshot!(dt, @r#"[Int32]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int32] \ + \n Projection: person.id, person.age\ + \n Filter: person.age = Int64(10)\ + \n TableScan: person"; + + let expected_dt = "[Int32]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, person.age - Filter: person.age = Int64(10) - TableScan: person - " - ); + let expected_plan = "Projection: person.id, person.age\ + \n Filter: person.age = Int64(10)\ + \n TableScan: person"; + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); ////////////////////////////////////////// // no embedded parameter and no declare it let sql = "PREPARE my_plan AS SELECT id, age FROM person WHERE age = 10"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [] - Projection: person.id, person.age - Filter: person.age = Int64(10) - TableScan: person - "# - ); - assert_snapshot!(dt, @r#"[]"#); + + let expected_plan = "Prepare: \"my_plan\" [] \ + \n Projection: person.id, person.age\ + \n Filter: person.age = Int64(10)\ + \n TableScan: person"; + + let expected_dt = "[]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values let param_values: Vec = vec![]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, person.age - Filter: person.age = Int64(10) - TableScan: person - " - ); + let expected_plan = "Projection: person.id, person.age\ + \n Filter: person.age = Int64(10)\ + \n TableScan: person"; + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] @@ -4480,14 +3356,12 @@ fn test_prepare_statement_to_plan_one_param_no_value_panic() { let plan = logical_plan(sql).unwrap(); // declare 1 param but provide 0 let param_values: Vec = vec![]; - - assert_snapshot!( + assert_eq!( plan.with_param_values(param_values) - .unwrap_err() - .strip_backtrace(), - @r###" - Error during planning: Expected 1 parameters, got 0 - "###); + .unwrap_err() + .strip_backtrace(), + "Error during planning: Expected 1 parameters, got 0" + ); } #[test] @@ -4497,14 +3371,11 @@ fn test_prepare_statement_to_plan_one_param_one_value_different_type_panic() { let plan = logical_plan(sql).unwrap(); // declare 1 param but provide 0 let param_values = vec![ScalarValue::Float64(Some(20.0))]; - - assert_snapshot!( + assert_eq!( plan.with_param_values(param_values) .unwrap_err() .strip_backtrace(), - @r###" - Error during planning: Expected parameter of type Int32, got Float64 at index 0 - "### + "Error during planning: Expected parameter of type Int32, got Float64 at index 0" ); } @@ -4515,80 +3386,56 @@ fn test_prepare_statement_to_plan_no_param_on_value_panic() { let plan = logical_plan(sql).unwrap(); // declare 1 param but provide 0 let param_values = vec![ScalarValue::Int32(Some(10))]; - - assert_snapshot!( + assert_eq!( plan.with_param_values(param_values) .unwrap_err() .strip_backtrace(), - @r###" - Error during planning: Expected 0 parameters, got 1 - "### + "Error during planning: Expected 0 parameters, got 1" ); } #[test] fn test_prepare_statement_to_plan_params_as_constants() { let sql = "PREPARE my_plan(INT) AS SELECT $1"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int32] - Projection: $1 - EmptyRelation - "# - ); - assert_snapshot!(dt, @r#"[Int32]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int32] \ + \n Projection: $1\n EmptyRelation"; + let expected_dt = "[Int32]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r" - Projection: Int32(10) AS $1 - EmptyRelation - " - ); + let expected_plan = "Projection: Int32(10) AS $1\n EmptyRelation"; + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); /////////////////////////////////////// let sql = "PREPARE my_plan(INT) AS SELECT 1 + $1"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int32] - Projection: Int64(1) + $1 - EmptyRelation - "# - ); - assert_snapshot!(dt, @r#"[Int32]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int32] \ + \n Projection: Int64(1) + $1\n EmptyRelation"; + let expected_dt = "[Int32]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r" - Projection: Int64(1) + Int32(10) AS Int64(1) + $1 - EmptyRelation - " - ); + let expected_plan = + "Projection: Int64(1) + Int32(10) AS Int64(1) + $1\n EmptyRelation"; + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); /////////////////////////////////////// let sql = "PREPARE my_plan(INT, DOUBLE) AS SELECT 1 + $1 + $2"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int32, Float64] - Projection: Int64(1) + $1 + $2 - EmptyRelation - "# - ); - assert_snapshot!(dt, @r#"[Int32, Float64]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int32, Float64] \ + \n Projection: Int64(1) + $1 + $2\n EmptyRelation"; + let expected_dt = "[Int32, Float64]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values @@ -4596,95 +3443,91 @@ fn test_prepare_statement_to_plan_params_as_constants() { ScalarValue::Int32(Some(10)), ScalarValue::Float64(Some(10.0)), ]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r" - Projection: Int64(1) + Int32(10) + Float64(10) AS Int64(1) + $1 + $2 - EmptyRelation - " - ); + let expected_plan = + "Projection: Int64(1) + Int32(10) + Float64(10) AS Int64(1) + $1 + $2\ + \n EmptyRelation"; + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] -fn test_infer_types_from_join() { +fn test_prepare_statement_infer_types_from_join() { let sql = "SELECT id, order_id FROM person JOIN orders ON id = customer_id and age = $1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id AND person.age = $1 - TableScan: person - TableScan: orders + let expected_plan = r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id AND person.age = $1 + TableScan: person + TableScan: orders "# - ); + .trim(); + + let expected_dt = "[Int32]"; + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([("$1".to_string(), Some(DataType::Int32))]); assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::Int32(Some(10))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, orders.order_id - Inner Join: Filter: person.id = orders.customer_id AND person.age = Int32(10) - TableScan: person - TableScan: orders - " - ); + let param_values = vec![ScalarValue::Int32(Some(10))].into(); + let expected_plan = r#" +Projection: person.id, orders.order_id + Inner Join: Filter: person.id = orders.customer_id AND person.age = Int32(10) + TableScan: person + TableScan: orders + "# + .trim(); + let plan = plan.replace_params_with_values(¶m_values).unwrap(); + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] -fn test_infer_types_from_predicate() { +fn test_prepare_statement_infer_types_from_predicate() { let sql = "SELECT id, age FROM person WHERE age = $1"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.id, person.age - Filter: person.age = $1 - TableScan: person - "# - ); + + let expected_plan = r#" +Projection: person.id, person.age + Filter: person.age = $1 + TableScan: person + "# + .trim(); + + let expected_dt = "[Int32]"; + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([("$1".to_string(), Some(DataType::Int32))]); assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::Int32(Some(10))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, person.age - Filter: person.age = Int32(10) - TableScan: person - " - ); + let param_values = vec![ScalarValue::Int32(Some(10))].into(); + let expected_plan = r#" +Projection: person.id, person.age + Filter: person.age = Int32(10) + TableScan: person + "# + .trim(); + let plan = plan.replace_params_with_values(¶m_values).unwrap(); + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] -fn test_infer_types_from_between_predicate() { +fn test_prepare_statement_infer_types_from_between_predicate() { let sql = "SELECT id, age FROM person WHERE age BETWEEN $1 AND $2"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.id, person.age - Filter: person.age BETWEEN $1 AND $2 - TableScan: person - "# - ); + let expected_plan = r#" +Projection: person.id, person.age + Filter: person.age BETWEEN $1 AND $2 + TableScan: person + "# + .trim(); + + let expected_dt = "[Int32]"; + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([ @@ -4694,75 +3537,74 @@ fn test_infer_types_from_between_predicate() { assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::Int32(Some(10)), ScalarValue::Int32(Some(30))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, person.age - Filter: person.age BETWEEN Int32(10) AND Int32(30) - TableScan: person - " - ); + let param_values = + vec![ScalarValue::Int32(Some(10)), ScalarValue::Int32(Some(30))].into(); + let expected_plan = r#" +Projection: person.id, person.age + Filter: person.age BETWEEN Int32(10) AND Int32(30) + TableScan: person + "# + .trim(); + let plan = plan.replace_params_with_values(¶m_values).unwrap(); + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] -fn test_infer_types_subquery() { +fn test_prepare_statement_infer_types_subquery() { let sql = "SELECT id, age FROM person WHERE age = (select max(age) from person where id = $1)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: person.id, person.age - Filter: person.age = () - Subquery: - Projection: max(person.age) - Aggregate: groupBy=[[]], aggr=[[max(person.age)]] - Filter: person.id = $1 - TableScan: person - TableScan: person - "# - ); + let expected_plan = r#" +Projection: person.id, person.age + Filter: person.age = () + Subquery: + Projection: max(person.age) + Aggregate: groupBy=[[]], aggr=[[max(person.age)]] + Filter: person.id = $1 + TableScan: person + TableScan: person + "# + .trim(); + + let expected_dt = "[Int32]"; + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([("$1".to_string(), Some(DataType::UInt32))]); assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::UInt32(Some(10))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, person.age - Filter: person.age = () - Subquery: - Projection: max(person.age) - Aggregate: groupBy=[[]], aggr=[[max(person.age)]] - Filter: person.id = UInt32(10) - TableScan: person - TableScan: person - " - ); + let param_values = vec![ScalarValue::UInt32(Some(10))].into(); + let expected_plan = r#" +Projection: person.id, person.age + Filter: person.age = () + Subquery: + Projection: max(person.age) + Aggregate: groupBy=[[]], aggr=[[max(person.age)]] + Filter: person.id = UInt32(10) + TableScan: person + TableScan: person + "# + .trim(); + let plan = plan.replace_params_with_values(¶m_values).unwrap(); + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] -fn test_update_infer() { +fn test_prepare_statement_update_infer() { let sql = "update person set age=$1 where id=$2"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Dml: op=[Update] table=[person] - Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, $1 AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 - Filter: person.id = $2 - TableScan: person - "# - ); + let expected_plan = r#" +Dml: op=[Update] table=[person] + Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, $1 AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 + Filter: person.id = $2 + TableScan: person + "# + .trim(); + + let expected_dt = "[Int32]"; + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([ @@ -4772,32 +3614,32 @@ fn test_update_infer() { assert_eq!(actual_types, expected_types); // replace params with values - let param_values = vec![ScalarValue::Int32(Some(42)), ScalarValue::UInt32(Some(1))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - - assert_snapshot!( - plan_with_params, - @r" - Dml: op=[Update] table=[person] - Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, Int32(42) AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 - Filter: person.id = UInt32(1) - TableScan: person - " - ); + let param_values = + vec![ScalarValue::Int32(Some(42)), ScalarValue::UInt32(Some(1))].into(); + let expected_plan = r#" +Dml: op=[Update] table=[person] + Projection: person.id AS id, person.first_name AS first_name, person.last_name AS last_name, Int32(42) AS age, person.state AS state, person.salary AS salary, person.birth_date AS birth_date, person.😀 AS 😀 + Filter: person.id = UInt32(1) + TableScan: person + "# + .trim(); + let plan = plan.replace_params_with_values(¶m_values).unwrap(); + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] -fn test_insert_infer() { +fn test_prepare_statement_insert_infer() { let sql = "insert into person (id, first_name, last_name) values ($1, $2, $3)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Dml: op=[Insert Into] table=[person] - Projection: column1 AS id, column2 AS first_name, column3 AS last_name, CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀 - Values: ($1, $2, $3) - "# - ); + + let expected_plan = "Dml: op=[Insert Into] table=[person]\ + \n Projection: column1 AS id, column2 AS first_name, column3 AS last_name, \ + CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, \ + CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀\ + \n Values: ($1, $2, $3)"; + + let expected_dt = "[Int32]"; + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); let actual_types = plan.get_parameter_types().unwrap(); let expected_types = HashMap::from([ @@ -4812,79 +3654,64 @@ fn test_insert_infer() { ScalarValue::UInt32(Some(1)), ScalarValue::from("Alan"), ScalarValue::from("Turing"), - ]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r#" - Dml: op=[Insert Into] table=[person] - Projection: column1 AS id, column2 AS first_name, column3 AS last_name, CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀 - Values: (UInt32(1) AS $1, Utf8("Alan") AS $2, Utf8("Turing") AS $3) - "# - ); + ] + .into(); + let expected_plan = "Dml: op=[Insert Into] table=[person]\ + \n Projection: column1 AS id, column2 AS first_name, column3 AS last_name, \ + CAST(NULL AS Int32) AS age, CAST(NULL AS Utf8) AS state, CAST(NULL AS Float64) AS salary, \ + CAST(NULL AS Timestamp(Nanosecond, None)) AS birth_date, CAST(NULL AS Int32) AS 😀\ + \n Values: (UInt32(1) AS $1, Utf8(\"Alan\") AS $2, Utf8(\"Turing\") AS $3)"; + let plan = plan.replace_params_with_values(¶m_values).unwrap(); + + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] fn test_prepare_statement_to_plan_one_param() { let sql = "PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age = $1"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int32] - Projection: person.id, person.age - Filter: person.age = $1 - TableScan: person - "# - ); - assert_snapshot!(dt, @r#"[Int32]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int32] \ + \n Projection: person.id, person.age\ + \n Filter: person.age = $1\ + \n TableScan: person"; + + let expected_dt = "[Int32]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values let param_values = vec![ScalarValue::Int32(Some(10))]; + let expected_plan = "Projection: person.id, person.age\ + \n Filter: person.age = Int32(10)\ + \n TableScan: person"; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, person.age - Filter: person.age = Int32(10) - TableScan: person - " - ); + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] fn test_prepare_statement_to_plan_data_type() { let sql = "PREPARE my_plan(DOUBLE) AS SELECT id, age FROM person WHERE age = $1"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - // age is defined as Int32 but prepare statement declares it as DOUBLE/Float64 - // Prepare statement and its logical plan should be created successfully - @r#" - Prepare: "my_plan" [Float64] - Projection: person.id, person.age - Filter: person.age = $1 - TableScan: person - "# - ); - assert_snapshot!(dt, @r#"[Float64]"#); + // age is defined as Int32 but prepare statement declares it as DOUBLE/Float64 + // Prepare statement and its logical plan should be created successfully + let expected_plan = "Prepare: \"my_plan\" [Float64] \ + \n Projection: person.id, person.age\ + \n Filter: person.age = $1\ + \n TableScan: person"; + + let expected_dt = "[Float64]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values still succeed and use Float64 let param_values = vec![ScalarValue::Float64(Some(10.0))]; + let expected_plan = "Projection: person.id, person.age\ + \n Filter: person.age = Float64(10)\ + \n TableScan: person"; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r" - Projection: person.id, person.age - Filter: person.age = Float64(10) - TableScan: person - " - ); + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] @@ -4893,17 +3720,15 @@ fn test_prepare_statement_to_plan_multi_params() { SELECT id, age, $6 FROM person WHERE age IN ($1, $4) AND salary > $3 and salary < $5 OR first_name < $2"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int32, Utf8, Float64, Int32, Float64, Utf8] - Projection: person.id, person.age, $6 - Filter: person.age IN ([$1, $4]) AND person.salary > $3 AND person.salary < $5 OR person.first_name < $2 - TableScan: person - "# - ); - assert_snapshot!(dt, @r#"[Int32, Utf8, Float64, Int32, Float64, Utf8]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int32, Utf8, Float64, Int32, Float64, Utf8] \ + \n Projection: person.id, person.age, $6\ + \n Filter: person.age IN ([$1, $4]) AND person.salary > $3 AND person.salary < $5 OR person.first_name < $2\ + \n TableScan: person"; + + let expected_dt = "[Int32, Utf8, Float64, Int32, Float64, Utf8]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values @@ -4915,16 +3740,12 @@ fn test_prepare_statement_to_plan_multi_params() { ScalarValue::Float64(Some(200.0)), ScalarValue::from("xyz"), ]; + let expected_plan = + "Projection: person.id, person.age, Utf8(\"xyz\") AS $6\ + \n Filter: person.age IN ([Int32(10), Int32(20)]) AND person.salary > Float64(100) AND person.salary < Float64(200) OR person.first_name < Utf8(\"abc\")\ + \n TableScan: person"; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r#" - Projection: person.id, person.age, Utf8("xyz") AS $6 - Filter: person.age IN ([Int32(10), Int32(20)]) AND person.salary > Float64(100) AND person.salary < Float64(200) OR person.first_name < Utf8("abc") - TableScan: person - "# - ); + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] @@ -4936,19 +3757,17 @@ fn test_prepare_statement_to_plan_having() { GROUP BY id HAVING sum(age) < $1 AND sum(age) > 10 OR sum(age) in ($3, $4)\ "; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int32, Float64, Float64, Float64] - Projection: person.id, sum(person.age) - Filter: sum(person.age) < $1 AND sum(person.age) > Int64(10) OR sum(person.age) IN ([$3, $4]) - Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]] - Filter: person.salary > $2 - TableScan: person - "# - ); - assert_snapshot!(dt, @r#"[Int32, Float64, Float64, Float64]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int32, Float64, Float64, Float64] \ + \n Projection: person.id, sum(person.age)\ + \n Filter: sum(person.age) < $1 AND sum(person.age) > Int64(10) OR sum(person.age) IN ([$3, $4])\ + \n Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]]\ + \n Filter: person.salary > $2\ + \n TableScan: person"; + + let expected_dt = "[Int32, Float64, Float64, Float64]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); /////////////////// // replace params with values @@ -4958,18 +3777,14 @@ fn test_prepare_statement_to_plan_having() { ScalarValue::Float64(Some(200.0)), ScalarValue::Float64(Some(300.0)), ]; + let expected_plan = + "Projection: person.id, sum(person.age)\ + \n Filter: sum(person.age) < Int32(10) AND sum(person.age) > Int64(10) OR sum(person.age) IN ([Float64(200), Float64(300)])\ + \n Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]]\ + \n Filter: person.salary > Float64(100)\ + \n TableScan: person"; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r#" - Projection: person.id, sum(person.age) - Filter: sum(person.age) < Int32(10) AND sum(person.age) > Int64(10) OR sum(person.age) IN ([Float64(200), Float64(300)]) - Aggregate: groupBy=[[person.id]], aggr=[[sum(person.age)]] - Filter: person.salary > Float64(100) - TableScan: person - "# - ); + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] @@ -4977,29 +3792,22 @@ fn test_prepare_statement_to_plan_limit() { let sql = "PREPARE my_plan(BIGINT, BIGINT) AS SELECT id FROM person \ OFFSET $1 LIMIT $2"; - let (plan, dt) = generate_prepare_stmt_and_data_types(sql); - assert_snapshot!( - plan, - @r#" - Prepare: "my_plan" [Int64, Int64] - Limit: skip=$1, fetch=$2 - Projection: person.id - TableScan: person - "# - ); - assert_snapshot!(dt, @r#"[Int64, Int64]"#); + + let expected_plan = "Prepare: \"my_plan\" [Int64, Int64] \ + \n Limit: skip=$1, fetch=$2\ + \n Projection: person.id\ + \n TableScan: person"; + + let expected_dt = "[Int64, Int64]"; + + let plan = prepare_stmt_quick_test(sql, expected_plan, expected_dt); // replace params with values let param_values = vec![ScalarValue::Int64(Some(10)), ScalarValue::Int64(Some(200))]; - let plan_with_params = plan.with_param_values(param_values).unwrap(); - assert_snapshot!( - plan_with_params, - @r#" - Limit: skip=10, fetch=200 - Projection: person.id - TableScan: person - "# - ); + let expected_plan = "Limit: skip=10, fetch=200\ + \n Projection: person.id\ + \n TableScan: person"; + prepare_stmt_replace_params_quick_test(plan, param_values, expected_plan); } #[test] @@ -5042,16 +3850,12 @@ fn test_inner_join_with_cast_key() { FROM person INNER JOIN orders ON cast(person.id as Int) = cast(orders.customer_id as Int)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.age - Inner Join: Filter: CAST(person.id AS Int32) = CAST(orders.customer_id AS Int32) - TableScan: person - TableScan: orders -"# - ); + + let expected = "Projection: person.id, person.age\ + \n Inner Join: Filter: CAST(person.id AS Int32) = CAST(orders.customer_id AS Int32)\ + \n TableScan: person\ + \n TableScan: orders"; + quick_test(sql, expected); } #[test] @@ -5061,107 +3865,74 @@ fn test_multi_grouping_sets() { GROUP BY person.id, GROUPING SETS ((person.age,person.salary),(person.age))"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.age - Aggregate: groupBy=[[GROUPING SETS ((person.id, person.age, person.salary), (person.id, person.age))]], aggr=[[]] - TableScan: person -"# - ); + + let expected = "Projection: person.id, person.age\ + \n Aggregate: groupBy=[[GROUPING SETS ((person.id, person.age, person.salary), (person.id, person.age))]], aggr=[[]]\ + \n TableScan: person"; + quick_test(sql, expected); + let sql = "SELECT person.id, person.age FROM person GROUP BY person.id, GROUPING SETS ((person.age, person.salary),(person.age)), ROLLUP(person.state, person.birth_date)"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: person.id, person.age - Aggregate: groupBy=[[GROUPING SETS ((person.id, person.age, person.salary), (person.id, person.age, person.salary, person.state), (person.id, person.age, person.salary, person.state, person.birth_date), (person.id, person.age), (person.id, person.age, person.state), (person.id, person.age, person.state, person.birth_date))]], aggr=[[]] - TableScan: person -"# - ); + + let expected = "Projection: person.id, person.age\ + \n Aggregate: groupBy=[[GROUPING SETS (\ + (person.id, person.age, person.salary), \ + (person.id, person.age, person.salary, person.state), \ + (person.id, person.age, person.salary, person.state, person.birth_date), \ + (person.id, person.age), \ + (person.id, person.age, person.state), \ + (person.id, person.age, person.state, person.birth_date))]], aggr=[[]]\ + \n TableScan: person"; + quick_test(sql, expected); } #[test] fn test_field_not_found_window_function() { let order_by_sql = "SELECT count() OVER (order by a);"; - let order_by_err = logical_plan(order_by_sql) - .expect_err("query should have failed") - .strip_backtrace(); - - assert_snapshot!( - order_by_err, - @r###" - Schema error: No field named a. - "### - ); + let order_by_err = logical_plan(order_by_sql).expect_err("query should have failed"); + let expected = "Schema error: No field named a."; + assert_eq!(order_by_err.strip_backtrace(), expected); let partition_by_sql = "SELECT count() OVER (PARTITION BY a);"; - let partition_by_err = logical_plan(partition_by_sql) - .expect_err("query should have failed") - .strip_backtrace(); - - assert_snapshot!( - partition_by_err, - @r###" - Schema error: No field named a. - "### - ); + let partition_by_err = + logical_plan(partition_by_sql).expect_err("query should have failed"); + let expected = "Schema error: No field named a."; + assert_eq!(partition_by_err.strip_backtrace(), expected); - let sql = "SELECT order_id, MAX(qty) OVER (PARTITION BY orders.order_id) from orders"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING - WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]] - TableScan: orders -"# - ); + let qualified_sql = + "SELECT order_id, MAX(qty) OVER (PARTITION BY orders.order_id) from orders"; + let expected = "Projection: orders.order_id, max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING\n WindowAggr: windowExpr=[[max(orders.qty) PARTITION BY [orders.order_id] ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING]]\n TableScan: orders"; + quick_test(qualified_sql, expected); } #[test] fn test_parse_escaped_string_literal_value() { let sql = r"SELECT character_length('\r\n') AS len"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" - Projection: character_length(Utf8("\r\n")) AS len - EmptyRelation - "# - ); - let sql = "SELECT character_length(E'\r\n') AS len"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @r#" -Projection: character_length(Utf8(" -")) AS len - EmptyRelation -"# - ); + let expected = "Projection: character_length(Utf8(\"\\r\\n\")) AS len\ + \n EmptyRelation"; + quick_test(sql, expected); + + let sql = r"SELECT character_length(E'\r\n') AS len"; + let expected = "Projection: character_length(Utf8(\"\r\n\")) AS len\ + \n EmptyRelation"; + quick_test(sql, expected); + let sql = r"SELECT character_length(E'\445') AS len, E'\x4B' AS hex, E'\u0001' AS unicode"; - let plan = logical_plan(sql).unwrap(); - assert_snapshot!( - plan, - @"Projection: character_length(Utf8(\"%\")) AS len, Utf8(\"K\") AS hex, Utf8(\"\u{1}\") AS unicode\n EmptyRelation" - ); + let expected = + "Projection: character_length(Utf8(\"%\")) AS len, Utf8(\"\u{004b}\") AS hex, Utf8(\"\u{0001}\") AS unicode\ + \n EmptyRelation"; + quick_test(sql, expected); let sql = r"SELECT character_length(E'\000') AS len"; - - assert_snapshot!( - logical_plan(sql).unwrap_err(), - @r###" - SQL error: TokenizerError("Unterminated encoded string literal at Line: 1, Column: 25") - "### - ); + assert_eq!( + logical_plan(sql).unwrap_err().strip_backtrace(), + "SQL error: TokenizerError(\"Unterminated encoded string literal at Line: 1, Column: 25\")" + ) } #[test] @@ -5277,36 +4048,22 @@ fn test_custom_type_plan() -> Result<()> { } let plan = plan_sql(sql); - - assert_snapshot!( - plan, - @r###" - Projection: CAST(Utf8("2001-01-01 18:00:00") AS Timestamp(Nanosecond, None)) - EmptyRelation - "### - ); + let expected = + "Projection: CAST(Utf8(\"2001-01-01 18:00:00\") AS Timestamp(Nanosecond, None))\ + \n EmptyRelation"; + assert_eq!(plan.to_string(), expected); let plan = plan_sql("SELECT CAST(TIMESTAMP '2001-01-01 18:00:00' AS DATETIME)"); - - assert_snapshot!( - plan, - @r###" - Projection: CAST(CAST(Utf8("2001-01-01 18:00:00") AS Timestamp(Nanosecond, None)) AS Timestamp(Nanosecond, None)) - EmptyRelation - "### - ); + let expected = "Projection: CAST(CAST(Utf8(\"2001-01-01 18:00:00\") AS Timestamp(Nanosecond, None)) AS Timestamp(Nanosecond, None))\ + \n EmptyRelation"; + assert_eq!(plan.to_string(), expected); let plan = plan_sql( "SELECT ARRAY[DATETIME '2001-01-01 18:00:00', DATETIME '2001-01-02 18:00:00']", ); - - assert_snapshot!( - plan, - @r###" - Projection: make_array(CAST(Utf8("2001-01-01 18:00:00") AS Timestamp(Nanosecond, None)), CAST(Utf8("2001-01-02 18:00:00") AS Timestamp(Nanosecond, None))) - EmptyRelation - "### - ); + let expected = "Projection: make_array(CAST(Utf8(\"2001-01-01 18:00:00\") AS Timestamp(Nanosecond, None)), CAST(Utf8(\"2001-01-02 18:00:00\") AS Timestamp(Nanosecond, None)))\ + \n EmptyRelation"; + assert_eq!(plan.to_string(), expected); Ok(()) } @@ -5337,7 +4094,7 @@ fn test_error_message_invalid_scalar_function_signature() { fn test_error_message_invalid_aggregate_function_signature() { error_message_test( "select sum()", - "Error during planning: Execution error: Function 'sum' user-defined coercion failed with \"Execution error: sum function requires 1 argument, got 0\"", + "Error during planning: 'sum' does not support zero arguments", ); // We keep two different prefixes because they clarify each other. // It might be incorrect, and we should consider keeping only one. @@ -5359,7 +4116,7 @@ fn test_error_message_invalid_window_function_signature() { fn test_error_message_invalid_window_aggregate_function_signature() { error_message_test( "select sum() over()", - "Error during planning: Execution error: Function 'sum' user-defined coercion failed with \"Execution error: sum function requires 1 argument, got 0\"", + "Error during planning: 'sum' does not support zero arguments", ); } diff --git a/datafusion/sqllogictest/Cargo.toml b/datafusion/sqllogictest/Cargo.toml index b2e02d7d5dd87..4c7ee6c1bb865 100644 --- a/datafusion/sqllogictest/Cargo.toml +++ b/datafusion/sqllogictest/Cargo.toml @@ -42,7 +42,7 @@ async-trait = { workspace = true } bigdecimal = { workspace = true } bytes = { workspace = true, optional = true } chrono = { workspace = true, optional = true } -clap = { version = "4.5.36", features = ["derive", "env"] } +clap = { version = "4.5.34", features = ["derive", "env"] } datafusion = { workspace = true, default-features = true, features = ["avro"] } futures = { workspace = true } half = { workspace = true, default-features = true } @@ -55,7 +55,7 @@ postgres-types = { version = "0.2.8", features = ["derive", "with-chrono-0_4"], rust_decimal = { version = "1.37.1", features = ["tokio-pg"] } # When updating the following dependency verify that sqlite test file regeneration works correctly # by running the regenerate_sqlite_files.sh script. -sqllogictest = "0.28.1" +sqllogictest = "0.28.0" sqlparser = { workspace = true } tempfile = { workspace = true } testcontainers = { version = "0.23", features = ["default"], optional = true } diff --git a/datafusion/sqllogictest/bin/sqllogictests.rs b/datafusion/sqllogictest/bin/sqllogictests.rs index 21dfe2ee08f4e..5894ec056a2eb 100644 --- a/datafusion/sqllogictest/bin/sqllogictests.rs +++ b/datafusion/sqllogictest/bin/sqllogictests.rs @@ -175,7 +175,12 @@ async fn run_tests() -> Result<()> { futures::stream::iter(match result { // Tokio panic error Err(e) => Some(DataFusionError::External(Box::new(e))), - Ok(thread_result) => thread_result.err(), + Ok(thread_result) => match thread_result { + // Test run error + Err(e) => Some(e), + // success + Ok(_) => None, + }, }) }) .collect() diff --git a/datafusion/sqllogictest/test_files/aggregate.slt b/datafusion/sqllogictest/test_files/aggregate.slt index 1f63e5fcad5c7..57728ef8c734a 100644 --- a/datafusion/sqllogictest/test_files/aggregate.slt +++ b/datafusion/sqllogictest/test_files/aggregate.slt @@ -133,50 +133,36 @@ SELECT approx_distinct(c9) count_c9, approx_distinct(cast(c9 as varchar)) count_ # csv_query_approx_percentile_cont_with_weight statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont_with_weight' function: coercion from \[Utf8, Int8, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont_with_weight(c2, 0.95) WITHIN GROUP (ORDER BY c1) FROM aggregate_test_100 +SELECT approx_percentile_cont_with_weight(c1, c2, 0.95) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont_with_weight' function: coercion from \[Int16, Utf8, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont_with_weight(c1, 0.95) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 +SELECT approx_percentile_cont_with_weight(c3, c1, 0.95) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont_with_weight' function: coercion from \[Int16, Int8, Utf8\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont_with_weight(c2, c1) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 +SELECT approx_percentile_cont_with_weight(c3, c2, c1) FROM aggregate_test_100 # csv_query_approx_percentile_cont_with_histogram_bins statement error DataFusion error: This feature is not implemented: Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be UInt > 0 literal \(got data type Int64\)\. -SELECT c1, approx_percentile_cont(0.95, -1000) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(c3, 0.95, -1000) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont' function: coercion from \[Int16, Float64, Utf8\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont(0.95, c1) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 +SELECT approx_percentile_cont(c3, 0.95, c1) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont' function: coercion from \[Int16, Float64, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont(0.95, 111.1) WITHIN GROUP (ORDER BY c3) FROM aggregate_test_100 +SELECT approx_percentile_cont(c3, 0.95, 111.1) FROM aggregate_test_100 statement error DataFusion error: Error during planning: Failed to coerce arguments to satisfy a call to 'approx_percentile_cont' function: coercion from \[Float64, Float64, Float64\] to the signature OneOf(.*) failed(.|\n)* -SELECT approx_percentile_cont(0.95, 111.1) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 +SELECT approx_percentile_cont(c12, 0.95, 111.1) FROM aggregate_test_100 statement error DataFusion error: This feature is not implemented: Percentile value for 'APPROX_PERCENTILE_CONT' must be a literal -SELECT approx_percentile_cont(c12) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 +SELECT approx_percentile_cont(c12, c12) FROM aggregate_test_100 statement error DataFusion error: This feature is not implemented: Tdigest max_size value for 'APPROX_PERCENTILE_CONT' must be a literal -SELECT approx_percentile_cont(0.95, c5) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 - -statement error DataFusion error: This feature is not implemented: Conflicting ordering requirements in aggregate functions is not supported -SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5), approx_percentile_cont(0.2) WITHIN GROUP (ORDER BY c12) FROM aggregate_test_100 - -statement error DataFusion error: Error during planning: \[IGNORE | RESPECT\] NULLS are not permitted for approx_percentile_cont -SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5) IGNORE NULLS FROM aggregate_test_100 - -statement error DataFusion error: Error during planning: \[IGNORE | RESPECT\] NULLS are not permitted for approx_percentile_cont -SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5) RESPECT NULLS FROM aggregate_test_100 - -statement error DataFusion error: This feature is not implemented: Only a single ordering expression is permitted in a WITHIN GROUP clause -SELECT approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c5, c12) FROM aggregate_test_100 +SELECT approx_percentile_cont(c12, 0.95, c5) FROM aggregate_test_100 # Not supported over sliding windows -query error DataFusion error: Error during planning: OVER and WITHIN GROUP clause are can not be used together. OVER is for window function, whereas WITHIN GROUP is for ordered set aggregate function -SELECT approx_percentile_cont(0.5) -WITHIN GROUP (ORDER BY c3) -OVER (ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) +query error This feature is not implemented: Aggregate can not be used as a sliding accumulator because `retract_batch` is not implemented +SELECT approx_percentile_cont(c3, 0.5) OVER (ROWS BETWEEN 4 PRECEDING AND CURRENT ROW) FROM aggregate_test_100 # array agg can use order by @@ -1290,173 +1276,173 @@ SELECT approx_distinct(c9) AS a, approx_distinct(c9) AS b FROM aggregate_test_10 #csv_query_approx_percentile_cont (c2) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c2) AS DOUBLE) / 1.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c2, 0.1) AS DOUBLE) / 1.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c2) AS DOUBLE) / 3.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c2, 0.5) AS DOUBLE) / 3.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c2) AS DOUBLE) / 5.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c2, 0.9) AS DOUBLE) / 5.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c3) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c3) AS DOUBLE) / -95.3) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c3, 0.1) AS DOUBLE) / -95.3) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c3) AS DOUBLE) / 15.5) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c3, 0.5) AS DOUBLE) / 15.5) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c3) AS DOUBLE) / 102.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c3, 0.9) AS DOUBLE) / 102.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c4) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c4) AS DOUBLE) / -22925.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c4, 0.1) AS DOUBLE) / -22925.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c4) AS DOUBLE) / 4599.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c4, 0.5) AS DOUBLE) / 4599.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c4) AS DOUBLE) / 25334.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c4, 0.9) AS DOUBLE) / 25334.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c5) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c5) AS DOUBLE) / -1882606710.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c5, 0.1) AS DOUBLE) / -1882606710.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c5) AS DOUBLE) / 377164262.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c5, 0.5) AS DOUBLE) / 377164262.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c5) AS DOUBLE) / 1991374996.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c5, 0.9) AS DOUBLE) / 1991374996.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c6) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c6) AS DOUBLE) / -7250000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c6, 0.1) AS DOUBLE) / -7250000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c6) AS DOUBLE) / 1130000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c6, 0.5) AS DOUBLE) / 1130000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c6) AS DOUBLE) / 7370000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c6, 0.9) AS DOUBLE) / 7370000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c7) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c7) AS DOUBLE) / 18.9) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c7, 0.1) AS DOUBLE) / 18.9) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c7) AS DOUBLE) / 134.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c7, 0.5) AS DOUBLE) / 134.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c7) AS DOUBLE) / 231.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c7, 0.9) AS DOUBLE) / 231.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c8) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c8) AS DOUBLE) / 2671.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c8, 0.1) AS DOUBLE) / 2671.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c8) AS DOUBLE) / 30634.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c8, 0.5) AS DOUBLE) / 30634.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c8) AS DOUBLE) / 57518.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c8, 0.9) AS DOUBLE) / 57518.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c9) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c9) AS DOUBLE) / 472608672.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c9, 0.1) AS DOUBLE) / 472608672.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c9) AS DOUBLE) / 2365817608.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c9, 0.5) AS DOUBLE) / 2365817608.0) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c9) AS DOUBLE) / 3776538487.0) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c9, 0.9) AS DOUBLE) / 3776538487.0) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c10) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c10) AS DOUBLE) / 1830000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c10, 0.1) AS DOUBLE) / 1830000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c10) AS DOUBLE) / 9300000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c10, 0.5) AS DOUBLE) / 9300000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c10) AS DOUBLE) / 16100000000000000000) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c10, 0.9) AS DOUBLE) / 16100000000000000000) < 0.05) AS q FROM aggregate_test_100 ---- true # csv_query_approx_percentile_cont (c11) query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.1) WITHIN GROUP (ORDER BY c11) AS DOUBLE) / 0.109) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c11, 0.1) AS DOUBLE) / 0.109) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY c11) AS DOUBLE) / 0.491) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c11, 0.5) AS DOUBLE) / 0.491) < 0.05) AS q FROM aggregate_test_100 ---- true query B -SELECT (ABS(1 - CAST(approx_percentile_cont(0.9) WITHIN GROUP (ORDER BY c11) AS DOUBLE) / 0.834) < 0.05) AS q FROM aggregate_test_100 +SELECT (ABS(1 - CAST(approx_percentile_cont(c11, 0.9) AS DOUBLE) / 0.834) < 0.05) AS q FROM aggregate_test_100 ---- true # percentile_cont_with_nulls query I -SELECT APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY v) FROM (VALUES (1), (2), (3), (NULL), (NULL), (NULL)) as t (v); +SELECT APPROX_PERCENTILE_CONT(v, 0.5) FROM (VALUES (1), (2), (3), (NULL), (NULL), (NULL)) as t (v); ---- 2 # percentile_cont_with_nulls_only query I -SELECT APPROX_PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY v) FROM (VALUES (CAST(NULL as INT))) as t (v); +SELECT APPROX_PERCENTILE_CONT(v, 0.5) FROM (VALUES (CAST(NULL as INT))) as t (v); ---- NULL @@ -1479,7 +1465,7 @@ NaN # ISSUE: https://github.com/apache/datafusion/issues/11870 query R -select APPROX_PERCENTILE_CONT(0.8) WITHIN GROUP (ORDER BY v2) from tmp_percentile_cont; +select APPROX_PERCENTILE_CONT(v2, 0.8) from tmp_percentile_cont; ---- NaN @@ -1487,10 +1473,10 @@ NaN # Note: `approx_percentile_cont_with_weight()` uses the same implementation as `approx_percentile_cont()` query R SELECT APPROX_PERCENTILE_CONT_WITH_WEIGHT( + v2, '+Inf'::Double, 0.9 ) -WITHIN GROUP (ORDER BY v2) FROM tmp_percentile_cont; ---- NaN @@ -1509,7 +1495,7 @@ INSERT INTO t1 VALUES (TRUE); # ISSUE: https://github.com/apache/datafusion/issues/12716 # This test verifies that approx_percentile_cont_with_weight does not panic when given 'NaN' and returns 'inf' query R -SELECT approx_percentile_cont_with_weight(0, 0) WITHIN GROUP (ORDER BY 'NaN'::DOUBLE) FROM t1 WHERE t1.v1; +SELECT approx_percentile_cont_with_weight('NaN'::DOUBLE, 0, 0) FROM t1 WHERE t1.v1; ---- Infinity @@ -1736,7 +1722,7 @@ b NULL NULL 7732.315789473684 # csv_query_approx_percentile_cont_with_weight query TI -SELECT c1, approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(c3, 0.95) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 73 b 68 @@ -1744,18 +1730,9 @@ c 122 d 124 e 115 -query TI -SELECT c1, approx_percentile_cont(0.95) WITHIN GROUP (ORDER BY c3 DESC) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ----- -a -101 -b -114 -c -109 -d -98 -e -93 - # csv_query_approx_percentile_cont_with_weight (2) query TI -SELECT c1, approx_percentile_cont_with_weight(1, 0.95) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont_with_weight(c3, 1, 0.95) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 73 b 68 @@ -1763,18 +1740,9 @@ c 122 d 124 e 115 -query TI -SELECT c1, approx_percentile_cont_with_weight(1, 0.95) WITHIN GROUP (ORDER BY c3 DESC) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ----- -a -101 -b -114 -c -109 -d -98 -e -93 - # csv_query_approx_percentile_cont_with_histogram_bins query TI -SELECT c1, approx_percentile_cont(0.95, 200) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(c3, 0.95, 200) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 73 b 68 @@ -1783,7 +1751,7 @@ d 124 e 115 query TI -SELECT c1, approx_percentile_cont_with_weight(c2, 0.95) WITHIN GROUP (ORDER BY c3) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont_with_weight(c3, c2, 0.95) AS c3_p95 FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 74 b 68 @@ -3073,7 +3041,7 @@ SELECT COUNT(DISTINCT c1) FROM test # test_approx_percentile_cont_decimal_support query TI -SELECT c1, approx_percentile_cont(cast(0.85 as decimal(10,2))) WITHIN GROUP (ORDER BY c2) apc FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 +SELECT c1, approx_percentile_cont(c2, cast(0.85 as decimal(10,2))) apc FROM aggregate_test_100 GROUP BY 1 ORDER BY 1 ---- a 4 b 5 @@ -5001,73 +4969,6 @@ select count(distinct column1), count(distinct column2) from dict_test group by statement ok drop table dict_test; -# avg_duration - -statement ok -create table d as values - (arrow_cast(1, 'Duration(Second)'), arrow_cast(2, 'Duration(Millisecond)'), arrow_cast(3, 'Duration(Microsecond)'), arrow_cast(4, 'Duration(Nanosecond)'), 1), - (arrow_cast(11, 'Duration(Second)'), arrow_cast(22, 'Duration(Millisecond)'), arrow_cast(33, 'Duration(Microsecond)'), arrow_cast(44, 'Duration(Nanosecond)'), 1); - -query ???? -SELECT avg(column1), avg(column2), avg(column3), avg(column4) FROM d; ----- -0 days 0 hours 0 mins 6 secs 0 days 0 hours 0 mins 0.012 secs 0 days 0 hours 0 mins 0.000018 secs 0 days 0 hours 0 mins 0.000000024 secs - -query ????I -SELECT avg(column1), avg(column2), avg(column3), avg(column4), column5 FROM d GROUP BY column5; ----- -0 days 0 hours 0 mins 6 secs 0 days 0 hours 0 mins 0.012 secs 0 days 0 hours 0 mins 0.000018 secs 0 days 0 hours 0 mins 0.000000024 secs 1 - -statement ok -drop table d; - -statement ok -create table d as values - (arrow_cast(1, 'Duration(Second)'), arrow_cast(2, 'Duration(Millisecond)'), arrow_cast(3, 'Duration(Microsecond)'), arrow_cast(4, 'Duration(Nanosecond)'), 1), - (arrow_cast(11, 'Duration(Second)'), arrow_cast(22, 'Duration(Millisecond)'), arrow_cast(33, 'Duration(Microsecond)'), arrow_cast(44, 'Duration(Nanosecond)'), 1), - (arrow_cast(5, 'Duration(Second)'), arrow_cast(10, 'Duration(Millisecond)'), arrow_cast(15, 'Duration(Microsecond)'), arrow_cast(20, 'Duration(Nanosecond)'), 2), - (arrow_cast(25, 'Duration(Second)'), arrow_cast(50, 'Duration(Millisecond)'), arrow_cast(75, 'Duration(Microsecond)'), arrow_cast(100, 'Duration(Nanosecond)'), 2), - (NULL, NULL, NULL, NULL, 1), - (NULL, NULL, NULL, NULL, 2); - - -query I? rowsort -SELECT column5, avg(column1) FROM d GROUP BY column5; ----- -1 0 days 0 hours 0 mins 6 secs -2 0 days 0 hours 0 mins 15 secs - -query I?? rowsort -SELECT column5, column1, avg(column1) OVER (PARTITION BY column5 ORDER BY column1 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) as window_avg -FROM d WHERE column1 IS NOT NULL; ----- -1 0 days 0 hours 0 mins 1 secs 0 days 0 hours 0 mins 1 secs -1 0 days 0 hours 0 mins 11 secs 0 days 0 hours 0 mins 6 secs -2 0 days 0 hours 0 mins 25 secs 0 days 0 hours 0 mins 15 secs -2 0 days 0 hours 0 mins 5 secs 0 days 0 hours 0 mins 5 secs - -# Cumulative average window function -query I?? -SELECT column5, column1, avg(column1) OVER (ORDER BY column5, column1 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as cumulative_avg -FROM d WHERE column1 IS NOT NULL; ----- -1 0 days 0 hours 0 mins 1 secs 0 days 0 hours 0 mins 1 secs -1 0 days 0 hours 0 mins 11 secs 0 days 0 hours 0 mins 6 secs -2 0 days 0 hours 0 mins 5 secs 0 days 0 hours 0 mins 5 secs -2 0 days 0 hours 0 mins 25 secs 0 days 0 hours 0 mins 10 secs - -# Centered average window function -query I?? -SELECT column5, column1, avg(column1) OVER (ORDER BY column5 ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) as centered_avg -FROM d WHERE column1 IS NOT NULL; ----- -1 0 days 0 hours 0 mins 1 secs 0 days 0 hours 0 mins 6 secs -1 0 days 0 hours 0 mins 11 secs 0 days 0 hours 0 mins 5 secs -2 0 days 0 hours 0 mins 5 secs 0 days 0 hours 0 mins 13 secs -2 0 days 0 hours 0 mins 25 secs 0 days 0 hours 0 mins 15 secs - -statement ok -drop table d; # Prepare the table with dictionary values for testing statement ok @@ -5721,11 +5622,6 @@ SELECT STRING_AGG(column1, '|') FROM (values (''), (null), ('')); ---- | -query T -SELECT STRING_AGG(DISTINCT column1, '|') FROM (values (''), (null), ('')); ----- -(empty) - statement ok CREATE TABLE strings(g INTEGER, x VARCHAR, y VARCHAR) @@ -5747,22 +5643,6 @@ SELECT STRING_AGG(x,',') FROM strings WHERE g > 100 ---- NULL -query T -SELECT STRING_AGG(DISTINCT x,',') FROM strings WHERE g > 100 ----- -NULL - -query T -SELECT STRING_AGG(DISTINCT x,'|' ORDER BY x) FROM strings ----- -a|b|i|j|p|x|y|z - -query error This feature is not implemented: The second argument of the string_agg function must be a string literal -SELECT STRING_AGG(DISTINCT x,y) FROM strings - -query error Execution error: In an aggregate with DISTINCT, ORDER BY expressions must appear in argument list -SELECT STRING_AGG(DISTINCT x,'|' ORDER BY y) FROM strings - statement ok drop table strings @@ -5777,17 +5657,6 @@ FROM my_data ---- text1, text1, text1 -query T -WITH my_data as ( -SELECT 'text1'::varchar(1000) as my_column union all -SELECT 'text1'::varchar(1000) as my_column union all -SELECT 'text1'::varchar(1000) as my_column -) -SELECT string_agg(DISTINCT my_column,', ') as my_string_agg -FROM my_data ----- -text1 - query T WITH my_data as ( SELECT 1 as dummy, 'text1'::varchar(1000) as my_column union all @@ -5800,18 +5669,6 @@ GROUP BY dummy ---- text1, text1, text1 -query T -WITH my_data as ( -SELECT 1 as dummy, 'text1'::varchar(1000) as my_column union all -SELECT 1 as dummy, 'text1'::varchar(1000) as my_column union all -SELECT 1 as dummy, 'text1'::varchar(1000) as my_column -) -SELECT string_agg(DISTINCT my_column,', ') as my_string_agg -FROM my_data -GROUP BY dummy ----- -text1 - # Tests for aggregating with NaN values statement ok CREATE TABLE float_table ( @@ -6859,7 +6716,7 @@ group1 0.0003 # median with all nulls statement ok create table group_median_all_nulls( - a STRING NOT NULL, + a STRING NOT NULL, b INT ) AS VALUES ( 'group0', NULL), @@ -6875,28 +6732,6 @@ SELECT a, median(b), arrow_typeof(median(b)) FROM group_median_all_nulls GROUP B group0 NULL Int32 group1 NULL Int32 -query I -with test AS (SELECT i as c1, i + 1 as c2 FROM generate_series(1, 10) t(i)) -select count(*) from test WHERE 1 = 1; ----- -10 - -query I -with test AS (SELECT i as c1, i + 1 as c2 FROM generate_series(1, 10) t(i)) -select count(c1) from test WHERE 1 = 1; ----- -10 - -query II rowsort -with test AS (SELECT i as c1, i + 1 as c2 FROM generate_series(1, 5) t(i)) -select c2, count(*) from test WHERE 1 = 1 group by c2; ----- -2 1 -3 1 -4 1 -5 1 -6 1 - statement ok create table t_decimal (c decimal(10, 4)) as values (100.00), (125.00), (175.00), (200.00), (200.00), (300.00), (null), (null); diff --git a/datafusion/sqllogictest/test_files/array.slt b/datafusion/sqllogictest/test_files/array.slt index 9772de3db3657..cb56686b64373 100644 --- a/datafusion/sqllogictest/test_files/array.slt +++ b/datafusion/sqllogictest/test_files/array.slt @@ -2396,11 +2396,6 @@ NULL NULL NULL NULL NULL NULL -query ? -select array_sort([struct('foo', 3), struct('foo', 1), struct('bar', 1)]) ----- -[{c0: bar, c1: 1}, {c0: foo, c1: 1}, {c0: foo, c1: 3}] - ## test with argument of incorrect types query error DataFusion error: Execution error: the second parameter of array_sort expects DESC or ASC select array_sort([1, 3, null, 5, NULL, -5], 1), array_sort([1, 3, null, 5, NULL, -5], 'DESC', 1), array_sort([1, 3, null, 5, NULL, -5], 1, 1); @@ -5992,7 +5987,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6021,7 +6016,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6050,7 +6045,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6081,7 +6076,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: array_has(LargeList([7f4b18de3cfeb9b4ac78c381ee2ad278, a, b, c]), substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32))) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6110,7 +6105,7 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: +05)--------Projection: 06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IN ([Utf8View("7f4b18de3cfeb9b4ac78c381ee2ad278"), Utf8View("a"), Utf8View("b"), Utf8View("c")]) 07)------------TableScan: tmp_table projection=[value] physical_plan @@ -6130,8 +6125,7 @@ select count(*) from test WHERE array_has([needle], needle); ---- 100000 -# The optimizer does not currently eliminate the filter; -# Instead, it's rewritten as `IS NULL OR NOT NULL` due to SQL null semantics +# TODO: this should probably be possible to completely remove the filter as always true? query TT explain with test AS (SELECT substr(md5(i::text)::text, 1, 32) as needle FROM generate_series(1, 100000) t(i)) select count(*) from test WHERE array_has([needle], needle); @@ -6141,9 +6135,10 @@ logical_plan 02)--Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] 03)----SubqueryAlias: test 04)------SubqueryAlias: t -05)--------Projection: -06)----------Filter: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) IS NOT NULL OR Boolean(NULL) -07)------------TableScan: tmp_table projection=[value] +05)--------Projection: +06)----------Filter: __common_expr_3 = __common_expr_3 +07)------------Projection: substr(CAST(md5(CAST(tmp_table.value AS Utf8)) AS Utf8), Int64(1), Int64(32)) AS __common_expr_3 +08)--------------TableScan: tmp_table projection=[value] physical_plan 01)ProjectionExec: expr=[count(Int64(1))@0 as count(*)] 02)--AggregateExec: mode=Final, gby=[], aggr=[count(Int64(1))] @@ -6151,9 +6146,10 @@ physical_plan 04)------AggregateExec: mode=Partial, gby=[], aggr=[count(Int64(1))] 05)--------ProjectionExec: expr=[] 06)----------CoalesceBatchesExec: target_batch_size=8192 -07)------------FilterExec: substr(md5(CAST(value@0 AS Utf8)), 1, 32) IS NOT NULL OR NULL -08)--------------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 -09)----------------LazyMemoryExec: partitions=1, batch_generators=[generate_series: start=1, end=100000, batch_size=8192] +07)------------FilterExec: __common_expr_3@0 = __common_expr_3@0 +08)--------------ProjectionExec: expr=[substr(md5(CAST(value@0 AS Utf8)), 1, 32) as __common_expr_3] +09)----------------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 +10)------------------LazyMemoryExec: partitions=1, batch_generators=[generate_series: start=1, end=100000, batch_size=8192] # any operator query ? @@ -7285,10 +7281,12 @@ select array_concat(column1, [7]) from arrays_values_v2; # flatten -query ? -select flatten(NULL); ----- -NULL +#TODO: https://github.com/apache/datafusion/issues/7142 +# follow DuckDB +#query ? +#select flatten(NULL); +#---- +#NULL # flatten with scalar values #1 query ??? @@ -7296,21 +7294,21 @@ select flatten(make_array(1, 2, 1, 3, 2)), flatten(make_array([1], [2, 3], [null], make_array(4, null, 5))), flatten(make_array([[1.1]], [[2.2]], [[3.3], [4.4]])); ---- -[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [[1.1], [2.2], [3.3], [4.4]] +[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [1.1, 2.2, 3.3, 4.4] query ??? select flatten(arrow_cast(make_array(1, 2, 1, 3, 2), 'LargeList(Int64)')), flatten(arrow_cast(make_array([1], [2, 3], [null], make_array(4, null, 5)), 'LargeList(LargeList(Int64))')), flatten(arrow_cast(make_array([[1.1]], [[2.2]], [[3.3], [4.4]]), 'LargeList(LargeList(LargeList(Float64)))')); ---- -[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [[1.1], [2.2], [3.3], [4.4]] +[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [1.1, 2.2, 3.3, 4.4] query ??? select flatten(arrow_cast(make_array(1, 2, 1, 3, 2), 'FixedSizeList(5, Int64)')), flatten(arrow_cast(make_array([1], [2, 3], [null], make_array(4, null, 5)), 'FixedSizeList(4, List(Int64))')), flatten(arrow_cast(make_array([[1.1], [2.2]], [[3.3], [4.4]]), 'FixedSizeList(2, List(List(Float64)))')); ---- -[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [[1.1], [2.2], [3.3], [4.4]] +[1, 2, 1, 3, 2] [1, 2, 3, NULL, 4, NULL, 5] [1.1, 2.2, 3.3, 4.4] # flatten with column values query ???? @@ -7320,8 +7318,8 @@ select flatten(column1), flatten(column4) from flatten_table; ---- -[1, 2, 3] [[1, 2, 3], [4, 5], [6]] [[[1]], [[2, 3]]] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] -[1, 2, 3, 4, 5, 6] [[8]] [[[1, 2]], [[3]]] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +[1, 2, 3] [1, 2, 3, 4, 5, 6] [1, 2, 3] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] +[1, 2, 3, 4, 5, 6] [8] [1, 2, 3] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] query ???? select flatten(column1), @@ -7330,8 +7328,8 @@ select flatten(column1), flatten(column4) from large_flatten_table; ---- -[1, 2, 3] [[1, 2, 3], [4, 5], [6]] [[[1]], [[2, 3]]] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] -[1, 2, 3, 4, 5, 6] [[8]] [[[1, 2]], [[3]]] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +[1, 2, 3] [1, 2, 3, 4, 5, 6] [1, 2, 3] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] +[1, 2, 3, 4, 5, 6] [8] [1, 2, 3] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] query ???? select flatten(column1), @@ -7340,19 +7338,8 @@ select flatten(column1), flatten(column4) from fixed_size_flatten_table; ---- -[1, 2, 3] [[1, 2, 3], [4, 5], [6]] [[[1]], [[2, 3]]] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] -[1, 2, 3, 4, 5, 6] [[8], [9, 10], [11, 12, 13]] [[[1, 2]], [[3]]] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] - -# flatten with different inner list type -query ?????? -select flatten(arrow_cast(make_array([1, 2], [3, 4]), 'List(FixedSizeList(2, Int64))')), - flatten(arrow_cast(make_array([[1, 2]], [[3, 4]]), 'List(FixedSizeList(1, List(Int64)))')), - flatten(arrow_cast(make_array([1, 2], [3, 4]), 'LargeList(List(Int64))')), - flatten(arrow_cast(make_array([[1, 2]], [[3, 4]]), 'LargeList(List(List(Int64)))')), - flatten(arrow_cast(make_array([1, 2], [3, 4]), 'LargeList(FixedSizeList(2, Int64))')), - flatten(arrow_cast(make_array([[1, 2]], [[3, 4]]), 'LargeList(FixedSizeList(1, List(Int64)))')) ----- -[1, 2, 3, 4] [[1, 2], [3, 4]] [1, 2, 3, 4] [[1, 2], [3, 4]] [1, 2, 3, 4] [[1, 2], [3, 4]] +[1, 2, 3] [1, 2, 3, 4, 5, 6] [1, 2, 3] [1.0, 2.1, 2.2, 3.2, 3.3, 3.4] +[1, 2, 3, 4, 5, 6] [8, 9, 10, 11, 12, 13] [1, 2, 3] [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] ## empty (aliases: `array_empty`, `list_empty`) # empty scalar function #1 diff --git a/datafusion/sqllogictest/test_files/binary.slt b/datafusion/sqllogictest/test_files/binary.slt index 5ac13779acd74..5c5f9d510e554 100644 --- a/datafusion/sqllogictest/test_files/binary.slt +++ b/datafusion/sqllogictest/test_files/binary.slt @@ -147,36 +147,8 @@ query error DataFusion error: Error during planning: Cannot infer common argumen SELECT column1, column1 = arrow_cast(X'0102', 'FixedSizeBinary(2)') FROM t # Comparison to different sized Binary -query ?B +query error DataFusion error: Error during planning: Cannot infer common argument type for comparison operation FixedSizeBinary\(3\) = Binary SELECT column1, column1 = X'0102' FROM t ----- -000102 false -003102 false -NULL NULL -ff0102 false -000102 false - -query ?B -SELECT column1, column1 = X'000102' FROM t ----- -000102 true -003102 false -NULL NULL -ff0102 false -000102 true - -# Plan should not have a cast of the column (should have casted the literal -# to FixedSizeBinary as that is much faster) - -query TT -explain SELECT column1, column1 = X'000102' FROM t ----- -logical_plan -01)Projection: t.column1, t.column1 = FixedSizeBinary(3, "0,1,2") AS t.column1 = Binary("0,1,2") -02)--TableScan: t projection=[column1] -physical_plan -01)ProjectionExec: expr=[column1@0 as column1, column1@0 = 000102 as t.column1 = Binary("0,1,2")] -02)--DataSourceExec: partitions=1, partition_sizes=[1] statement ok drop table t_source diff --git a/datafusion/sqllogictest/test_files/clickbench.slt b/datafusion/sqllogictest/test_files/clickbench.slt index 4c60a4365ee26..dfcd924758574 100644 --- a/datafusion/sqllogictest/test_files/clickbench.slt +++ b/datafusion/sqllogictest/test_files/clickbench.slt @@ -64,10 +64,10 @@ SELECT COUNT(DISTINCT "SearchPhrase") FROM hits; ---- 1 -query II -SELECT MIN("EventDate"), MAX("EventDate") FROM hits; +query DD +SELECT MIN("EventDate"::INT::DATE), MAX("EventDate"::INT::DATE) FROM hits; ---- -15901 15901 +2013-07-15 2013-07-15 query II SELECT "AdvEngineID", COUNT(*) FROM hits WHERE "AdvEngineID" <> 0 GROUP BY "AdvEngineID" ORDER BY COUNT(*) DESC; @@ -168,11 +168,11 @@ SELECT "SearchPhrase", MIN("URL"), MIN("Title"), COUNT(*) AS c, COUNT(DISTINCT " ---- query IITIIIIIIIIIITTIIIIIIIIIITIIITIIIITTIIITIIIIIIIIIITIIIIITIIIIIITIIIIIIIIIITTTTIIIIIIIITITTITTTTTTTTTTIIII -SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY "EventTime" LIMIT 10; +SELECT * FROM hits WHERE "URL" LIKE '%google%' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; ---- query T -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime" LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime") LIMIT 10; ---- query T @@ -180,7 +180,7 @@ SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "SearchPhras ---- query T -SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY "EventTime", "SearchPhrase" LIMIT 10; +SELECT "SearchPhrase" FROM hits WHERE "SearchPhrase" <> '' ORDER BY to_timestamp_seconds("EventTime"), "SearchPhrase" LIMIT 10; ---- query IRI @@ -247,31 +247,31 @@ SELECT "ClientIP", "ClientIP" - 1, "ClientIP" - 2, "ClientIP" - 3, COUNT(*) AS c 1615432634 1615432633 1615432632 1615432631 1 query TI -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "URL" <> '' GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10; ---- query TI -SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; +SELECT "Title", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "DontCountHits" = 0 AND "IsRefresh" = 0 AND "Title" <> '' GROUP BY "Title" ORDER BY PageViews DESC LIMIT 10; ---- query TI -SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "URL", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "IsLink" <> 0 AND "IsDownload" = 0 GROUP BY "URL" ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; ---- query IIITTI -SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; +SELECT "TraficSourceID", "SearchEngineID", "AdvEngineID", CASE WHEN ("SearchEngineID" = 0 AND "AdvEngineID" = 0) THEN "Referer" ELSE '' END AS Src, "URL" AS Dst, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 GROUP BY "TraficSourceID", "SearchEngineID", "AdvEngineID", Src, Dst ORDER BY PageViews DESC LIMIT 10 OFFSET 1000; ---- -query III -SELECT "URLHash", "EventDate", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate" ORDER BY PageViews DESC LIMIT 10 OFFSET 100; +query IDI +SELECT "URLHash", "EventDate"::INT::DATE, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "TraficSourceID" IN (-1, 6) AND "RefererHash" = 3594120000172545465 GROUP BY "URLHash", "EventDate"::INT::DATE ORDER BY PageViews DESC LIMIT 10 OFFSET 100; ---- query III -SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-01' AND "EventDate" <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; +SELECT "WindowClientWidth", "WindowClientHeight", COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-01' AND "EventDate"::INT::DATE <= '2013-07-31' AND "IsRefresh" = 0 AND "DontCountHits" = 0 AND "URLHash" = 2868770270353813622 GROUP BY "WindowClientWidth", "WindowClientHeight" ORDER BY PageViews DESC LIMIT 10 OFFSET 10000; ---- query PI -SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate" >= '2013-07-14' AND "EventDate" <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; +SELECT DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) AS M, COUNT(*) AS PageViews FROM hits WHERE "CounterID" = 62 AND "EventDate"::INT::DATE >= '2013-07-14' AND "EventDate"::INT::DATE <= '2013-07-15' AND "IsRefresh" = 0 AND "DontCountHits" = 0 GROUP BY DATE_TRUNC('minute', to_timestamp_seconds("EventTime")) ORDER BY DATE_TRUNC('minute', M) LIMIT 10 OFFSET 1000; ---- # Clickbench "Extended" queries that test count distinct diff --git a/datafusion/sqllogictest/test_files/copy.slt b/datafusion/sqllogictest/test_files/copy.slt index 5eeb05e814ace..925f96bd4ac0c 100644 --- a/datafusion/sqllogictest/test_files/copy.slt +++ b/datafusion/sqllogictest/test_files/copy.slt @@ -637,7 +637,7 @@ query error DataFusion error: SQL error: ParserError\("Expected: \), found: EOF" COPY (select col2, sum(col1) from source_table # Copy from table with non literal -query error DataFusion error: SQL error: ParserError\("Expected: end of statement or ;, found: \( at Line: 1, Column: 44"\) +query error DataFusion error: SQL error: ParserError\("Unexpected token \("\) COPY source_table to '/tmp/table.parquet' (row_group_size 55 + 102); # Copy using execution.keep_partition_by_columns with an invalid value diff --git a/datafusion/sqllogictest/test_files/create_external_table.slt b/datafusion/sqllogictest/test_files/create_external_table.slt index 03cb5edb5fcce..bb66aef2514c9 100644 --- a/datafusion/sqllogictest/test_files/create_external_table.slt +++ b/datafusion/sqllogictest/test_files/create_external_table.slt @@ -77,7 +77,7 @@ statement error DataFusion error: SQL error: ParserError\("Expected: HEADER, fou CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV WITH LOCATION 'foo.csv'; # Unrecognized random clause -statement error DataFusion error: SQL error: ParserError\("Expected: end of statement or ;, found: FOOBAR at Line: 1, Column: 47"\) +statement error DataFusion error: SQL error: ParserError\("Unexpected token FOOBAR"\) CREATE EXTERNAL TABLE t(c1 int) STORED AS CSV FOOBAR BARBAR BARFOO LOCATION 'foo.csv'; # Missing partition column diff --git a/datafusion/sqllogictest/test_files/cte.slt b/datafusion/sqllogictest/test_files/cte.slt index 32320a06f4fb0..e019af9775a42 100644 --- a/datafusion/sqllogictest/test_files/cte.slt +++ b/datafusion/sqllogictest/test_files/cte.slt @@ -722,7 +722,7 @@ logical_plan 03)----Projection: Int64(1) AS val 04)------EmptyRelation 05)----Projection: Int64(2) AS val -06)------Cross Join: +06)------Cross Join: 07)--------Filter: recursive_cte.val < Int64(2) 08)----------TableScan: recursive_cte 09)--------SubqueryAlias: sub_cte diff --git a/datafusion/sqllogictest/test_files/dates.slt b/datafusion/sqllogictest/test_files/dates.slt index 148f0dfe64bb7..4425eee333735 100644 --- a/datafusion/sqllogictest/test_files/dates.slt +++ b/datafusion/sqllogictest/test_files/dates.slt @@ -183,7 +183,7 @@ query error input contains invalid characters SELECT to_date('2020-09-08 12/00/00+00:00', '%c', '%+') # to_date with broken formatting -query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input +query error bad or unsupported format string SELECT to_date('2020-09-08 12/00/00+00:00', '%q') statement ok diff --git a/datafusion/sqllogictest/test_files/dictionary.slt b/datafusion/sqllogictest/test_files/dictionary.slt index d241e61f33ffd..778b3537d1bff 100644 --- a/datafusion/sqllogictest/test_files/dictionary.slt +++ b/datafusion/sqllogictest/test_files/dictionary.slt @@ -450,10 +450,3 @@ query I select dense_rank() over (order by arrow_cast('abc', 'Dictionary(UInt16, Utf8)')); ---- 1 - -# Test dictionary encoded column to partition column casting -statement ok -CREATE TABLE test0 AS VALUES ('foo',1), ('bar',2), ('foo',3); - -statement ok -COPY (SELECT arrow_cast(column1, 'Dictionary(Int32, Utf8)') AS column1, column2 FROM test0) TO 'test_files/scratch/copy/part_dict_test' STORED AS PARQUET PARTITIONED BY (column1); diff --git a/datafusion/sqllogictest/test_files/explain.slt b/datafusion/sqllogictest/test_files/explain.slt index ba2596551f1d5..deff793e51106 100644 --- a/datafusion/sqllogictest/test_files/explain.slt +++ b/datafusion/sqllogictest/test_files/explain.slt @@ -237,7 +237,6 @@ physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after coalesce_batches SAME TEXT AS ABOVE physical_plan after OutputRequirements DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/core/tests/data/example.csv]]}, projection=[a, b, c], file_type=csv, has_header=true physical_plan after LimitAggregation SAME TEXT AS ABOVE -physical_plan after PushdownFilter SAME TEXT AS ABOVE physical_plan after LimitPushdown SAME TEXT AS ABOVE physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after SanityCheckPlan SAME TEXT AS ABOVE @@ -314,7 +313,6 @@ physical_plan after OutputRequirements 01)GlobalLimitExec: skip=0, fetch=10, statistics=[Rows=Exact(8), Bytes=Exact(671), [(Col[0]:),(Col[1]:),(Col[2]:),(Col[3]:),(Col[4]:),(Col[5]:),(Col[6]:),(Col[7]:),(Col[8]:),(Col[9]:),(Col[10]:)]] 02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet, statistics=[Rows=Exact(8), Bytes=Exact(671), [(Col[0]:),(Col[1]:),(Col[2]:),(Col[3]:),(Col[4]:),(Col[5]:),(Col[6]:),(Col[7]:),(Col[8]:),(Col[9]:),(Col[10]:)]] physical_plan after LimitAggregation SAME TEXT AS ABOVE -physical_plan after PushdownFilter SAME TEXT AS ABOVE physical_plan after LimitPushdown DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet, statistics=[Rows=Exact(8), Bytes=Exact(671), [(Col[0]:),(Col[1]:),(Col[2]:),(Col[3]:),(Col[4]:),(Col[5]:),(Col[6]:),(Col[7]:),(Col[8]:),(Col[9]:),(Col[10]:)]] physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after SanityCheckPlan SAME TEXT AS ABOVE @@ -355,7 +353,6 @@ physical_plan after OutputRequirements 01)GlobalLimitExec: skip=0, fetch=10 02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet physical_plan after LimitAggregation SAME TEXT AS ABOVE -physical_plan after PushdownFilter SAME TEXT AS ABOVE physical_plan after LimitPushdown DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/parquet-testing/data/alltypes_plain.parquet]]}, projection=[id, bool_col, tinyint_col, smallint_col, int_col, bigint_col, float_col, double_col, date_string_col, string_col, timestamp_col], limit=10, file_type=parquet physical_plan after ProjectionPushdown SAME TEXT AS ABOVE physical_plan after SanityCheckPlan SAME TEXT AS ABOVE diff --git a/datafusion/sqllogictest/test_files/explain_tree.slt b/datafusion/sqllogictest/test_files/explain_tree.slt index 15bf615765713..7a0e322eb8bcd 100644 --- a/datafusion/sqllogictest/test_files/explain_tree.slt +++ b/datafusion/sqllogictest/test_files/explain_tree.slt @@ -180,8 +180,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -218,8 +218,8 @@ physical_plan 18)┌─────────────┴─────────────┐ 19)│ RepartitionExec │ 20)│ -------------------- │ -21)│ partition_count(in->out): │ -22)│ 4 -> 4 │ +21)│ output_partition_count: │ +22)│ 4 │ 23)│ │ 24)│ partitioning_scheme: │ 25)│ Hash([string_col@0], 4) │ @@ -236,8 +236,8 @@ physical_plan 36)┌─────────────┴─────────────┐ 37)│ RepartitionExec │ 38)│ -------------------- │ -39)│ partition_count(in->out): │ -40)│ 1 -> 4 │ +39)│ output_partition_count: │ +40)│ 1 │ 41)│ │ 42)│ partitioning_scheme: │ 43)│ RoundRobinBatch(4) │ @@ -311,8 +311,8 @@ physical_plan 19)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 20)│ RepartitionExec ││ RepartitionExec │ 21)│ -------------------- ││ -------------------- │ -22)│ partition_count(in->out): ││ partition_count(in->out): │ -23)│ 4 -> 4 ││ 4 -> 4 │ +22)│ output_partition_count: ││ output_partition_count: │ +23)│ 4 ││ 4 │ 24)│ ││ │ 25)│ partitioning_scheme: ││ partitioning_scheme: │ 26)│ Hash([int_col@0], 4) ││ Hash([int_col@0], 4) │ @@ -320,8 +320,8 @@ physical_plan 28)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 29)│ RepartitionExec ││ RepartitionExec │ 30)│ -------------------- ││ -------------------- │ -31)│ partition_count(in->out): ││ partition_count(in->out): │ -32)│ 1 -> 4 ││ 1 -> 4 │ +31)│ output_partition_count: ││ output_partition_count: │ +32)│ 1 ││ 1 │ 33)│ ││ │ 34)│ partitioning_scheme: ││ partitioning_scheme: │ 35)│ RoundRobinBatch(4) ││ RoundRobinBatch(4) │ @@ -386,8 +386,8 @@ physical_plan 40)-----------------------------┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 41)-----------------------------│ RepartitionExec ││ RepartitionExec │ 42)-----------------------------│ -------------------- ││ -------------------- │ -43)-----------------------------│ partition_count(in->out): ││ partition_count(in->out): │ -44)-----------------------------│ 4 -> 4 ││ 4 -> 4 │ +43)-----------------------------│ output_partition_count: ││ output_partition_count: │ +44)-----------------------------│ 4 ││ 4 │ 45)-----------------------------│ ││ │ 46)-----------------------------│ partitioning_scheme: ││ partitioning_scheme: │ 47)-----------------------------│ Hash([int_col@0], 4) ││ Hash([int_col@0], 4) │ @@ -395,8 +395,8 @@ physical_plan 49)-----------------------------┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 50)-----------------------------│ RepartitionExec ││ RepartitionExec │ 51)-----------------------------│ -------------------- ││ -------------------- │ -52)-----------------------------│ partition_count(in->out): ││ partition_count(in->out): │ -53)-----------------------------│ 1 -> 4 ││ 1 -> 4 │ +52)-----------------------------│ output_partition_count: ││ output_partition_count: │ +53)-----------------------------│ 1 ││ 1 │ 54)-----------------------------│ ││ │ 55)-----------------------------│ partitioning_scheme: ││ partitioning_scheme: │ 56)-----------------------------│ RoundRobinBatch(4) ││ RoundRobinBatch(4) │ @@ -434,8 +434,8 @@ physical_plan 17)┌─────────────┴─────────────┐ 18)│ RepartitionExec │ 19)│ -------------------- │ -20)│ partition_count(in->out): │ -21)│ 1 -> 4 │ +20)│ output_partition_count: │ +21)│ 1 │ 22)│ │ 23)│ partitioning_scheme: │ 24)│ RoundRobinBatch(4) │ @@ -496,8 +496,8 @@ physical_plan 41)┌─────────────┴─────────────┐ 42)│ RepartitionExec │ 43)│ -------------------- │ -44)│ partition_count(in->out): │ -45)│ 1 -> 4 │ +44)│ output_partition_count: │ +45)│ 1 │ 46)│ │ 47)│ partitioning_scheme: │ 48)│ RoundRobinBatch(4) │ @@ -530,8 +530,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -566,8 +566,8 @@ physical_plan 15)┌─────────────┴─────────────┐ 16)│ RepartitionExec │ 17)│ -------------------- │ -18)│ partition_count(in->out): │ -19)│ 1 -> 4 │ +18)│ output_partition_count: │ +19)│ 1 │ 20)│ │ 21)│ partitioning_scheme: │ 22)│ RoundRobinBatch(4) │ @@ -599,8 +599,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -633,8 +633,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -694,8 +694,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -727,8 +727,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -889,7 +889,7 @@ explain SELECT * FROM table1 ORDER BY string_col LIMIT 1; ---- physical_plan 01)┌───────────────────────────┐ -02)│ SortExec(TopK) │ +02)│ SortExec │ 03)│ -------------------- │ 04)│ limit: 1 │ 05)│ │ @@ -922,8 +922,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -1031,8 +1031,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -1089,8 +1089,8 @@ physical_plan 12)┌─────────────┴─────────────┐ 13)│ RepartitionExec │ 14)│ -------------------- │ -15)│ partition_count(in->out): │ -16)│ 1 -> 4 │ +15)│ output_partition_count: │ +16)│ 1 │ 17)│ │ 18)│ partitioning_scheme: │ 19)│ RoundRobinBatch(4) │ @@ -1123,8 +1123,8 @@ physical_plan 13)┌─────────────┴─────────────┐ 14)│ RepartitionExec │ 15)│ -------------------- │ -16)│ partition_count(in->out): │ -17)│ 1 -> 4 │ +16)│ output_partition_count: │ +17)│ 1 │ 18)│ │ 19)│ partitioning_scheme: │ 20)│ RoundRobinBatch(4) │ @@ -1209,8 +1209,8 @@ physical_plan 22)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 23)│ RepartitionExec ││ RepartitionExec │ 24)│ -------------------- ││ -------------------- │ -25)│ partition_count(in->out): ││ partition_count(in->out): │ -26)│ 4 -> 4 ││ 4 -> 4 │ +25)│ output_partition_count: ││ output_partition_count: │ +26)│ 4 ││ 4 │ 27)│ ││ │ 28)│ partitioning_scheme: ││ partitioning_scheme: │ 29)│ Hash([int_col@0, CAST ││ Hash([int_col@0, │ @@ -1220,8 +1220,8 @@ physical_plan 33)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 34)│ ProjectionExec ││ RepartitionExec │ 35)│ -------------------- ││ -------------------- │ -36)│ CAST(table1.string_col AS ││ partition_count(in->out): │ -37)│ Utf8View): ││ 1 -> 4 │ +36)│ CAST(table1.string_col AS ││ output_partition_count: │ +37)│ Utf8View): ││ 1 │ 38)│ CAST(string_col AS ││ │ 39)│ Utf8View) ││ partitioning_scheme: │ 40)│ ││ RoundRobinBatch(4) │ @@ -1237,8 +1237,8 @@ physical_plan 50)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 51)│ RepartitionExec ││ DataSourceExec │ 52)│ -------------------- ││ -------------------- │ -53)│ partition_count(in->out): ││ files: 1 │ -54)│ 1 -> 4 ││ format: parquet │ +53)│ output_partition_count: ││ files: 1 │ +54)│ 1 ││ format: parquet │ 55)│ ││ │ 56)│ partitioning_scheme: ││ │ 57)│ RoundRobinBatch(4) ││ │ @@ -1281,8 +1281,8 @@ physical_plan 24)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 25)│ RepartitionExec ││ RepartitionExec │ 26)│ -------------------- ││ -------------------- │ -27)│ partition_count(in->out): ││ partition_count(in->out): │ -28)│ 4 -> 4 ││ 4 -> 4 │ +27)│ output_partition_count: ││ output_partition_count: │ +28)│ 4 ││ 4 │ 29)│ ││ │ 30)│ partitioning_scheme: ││ partitioning_scheme: │ 31)│ Hash([int_col@0, CAST ││ Hash([int_col@0, │ @@ -1292,8 +1292,8 @@ physical_plan 35)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 36)│ ProjectionExec ││ RepartitionExec │ 37)│ -------------------- ││ -------------------- │ -38)│ CAST(table1.string_col AS ││ partition_count(in->out): │ -39)│ Utf8View): ││ 1 -> 4 │ +38)│ CAST(table1.string_col AS ││ output_partition_count: │ +39)│ Utf8View): ││ 1 │ 40)│ CAST(string_col AS ││ │ 41)│ Utf8View) ││ partitioning_scheme: │ 42)│ ││ RoundRobinBatch(4) │ @@ -1309,8 +1309,8 @@ physical_plan 52)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 53)│ RepartitionExec ││ DataSourceExec │ 54)│ -------------------- ││ -------------------- │ -55)│ partition_count(in->out): ││ files: 1 │ -56)│ 1 -> 4 ││ format: parquet │ +55)│ output_partition_count: ││ files: 1 │ +56)│ 1 ││ format: parquet │ 57)│ ││ │ 58)│ partitioning_scheme: ││ │ 59)│ RoundRobinBatch(4) ││ │ @@ -1356,8 +1356,8 @@ physical_plan 27)-----------------------------┌─────────────┴─────────────┐ 28)-----------------------------│ RepartitionExec │ 29)-----------------------------│ -------------------- │ -30)-----------------------------│ partition_count(in->out): │ -31)-----------------------------│ 1 -> 4 │ +30)-----------------------------│ output_partition_count: │ +31)-----------------------------│ 1 │ 32)-----------------------------│ │ 33)-----------------------------│ partitioning_scheme: │ 34)-----------------------------│ RoundRobinBatch(4) │ @@ -1380,8 +1380,8 @@ physical_plan 04)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 05)│ DataSourceExec ││ RepartitionExec │ 06)│ -------------------- ││ -------------------- │ -07)│ files: 1 ││ partition_count(in->out): │ -08)│ format: csv ││ 1 -> 4 │ +07)│ files: 1 ││ output_partition_count: │ +08)│ format: csv ││ 1 │ 09)│ ││ │ 10)│ ││ partitioning_scheme: │ 11)│ ││ RoundRobinBatch(4) │ @@ -1505,8 +1505,8 @@ physical_plan 33)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 34)│ RepartitionExec ││ RepartitionExec │ 35)│ -------------------- ││ -------------------- │ -36)│ partition_count(in->out): ││ partition_count(in->out): │ -37)│ 4 -> 4 ││ 4 -> 4 │ +36)│ output_partition_count: ││ output_partition_count: │ +37)│ 4 ││ 4 │ 38)│ ││ │ 39)│ partitioning_scheme: ││ partitioning_scheme: │ 40)│ Hash([name@0], 4) ││ Hash([name@0], 4) │ @@ -1514,8 +1514,8 @@ physical_plan 42)┌─────────────┴─────────────┐┌─────────────┴─────────────┐ 43)│ RepartitionExec ││ RepartitionExec │ 44)│ -------------------- ││ -------------------- │ -45)│ partition_count(in->out): ││ partition_count(in->out): │ -46)│ 1 -> 4 ││ 1 -> 4 │ +45)│ output_partition_count: ││ output_partition_count: │ +46)│ 1 ││ 1 │ 47)│ ││ │ 48)│ partitioning_scheme: ││ partitioning_scheme: │ 49)│ RoundRobinBatch(4) ││ RoundRobinBatch(4) │ @@ -1606,8 +1606,8 @@ physical_plan 18)┌─────────────┴─────────────┐ 19)│ RepartitionExec │ 20)│ -------------------- │ -21)│ partition_count(in->out): │ -22)│ 1 -> 4 │ +21)│ output_partition_count: │ +22)│ 1 │ 23)│ │ 24)│ partitioning_scheme: │ 25)│ RoundRobinBatch(4) │ @@ -1648,8 +1648,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ partition_count(in->out): │ -23)│ 1 -> 4 │ +22)│ output_partition_count: │ +23)│ 1 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ @@ -1689,8 +1689,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ partition_count(in->out): │ -23)│ 1 -> 4 │ +22)│ output_partition_count: │ +23)│ 1 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ @@ -1728,8 +1728,8 @@ physical_plan 17)┌─────────────┴─────────────┐ 18)│ RepartitionExec │ 19)│ -------------------- │ -20)│ partition_count(in->out): │ -21)│ 1 -> 4 │ +20)│ output_partition_count: │ +21)│ 1 │ 22)│ │ 23)│ partitioning_scheme: │ 24)│ RoundRobinBatch(4) │ @@ -1771,8 +1771,8 @@ physical_plan 20)┌─────────────┴─────────────┐ 21)│ RepartitionExec │ 22)│ -------------------- │ -23)│ partition_count(in->out): │ -24)│ 1 -> 4 │ +23)│ output_partition_count: │ +24)│ 1 │ 25)│ │ 26)│ partitioning_scheme: │ 27)│ RoundRobinBatch(4) │ @@ -1815,8 +1815,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ partition_count(in->out): │ -23)│ 1 -> 4 │ +22)│ output_partition_count: │ +23)│ 1 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ @@ -1869,8 +1869,8 @@ physical_plan 25)-----------------------------┌─────────────┴─────────────┐ 26)-----------------------------│ RepartitionExec │ 27)-----------------------------│ -------------------- │ -28)-----------------------------│ partition_count(in->out): │ -29)-----------------------------│ 1 -> 4 │ +28)-----------------------------│ output_partition_count: │ +29)-----------------------------│ 1 │ 30)-----------------------------│ │ 31)-----------------------------│ partitioning_scheme: │ 32)-----------------------------│ RoundRobinBatch(4) │ @@ -1983,8 +1983,8 @@ physical_plan 22)┌─────────────┴─────────────┐ 23)│ RepartitionExec │ 24)│ -------------------- │ -25)│ partition_count(in->out): │ -26)│ 1 -> 4 │ +25)│ output_partition_count: │ +26)│ 1 │ 27)│ │ 28)│ partitioning_scheme: │ 29)│ RoundRobinBatch(4) │ @@ -2062,8 +2062,8 @@ physical_plan 19)┌─────────────┴─────────────┐ 20)│ RepartitionExec │ 21)│ -------------------- │ -22)│ partition_count(in->out): │ -23)│ 1 -> 4 │ +22)│ output_partition_count: │ +23)│ 1 │ 24)│ │ 25)│ partitioning_scheme: │ 26)│ RoundRobinBatch(4) │ diff --git a/datafusion/sqllogictest/test_files/expr/date_part.slt b/datafusion/sqllogictest/test_files/expr/date_part.slt index 39c42cbe1e97f..dec796aa59cb5 100644 --- a/datafusion/sqllogictest/test_files/expr/date_part.slt +++ b/datafusion/sqllogictest/test_files/expr/date_part.slt @@ -884,7 +884,7 @@ SELECT extract(day from arrow_cast('14400 minutes', 'Interval(DayTime)')) query I SELECT extract(minute from arrow_cast('14400 minutes', 'Interval(DayTime)')) ---- -0 +14400 query I SELECT extract(second from arrow_cast('5.1 seconds', 'Interval(DayTime)')) @@ -894,7 +894,7 @@ SELECT extract(second from arrow_cast('5.1 seconds', 'Interval(DayTime)')) query I SELECT extract(second from arrow_cast('14400 minutes', 'Interval(DayTime)')) ---- -0 +864000 query I SELECT extract(second from arrow_cast('2 months', 'Interval(MonthDayNano)')) @@ -954,7 +954,7 @@ from t order by id; ---- 0 0 5 -1 0 3 +1 0 15 2 0 0 3 2 0 4 0 8 diff --git a/datafusion/sqllogictest/test_files/functions.slt b/datafusion/sqllogictest/test_files/functions.slt index 20f79622a62c6..de1dbf74c29bf 100644 --- a/datafusion/sqllogictest/test_files/functions.slt +++ b/datafusion/sqllogictest/test_files/functions.slt @@ -858,7 +858,7 @@ SELECT greatest(-1, 1, 2.3, 123456789, 3 + 5, -(-4), abs(-9.0)) 123456789 -query error Function 'greatest' user-defined coercion failed with "Error during planning: greatest was called without any arguments. It requires at least 1." +query error 'greatest' does not support zero argument SELECT greatest() query I @@ -1056,7 +1056,7 @@ SELECT least(-1, 1, 2.3, 123456789, 3 + 5, -(-4), abs(-9.0)) -1 -query error Function 'least' user-defined coercion failed with "Error during planning: least was called without any arguments. It requires at least 1." +query error 'least' does not support zero arguments SELECT least() query I diff --git a/datafusion/sqllogictest/test_files/group_by.slt b/datafusion/sqllogictest/test_files/group_by.slt index 9e67018ecd0b9..4c4999a364d12 100644 --- a/datafusion/sqllogictest/test_files/group_by.slt +++ b/datafusion/sqllogictest/test_files/group_by.slt @@ -2232,7 +2232,7 @@ physical_plan 03)----StreamingTableExec: partition_sizes=1, projection=[a, b, c], infinite_source=true, output_ordering=[a@0 ASC NULLS LAST, b@1 ASC NULLS LAST, c@2 ASC NULLS LAST] query III -SELECT a, b, LAST_VALUE(c order by c) as last_c +SELECT a, b, LAST_VALUE(c) as last_c FROM annotated_data_infinite2 GROUP BY a, b ---- @@ -2706,29 +2706,6 @@ select k, first_value(val order by o) respect NULLS from first_null group by k; 1 1 -statement ok -CREATE TABLE last_null ( - k INT, - val INT, - o int - ) as VALUES - (0, NULL, 9), - (0, 1, 1), - (1, 1, 1); - -query II rowsort -select k, last_value(val order by o) IGNORE NULLS from last_null group by k; ----- -0 1 -1 1 - -query II rowsort -select k, last_value(val order by o) respect NULLS from last_null group by k; ----- -0 NULL -1 1 - - query TT EXPLAIN SELECT country, ARRAY_AGG(amount ORDER BY amount DESC) AS amounts, FIRST_VALUE(amount ORDER BY amount ASC) AS fv1, @@ -3798,7 +3775,7 @@ ORDER BY x; 2 2 query II -SELECT y, LAST_VALUE(x order by x desc) +SELECT y, LAST_VALUE(x) FROM FOO GROUP BY y ORDER BY y; diff --git a/datafusion/sqllogictest/test_files/information_schema.slt b/datafusion/sqllogictest/test_files/information_schema.slt index 87abaadb516f3..496f24abf6ed7 100644 --- a/datafusion/sqllogictest/test_files/information_schema.slt +++ b/datafusion/sqllogictest/test_files/information_schema.slt @@ -149,39 +149,6 @@ drop table t statement ok drop table t2 - -############ -## 0 to represent the default value (target_partitions and planning_concurrency) -########### - -statement ok -SET datafusion.execution.target_partitions = 3; - -statement ok -SET datafusion.execution.planning_concurrency = 3; - -# when setting target_partitions and planning_concurrency to 3, their values will be 3 -query TB rowsort -SELECT name, value = 3 FROM information_schema.df_settings WHERE name IN ('datafusion.execution.target_partitions', 'datafusion.execution.planning_concurrency'); ----- -datafusion.execution.planning_concurrency true -datafusion.execution.target_partitions true - -statement ok -SET datafusion.execution.target_partitions = 0; - -statement ok -SET datafusion.execution.planning_concurrency = 0; - -# when setting target_partitions and planning_concurrency to 0, their values will be equal to the -# default values, which are different from 0 (which is invalid) -query TB rowsort -SELECT name, value = 0 FROM information_schema.df_settings WHERE name IN ('datafusion.execution.target_partitions', 'datafusion.execution.planning_concurrency'); ----- -datafusion.execution.planning_concurrency false -datafusion.execution.target_partitions false - - ############ ## SHOW VARIABLES should work ########### @@ -230,7 +197,6 @@ datafusion.execution.parquet.bloom_filter_fpp NULL datafusion.execution.parquet.bloom_filter_ndv NULL datafusion.execution.parquet.bloom_filter_on_read true datafusion.execution.parquet.bloom_filter_on_write false -datafusion.execution.parquet.coerce_int96 NULL datafusion.execution.parquet.column_index_truncate_length 64 datafusion.execution.parquet.compression zstd(3) datafusion.execution.parquet.created_by datafusion @@ -330,7 +296,6 @@ datafusion.execution.parquet.bloom_filter_fpp NULL (writing) Sets bloom filter f datafusion.execution.parquet.bloom_filter_ndv NULL (writing) Sets bloom filter number of distinct values. If NULL, uses default parquet writer setting datafusion.execution.parquet.bloom_filter_on_read true (writing) Use any available bloom filters when reading parquet files datafusion.execution.parquet.bloom_filter_on_write false (writing) Write bloom filters for all columns when creating parquet files -datafusion.execution.parquet.coerce_int96 NULL (reading) If true, parquet reader will read columns of physical type int96 as originating from a different resolution than nanosecond. This is useful for reading data from systems like Spark which stores microsecond resolution timestamps in an int96 allowing it to write values with a larger date range than 64-bit timestamps with nanosecond resolution. datafusion.execution.parquet.column_index_truncate_length 64 (writing) Sets column index truncate length datafusion.execution.parquet.compression zstd(3) (writing) Sets default parquet compression codec. Valid values are: uncompressed, snappy, gzip(level), lzo, brotli(level), lz4, zstd(level), and lz4_raw. These values are not case sensitive. If NULL, uses default parquet writer setting Note that this default setting is not the same as the default parquet writer setting. datafusion.execution.parquet.created_by datafusion (writing) Sets "created by" property @@ -684,7 +649,7 @@ datafusion public date_trunc datafusion public date_trunc FUNCTION true Timestam datafusion public date_trunc datafusion public date_trunc FUNCTION true Timestamp(Second, None) SCALAR Truncates a timestamp value to a specified precision. date_trunc(precision, expression) datafusion public date_trunc datafusion public date_trunc FUNCTION true Timestamp(Second, Some("+TZ")) SCALAR Truncates a timestamp value to a specified precision. date_trunc(precision, expression) datafusion public rank datafusion public rank FUNCTION true NULL WINDOW Returns the rank of the current row within its partition, allowing gaps between ranks. This function provides a ranking similar to `row_number`, but skips ranks for identical values. rank() -datafusion public string_agg datafusion public string_agg FUNCTION true LargeUtf8 AGGREGATE Concatenates the values of string expressions and places separator values between them. If ordering is required, strings are concatenated in the specified order. This aggregation function can only mix DISTINCT and ORDER BY if the ordering expression is exactly the same as the first argument expression. string_agg([DISTINCT] expression, delimiter [ORDER BY expression]) +datafusion public string_agg datafusion public string_agg FUNCTION true LargeUtf8 AGGREGATE Concatenates the values of string expressions and places separator values between them. string_agg(expression, delimiter) query B select is_deterministic from information_schema.routines where routine_name = 'now'; @@ -752,15 +717,6 @@ datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 1 datafusion public string_agg 1 IN expression LargeUtf8 NULL false 2 datafusion public string_agg 2 IN delimiter Null NULL false 2 datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 2 -datafusion public string_agg 1 IN expression Utf8 NULL false 3 -datafusion public string_agg 2 IN delimiter Utf8 NULL false 3 -datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 3 -datafusion public string_agg 1 IN expression Utf8 NULL false 4 -datafusion public string_agg 2 IN delimiter LargeUtf8 NULL false 4 -datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 4 -datafusion public string_agg 1 IN expression Utf8 NULL false 5 -datafusion public string_agg 2 IN delimiter Null NULL false 5 -datafusion public string_agg 1 OUT NULL LargeUtf8 NULL false 5 # test variable length arguments query TTTBI rowsort diff --git a/datafusion/sqllogictest/test_files/joins.slt b/datafusion/sqllogictest/test_files/joins.slt index ddf701ba04efe..ca86dbfcc3c16 100644 --- a/datafusion/sqllogictest/test_files/joins.slt +++ b/datafusion/sqllogictest/test_files/joins.slt @@ -4742,51 +4742,3 @@ drop table person; statement count 0 drop table orders; - -# Create tables for testing compound field access in JOIN conditions -statement ok -CREATE TABLE compound_field_table_t -AS VALUES -({r: 'a', c: 1}), -({r: 'b', c: 2.3}); - -statement ok -CREATE TABLE compound_field_table_u -AS VALUES -({r: 'a', c: 1}), -({r: 'b', c: 2.3}); - -# Test compound field access in JOIN condition with table aliases -query ?? -SELECT * FROM compound_field_table_t tee JOIN compound_field_table_u you ON tee.column1['r'] = you.column1['r'] ----- -{r: a, c: 1.0} {r: a, c: 1.0} -{r: b, c: 2.3} {r: b, c: 2.3} - -# Test compound field access in JOIN condition without table aliases -query ?? -SELECT * FROM compound_field_table_t JOIN compound_field_table_u ON compound_field_table_t.column1['r'] = compound_field_table_u.column1['r'] ----- -{r: a, c: 1.0} {r: a, c: 1.0} -{r: b, c: 2.3} {r: b, c: 2.3} - -# Test compound field access with numeric field access -query ?? -SELECT * FROM compound_field_table_t tee JOIN compound_field_table_u you ON tee.column1['c'] = you.column1['c'] ----- -{r: a, c: 1.0} {r: a, c: 1.0} -{r: b, c: 2.3} {r: b, c: 2.3} - -# Test compound field access with mixed field types -query ?? -SELECT * FROM compound_field_table_t tee JOIN compound_field_table_u you ON tee.column1['r'] = you.column1['r'] AND tee.column1['c'] = you.column1['c'] ----- -{r: a, c: 1.0} {r: a, c: 1.0} -{r: b, c: 2.3} {r: b, c: 2.3} - -# Clean up compound field tables -statement ok -DROP TABLE compound_field_table_t; - -statement ok -DROP TABLE compound_field_table_u; diff --git a/datafusion/sqllogictest/test_files/parquet.slt b/datafusion/sqllogictest/test_files/parquet.slt index 0823a9218268e..2970b2effb3e9 100644 --- a/datafusion/sqllogictest/test_files/parquet.slt +++ b/datafusion/sqllogictest/test_files/parquet.slt @@ -629,21 +629,3 @@ physical_plan statement ok drop table foo - - -statement ok -set datafusion.execution.parquet.coerce_int96 = ms; - -statement ok -CREATE EXTERNAL TABLE int96_from_spark -STORED AS PARQUET -LOCATION '../../parquet-testing/data/int96_from_spark.parquet'; - -# Print schema -query TTT -describe int96_from_spark; ----- -a Timestamp(Millisecond, None) YES - -statement ok -set datafusion.execution.parquet.coerce_int96 = ns; diff --git a/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt b/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt index a10243f627209..d325ca423daca 100644 --- a/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt +++ b/datafusion/sqllogictest/test_files/parquet_sorted_statistics.slt @@ -109,9 +109,7 @@ ORDER BY int_col, bigint_col; logical_plan 01)Sort: test_table.int_col ASC NULLS LAST, test_table.bigint_col ASC NULLS LAST 02)--TableScan: test_table projection=[int_col, bigint_col] -physical_plan -01)SortPreservingMergeExec: [int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST] -02)--DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[int_col, bigint_col], output_ordering=[int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet +physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet]]}, projection=[int_col, bigint_col], output_ordering=[int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet # Another planning test, but project on a column with unsupported statistics # We should be able to ignore this and look at only the relevant statistics @@ -125,10 +123,7 @@ logical_plan 02)--Sort: test_table.int_col ASC NULLS LAST, test_table.bigint_col ASC NULLS LAST 03)----Projection: test_table.string_col, test_table.int_col, test_table.bigint_col 04)------TableScan: test_table projection=[int_col, string_col, bigint_col] -physical_plan -01)ProjectionExec: expr=[string_col@0 as string_col] -02)--SortPreservingMergeExec: [int_col@1 ASC NULLS LAST, bigint_col@2 ASC NULLS LAST] -03)----DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[string_col, int_col, bigint_col], output_ordering=[int_col@1 ASC NULLS LAST, bigint_col@2 ASC NULLS LAST], file_type=parquet +physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet]]}, projection=[string_col], file_type=parquet # Clean up & recreate but sort on descending column statement ok @@ -160,9 +155,7 @@ ORDER BY descending_col DESC NULLS LAST, bigint_col ASC NULLS LAST; logical_plan 01)Sort: test_table.descending_col DESC NULLS LAST, test_table.bigint_col ASC NULLS LAST 02)--TableScan: test_table projection=[descending_col, bigint_col] -physical_plan -01)SortPreservingMergeExec: [descending_col@0 DESC NULLS LAST, bigint_col@1 ASC NULLS LAST] -02)--DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[descending_col, bigint_col], output_ordering=[descending_col@0 DESC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet +physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet]]}, projection=[descending_col, bigint_col], output_ordering=[descending_col@0 DESC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet # Clean up & re-create with partition columns in sort order statement ok @@ -196,9 +189,7 @@ ORDER BY partition_col, int_col, bigint_col; logical_plan 01)Sort: test_table.partition_col ASC NULLS LAST, test_table.int_col ASC NULLS LAST, test_table.bigint_col ASC NULLS LAST 02)--TableScan: test_table projection=[int_col, bigint_col, partition_col] -physical_plan -01)SortPreservingMergeExec: [partition_col@2 ASC NULLS LAST, int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST] -02)--DataSourceExec: file_groups={2 groups: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet], [WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet]]}, projection=[int_col, bigint_col, partition_col], output_ordering=[partition_col@2 ASC NULLS LAST, int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet +physical_plan DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=A/0.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=B/1.parquet, WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/parquet_sorted_statistics/test_table/partition_col=C/2.parquet]]}, projection=[int_col, bigint_col, partition_col], output_ordering=[partition_col@2 ASC NULLS LAST, int_col@0 ASC NULLS LAST, bigint_col@1 ASC NULLS LAST], file_type=parquet # Clean up & re-create with overlapping column in sort order # This will test the ability to sort files with overlapping statistics diff --git a/datafusion/sqllogictest/test_files/regexp.slt b/datafusion/sqllogictest/test_files/regexp.slt new file mode 100644 index 0000000000000..44ba61e877d97 --- /dev/null +++ b/datafusion/sqllogictest/test_files/regexp.slt @@ -0,0 +1,898 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +statement ok +CREATE TABLE t (str varchar, pattern varchar, start int, flags varchar) AS VALUES + ('abc', '^(a)', 1, 'i'), + ('ABC', '^(A).*', 1, 'i'), + ('aBc', '(b|d)', 1, 'i'), + ('AbC', '(B|D)', 2, null), + ('aBC', '^(b|c)', 3, null), + ('4000', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 1, null), + ('4010', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 2, null), + ('Düsseldorf','[\p{Letter}-]+', 3, null), + ('Москва', '[\p{L}-]+', 4, null), + ('Köln', '[a-zA-Z]ö[a-zA-Z]{2}', 1, null), + ('إسرائيل', '^\p{Arabic}+$', 2, null); + +# +# regexp_like tests +# + +query B +SELECT regexp_like(str, pattern, flags) FROM t; +---- +true +true +true +false +false +false +true +true +true +true +true + +query B +SELECT str ~ NULL FROM t; +---- +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL + +query B +select str ~ right('foo', NULL) FROM t; +---- +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL + +query B +select right('foo', NULL) !~ str FROM t; +---- +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL +NULL + +query B +SELECT regexp_like('foobarbequebaz', ''); +---- +true + +query B +SELECT regexp_like('', ''); +---- +true + +query B +SELECT regexp_like('foobarbequebaz', '(bar)(beque)'); +---- +true + +query B +SELECT regexp_like('fooBarb +eQuebaz', '(bar).*(que)', 'is'); +---- +true + +query B +SELECT regexp_like('foobarbequebaz', '(ba3r)(bequ34e)'); +---- +false + +query B +SELECT regexp_like('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); +---- +true + +query B +SELECT regexp_like('aaa-0', '.*-(\d)'); +---- +true + +query B +SELECT regexp_like('bb-1', '.*-(\d)'); +---- +true + +query B +SELECT regexp_like('aa', '.*-(\d)'); +---- +false + +query B +SELECT regexp_like(NULL, '.*-(\d)'); +---- +NULL + +query B +SELECT regexp_like('aaa-0', NULL); +---- +NULL + +query B +SELECT regexp_like(null, '.*-(\d)'); +---- +NULL + +query error Error during planning: regexp_like\(\) does not support the "global" option +SELECT regexp_like('bb-1', '.*-(\d)', 'g'); + +query error Error during planning: regexp_like\(\) does not support the "global" option +SELECT regexp_like('bb-1', '.*-(\d)', 'g'); + +query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) +SELECT regexp_like('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); + +# look-around is not supported and will just return false +query B +SELECT regexp_like('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); +---- +false + +query B +select regexp_like('aaa-555', '.*-(\d*)'); +---- +true + +# +# regexp_match tests +# + +query ? +SELECT regexp_match(str, pattern, flags) FROM t; +---- +[a] +[A] +[B] +NULL +NULL +NULL +[010] +[Düsseldorf] +[Москва] +[Köln] +[إسرائيل] + +# test string view +statement ok +CREATE TABLE t_stringview AS +SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM t; + +query ? +SELECT regexp_match(str, pattern, flags) FROM t_stringview; +---- +[a] +[A] +[B] +NULL +NULL +NULL +[010] +[Düsseldorf] +[Москва] +[Köln] +[إسرائيل] + +statement ok +DROP TABLE t_stringview; + +query ? +SELECT regexp_match('foobarbequebaz', ''); +---- +[] + +query ? +SELECT regexp_match('', ''); +---- +[] + +query ? +SELECT regexp_match('foobarbequebaz', '(bar)(beque)'); +---- +[bar, beque] + +query ? +SELECT regexp_match('fooBarb +eQuebaz', '(bar).*(que)', 'is'); +---- +[Bar, Que] + +query ? +SELECT regexp_match('foobarbequebaz', '(ba3r)(bequ34e)'); +---- +NULL + +query ? +SELECT regexp_match('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); +---- +[barbeque] + +query ? +SELECT regexp_match('aaa-0', '.*-(\d)'); +---- +[0] + +query ? +SELECT regexp_match('bb-1', '.*-(\d)'); +---- +[1] + +query ? +SELECT regexp_match('aa', '.*-(\d)'); +---- +NULL + +query ? +SELECT regexp_match(NULL, '.*-(\d)'); +---- +NULL + +query ? +SELECT regexp_match('aaa-0', NULL); +---- +NULL + +query ? +SELECT regexp_match(null, '.*-(\d)'); +---- +NULL + +query error Error during planning: regexp_match\(\) does not support the "global" option +SELECT regexp_match('bb-1', '.*-(\d)', 'g'); + +query error Error during planning: regexp_match\(\) does not support the "global" option +SELECT regexp_match('bb-1', '.*-(\d)', 'g'); + +query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) +SELECT regexp_match('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); + +# look-around is not supported and will just return null +query ? +SELECT regexp_match('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); +---- +NULL + +# ported test +query ? +SELECT regexp_match('aaa-555', '.*-(\d*)'); +---- +[555] + +query B +select 'abc' ~ null; +---- +NULL + +query B +select null ~ null; +---- +NULL + +query B +select null ~ 'abc'; +---- +NULL + +query B +select 'abc' ~* null; +---- +NULL + +query B +select null ~* null; +---- +NULL + +query B +select null ~* 'abc'; +---- +NULL + +query B +select 'abc' !~ null; +---- +NULL + +query B +select null !~ null; +---- +NULL + +query B +select null !~ 'abc'; +---- +NULL + +query B +select 'abc' !~* null; +---- +NULL + +query B +select null !~* null; +---- +NULL + +query B +select null !~* 'abc'; +---- +NULL + +# +# regexp_replace tests +# + +query T +SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM t; +---- +Xbc +X +aXc +AbC +aBC +4000 +X +X +X +X +X + +# test string view +statement ok +CREATE TABLE t_stringview AS +SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM t; + +query T +SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM t_stringview; +---- +Xbc +X +aXc +AbC +aBC +4000 +X +X +X +X +X + +statement ok +DROP TABLE t_stringview; + +query T +SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'gi'); +---- +XXX + +query T +SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'i'); +---- +XabcABC + +query T +SELECT regexp_replace('foobarbaz', 'b..', 'X', 'g'); +---- +fooXX + +query T +SELECT regexp_replace('foobarbaz', 'b..', 'X'); +---- +fooXbaz + +query T +SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', 'g'); +---- +fooXarYXazY + +query T +SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', NULL); +---- +NULL + +query T +SELECT regexp_replace('foobarbaz', 'b(..)', NULL, 'g'); +---- +NULL + +query T +SELECT regexp_replace('foobarbaz', NULL, 'X\\1Y', 'g'); +---- +NULL + +query T +SELECT regexp_replace('Thomas', '.[mN]a.', 'M'); +---- +ThM + +query T +SELECT regexp_replace(NULL, 'b(..)', 'X\\1Y', 'g'); +---- +NULL + +query T +SELECT regexp_replace('foobar', 'bar', 'xx', 'gi') +---- +fooxx + +query T +SELECT regexp_replace(arrow_cast('foobar', 'Dictionary(Int32, Utf8)'), 'bar', 'xx', 'gi') +---- +fooxx + +query TTT +select + regexp_replace(col, NULL, 'c'), + regexp_replace(col, 'a', NULL), + regexp_replace(col, 'a', 'c', NULL) +from (values ('a'), ('b')) as tbl(col); +---- +NULL NULL NULL +NULL NULL NULL + +# multiline string +query B +SELECT 'foo\nbar\nbaz' ~ 'bar'; +---- +true + +statement error +Error during planning: Cannot infer common argument type for regex operation List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata +: {} }) ~ List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata: {} }) +select [1,2] ~ [3]; + +query B +SELECT 'foo\nbar\nbaz' LIKE '%bar%'; +---- +true + +query B +SELECT NULL LIKE NULL; +---- +NULL + +query B +SELECT NULL iLIKE NULL; +---- +NULL + +query B +SELECT NULL not LIKE NULL; +---- +NULL + +query B +SELECT NULL not iLIKE NULL; +---- +NULL + +# regexp_count tests + +# regexp_count tests from postgresql +# https://github.com/postgres/postgres/blob/56d23855c864b7384970724f3ad93fb0fc319e51/src/test/regress/sql/strings.sql#L226-L235 + +query I +SELECT regexp_count('123123123123123', '(12)3'); +---- +5 + +query I +SELECT regexp_count('123123123123', '123', 1); +---- +4 + +query I +SELECT regexp_count('123123123123', '123', 3); +---- +3 + +query I +SELECT regexp_count('123123123123', '123', 33); +---- +0 + +query I +SELECT regexp_count('ABCABCABCABC', 'Abc', 1, ''); +---- +0 + +query I +SELECT regexp_count('ABCABCABCABC', 'Abc', 1, 'i'); +---- +4 + +statement error +External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based +SELECT regexp_count('123123123123', '123', 0); + +statement error +External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based +SELECT regexp_count('123123123123', '123', -3); + +statement error +External error: statement failed: DataFusion error: Arrow error: Compute error: regexp_count() does not support global flag +SELECT regexp_count('123123123123', '123', 1, 'g'); + +query I +SELECT regexp_count(str, '\w') from t; +---- +3 +3 +3 +3 +3 +4 +4 +10 +6 +4 +7 + +query I +SELECT regexp_count(str, '\w{2}', start) from t; +---- +1 +1 +1 +1 +0 +2 +1 +4 +1 +2 +3 + +query I +SELECT regexp_count(str, 'ab', 1, 'i') from t; +---- +1 +1 +1 +1 +1 +0 +0 +0 +0 +0 +0 + + +query I +SELECT regexp_count(str, pattern) from t; +---- +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start) from t; +---- +1 +1 +0 +0 +0 +0 +0 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start, flags) from t; +---- +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# test type coercion +query I +SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from t; +---- +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# test string views + +statement ok +CREATE TABLE t_stringview AS +SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(start, 'Int64') as start, arrow_cast(flags, 'Utf8View') as flags FROM t; + +query I +SELECT regexp_count(str, '\w') from t_stringview; +---- +3 +3 +3 +3 +3 +4 +4 +10 +6 +4 +7 + +query I +SELECT regexp_count(str, '\w{2}', start) from t_stringview; +---- +1 +1 +1 +1 +0 +2 +1 +4 +1 +2 +3 + +query I +SELECT regexp_count(str, 'ab', 1, 'i') from t_stringview; +---- +1 +1 +1 +1 +1 +0 +0 +0 +0 +0 +0 + + +query I +SELECT regexp_count(str, pattern) from t_stringview; +---- +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start) from t_stringview; +---- +1 +1 +0 +0 +0 +0 +0 +1 +1 +1 +1 + +query I +SELECT regexp_count(str, pattern, start, flags) from t_stringview; +---- +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# test type coercion +query I +SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from t_stringview; +---- +1 +1 +1 +0 +0 +0 +0 +1 +1 +1 +1 + +# NULL tests + +query I +SELECT regexp_count(NULL, NULL); +---- +0 + +query I +SELECT regexp_count(NULL, 'a'); +---- +0 + +query I +SELECT regexp_count('a', NULL); +---- +0 + +query I +SELECT regexp_count(NULL, NULL, NULL, NULL); +---- +0 + +statement ok +CREATE TABLE empty_table (str varchar, pattern varchar, start int, flags varchar); + +query I +SELECT regexp_count(str, pattern, start, flags) from empty_table; +---- + +statement ok +INSERT INTO empty_table VALUES ('a', NULL, 1, 'i'), (NULL, 'a', 1, 'i'), (NULL, NULL, 1, 'i'), (NULL, NULL, NULL, 'i'); + +query I +SELECT regexp_count(str, pattern, start, flags) from empty_table; +---- +0 +0 +0 +0 + +statement ok +drop table t; + +statement ok +create or replace table strings as values + ('FooBar'), + ('Foo'), + ('Foo'), + ('Bar'), + ('FooBar'), + ('Bar'), + ('Baz'); + +statement ok +create or replace table dict_table as +select arrow_cast(column1, 'Dictionary(Int32, Utf8)') as column1 +from strings; + +query T +select column1 from dict_table where column1 LIKE '%oo%'; +---- +FooBar +Foo +Foo +FooBar + +query T +select column1 from dict_table where column1 NOT LIKE '%oo%'; +---- +Bar +Bar +Baz + +query T +select column1 from dict_table where column1 ILIKE '%oO%'; +---- +FooBar +Foo +Foo +FooBar + +query T +select column1 from dict_table where column1 NOT ILIKE '%oO%'; +---- +Bar +Bar +Baz + + +# plan should not cast the column, instead it should use the dictionary directly +query TT +explain select column1 from dict_table where column1 LIKE '%oo%'; +---- +logical_plan +01)Filter: dict_table.column1 LIKE Utf8("%oo%") +02)--TableScan: dict_table projection=[column1] +physical_plan +01)CoalesceBatchesExec: target_batch_size=8192 +02)--FilterExec: column1@0 LIKE %oo% +03)----DataSourceExec: partitions=1, partition_sizes=[1] + +# Ensure casting / coercion works for all operators +# (there should be no casts to Utf8) +query TT +explain select + column1 LIKE '%oo%', + column1 NOT LIKE '%oo%', + column1 ILIKE '%oo%', + column1 NOT ILIKE '%oo%' +from dict_table; +---- +logical_plan +01)Projection: dict_table.column1 LIKE Utf8("%oo%"), dict_table.column1 NOT LIKE Utf8("%oo%"), dict_table.column1 ILIKE Utf8("%oo%"), dict_table.column1 NOT ILIKE Utf8("%oo%") +02)--TableScan: dict_table projection=[column1] +physical_plan +01)ProjectionExec: expr=[column1@0 LIKE %oo% as dict_table.column1 LIKE Utf8("%oo%"), column1@0 NOT LIKE %oo% as dict_table.column1 NOT LIKE Utf8("%oo%"), column1@0 ILIKE %oo% as dict_table.column1 ILIKE Utf8("%oo%"), column1@0 NOT ILIKE %oo% as dict_table.column1 NOT ILIKE Utf8("%oo%")] +02)--DataSourceExec: partitions=1, partition_sizes=[1] + +statement ok +drop table strings + +statement ok +drop table dict_table diff --git a/datafusion/sqllogictest/test_files/regexp/README.md b/datafusion/sqllogictest/test_files/regexp/README.md deleted file mode 100644 index 7e5efc5b5ddf2..0000000000000 --- a/datafusion/sqllogictest/test_files/regexp/README.md +++ /dev/null @@ -1,59 +0,0 @@ - - -# Regexp Test Files - -This directory contains test files for regular expression (regexp) functions in DataFusion. - -## Directory Structure - -``` -regexp/ - - init_data.slt.part // Shared test data for regexp functions - - regexp_like.slt // Tests for regexp_like function - - regexp_count.slt // Tests for regexp_count function - - regexp_match.slt // Tests for regexp_match function - - regexp_replace.slt // Tests for regexp_replace function -``` - -## Tested Functions - -1. `regexp_like`: Check if a string matches a regular expression -2. `regexp_count`: Count occurrences of a pattern in a string -3. `regexp_match`: Extract matching substrings -4. `regexp_replace`: Replace matched substrings - -## Test Data - -Test data is centralized in the `init_data.slt.part` file and imported into each test file using the `include` directive. This approach ensures: - -Consistent test data across different regexp function tests -Easy maintenance of test data -Reduced duplication - -## Test Coverage - -Each test file covers: - -Basic functionality -Case-insensitive matching -Null handling -Start position tests -Capture group handling -Different string types (UTF-8, Unicode) diff --git a/datafusion/sqllogictest/test_files/regexp/init_data.slt.part b/datafusion/sqllogictest/test_files/regexp/init_data.slt.part deleted file mode 100644 index ed6fb0e872df9..0000000000000 --- a/datafusion/sqllogictest/test_files/regexp/init_data.slt.part +++ /dev/null @@ -1,31 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -statement ok -create table regexp_test_data (str varchar, pattern varchar, start int, flags varchar) as values - (NULL, '^(a)', 1, 'i'), - ('abc', '^(a)', 1, 'i'), - ('ABC', '^(A).*', 1, 'i'), - ('aBc', '(b|d)', 1, 'i'), - ('AbC', '(B|D)', 2, null), - ('aBC', '^(b|c)', 3, null), - ('4000', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 1, null), - ('4010', '\b4([1-9]\d\d|\d[1-9]\d|\d\d[1-9])\b', 2, null), - ('Düsseldorf','[\p{Letter}-]+', 3, null), - ('Москва', '[\p{L}-]+', 4, null), - ('Köln', '[a-zA-Z]ö[a-zA-Z]{2}', 1, null), - ('إسرائيل', '^\p{Arabic}+$', 2, null); diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_count.slt b/datafusion/sqllogictest/test_files/regexp/regexp_count.slt deleted file mode 100644 index f64705429bfa4..0000000000000 --- a/datafusion/sqllogictest/test_files/regexp/regexp_count.slt +++ /dev/null @@ -1,344 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# Import common test data -include ./init_data.slt.part - -# regexp_count tests from postgresql -# https://github.com/postgres/postgres/blob/56d23855c864b7384970724f3ad93fb0fc319e51/src/test/regress/sql/strings.sql#L226-L235 - -query I -SELECT regexp_count('123123123123123', '(12)3'); ----- -5 - -query I -SELECT regexp_count('123123123123', '123', 1); ----- -4 - -query I -SELECT regexp_count('123123123123', '123', 3); ----- -3 - -query I -SELECT regexp_count('123123123123', '123', 33); ----- -0 - -query I -SELECT regexp_count('ABCABCABCABC', 'Abc', 1, ''); ----- -0 - -query I -SELECT regexp_count('ABCABCABCABC', 'Abc', 1, 'i'); ----- -4 - -statement error -External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based -SELECT regexp_count('123123123123', '123', 0); - -statement error -External error: query failed: DataFusion error: Arrow error: Compute error: regexp_count() requires start to be 1 based -SELECT regexp_count('123123123123', '123', -3); - -statement error -External error: statement failed: DataFusion error: Arrow error: Compute error: regexp_count() does not support global flag -SELECT regexp_count('123123123123', '123', 1, 'g'); - -query I -SELECT regexp_count(str, '\w') from regexp_test_data; ----- -0 -3 -3 -3 -3 -3 -4 -4 -10 -6 -4 -7 - -query I -SELECT regexp_count(str, '\w{2}', start) from regexp_test_data; ----- -0 -1 -1 -1 -1 -0 -2 -1 -4 -1 -2 -3 - -query I -SELECT regexp_count(str, 'ab', 1, 'i') from regexp_test_data; ----- -0 -1 -1 -1 -1 -1 -0 -0 -0 -0 -0 -0 - - -query I -SELECT regexp_count(str, pattern) from regexp_test_data; ----- -0 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start) from regexp_test_data; ----- -0 -1 -1 -0 -0 -0 -0 -0 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start, flags) from regexp_test_data; ----- -0 -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# test type coercion -query I -SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from regexp_test_data; ----- -0 -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# test string views - -statement ok -CREATE TABLE t_stringview AS -SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(start, 'Int64') as start, arrow_cast(flags, 'Utf8View') as flags FROM regexp_test_data; - -query I -SELECT regexp_count(str, '\w') from t_stringview; ----- -0 -3 -3 -3 -3 -3 -4 -4 -10 -6 -4 -7 - -query I -SELECT regexp_count(str, '\w{2}', start) from t_stringview; ----- -0 -1 -1 -1 -1 -0 -2 -1 -4 -1 -2 -3 - -query I -SELECT regexp_count(str, 'ab', 1, 'i') from t_stringview; ----- -0 -1 -1 -1 -1 -1 -0 -0 -0 -0 -0 -0 - - -query I -SELECT regexp_count(str, pattern) from t_stringview; ----- -0 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start) from t_stringview; ----- -0 -1 -1 -0 -0 -0 -0 -0 -1 -1 -1 -1 - -query I -SELECT regexp_count(str, pattern, start, flags) from t_stringview; ----- -0 -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# test type coercion -query I -SELECT regexp_count(arrow_cast(str, 'Utf8'), arrow_cast(pattern, 'LargeUtf8'), arrow_cast(start, 'Int32'), flags) from t_stringview; ----- -0 -1 -1 -1 -0 -0 -0 -0 -1 -1 -1 -1 - -# NULL tests - -query I -SELECT regexp_count(NULL, NULL); ----- -0 - -query I -SELECT regexp_count(NULL, 'a'); ----- -0 - -query I -SELECT regexp_count('a', NULL); ----- -0 - -query I -SELECT regexp_count(NULL, NULL, NULL, NULL); ----- -0 - -statement ok -CREATE TABLE empty_table (str varchar, pattern varchar, start int, flags varchar); - -query I -SELECT regexp_count(str, pattern, start, flags) from empty_table; ----- - -statement ok -INSERT INTO empty_table VALUES ('a', NULL, 1, 'i'), (NULL, 'a', 1, 'i'), (NULL, NULL, 1, 'i'), (NULL, NULL, NULL, 'i'); - -query I -SELECT regexp_count(str, pattern, start, flags) from empty_table; ----- -0 -0 -0 -0 - -statement ok -drop table t_stringview; - -statement ok -drop table empty_table; \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_like.slt b/datafusion/sqllogictest/test_files/regexp/regexp_like.slt deleted file mode 100644 index ec48d62499c84..0000000000000 --- a/datafusion/sqllogictest/test_files/regexp/regexp_like.slt +++ /dev/null @@ -1,280 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# Import common test data -include ./init_data.slt.part - -query B -SELECT regexp_like(str, pattern, flags) FROM regexp_test_data; ----- -NULL -true -true -true -false -false -false -true -true -true -true -true - -query B -SELECT str ~ NULL FROM regexp_test_data; ----- -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL - -query B -select str ~ right('foo', NULL) FROM regexp_test_data; ----- -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL - -query B -select right('foo', NULL) !~ str FROM regexp_test_data; ----- -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL -NULL - -query B -SELECT regexp_like('foobarbequebaz', ''); ----- -true - -query B -SELECT regexp_like('', ''); ----- -true - -query B -SELECT regexp_like('foobarbequebaz', '(bar)(beque)'); ----- -true - -query B -SELECT regexp_like('fooBarbeQuebaz', '(bar).*(que)', 'is'); ----- -true - -query B -SELECT regexp_like('foobarbequebaz', '(ba3r)(bequ34e)'); ----- -false - -query B -SELECT regexp_like('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); ----- -true - -query B -SELECT regexp_like('aaa-0', '.*-(\d)'); ----- -true - -query B -SELECT regexp_like('bb-1', '.*-(\d)'); ----- -true - -query B -SELECT regexp_like('aa', '.*-(\d)'); ----- -false - -query B -SELECT regexp_like(NULL, '.*-(\d)'); ----- -NULL - -query B -SELECT regexp_like('aaa-0', NULL); ----- -NULL - -query B -SELECT regexp_like(null, '.*-(\d)'); ----- -NULL - -query error Error during planning: regexp_like\(\) does not support the "global" option -SELECT regexp_like('bb-1', '.*-(\d)', 'g'); - -query error Error during planning: regexp_like\(\) does not support the "global" option -SELECT regexp_like('bb-1', '.*-(\d)', 'g'); - -query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) -SELECT regexp_like('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); - -# look-around is not supported and will just return false -query B -SELECT regexp_like('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); ----- -false - -query B -select regexp_like('aaa-555', '.*-(\d*)'); ----- -true - -# multiline string -query B -SELECT 'foo\nbar\nbaz' ~ 'bar'; ----- -true - -statement error -Error during planning: Cannot infer common argument type for regex operation List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata -: {} }) ~ List(Field { name: "item", data_type: Int64, nullable: true, dict_is_ordered: false, metadata: {} }) -select [1,2] ~ [3]; - -query B -SELECT 'foo\nbar\nbaz' LIKE '%bar%'; ----- -true - -query B -SELECT NULL LIKE NULL; ----- -NULL - -query B -SELECT NULL iLIKE NULL; ----- -NULL - -query B -SELECT NULL not LIKE NULL; ----- -NULL - -query B -SELECT NULL not iLIKE NULL; ----- -NULL - -statement ok -create or replace table strings as values - ('FooBar'), - ('Foo'), - ('Foo'), - ('Bar'), - ('FooBar'), - ('Bar'), - ('Baz'); - -statement ok -create or replace table dict_table as -select arrow_cast(column1, 'Dictionary(Int32, Utf8)') as column1 -from strings; - -query T -select column1 from dict_table where column1 LIKE '%oo%'; ----- -FooBar -Foo -Foo -FooBar - -query T -select column1 from dict_table where column1 NOT LIKE '%oo%'; ----- -Bar -Bar -Baz - -query T -select column1 from dict_table where column1 ILIKE '%oO%'; ----- -FooBar -Foo -Foo -FooBar - -query T -select column1 from dict_table where column1 NOT ILIKE '%oO%'; ----- -Bar -Bar -Baz - - -# plan should not cast the column, instead it should use the dictionary directly -query TT -explain select column1 from dict_table where column1 LIKE '%oo%'; ----- -logical_plan -01)Filter: dict_table.column1 LIKE Utf8("%oo%") -02)--TableScan: dict_table projection=[column1] -physical_plan -01)CoalesceBatchesExec: target_batch_size=8192 -02)--FilterExec: column1@0 LIKE %oo% -03)----DataSourceExec: partitions=1, partition_sizes=[1] - -# Ensure casting / coercion works for all operators -# (there should be no casts to Utf8) -query TT -explain select - column1 LIKE '%oo%', - column1 NOT LIKE '%oo%', - column1 ILIKE '%oo%', - column1 NOT ILIKE '%oo%' -from dict_table; ----- -logical_plan -01)Projection: dict_table.column1 LIKE Utf8("%oo%"), dict_table.column1 NOT LIKE Utf8("%oo%"), dict_table.column1 ILIKE Utf8("%oo%"), dict_table.column1 NOT ILIKE Utf8("%oo%") -02)--TableScan: dict_table projection=[column1] -physical_plan -01)ProjectionExec: expr=[column1@0 LIKE %oo% as dict_table.column1 LIKE Utf8("%oo%"), column1@0 NOT LIKE %oo% as dict_table.column1 NOT LIKE Utf8("%oo%"), column1@0 ILIKE %oo% as dict_table.column1 ILIKE Utf8("%oo%"), column1@0 NOT ILIKE %oo% as dict_table.column1 NOT ILIKE Utf8("%oo%")] -02)--DataSourceExec: partitions=1, partition_sizes=[1] - -statement ok -drop table strings - -statement ok -drop table dict_table \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_match.slt b/datafusion/sqllogictest/test_files/regexp/regexp_match.slt deleted file mode 100644 index 4b4cf4f134d8e..0000000000000 --- a/datafusion/sqllogictest/test_files/regexp/regexp_match.slt +++ /dev/null @@ -1,201 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# Import common test data -include ./init_data.slt.part - -query ? -SELECT regexp_match(str, pattern, flags) FROM regexp_test_data; ----- -NULL -[a] -[A] -[B] -NULL -NULL -NULL -[010] -[Düsseldorf] -[Москва] -[Köln] -[إسرائيل] - -# test string view -statement ok -CREATE TABLE t_stringview AS -SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM regexp_test_data; - -query ? -SELECT regexp_match(str, pattern, flags) FROM t_stringview; ----- -NULL -[a] -[A] -[B] -NULL -NULL -NULL -[010] -[Düsseldorf] -[Москва] -[Köln] -[إسرائيل] - -statement ok -DROP TABLE t_stringview; - -query ? -SELECT regexp_match('foobarbequebaz', ''); ----- -[] - -query ? -SELECT regexp_match('', ''); ----- -[] - -query ? -SELECT regexp_match('foobarbequebaz', '(bar)(beque)'); ----- -[bar, beque] - -query ? -SELECT regexp_match('fooBarb -eQuebaz', '(bar).*(que)', 'is'); ----- -[Bar, Que] - -query ? -SELECT regexp_match('foobarbequebaz', '(ba3r)(bequ34e)'); ----- -NULL - -query ? -SELECT regexp_match('foobarbequebaz', '^.*(barbequ[0-9]*e).*$', 'm'); ----- -[barbeque] - -query ? -SELECT regexp_match('aaa-0', '.*-(\d)'); ----- -[0] - -query ? -SELECT regexp_match('bb-1', '.*-(\d)'); ----- -[1] - -query ? -SELECT regexp_match('aa', '.*-(\d)'); ----- -NULL - -query ? -SELECT regexp_match(NULL, '.*-(\d)'); ----- -NULL - -query ? -SELECT regexp_match('aaa-0', NULL); ----- -NULL - -query ? -SELECT regexp_match(null, '.*-(\d)'); ----- -NULL - -query error Error during planning: regexp_match\(\) does not support the "global" option -SELECT regexp_match('bb-1', '.*-(\d)', 'g'); - -query error Error during planning: regexp_match\(\) does not support the "global" option -SELECT regexp_match('bb-1', '.*-(\d)', 'g'); - -query error Arrow error: Compute error: Regular expression did not compile: CompiledTooBig\(10485760\) -SELECT regexp_match('aaaaa', 'a{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}{5}'); - -# look-around is not supported and will just return null -query ? -SELECT regexp_match('(?<=[A-Z]\w )Smith', 'John Smith', 'i'); ----- -NULL - -# ported test -query ? -SELECT regexp_match('aaa-555', '.*-(\d*)'); ----- -[555] - -query B -select 'abc' ~ null; ----- -NULL - -query B -select null ~ null; ----- -NULL - -query B -select null ~ 'abc'; ----- -NULL - -query B -select 'abc' ~* null; ----- -NULL - -query B -select null ~* null; ----- -NULL - -query B -select null ~* 'abc'; ----- -NULL - -query B -select 'abc' !~ null; ----- -NULL - -query B -select null !~ null; ----- -NULL - -query B -select null !~ 'abc'; ----- -NULL - -query B -select 'abc' !~* null; ----- -NULL - -query B -select null !~* null; ----- -NULL - -query B -select null !~* 'abc'; ----- -NULL \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/regexp/regexp_replace.slt b/datafusion/sqllogictest/test_files/regexp/regexp_replace.slt deleted file mode 100644 index d54261f02b81a..0000000000000 --- a/datafusion/sqllogictest/test_files/regexp/regexp_replace.slt +++ /dev/null @@ -1,129 +0,0 @@ -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - -# Import common test data -include ./init_data.slt.part - -query T -SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM regexp_test_data; ----- -NULL -Xbc -X -aXc -AbC -aBC -4000 -X -X -X -X -X - -# test string view -statement ok -CREATE TABLE t_stringview AS -SELECT arrow_cast(str, 'Utf8View') as str, arrow_cast(pattern, 'Utf8View') as pattern, arrow_cast(flags, 'Utf8View') as flags FROM regexp_test_data; - -query T -SELECT regexp_replace(str, pattern, 'X', concat('g', flags)) FROM t_stringview; ----- -NULL -Xbc -X -aXc -AbC -aBC -4000 -X -X -X -X -X - -statement ok -DROP TABLE t_stringview; - -query T -SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'gi'); ----- -XXX - -query T -SELECT regexp_replace('ABCabcABC', '(abc)', 'X', 'i'); ----- -XabcABC - -query T -SELECT regexp_replace('foobarbaz', 'b..', 'X', 'g'); ----- -fooXX - -query T -SELECT regexp_replace('foobarbaz', 'b..', 'X'); ----- -fooXbaz - -query T -SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', 'g'); ----- -fooXarYXazY - -query T -SELECT regexp_replace('foobarbaz', 'b(..)', 'X\\1Y', NULL); ----- -NULL - -query T -SELECT regexp_replace('foobarbaz', 'b(..)', NULL, 'g'); ----- -NULL - -query T -SELECT regexp_replace('foobarbaz', NULL, 'X\\1Y', 'g'); ----- -NULL - -query T -SELECT regexp_replace('Thomas', '.[mN]a.', 'M'); ----- -ThM - -query T -SELECT regexp_replace(NULL, 'b(..)', 'X\\1Y', 'g'); ----- -NULL - -query T -SELECT regexp_replace('foobar', 'bar', 'xx', 'gi') ----- -fooxx - -query T -SELECT regexp_replace(arrow_cast('foobar', 'Dictionary(Int32, Utf8)'), 'bar', 'xx', 'gi') ----- -fooxx - -query TTT -select - regexp_replace(col, NULL, 'c'), - regexp_replace(col, 'a', NULL), - regexp_replace(col, 'a', 'c', NULL) -from (values ('a'), ('b')) as tbl(col); ----- -NULL NULL NULL -NULL NULL NULL \ No newline at end of file diff --git a/datafusion/sqllogictest/test_files/simplify_expr.slt b/datafusion/sqllogictest/test_files/simplify_expr.slt index 075ccafcfd2e0..43193fb41cfad 100644 --- a/datafusion/sqllogictest/test_files/simplify_expr.slt +++ b/datafusion/sqllogictest/test_files/simplify_expr.slt @@ -63,47 +63,5 @@ query T select b from t where b !~ '.*' ---- -query TT -explain select * from t where a = a; ----- -logical_plan -01)Filter: t.a IS NOT NULL OR Boolean(NULL) -02)--TableScan: t projection=[a, b] -physical_plan -01)CoalesceBatchesExec: target_batch_size=8192 -02)--FilterExec: a@0 IS NOT NULL OR NULL -03)----DataSourceExec: partitions=1, partition_sizes=[1] - statement ok drop table t; - -# test decimal precision -query B -SELECT a * 1.000::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); ----- -false - -query B -SELECT 1.000::DECIMAL(4,3) * a > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); ----- -false - -query B -SELECT NULL::DECIMAL(4,3) * a > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); ----- -NULL - -query B -SELECT a * NULL::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); ----- -NULL - -query B -SELECT a / 1.000::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); ----- -false - -query B -SELECT a / NULL::DECIMAL(4,3) > 1.2::decimal(2,1) FROM VALUES (1) AS t(a); ----- -NULL diff --git a/datafusion/sqllogictest/test_files/subquery.slt b/datafusion/sqllogictest/test_files/subquery.slt index a0ac15b740d72..aaccaaa43ce49 100644 --- a/datafusion/sqllogictest/test_files/subquery.slt +++ b/datafusion/sqllogictest/test_files/subquery.slt @@ -921,7 +921,7 @@ query TT explain SELECT t1_id, (SELECT count(*) + 2 as cnt_plus_2 FROM t2 WHERE t2.t2_int = t1.t1_int having count(*) = 0) from t1 ---- logical_plan -01)Projection: t1.t1_id, CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN Int64(NULL) ELSE __scalar_sq_1.cnt_plus_2 END AS cnt_plus_2 +01)Projection: t1.t1_id, CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN NULL ELSE __scalar_sq_1.cnt_plus_2 END AS cnt_plus_2 02)--Left Join: t1.t1_int = __scalar_sq_1.t2_int 03)----TableScan: t1 projection=[t1_id, t1_int] 04)----SubqueryAlias: __scalar_sq_1 @@ -995,7 +995,7 @@ select t1.t1_int from t1 where ( ---- logical_plan 01)Projection: t1.t1_int -02)--Filter: CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN Int64(NULL) ELSE __scalar_sq_1.cnt_plus_two END = Int64(2) +02)--Filter: CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(2) WHEN __scalar_sq_1.count(Int64(1)) != Int64(0) THEN NULL ELSE __scalar_sq_1.cnt_plus_two END = Int64(2) 03)----Projection: t1.t1_int, __scalar_sq_1.cnt_plus_two, __scalar_sq_1.count(Int64(1)), __scalar_sq_1.__always_true 04)------Left Join: t1.t1_int = __scalar_sq_1.t2_int 05)--------TableScan: t1 projection=[t1_int] @@ -1049,46 +1049,6 @@ false true true -query IT rowsort -SELECT t1_id, (SELECT case when max(t2.t2_id) > 1 then 'a' else 'b' end FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 ----- -11 a -22 b -33 a -44 b - -query IB rowsort -SELECT t1_id, (SELECT max(t2.t2_id) is null FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 ----- -11 false -22 true -33 false -44 true - -query TT -explain SELECT t1_id, (SELECT max(t2.t2_id) is null FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 ----- -logical_plan -01)Projection: t1.t1_id, __scalar_sq_1.__always_true IS NULL OR __scalar_sq_1.__always_true IS NOT NULL AND __scalar_sq_1.max(t2.t2_id) IS NULL AS x -02)--Left Join: t1.t1_int = __scalar_sq_1.t2_int -03)----TableScan: t1 projection=[t1_id, t1_int] -04)----SubqueryAlias: __scalar_sq_1 -05)------Projection: max(t2.t2_id) IS NULL, t2.t2_int, Boolean(true) AS __always_true -06)--------Aggregate: groupBy=[[t2.t2_int]], aggr=[[max(t2.t2_id)]] -07)----------TableScan: t2 projection=[t2_id, t2_int] - -query TT -explain SELECT t1_id, (SELECT max(t2.t2_id) FROM t2 WHERE t2.t2_int = t1.t1_int) x from t1 ----- -logical_plan -01)Projection: t1.t1_id, __scalar_sq_1.max(t2.t2_id) AS x -02)--Left Join: t1.t1_int = __scalar_sq_1.t2_int -03)----TableScan: t1 projection=[t1_id, t1_int] -04)----SubqueryAlias: __scalar_sq_1 -05)------Projection: max(t2.t2_id), t2.t2_int -06)--------Aggregate: groupBy=[[t2.t2_int]], aggr=[[max(t2.t2_id)]] -07)----------TableScan: t2 projection=[t2_id, t2_int] - # in_subquery_to_join_with_correlated_outer_filter_disjunction query TT explain select t1.t1_id, diff --git a/datafusion/sqllogictest/test_files/timestamps.slt b/datafusion/sqllogictest/test_files/timestamps.slt index 44d0f1f97d4d5..dcbcfbfa439d5 100644 --- a/datafusion/sqllogictest/test_files/timestamps.slt +++ b/datafusion/sqllogictest/test_files/timestamps.slt @@ -416,33 +416,6 @@ SELECT to_timestamp(123456789.123456789) as c1, cast(123456789.123456789 as time ---- 1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 -# to_timestamp Decimal128 inputs - -query PPP -SELECT to_timestamp(arrow_cast(1.1, 'Decimal128(2,1)')) as c1, cast(arrow_cast(1.1, 'Decimal128(2,1)') as timestamp) as c2, arrow_cast(1.1, 'Decimal128(2,1)')::timestamp as c3; ----- -1970-01-01T00:00:01.100 1970-01-01T00:00:01.100 1970-01-01T00:00:01.100 - -query PPP -SELECT to_timestamp(arrow_cast(-1.1, 'Decimal128(2,1)')) as c1, cast(arrow_cast(-1.1, 'Decimal128(2,1)') as timestamp) as c2, arrow_cast(-1.1, 'Decimal128(2,1)')::timestamp as c3; ----- -1969-12-31T23:59:58.900 1969-12-31T23:59:58.900 1969-12-31T23:59:58.900 - -query PPP -SELECT to_timestamp(arrow_cast(0.0, 'Decimal128(2,1)')) as c1, cast(arrow_cast(0.0, 'Decimal128(2,1)') as timestamp) as c2, arrow_cast(0.0, 'Decimal128(2,1)')::timestamp as c3; ----- -1970-01-01T00:00:00 1970-01-01T00:00:00 1970-01-01T00:00:00 - -query PPP -SELECT to_timestamp(arrow_cast(1.23456789, 'Decimal128(9,8)')) as c1, cast(arrow_cast(1.23456789, 'Decimal128(9,8)') as timestamp) as c2, arrow_cast(1.23456789, 'Decimal128(9,8)')::timestamp as c3; ----- -1970-01-01T00:00:01.234567890 1970-01-01T00:00:01.234567890 1970-01-01T00:00:01.234567890 - -query PPP -SELECT to_timestamp(arrow_cast(123456789.123456789, 'Decimal128(18,9)')) as c1, cast(arrow_cast(123456789.123456789, 'Decimal128(18,9)') as timestamp) as c2, arrow_cast(123456789.123456789, 'Decimal128(18,9)')::timestamp as c3; ----- -1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 1973-11-29T21:33:09.123456784 - # from_unixtime @@ -2268,23 +2241,23 @@ query error input contains invalid characters SELECT to_timestamp_seconds('2020-09-08 12/00/00+00:00', '%c', '%+') # to_timestamp with broken formatting -query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input +query error bad or unsupported format string SELECT to_timestamp('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_nanos with broken formatting -query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input +query error bad or unsupported format string SELECT to_timestamp_nanos('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_millis with broken formatting -query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input +query error bad or unsupported format string SELECT to_timestamp_millis('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_micros with broken formatting -query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input +query error bad or unsupported format string SELECT to_timestamp_micros('2020-09-08 12/00/00+00:00', '%q') # to_timestamp_seconds with broken formatting -query error DataFusion error: Execution error: Error parsing timestamp from '2020\-09\-08 12/00/00\+00:00' using format '%q': trailing input +query error bad or unsupported format string SELECT to_timestamp_seconds('2020-09-08 12/00/00+00:00', '%q') # Create string timestamp table with different formats @@ -2842,11 +2815,6 @@ select to_char(arrow_cast(TIMESTAMP '2023-08-03 14:38:50Z', 'Timestamp(Second, N ---- 03-08-2023 14-38-50 -query T -select to_char(arrow_cast('2023-09-04'::date, 'Timestamp(Second, Some("UTC"))'), '%Y-%m-%dT%H:%M:%S%.3f'); ----- -2023-09-04T00:00:00.000 - query T select to_char(arrow_cast(123456, 'Duration(Second)'), 'pretty'); ---- diff --git a/datafusion/sqllogictest/test_files/topk.slt b/datafusion/sqllogictest/test_files/topk.slt index ce23fe26528c3..b5ff95c358d8e 100644 --- a/datafusion/sqllogictest/test_files/topk.slt +++ b/datafusion/sqllogictest/test_files/topk.slt @@ -233,165 +233,3 @@ d 1 -98 y7C453hRWd4E7ImjNDWlpexB8nUqjh y7C453hRWd4E7ImjNDWlpexB8nUqjh e 2 52 xipQ93429ksjNcXPX5326VSg1xJZcW xipQ93429ksjNcXPX5326VSg1xJZcW d 1 -72 wwXqSGKLyBQyPkonlzBNYUJTCo4LRS wwXqSGKLyBQyPkonlzBNYUJTCo4LRS a 1 -5 waIGbOGl1PM6gnzZ4uuZt4E2yDWRHs waIGbOGl1PM6gnzZ4uuZt4E2yDWRHs - -##################################### -## Test TopK with Partially Sorted Inputs -##################################### - - -# Create an external table where data is pre-sorted by (number DESC, letter ASC) only. -statement ok -CREATE EXTERNAL TABLE partial_sorted ( - number INT, - letter VARCHAR, - age INT -) -STORED AS parquet -LOCATION 'test_files/scratch/topk/partial_sorted/1.parquet' -WITH ORDER (number DESC, letter ASC); - -# Insert test data into the external table. -query I -COPY ( - SELECT * - FROM ( - VALUES - (1, 'F', 100), - (1, 'B', 50), - (2, 'C', 70), - (2, 'D', 80), - (3, 'A', 60), - (3, 'E', 90) - ) AS t(number, letter, age) - ORDER BY number DESC, letter ASC -) -TO 'test_files/scratch/topk/partial_sorted/1.parquet'; ----- -6 - -## explain physical_plan only -statement ok -set datafusion.explain.physical_plan_only = true - -## batch size smaller than number of rows in the table and result -statement ok -set datafusion.execution.batch_size = 2 - -# Run a TopK query that orders by all columns. -# Although the table is only guaranteed to be sorted by (number DESC, letter ASC), -# DataFusion should use the common prefix optimization -# and return the correct top 3 rows when ordering by all columns. -query ITI -select number, letter, age from partial_sorted order by number desc, letter asc, age desc limit 3; ----- -3 A 60 -3 E 90 -2 C 70 - -# A more complex example with a projection that includes an expression (see further down for the explained plan) -query IIITI -select - number + 1 as number_plus, - number, - number + 1 as other_number_plus, - letter, - age -from partial_sorted -order by - number_plus desc, - number desc, - other_number_plus desc, - letter asc, - age desc -limit 3; ----- -4 3 4 A 60 -4 3 4 E 90 -3 2 3 C 70 - -# Verify that the physical plan includes the sort prefix. -# The output should display a "sort_prefix" in the SortExec node. -query TT -explain select number, letter, age from partial_sorted order by number desc, letter asc, age desc limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[number@0 DESC, letter@1 ASC NULLS LAST, age@2 DESC], preserve_partitioning=[false], sort_prefix=[number@0 DESC, letter@1 ASC NULLS LAST] -02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - - -# Explain variations of the above query with different orderings, and different sort prefixes. -# The "sort_prefix" in the SortExec node should only be present if the TopK's ordering starts with either (number DESC, letter ASC) or just (number DESC). -query TT -explain select number, letter, age from partial_sorted order by age desc limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[age@2 DESC], preserve_partitioning=[false] -02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - -query TT -explain select number, letter, age from partial_sorted order by number desc, letter desc limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[number@0 DESC, letter@1 DESC], preserve_partitioning=[false], sort_prefix=[number@0 DESC] -02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - -query TT -explain select number, letter, age from partial_sorted order by number asc limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[number@0 ASC NULLS LAST], preserve_partitioning=[false] -02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - -query TT -explain select number, letter, age from partial_sorted order by letter asc, number desc limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[letter@1 ASC NULLS LAST, number@0 DESC], preserve_partitioning=[false] -02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - -# Explicit NULLS ordering cases (reversing the order of the NULLS on the number and letter orderings) -query TT -explain select number, letter, age from partial_sorted order by number desc, letter asc NULLS FIRST limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[number@0 DESC, letter@1 ASC], preserve_partitioning=[false], sort_prefix=[number@0 DESC] -02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - -query TT -explain select number, letter, age from partial_sorted order by number desc NULLS LAST, letter asc limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[number@0 DESC NULLS LAST, letter@1 ASC NULLS LAST], preserve_partitioning=[false] -02)--DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - - -# Verify that the sort prefix is correctly computed on the normalized ordering (removing redundant aliased columns) -query TT -explain select number, letter, age, number as column4, letter as column5 from partial_sorted order by number desc, column4 desc, letter asc, column5 asc, age desc limit 3; ----- -physical_plan -01)SortExec: TopK(fetch=3), expr=[number@0 DESC, column4@3 DESC, letter@1 ASC NULLS LAST, column5@4 ASC NULLS LAST, age@2 DESC], preserve_partitioning=[false], sort_prefix=[number@0 DESC, letter@1 ASC NULLS LAST] -02)--ProjectionExec: expr=[number@0 as number, letter@1 as letter, age@2 as age, number@0 as column4, letter@1 as column5] -03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, letter, age], output_ordering=[number@0 DESC, letter@1 ASC NULLS LAST], file_type=parquet - -# Verify that the sort prefix is correctly computed over normalized, order-maintaining projections (number + 1, number, number + 1, age) -query TT -explain select number + 1 as number_plus, number, number + 1 as other_number_plus, age from partial_sorted order by number_plus desc, number desc, other_number_plus desc, age asc limit 3; ----- -physical_plan -01)SortPreservingMergeExec: [number_plus@0 DESC, number@1 DESC, other_number_plus@2 DESC, age@3 ASC NULLS LAST], fetch=3 -02)--SortExec: TopK(fetch=3), expr=[number_plus@0 DESC, number@1 DESC, other_number_plus@2 DESC, age@3 ASC NULLS LAST], preserve_partitioning=[true], sort_prefix=[number_plus@0 DESC, number@1 DESC] -03)----ProjectionExec: expr=[__common_expr_1@0 as number_plus, number@1 as number, __common_expr_1@0 as other_number_plus, age@2 as age] -04)------ProjectionExec: expr=[CAST(number@0 AS Int64) + 1 as __common_expr_1, number@0 as number, age@1 as age] -05)--------RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 -06)----------DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/datafusion/sqllogictest/test_files/scratch/topk/partial_sorted/1.parquet]]}, projection=[number, age], output_ordering=[number@0 DESC], file_type=parquet - -# Cleanup -statement ok -DROP TABLE partial_sorted; - -statement ok -set datafusion.explain.physical_plan_only = false - -statement ok -set datafusion.execution.batch_size = 8192 diff --git a/datafusion/sqllogictest/test_files/window.slt b/datafusion/sqllogictest/test_files/window.slt index 52cc80eae1c8a..76e3751e4b8e4 100644 --- a/datafusion/sqllogictest/test_files/window.slt +++ b/datafusion/sqllogictest/test_files/window.slt @@ -2356,7 +2356,7 @@ logical_plan 03)----WindowAggr: windowExpr=[[row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW]] 04)------TableScan: aggregate_test_100 projection=[c9] physical_plan -01)SortExec: TopK(fetch=5), expr=[rn1@1 ASC NULLS LAST, c9@0 ASC NULLS LAST], preserve_partitioning=[false], sort_prefix=[rn1@1 ASC NULLS LAST] +01)SortExec: TopK(fetch=5), expr=[rn1@1 ASC NULLS LAST, c9@0 ASC NULLS LAST], preserve_partitioning=[false] 02)--ProjectionExec: expr=[c9@0 as c9, row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW@1 as rn1] 03)----BoundedWindowAggExec: wdw=[row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: Ok(Field { name: "row_number() ORDER BY [aggregate_test_100.c9 DESC NULLS FIRST] RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW", data_type: UInt64, nullable: false, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Range, start_bound: Preceding(UInt64(NULL)), end_bound: CurrentRow, is_causal: false }], mode=[Sorted] 04)------SortExec: expr=[c9@0 DESC], preserve_partitioning=[false] @@ -5537,21 +5537,6 @@ physical_plan 02)--WindowAggExec: wdw=[max(aggregate_test_100_ordered.c5) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING: Ok(Field { name: "max(aggregate_test_100_ordered.c5) ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING", data_type: Int32, nullable: true, dict_id: 0, dict_is_ordered: false, metadata: {} }), frame: WindowFrame { units: Rows, start_bound: Preceding(UInt64(NULL)), end_bound: Following(UInt64(NULL)), is_causal: false }] 03)----DataSourceExec: file_groups={1 group: [[WORKSPACE_ROOT/testing/data/csv/aggregate_test_100.csv]]}, projection=[c5], file_type=csv, has_header=true -query II rowsort -SELECT - t1.v1, - SUM(t1.v1) OVER w + 1 -FROM - generate_series(1, 5) AS t1(v1) -WINDOW - w AS (ORDER BY t1.v1); ----- -1 2 -2 4 -3 7 -4 11 -5 16 - # Testing Utf8View with window statement ok CREATE TABLE aggregate_test_100_utf8view AS SELECT @@ -5610,35 +5595,3 @@ DROP TABLE aggregate_test_100_utf8view; statement ok DROP TABLE aggregate_test_100 - -# window definitions with aliases -query II rowsort -SELECT - t1.v1, - SUM(t1.v1) OVER W + 1 -FROM - generate_series(1, 5) AS t1(v1) -WINDOW - w AS (ORDER BY t1.v1); ----- -1 2 -2 4 -3 7 -4 11 -5 16 - -# window definitions with aliases -query II rowsort -SELECT - t1.v1, - SUM(t1.v1) OVER w + 1 -FROM - generate_series(1, 5) AS t1(v1) -WINDOW - W AS (ORDER BY t1.v1); ----- -1 2 -2 4 -3 7 -4 11 -5 16 diff --git a/datafusion/substrait/src/logical_plan/consumer.rs b/datafusion/substrait/src/logical_plan/consumer.rs index 1442267d3dbb6..61f3379735c7d 100644 --- a/datafusion/substrait/src/logical_plan/consumer.rs +++ b/datafusion/substrait/src/logical_plan/consumer.rs @@ -1835,7 +1835,8 @@ fn requalify_sides_if_needed( }) }) { // These names have no connection to the original plan, but they'll make the columns - // (mostly) unique. + // (mostly) unique. There may be cases where this still causes duplicates, if either left + // or right side itself contains duplicate names with different qualifiers. Ok(( left.alias(TableReference::bare("left"))?, right.alias(TableReference::bare("right"))?, diff --git a/datafusion/substrait/src/physical_plan/producer.rs b/datafusion/substrait/src/physical_plan/producer.rs index cb725a7277fd3..9ba0e0c964e9e 100644 --- a/datafusion/substrait/src/physical_plan/producer.rs +++ b/datafusion/substrait/src/physical_plan/producer.rs @@ -61,7 +61,7 @@ pub fn to_substrait_rel( substrait_files.push(FileOrFiles { partition_index: partition_index.try_into().unwrap(), start: 0, - length: file.object_meta.size, + length: file.object_meta.size as u64, path_type: Some(PathType::UriPath( file.object_meta.location.as_ref().to_string(), )), diff --git a/datafusion/substrait/tests/cases/consumer_integration.rs b/datafusion/substrait/tests/cases/consumer_integration.rs index bdeeeb585c0cb..af9d92378298a 100644 --- a/datafusion/substrait/tests/cases/consumer_integration.rs +++ b/datafusion/substrait/tests/cases/consumer_integration.rs @@ -519,33 +519,6 @@ mod tests { Ok(()) } - #[tokio::test] - async fn test_multiple_joins() -> Result<()> { - let plan_str = test_plan_to_string("multiple_joins.json").await?; - assert_eq!( - plan_str, - "Projection: left.count(Int64(1)) AS count_first, left.category, left.count(Int64(1)):1 AS count_second, right.count(Int64(1)) AS count_third\ - \n Left Join: left.id = right.id\ - \n SubqueryAlias: left\ - \n Left Join: left.id = right.id\ - \n SubqueryAlias: left\ - \n Left Join: left.id = right.id\ - \n SubqueryAlias: left\ - \n Aggregate: groupBy=[[id]], aggr=[[count(Int64(1))]]\ - \n Values: (Int64(1)), (Int64(2))\ - \n SubqueryAlias: right\ - \n Aggregate: groupBy=[[id, category]], aggr=[[]]\ - \n Values: (Int64(1), Utf8(\"info\")), (Int64(2), Utf8(\"low\"))\ - \n SubqueryAlias: right\ - \n Aggregate: groupBy=[[id]], aggr=[[count(Int64(1))]]\ - \n Values: (Int64(1)), (Int64(2))\ - \n SubqueryAlias: right\ - \n Aggregate: groupBy=[[id]], aggr=[[count(Int64(1))]]\ - \n Values: (Int64(1)), (Int64(2))" - ); - Ok(()) - } - #[tokio::test] async fn test_select_window_count() -> Result<()> { let plan_str = test_plan_to_string("select_window_count.substrait.json").await?; diff --git a/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs b/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs index 9a85f3e6c4dc4..f989d05c80dd1 100644 --- a/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs +++ b/datafusion/substrait/tests/cases/roundtrip_logical_plan.rs @@ -37,7 +37,6 @@ use datafusion::logical_expr::{ }; use datafusion::optimizer::simplify_expressions::expr_simplifier::THRESHOLD_INLINE_INLIST; use datafusion::prelude::*; -use insta::assert_snapshot; use std::hash::Hash; use std::sync::Arc; use substrait::proto::extensions::simple_extension_declaration::MappingType; @@ -189,16 +188,13 @@ async fn simple_select() -> Result<()> { #[tokio::test] async fn wildcard_select() -> Result<()> { - let plan = generate_plan_from_sql("SELECT * FROM data", true, false).await?; - - assert_snapshot!( - plan, - @r#" - Projection: data.a, data.b, data.c, data.d, data.e, data.f - TableScan: data - "# - ); - Ok(()) + assert_expected_plan_unoptimized( + "SELECT * FROM data", + "Projection: data.a, data.b, data.c, data.d, data.e, data.f\ + \n TableScan: data", + true, + ) + .await } #[tokio::test] @@ -303,42 +299,24 @@ async fn aggregate_grouping_sets() -> Result<()> { #[tokio::test] async fn aggregate_grouping_rollup() -> Result<()> { - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT a, c, e, avg(b) FROM data GROUP BY ROLLUP (a, c, e)", - true, - true, - ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: data.a, data.c, data.e, avg(data.b) - Aggregate: groupBy=[[GROUPING SETS ((data.a, data.c, data.e), (data.a, data.c), (data.a), ())]], aggr=[[avg(data.b)]] - TableScan: data projection=[a, b, c, e] - "# - ); - Ok(()) + "Projection: data.a, data.c, data.e, avg(data.b)\ + \n Aggregate: groupBy=[[GROUPING SETS ((data.a, data.c, data.e), (data.a, data.c), (data.a), ())]], aggr=[[avg(data.b)]]\ + \n TableScan: data projection=[a, b, c, e]", + true + ).await } #[tokio::test] async fn multilayer_aggregate() -> Result<()> { - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT a, sum(partial_count_b) FROM (SELECT a, count(b) as partial_count_b FROM data GROUP BY a) GROUP BY a", - true, - true, - ) - .await?; - - assert_snapshot!( - plan, - @r#" - Aggregate: groupBy=[[data.a]], aggr=[[sum(count(data.b)) AS sum(partial_count_b)]] - Aggregate: groupBy=[[data.a]], aggr=[[count(data.b)]] - TableScan: data projection=[a, b] - "# - ); - Ok(()) + "Aggregate: groupBy=[[data.a]], aggr=[[sum(count(data.b)) AS sum(partial_count_b)]]\ + \n Aggregate: groupBy=[[data.a]], aggr=[[count(data.b)]]\ + \n TableScan: data projection=[a, b]", + true + ).await } #[tokio::test] @@ -476,21 +454,13 @@ async fn try_cast_decimal_to_string() -> Result<()> { #[tokio::test] async fn aggregate_case() -> Result<()> { - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT sum(CASE WHEN a > 0 THEN 1 ELSE NULL END) FROM data", - true, - true, + "Aggregate: groupBy=[[]], aggr=[[sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE Int64(NULL) END) AS sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE NULL END)]]\ + \n TableScan: data projection=[a]", + true ) - .await?; - - assert_snapshot!( - plan, - @r#" - Aggregate: groupBy=[[]], aggr=[[sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE Int64(NULL) END) AS sum(CASE WHEN data.a > Int64(0) THEN Int64(1) ELSE NULL END)]] - TableScan: data projection=[a] - "# - ); - Ok(()) + .await } #[tokio::test] @@ -523,27 +493,18 @@ async fn roundtrip_inlist_4() -> Result<()> { #[tokio::test] async fn roundtrip_inlist_5() -> Result<()> { // on roundtrip there is an additional projection during TableScan which includes all column of the table, - // using assert_and_generate_plan and assert_snapshot! here as a workaround - let plan = generate_plan_from_sql( + // using assert_expected_plan here as a workaround + assert_expected_plan( "SELECT a, f FROM data WHERE (f IN ('a', 'b', 'c') OR a in (SELECT data2.a FROM data2 WHERE f IN ('b', 'c', 'd')))", - true, - true, - ) - .await?; - assert_snapshot!( - plan, - @r#" - Projection: data.a, data.f - Filter: data.f = Utf8("a") OR data.f = Utf8("b") OR data.f = Utf8("c") OR data2.mark - LeftMark Join: data.a = data2.a - TableScan: data projection=[a, f] - Projection: data2.a - Filter: data2.f = Utf8("b") OR data2.f = Utf8("c") OR data2.f = Utf8("d") - TableScan: data2 projection=[a, f], partial_filters=[data2.f = Utf8("b") OR data2.f = Utf8("c") OR data2.f = Utf8("d")] - "# - ); - Ok(()) + "Projection: data.a, data.f\ + \n Filter: data.f = Utf8(\"a\") OR data.f = Utf8(\"b\") OR data.f = Utf8(\"c\") OR data2.mark\ + \n LeftMark Join: data.a = data2.a\ + \n TableScan: data projection=[a, f]\ + \n Projection: data2.a\ + \n Filter: data2.f = Utf8(\"b\") OR data2.f = Utf8(\"c\") OR data2.f = Utf8(\"d\")\ + \n TableScan: data2 projection=[a, f], partial_filters=[data2.f = Utf8(\"b\") OR data2.f = Utf8(\"c\") OR data2.f = Utf8(\"d\")]", + true).await } #[tokio::test] @@ -574,44 +535,27 @@ async fn roundtrip_non_equi_join() -> Result<()> { #[tokio::test] async fn roundtrip_exists_filter() -> Result<()> { - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT b FROM data d1 WHERE EXISTS (SELECT * FROM data2 d2 WHERE d2.a = d1.a AND d2.e != d1.e)", - false, - true, - ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: data.b - LeftSemi Join: data.a = data2.a Filter: data2.e != CAST(data.e AS Int64) - TableScan: data projection=[a, b, e] - TableScan: data2 projection=[a, e] - "# - ); - Ok(()) + "Projection: data.b\ + \n LeftSemi Join: data.a = data2.a Filter: data2.e != CAST(data.e AS Int64)\ + \n TableScan: data projection=[a, b, e]\ + \n TableScan: data2 projection=[a, e]", + false // "d1" vs "data" field qualifier + ).await } #[tokio::test] async fn inner_join() -> Result<()> { - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT data.a FROM data JOIN data2 ON data.a = data2.a", - true, + "Projection: data.a\ + \n Inner Join: data.a = data2.a\ + \n TableScan: data projection=[a]\ + \n TableScan: data2 projection=[a]", true, ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: data.a - Inner Join: data.a = data2.a - TableScan: data projection=[a] - TableScan: data2 projection=[a] - "# - ); - Ok(()) + .await } #[tokio::test] @@ -648,25 +592,17 @@ async fn roundtrip_self_implicit_cross_join() -> Result<()> { #[tokio::test] async fn self_join_introduces_aliases() -> Result<()> { - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT d1.b, d2.c FROM data d1 JOIN data d2 ON d1.b = d2.b", + "Projection: left.b, right.c\ + \n Inner Join: left.b = right.b\ + \n SubqueryAlias: left\ + \n TableScan: data projection=[b]\ + \n SubqueryAlias: right\ + \n TableScan: data projection=[b, c]", false, - true, ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: left.b, right.c - Inner Join: left.b = right.b - SubqueryAlias: left - TableScan: data projection=[b] - SubqueryAlias: right - TableScan: data projection=[b, c] - "# - ); - Ok(()) + .await } #[tokio::test] @@ -811,15 +747,12 @@ async fn aggregate_wo_projection_consume() -> Result<()> { let proto_plan = read_json("tests/testdata/test_plans/aggregate_no_project.substrait.json"); - let plan = generate_plan_from_substrait(proto_plan).await?; - assert_snapshot!( - plan, - @r#" - Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]] - TableScan: data projection=[a] - "# - ); - Ok(()) + assert_expected_plan_substrait( + proto_plan, + "Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]]\ + \n TableScan: data projection=[a]", + ) + .await } #[tokio::test] @@ -827,15 +760,12 @@ async fn aggregate_wo_projection_group_expression_ref_consume() -> Result<()> { let proto_plan = read_json("tests/testdata/test_plans/aggregate_no_project_group_expression_ref.substrait.json"); - let plan = generate_plan_from_substrait(proto_plan).await?; - assert_snapshot!( - plan, - @r#" - Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]] - TableScan: data projection=[a] - "# - ); - Ok(()) + assert_expected_plan_substrait( + proto_plan, + "Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) AS countA]]\ + \n TableScan: data projection=[a]", + ) + .await } #[tokio::test] @@ -843,15 +773,12 @@ async fn aggregate_wo_projection_sorted_consume() -> Result<()> { let proto_plan = read_json("tests/testdata/test_plans/aggregate_sorted_no_project.substrait.json"); - let plan = generate_plan_from_substrait(proto_plan).await?; - assert_snapshot!( - plan, - @r#" - Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) ORDER BY [data.a DESC NULLS FIRST] AS countA]] - TableScan: data projection=[a] - "# - ); - Ok(()) + assert_expected_plan_substrait( + proto_plan, + "Aggregate: groupBy=[[data.a]], aggr=[[count(data.a) ORDER BY [data.a DESC NULLS FIRST] AS countA]]\ + \n TableScan: data projection=[a]", + ) + .await } #[tokio::test] @@ -1059,67 +986,19 @@ async fn roundtrip_literal_list() -> Result<()> { #[tokio::test] async fn roundtrip_literal_struct() -> Result<()> { - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT STRUCT(1, true, CAST(NULL AS STRING)) FROM data", - true, - true, - ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: Struct({c0:1,c1:true,c2:}) AS struct(Int64(1),Boolean(true),NULL) - TableScan: data projection=[] - "# - ); - Ok(()) -} - -#[tokio::test] -async fn roundtrip_literal_named_struct() -> Result<()> { - let plan = generate_plan_from_sql( - "SELECT STRUCT(1 as int_field, true as boolean_field, CAST(NULL AS STRING) as string_field) FROM data", - true, - true, - ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: Struct({int_field:1,boolean_field:true,string_field:}) AS named_struct(Utf8("int_field"),Int64(1),Utf8("boolean_field"),Boolean(true),Utf8("string_field"),NULL) - TableScan: data projection=[] - "# - ); - Ok(()) -} - -#[tokio::test] -async fn roundtrip_literal_renamed_struct() -> Result<()> { - // This test aims to hit a case where the struct column itself has the expected name, but its - // inner field needs to be renamed. - let plan = generate_plan_from_sql( - "SELECT CAST((STRUCT(1)) AS Struct<\"int_field\"Int>) AS 'Struct({c0:1})' FROM data", - true, - true, + "Projection: Struct({c0:1,c1:true,c2:}) AS struct(Int64(1),Boolean(true),NULL)\ + \n TableScan: data projection=[]", + false, // "Struct(..)" vs "struct(..)" ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: Struct({int_field:1}) AS Struct({c0:1}) - TableScan: data projection=[] - "# - ); - Ok(()) + .await } #[tokio::test] async fn roundtrip_values() -> Result<()> { // TODO: would be nice to have a struct inside the LargeList, but arrow_cast doesn't support that currently - let plan = generate_plan_from_sql( + assert_expected_plan( "VALUES \ (\ 1, \ @@ -1130,18 +1009,17 @@ async fn roundtrip_values() -> Result<()> { [STRUCT(STRUCT('a' AS string_field) AS struct_field), STRUCT(STRUCT('b' AS string_field) AS struct_field)]\ ), \ (NULL, NULL, NULL, NULL, NULL, NULL)", - true, - true, - ) - .await?; - - assert_snapshot!( - plan, - @r#" - Values: (Int64(1), Utf8("a"), List([[-213.1, , 5.5, 2.0, 1.0], []]), LargeList([1, 2, 3]), Struct({c0:true,int_field:1,c2:}), List([{struct_field: {string_field: a}}, {struct_field: {string_field: b}}])), (Int64(NULL), Utf8(NULL), List(), LargeList(), Struct({c0:,int_field:,c2:}), List()) - "# - ); - Ok(()) + "Values: \ + (\ + Int64(1), \ + Utf8(\"a\"), \ + List([[-213.1, , 5.5, 2.0, 1.0], []]), \ + LargeList([1, 2, 3]), \ + Struct({c0:true,int_field:1,c2:}), \ + List([{struct_field: {string_field: a}}, {struct_field: {string_field: b}}])\ + ), \ + (Int64(NULL), Utf8(NULL), List(), LargeList(), Struct({c0:,int_field:,c2:}), List())", + true).await } #[tokio::test] @@ -1183,22 +1061,14 @@ async fn duplicate_column() -> Result<()> { // only. DataFusion however, is strict about not having duplicate column names appear in the plan. // This test confirms that we generate aliases for columns in the plan which would otherwise have // colliding names. - let plan = generate_plan_from_sql( + assert_expected_plan( "SELECT a + 1 as sum_a, a + 1 as sum_a_2 FROM data", - true, + "Projection: data.a + Int64(1) AS sum_a, data.a + Int64(1) AS data.a + Int64(1)__temp__0 AS sum_a_2\ + \n Projection: data.a + Int64(1)\ + \n TableScan: data projection=[a]", true, ) - .await?; - - assert_snapshot!( - plan, - @r#" - Projection: data.a + Int64(1) AS sum_a, data.a + Int64(1) AS data.a + Int64(1)__temp__0 AS sum_a_2 - Projection: data.a + Int64(1) - TableScan: data projection=[a] - "# - ); - Ok(()) + .await } /// Construct a plan that cast columns. Only those SQL types are supported for now. @@ -1504,32 +1374,30 @@ async fn assert_read_filter_count( Ok(()) } -async fn generate_plan_from_sql( +async fn assert_expected_plan_unoptimized( sql: &str, + expected_plan_str: &str, assert_schema: bool, - optimized: bool, -) -> Result { +) -> Result<()> { let ctx = create_context().await?; - let df: DataFrame = ctx.sql(sql).await?; - - let plan = if optimized { - df.into_optimized_plan()? - } else { - df.into_unoptimized_plan() - }; + let df = ctx.sql(sql).await?; + let plan = df.into_unoptimized_plan(); let proto = to_substrait_plan(&plan, &ctx.state())?; - let plan2 = if optimized { - let temp = from_substrait_plan(&ctx.state(), &proto).await?; - ctx.state().optimize(&temp)? - } else { - from_substrait_plan(&ctx.state(), &proto).await? - }; + let plan2 = from_substrait_plan(&ctx.state(), &proto).await?; + + println!("{plan}"); + println!("{plan2}"); + + println!("{proto:?}"); if assert_schema { assert_eq!(plan.schema(), plan2.schema()); } - Ok(plan2) + let plan2str = format!("{plan2}"); + assert_eq!(expected_plan_str, &plan2str); + + Ok(()) } async fn assert_expected_plan( @@ -1544,6 +1412,11 @@ async fn assert_expected_plan( let plan2 = from_substrait_plan(&ctx.state(), &proto).await?; let plan2 = ctx.state().optimize(&plan2)?; + println!("{plan}"); + println!("{plan2}"); + + println!("{proto:?}"); + if assert_schema { assert_eq!(plan.schema(), plan2.schema()); } @@ -1554,14 +1427,20 @@ async fn assert_expected_plan( Ok(()) } -async fn generate_plan_from_substrait(substrait_plan: Plan) -> Result { +async fn assert_expected_plan_substrait( + substrait_plan: Plan, + expected_plan_str: &str, +) -> Result<()> { let ctx = create_context().await?; let plan = from_substrait_plan(&ctx.state(), &substrait_plan).await?; let plan = ctx.state().optimize(&plan)?; - Ok(plan) + let planstr = format!("{plan}"); + assert_eq!(planstr, expected_plan_str); + + Ok(()) } async fn assert_substrait_sql(substrait_plan: Plan, sql: &str) -> Result<()> { @@ -1612,6 +1491,9 @@ async fn test_alias(sql_with_alias: &str, sql_no_alias: &str) -> Result<()> { let proto = to_substrait_plan(&df.into_optimized_plan()?, &ctx.state())?; let plan = from_substrait_plan(&ctx.state(), &proto).await?; + println!("{plan_with_alias}"); + println!("{plan}"); + let plan1str = format!("{plan_with_alias}"); let plan2str = format!("{plan}"); assert_eq!(plan1str, plan2str); @@ -1628,6 +1510,11 @@ async fn roundtrip_logical_plan_with_ctx( let plan2 = from_substrait_plan(&ctx.state(), &proto).await?; let plan2 = ctx.state().optimize(&plan2)?; + println!("{plan}"); + println!("{plan2}"); + + println!("{proto:?}"); + let plan1str = format!("{plan}"); let plan2str = format!("{plan2}"); assert_eq!(plan1str, plan2str); diff --git a/datafusion/substrait/tests/testdata/test_plans/multiple_joins.json b/datafusion/substrait/tests/testdata/test_plans/multiple_joins.json deleted file mode 100644 index e88cce648da7c..0000000000000 --- a/datafusion/substrait/tests/testdata/test_plans/multiple_joins.json +++ /dev/null @@ -1,536 +0,0 @@ -{ - "extensionUris": [{ - "extensionUriAnchor": 1, - "uri": "/functions_aggregate_generic.yaml" - }, { - "extensionUriAnchor": 2, - "uri": "/functions_comparison.yaml" - }], - "extensions": [{ - "extensionFunction": { - "extensionUriReference": 1, - "functionAnchor": 0, - "name": "count:" - } - }, { - "extensionFunction": { - "extensionUriReference": 2, - "functionAnchor": 1, - "name": "equal:any_any" - } - }], - "relations": [{ - "root": { - "input": { - "project": { - "common": { - "emit": { - "outputMapping": [8, 9, 10, 11] - } - }, - "input": { - "join": { - "common": { - "direct": { - } - }, - "left": { - "join": { - "common": { - "direct": { - } - }, - "left": { - "join": { - "common": { - "direct": { - } - }, - "left": { - "aggregate": { - "common": { - "direct": { - } - }, - "input": { - "read": { - "common": { - "direct": { - } - }, - "baseSchema": { - "names": ["id"], - "struct": { - "types": [{ - "i64": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }], - "typeVariationReference": 0, - "nullability": "NULLABILITY_REQUIRED" - } - }, - "virtualTable": { - "values": [{ - "fields": [{ - "i64": "1", - "nullable": true, - "typeVariationReference": 0 - }] - }, { - "fields": [{ - "i64": "2", - "nullable": true, - "typeVariationReference": 0 - }] - }] - } - } - }, - "groupings": [{ - "groupingExpressions": [{ - "selection": { - "directReference": { - "structField": { - "field": 0 - } - }, - "rootReference": { - } - } - }], - "expressionReferences": [] - }], - "measures": [{ - "measure": { - "functionReference": 0, - "args": [], - "sorts": [], - "phase": "AGGREGATION_PHASE_INITIAL_TO_RESULT", - "outputType": { - "i64": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_REQUIRED" - } - }, - "invocation": "AGGREGATION_INVOCATION_ALL", - "arguments": [], - "options": [] - } - }], - "groupingExpressions": [] - } - }, - "right": { - "aggregate": { - "common": { - "direct": { - } - }, - "input": { - "read": { - "common": { - "direct": { - } - }, - "baseSchema": { - "names": ["id", "category"], - "struct": { - "types": [{ - "i64": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }, { - "string": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }], - "typeVariationReference": 0, - "nullability": "NULLABILITY_REQUIRED" - } - }, - "virtualTable": { - "values": [{ - "fields": [{ - "i64": "1", - "nullable": true, - "typeVariationReference": 0 - }, { - "string": "info", - "nullable": true, - "typeVariationReference": 0 - }] - }, { - "fields": [{ - "i64": "2", - "nullable": true, - "typeVariationReference": 0 - }, { - "string": "low", - "nullable": true, - "typeVariationReference": 0 - }] - }] - } - } - }, - "groupings": [{ - "groupingExpressions": [{ - "selection": { - "directReference": { - "structField": { - "field": 0 - } - }, - "rootReference": { - } - } - }, { - "selection": { - "directReference": { - "structField": { - "field": 1 - } - }, - "rootReference": { - } - } - }], - "expressionReferences": [] - }], - "measures": [], - "groupingExpressions": [] - } - }, - "expression": { - "scalarFunction": { - "functionReference": 1, - "args": [], - "outputType": { - "bool": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }, - "arguments": [{ - "value": { - "selection": { - "directReference": { - "structField": { - "field": 0 - } - }, - "rootReference": { - } - } - } - }, { - "value": { - "selection": { - "directReference": { - "structField": { - "field": 2 - } - }, - "rootReference": { - } - } - } - }], - "options": [] - } - }, - "type": "JOIN_TYPE_LEFT" - } - }, - "right": { - "aggregate": { - "common": { - "direct": { - } - }, - "input": { - "read": { - "common": { - "direct": { - } - }, - "baseSchema": { - "names": ["id"], - "struct": { - "types": [{ - "i64": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }], - "typeVariationReference": 0, - "nullability": "NULLABILITY_REQUIRED" - } - }, - "virtualTable": { - "values": [{ - "fields": [{ - "i64": "1", - "nullable": true, - "typeVariationReference": 0 - }] - }, { - "fields": [{ - "i64": "2", - "nullable": true, - "typeVariationReference": 0 - }] - }] - } - } - }, - "groupings": [{ - "groupingExpressions": [{ - "selection": { - "directReference": { - "structField": { - "field": 0 - } - }, - "rootReference": { - } - } - }], - "expressionReferences": [] - }], - "measures": [{ - "measure": { - "functionReference": 0, - "args": [], - "sorts": [], - "phase": "AGGREGATION_PHASE_INITIAL_TO_RESULT", - "outputType": { - "i64": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_REQUIRED" - } - }, - "invocation": "AGGREGATION_INVOCATION_ALL", - "arguments": [], - "options": [] - } - }], - "groupingExpressions": [] - } - }, - "expression": { - "scalarFunction": { - "functionReference": 1, - "args": [], - "outputType": { - "bool": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }, - "arguments": [{ - "value": { - "selection": { - "directReference": { - "structField": { - "field": 0 - } - }, - "rootReference": { - } - } - } - }, { - "value": { - "selection": { - "directReference": { - "structField": { - "field": 4 - } - }, - "rootReference": { - } - } - } - }], - "options": [] - } - }, - "type": "JOIN_TYPE_LEFT" - } - }, - "right": { - "aggregate": { - "common": { - "direct": { - } - }, - "input": { - "read": { - "common": { - "direct": { - } - }, - "baseSchema": { - "names": ["id"], - "struct": { - "types": [{ - "i64": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }], - "typeVariationReference": 0, - "nullability": "NULLABILITY_REQUIRED" - } - }, - "virtualTable": { - "values": [{ - "fields": [{ - "i64": "1", - "nullable": true, - "typeVariationReference": 0 - }] - }, { - "fields": [{ - "i64": "2", - "nullable": true, - "typeVariationReference": 0 - }] - }] - } - } - }, - "groupings": [{ - "groupingExpressions": [{ - "selection": { - "directReference": { - "structField": { - "field": 0 - } - }, - "rootReference": { - } - } - }], - "expressionReferences": [] - }], - "measures": [{ - "measure": { - "functionReference": 0, - "args": [], - "sorts": [], - "phase": "AGGREGATION_PHASE_INITIAL_TO_RESULT", - "outputType": { - "i64": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_REQUIRED" - } - }, - "invocation": "AGGREGATION_INVOCATION_ALL", - "arguments": [], - "options": [] - } - }], - "groupingExpressions": [] - } - }, - "expression": { - "scalarFunction": { - "functionReference": 1, - "args": [], - "outputType": { - "bool": { - "typeVariationReference": 0, - "nullability": "NULLABILITY_NULLABLE" - } - }, - "arguments": [{ - "value": { - "selection": { - "directReference": { - "structField": { - "field": 0 - } - }, - "rootReference": { - } - } - } - }, { - "value": { - "selection": { - "directReference": { - "structField": { - "field": 6 - } - }, - "rootReference": { - } - } - } - }], - "options": [] - } - }, - "type": "JOIN_TYPE_LEFT" - } - }, - "expressions": [{ - "selection": { - "directReference": { - "structField": { - "field": 1 - } - }, - "rootReference": { - } - } - }, { - "selection": { - "directReference": { - "structField": { - "field": 3 - } - }, - "rootReference": { - } - } - }, { - "selection": { - "directReference": { - "structField": { - "field": 5 - } - }, - "rootReference": { - } - } - }, { - "selection": { - "directReference": { - "structField": { - "field": 7 - } - }, - "rootReference": { - } - } - }] - } - }, - "names": ["count_first", "category", "count_second", "count_third"] - } - }], - "expectedTypeUrls": [], - "version": { - "majorNumber": 0, - "minorNumber": 52, - "patchNumber": 0, - "gitHash": "" - } -} \ No newline at end of file diff --git a/datafusion/wasmtest/README.md b/datafusion/wasmtest/README.md index 70f4daef91034..8843eed697eca 100644 --- a/datafusion/wasmtest/README.md +++ b/datafusion/wasmtest/README.md @@ -71,6 +71,8 @@ wasm-pack test --headless --chrome wasm-pack test --headless --safari ``` +**Note:** In GitHub Actions we test the compilation with `wasm-build`, but we don't currently invoke `wasm-pack test`. This is because the headless mode is not yet working. Document of adding a GitHub Action job: https://rustwasm.github.io/docs/wasm-bindgen/wasm-bindgen-test/continuous-integration.html#github-actions. + To tweak timeout setting, use `WASM_BINDGEN_TEST_TIMEOUT` environment variable. E.g., `WASM_BINDGEN_TEST_TIMEOUT=300 wasm-pack test --firefox --headless`. ## Compatibility diff --git a/datafusion/wasmtest/datafusion-wasm-app/package-lock.json b/datafusion/wasmtest/datafusion-wasm-app/package-lock.json index c018e779fcbf3..65d8bdbb5e931 100644 --- a/datafusion/wasmtest/datafusion-wasm-app/package-lock.json +++ b/datafusion/wasmtest/datafusion-wasm-app/package-lock.json @@ -2007,11 +2007,10 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", "dev": true, - "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -5563,9 +5562,9 @@ } }, "http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", "dev": true, "requires": { "@types/http-proxy": "^1.17.8", diff --git a/datafusion/wasmtest/src/lib.rs b/datafusion/wasmtest/src/lib.rs index 0a7e546b4b18d..6c7be9056eb43 100644 --- a/datafusion/wasmtest/src/lib.rs +++ b/datafusion/wasmtest/src/lib.rs @@ -82,6 +82,7 @@ pub fn basic_parse() { #[cfg(test)] mod test { use super::*; + use datafusion::execution::options::ParquetReadOptions; use datafusion::{ arrow::{ array::{ArrayRef, Int32Array, RecordBatch, StringArray}, @@ -97,6 +98,7 @@ mod test { }; use datafusion_physical_plan::collect; use datafusion_sql::parser::DFParser; + use insta::assert_snapshot; use object_store::{memory::InMemory, path::Path, ObjectStore}; use url::Url; use wasm_bindgen_test::wasm_bindgen_test; @@ -238,24 +240,22 @@ mod test { let url = Url::parse("memory://").unwrap(); session_ctx.register_object_store(&url, Arc::new(store)); - session_ctx - .register_parquet("a", "memory:///a.parquet", Default::default()) + + let df = session_ctx + .read_parquet("memory:///", ParquetReadOptions::new()) .await .unwrap(); - let df = session_ctx.sql("SELECT * FROM a").await.unwrap(); - let result = df.collect().await.unwrap(); - assert_eq!( - batches_to_string(&result), - "+----+-------+\n\ - | id | value |\n\ - +----+-------+\n\ - | 1 | a |\n\ - | 2 | b |\n\ - | 3 | c |\n\ - +----+-------+" - ); + assert_snapshot!(batches_to_string(&result), @r" + +----+-------+ + | id | value | + +----+-------+ + | 1 | a | + | 2 | b | + | 3 | c | + +----+-------+ + "); } } diff --git a/datafusion/wasmtest/webdriver.json b/datafusion/wasmtest/webdriver.json deleted file mode 100644 index f59a2be9955f1..0000000000000 --- a/datafusion/wasmtest/webdriver.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "moz:firefoxOptions": { - "prefs": { - "media.navigator.streams.fake": true, - "media.navigator.permission.disabled": true - }, - "args": [] - }, - "goog:chromeOptions": { - "args": [ - "--use-fake-device-for-media-stream", - "--use-fake-ui-for-media-stream" - ] - } -} \ No newline at end of file diff --git a/dev/changelog/47.0.0.md b/dev/changelog/47.0.0.md deleted file mode 100644 index 64ca2e157a9e3..0000000000000 --- a/dev/changelog/47.0.0.md +++ /dev/null @@ -1,506 +0,0 @@ - - -# Apache DataFusion 47.0.0 Changelog - -This release consists of 364 commits from 94 contributors. See credits at the end of this changelog for more information. - -**Breaking changes:** - -- chore: cleanup deprecated API since `version <= 40` [#15027](https://github.com/apache/datafusion/pull/15027) (qazxcdswe123) -- fix: mark ScalarUDFImpl::invoke_batch as deprecated [#15049](https://github.com/apache/datafusion/pull/15049) (Blizzara) -- feat: support customize metadata in alias for dataframe api [#15120](https://github.com/apache/datafusion/pull/15120) (chenkovsky) -- Refactor: add `FileGroup` structure for `Vec` [#15379](https://github.com/apache/datafusion/pull/15379) (xudong963) -- Change default `EXPLAIN` format in `datafusion-cli` to `tree` format [#15427](https://github.com/apache/datafusion/pull/15427) (alamb) -- Support computing statistics for FileGroup [#15432](https://github.com/apache/datafusion/pull/15432) (xudong963) -- Remove redundant statistics from FileScanConfig [#14955](https://github.com/apache/datafusion/pull/14955) (Standing-Man) -- parquet reader: move pruning predicate creation from ParquetSource to ParquetOpener [#15561](https://github.com/apache/datafusion/pull/15561) (adriangb) -- feat: Add unique id for every memory consumer [#15613](https://github.com/apache/datafusion/pull/15613) (EmilyMatt) - -**Performance related:** - -- Fix sequential metadata fetching in ListingTable causing high latency [#14918](https://github.com/apache/datafusion/pull/14918) (geoffreyclaude) -- Implement GroupsAccumulator for min/max Duration [#15322](https://github.com/apache/datafusion/pull/15322) (shruti2522) -- [Minor] Remove/reorder logical plan rules [#15421](https://github.com/apache/datafusion/pull/15421) (Dandandan) -- Improve performance of `first_value` by implementing special `GroupsAccumulator` [#15266](https://github.com/apache/datafusion/pull/15266) (UBarney) -- perf: unwrap cast for comparing ints =/!= strings [#15110](https://github.com/apache/datafusion/pull/15110) (alan910127) -- Improve performance sort TPCH q3 with Utf8Vew ( Sort-preserving mergi… [#15447](https://github.com/apache/datafusion/pull/15447) (zhuqi-lucas) -- perf: Reuse row converter during sort [#15302](https://github.com/apache/datafusion/pull/15302) (2010YOUY01) -- perf: Add TopK benchmarks as variation over the `sort_tpch` benchmarks [#15560](https://github.com/apache/datafusion/pull/15560) (geoffreyclaude) -- Perf: remove `clone` on `uninitiated_partitions` in SortPreservingMergeStream [#15562](https://github.com/apache/datafusion/pull/15562) (rluvaton) -- Add short circuit evaluation for `AND` and `OR` [#15462](https://github.com/apache/datafusion/pull/15462) (acking-you) -- perf: Introduce sort prefix computation for early TopK exit optimization on partially sorted input (10x speedup on top10 bench) [#15563](https://github.com/apache/datafusion/pull/15563) (geoffreyclaude) -- Improve performance of `last_value` by implementing special `GroupsAccumulator` [#15542](https://github.com/apache/datafusion/pull/15542) (UBarney) -- Enhance: simplify `x=x` --> `x IS NOT NULL OR NULL` [#15589](https://github.com/apache/datafusion/pull/15589) (ding-young) - -**Implemented enhancements:** - -- feat: Add `tree` / pretty explain mode [#14677](https://github.com/apache/datafusion/pull/14677) (irenjj) -- feat: Add `array_max` function support [#14470](https://github.com/apache/datafusion/pull/14470) (erenavsarogullari) -- feat: implement tree explain for `ProjectionExec` [#15082](https://github.com/apache/datafusion/pull/15082) (Standing-Man) -- feat: support ApproxDistinct with utf8view [#15200](https://github.com/apache/datafusion/pull/15200) (zhuqi-lucas) -- feat: Attach `Diagnostic` to more than one column errors in scalar_subquery and in_subquery [#15143](https://github.com/apache/datafusion/pull/15143) (changsun20) -- feat: topk functionality for aggregates should support utf8view and largeutf8 [#15152](https://github.com/apache/datafusion/pull/15152) (zhuqi-lucas) -- feat: Native support utf8view for regex string operators [#15275](https://github.com/apache/datafusion/pull/15275) (zhuqi-lucas) -- feat: introduce `JoinSetTracer` trait for tracing context propagation in spawned tasks [#14547](https://github.com/apache/datafusion/pull/14547) (geoffreyclaude) -- feat: Support serde for JsonSource PhysicalPlan [#15311](https://github.com/apache/datafusion/pull/15311) (westhide) -- feat: Support serde for FileScanConfig `batch_size` [#15335](https://github.com/apache/datafusion/pull/15335) (westhide) -- feat: simplify regex wildcard pattern [#15299](https://github.com/apache/datafusion/pull/15299) (waynexia) -- feat: Add union_by_name, union_by_name_distinct to DataFrame api [#15489](https://github.com/apache/datafusion/pull/15489) (Omega359) -- feat: Add config `max_temp_directory_size` to limit max disk usage for spilling queries [#15520](https://github.com/apache/datafusion/pull/15520) (2010YOUY01) -- feat: Add tracing regression tests [#15673](https://github.com/apache/datafusion/pull/15673) (geoffreyclaude) - -**Fixed bugs:** - -- fix: External sort failing on an edge case [#15017](https://github.com/apache/datafusion/pull/15017) (2010YOUY01) -- fix: graceful NULL and type error handling in array functions [#14737](https://github.com/apache/datafusion/pull/14737) (alan910127) -- fix: Support datatype cast for insert api same as insert into sql [#15091](https://github.com/apache/datafusion/pull/15091) (zhuqi-lucas) -- fix: unparse for subqueryalias [#15068](https://github.com/apache/datafusion/pull/15068) (chenkovsky) -- fix: date_trunc bench broken by #15049 [#15169](https://github.com/apache/datafusion/pull/15169) (Blizzara) -- fix: compound_field_access doesn't identifier qualifier. [#15153](https://github.com/apache/datafusion/pull/15153) (chenkovsky) -- fix: unparsing left/ right semi/mark join [#15212](https://github.com/apache/datafusion/pull/15212) (chenkovsky) -- fix: handle duplicate WindowFunction expressions in Substrait consumer [#15211](https://github.com/apache/datafusion/pull/15211) (Blizzara) -- fix: write hive partitions for any int/uint/float [#15337](https://github.com/apache/datafusion/pull/15337) (christophermcdermott) -- fix: `core_expressions` feature flag broken, move `overlay` into `core` functions [#15217](https://github.com/apache/datafusion/pull/15217) (shruti2522) -- fix: Redundant files spilled during external sort + introduce `SpillManager` [#15355](https://github.com/apache/datafusion/pull/15355) (2010YOUY01) -- fix: typo of DropFunction [#15434](https://github.com/apache/datafusion/pull/15434) (chenkovsky) -- fix: Unconditionally wrap UNION BY NAME input nodes w/ `Projection` [#15242](https://github.com/apache/datafusion/pull/15242) (rkrishn7) -- fix: the average time for clickbench query compute should use new vec to make it compute for each query [#15472](https://github.com/apache/datafusion/pull/15472) (zhuqi-lucas) -- fix: Assertion fail in external sort [#15469](https://github.com/apache/datafusion/pull/15469) (2010YOUY01) -- fix: aggregation corner case [#15457](https://github.com/apache/datafusion/pull/15457) (chenkovsky) -- fix: update group by columns for merge phase after spill [#15531](https://github.com/apache/datafusion/pull/15531) (rluvaton) -- fix: Queries similar to `count-bug` produce incorrect results [#15281](https://github.com/apache/datafusion/pull/15281) (suibianwanwank) -- fix: ffi aggregation [#15576](https://github.com/apache/datafusion/pull/15576) (chenkovsky) -- fix: nested window function [#15033](https://github.com/apache/datafusion/pull/15033) (chenkovsky) -- fix: dictionary encoded column to partition column casting bug [#15652](https://github.com/apache/datafusion/pull/15652) (haruband) -- fix: recursion protection for physical plan node [#15600](https://github.com/apache/datafusion/pull/15600) (chenkovsky) -- fix: add map coercion for binary ops [#15551](https://github.com/apache/datafusion/pull/15551) (alexwilcoxson-rel) -- fix: Rewrite `date_trunc` and `from_unixtime` for the SQLite unparser [#15630](https://github.com/apache/datafusion/pull/15630) (peasee) -- fix(substrait): fix regressed edge case in renaming inner struct fields [#15634](https://github.com/apache/datafusion/pull/15634) (Blizzara) -- fix: normalize window ident [#15639](https://github.com/apache/datafusion/pull/15639) (chenkovsky) -- fix: unparse join without projection [#15693](https://github.com/apache/datafusion/pull/15693) (chenkovsky) - -**Documentation updates:** - -- MINOR fix(docs): set the proper link for dev-env setup in contrib guide [#14960](https://github.com/apache/datafusion/pull/14960) (clflushopt) -- Add Upgrade Guide for DataFusion 46.0.0 [#14891](https://github.com/apache/datafusion/pull/14891) (alamb) -- Improve `SessionStateBuilder::new` documentation [#14980](https://github.com/apache/datafusion/pull/14980) (alamb) -- Minor: Replace Star and Fork buttons in docs with static versions [#14988](https://github.com/apache/datafusion/pull/14988) (amoeba) -- Fix documentation warnings and error if anymore occur [#14952](https://github.com/apache/datafusion/pull/14952) (AmosAidoo) -- docs: Improve docs on AggregateFunctionExpr construction [#15044](https://github.com/apache/datafusion/pull/15044) (ctsk) -- Minor: More comment to aggregation fuzzer [#15048](https://github.com/apache/datafusion/pull/15048) (2010YOUY01) -- Improve benchmark documentation [#15054](https://github.com/apache/datafusion/pull/15054) (carols10cents) -- doc: update RecordBatchReceiverStreamBuilder::spawn_blocking task behaviour [#14995](https://github.com/apache/datafusion/pull/14995) (shruti2522) -- doc: Correct benchmark command [#15094](https://github.com/apache/datafusion/pull/15094) (qazxcdswe123) -- Add `insta` / snapshot testing to CLI & set up AWS mock [#13672](https://github.com/apache/datafusion/pull/13672) (blaginin) -- Config: Add support default sql varchar to view types [#15104](https://github.com/apache/datafusion/pull/15104) (zhuqi-lucas) -- Support `EXPLAIN ... FORMAT ...` [#15166](https://github.com/apache/datafusion/pull/15166) (alamb) -- Update version to 46.0.1, add CHANGELOG (#15243) [#15244](https://github.com/apache/datafusion/pull/15244) (xudong963) -- docs: update documentation for Final GroupBy in accumulator.rs [#15279](https://github.com/apache/datafusion/pull/15279) (qazxcdswe123) -- minor: fix `data/sqlite` link [#15286](https://github.com/apache/datafusion/pull/15286) (sdht0) -- Add upgrade notes for array signatures [#15237](https://github.com/apache/datafusion/pull/15237) (jkosh44) -- Add doc for the `statistics_from_parquet_meta_calc method` [#15330](https://github.com/apache/datafusion/pull/15330) (xudong963) -- added explaination for Schema and DFSchema to documentation [#15329](https://github.com/apache/datafusion/pull/15329) (Jiashu-Hu) -- Documentation: Plan custom expressions [#15353](https://github.com/apache/datafusion/pull/15353) (Jiashu-Hu) -- Update concepts-readings-events.md [#15440](https://github.com/apache/datafusion/pull/15440) (berkaysynnada) -- Add support for DISTINCT + ORDER BY in `ARRAY_AGG` [#14413](https://github.com/apache/datafusion/pull/14413) (gabotechs) -- Update the copyright year [#15453](https://github.com/apache/datafusion/pull/15453) (omkenge) -- Docs: Formatting and Added Extra resources [#15450](https://github.com/apache/datafusion/pull/15450) (2SpaceMasterRace) -- Add documentation for `Run extended tests` command [#15463](https://github.com/apache/datafusion/pull/15463) (alamb) -- bench: Document how to use cross platform Samply profiler [#15481](https://github.com/apache/datafusion/pull/15481) (comphead) -- Update user guide to note decimal is not experimental anymore [#15515](https://github.com/apache/datafusion/pull/15515) (Jiashu-Hu) -- datafusion-cli: document reading partitioned parquet [#15505](https://github.com/apache/datafusion/pull/15505) (marvelshan) -- Update concepts-readings-events.md [#15541](https://github.com/apache/datafusion/pull/15541) (oznur-synnada) -- Add documentation example for `AggregateExprBuilder` [#15504](https://github.com/apache/datafusion/pull/15504) (Shreyaskr1409) -- Docs : Added Sql examples for window Functions : `nth_val` , etc [#15555](https://github.com/apache/datafusion/pull/15555) (Adez017) -- Add disk usage limit configuration to datafusion-cli [#15586](https://github.com/apache/datafusion/pull/15586) (jsai28) -- Bug fix : fix the bug in docs in 'cum_dist()' Example [#15618](https://github.com/apache/datafusion/pull/15618) (Adez017) -- Make tree the Default EXPLAIN Format and Reorder Documentation Sections [#15706](https://github.com/apache/datafusion/pull/15706) (kosiew) -- Add coerce int96 option for Parquet to support different TimeUnits, test int96_from_spark.parquet from parquet-testing [#15537](https://github.com/apache/datafusion/pull/15537) (mbutrovich) -- STRING_AGG missing functionality [#14412](https://github.com/apache/datafusion/pull/14412) (gabotechs) -- doc : update RepartitionExec display tree [#15710](https://github.com/apache/datafusion/pull/15710) (getChan) -- Update version to 47.0.0, add CHANGELOG [#15731](https://github.com/apache/datafusion/pull/15731) (xudong963) - -**Other:** - -- Improve documentation for `DataSourceExec`, `FileScanConfig`, `DataSource` etc [#14941](https://github.com/apache/datafusion/pull/14941) (alamb) -- Do not swap with projection when file is partitioned [#14956](https://github.com/apache/datafusion/pull/14956) (blaginin) -- Minor: Add more projection pushdown tests, clarify comments [#14963](https://github.com/apache/datafusion/pull/14963) (alamb) -- Update labeler components [#14942](https://github.com/apache/datafusion/pull/14942) (alamb) -- Deprecate `Expr::Wildcard` [#14959](https://github.com/apache/datafusion/pull/14959) (linhr) -- Minor: use FileScanConfig builder API in some tests [#14938](https://github.com/apache/datafusion/pull/14938) (alamb) -- Minor: improve documentation of `AggregateMode` [#14946](https://github.com/apache/datafusion/pull/14946) (alamb) -- chore(deps): bump thiserror from 2.0.11 to 2.0.12 [#14971](https://github.com/apache/datafusion/pull/14971) (dependabot[bot]) -- chore(deps): bump pyo3 from 0.23.4 to 0.23.5 [#14972](https://github.com/apache/datafusion/pull/14972) (dependabot[bot]) -- chore(deps): bump async-trait from 0.1.86 to 0.1.87 [#14973](https://github.com/apache/datafusion/pull/14973) (dependabot[bot]) -- Fix verification script and extended tests due to `rustup` changes [#14990](https://github.com/apache/datafusion/pull/14990) (alamb) -- Split out avro, parquet, json and csv into individual crates [#14951](https://github.com/apache/datafusion/pull/14951) (AdamGS) -- Minor: Add `backtrace` feature in datafusion-cli [#14997](https://github.com/apache/datafusion/pull/14997) (2010YOUY01) -- chore: Update `SessionStateBuilder::with_default_features` does not replace existing features [#14935](https://github.com/apache/datafusion/pull/14935) (irenjj) -- Make `create_ordering` pub and add doc for it [#14996](https://github.com/apache/datafusion/pull/14996) (xudong963) -- Simplify Between expression to Eq [#14994](https://github.com/apache/datafusion/pull/14994) (jayzhan211) -- Count wildcard alias [#14927](https://github.com/apache/datafusion/pull/14927) (jayzhan211) -- replace TypeSignature::String with TypeSignature::Coercible [#14917](https://github.com/apache/datafusion/pull/14917) (zjregee) -- Minor: Add indentation to EnforceDistribution test plans. [#15007](https://github.com/apache/datafusion/pull/15007) (wiedld) -- Minor: add method `SessionStateBuilder::new_with_default_features()` [#14998](https://github.com/apache/datafusion/pull/14998) (shruti2522) -- Implement `tree` explain for FilterExec [#15001](https://github.com/apache/datafusion/pull/15001) (alamb) -- Unparser add `AtArrow` and `ArrowAt` conversion to BinaryOperator [#14968](https://github.com/apache/datafusion/pull/14968) (cetra3) -- Add dependency checks to verify-release-candidate script [#15009](https://github.com/apache/datafusion/pull/15009) (waynexia) -- Fix: to_char Function Now Correctly Handles DATE Values in DataFusion [#14970](https://github.com/apache/datafusion/pull/14970) (kosiew) -- Make Substrait Schema Structs always non-nullable [#15011](https://github.com/apache/datafusion/pull/15011) (amoeba) -- Adjust physical optimizer rule order, put `ProjectionPushdown` at last [#15040](https://github.com/apache/datafusion/pull/15040) (xudong963) -- Move `UnwrapCastInComparison` into `Simplifier` [#15012](https://github.com/apache/datafusion/pull/15012) (jayzhan211) -- chore(deps): bump aws-config from 1.5.17 to 1.5.18 [#15041](https://github.com/apache/datafusion/pull/15041) (dependabot[bot]) -- chore(deps): bump bytes from 1.10.0 to 1.10.1 [#15042](https://github.com/apache/datafusion/pull/15042) (dependabot[bot]) -- Minor: Deprecate `ScalarValue::raw_data` [#15016](https://github.com/apache/datafusion/pull/15016) (qazxcdswe123) -- Implement tree explain for `DataSourceExec` [#15029](https://github.com/apache/datafusion/pull/15029) (alamb) -- Refactor test suite in EnforceDistribution, to use standard test config. [#15010](https://github.com/apache/datafusion/pull/15010) (wiedld) -- Update ring to v0.17.13 [#15063](https://github.com/apache/datafusion/pull/15063) (alamb) -- Remove deprecated function `OptimizerRule::try_optimize` [#15051](https://github.com/apache/datafusion/pull/15051) (qazxcdswe123) -- Minor: fix CI to make the sqllogic testing result consistent [#15059](https://github.com/apache/datafusion/pull/15059) (zhuqi-lucas) -- Refactor SortPushdown using the standard top-down visitor and using `EquivalenceProperties` [#14821](https://github.com/apache/datafusion/pull/14821) (wiedld) -- Improve explain tree formatting for longer lines / word wrap [#15031](https://github.com/apache/datafusion/pull/15031) (irenjj) -- chore(deps): bump sqllogictest from 0.27.2 to 0.28.0 [#15060](https://github.com/apache/datafusion/pull/15060) (dependabot[bot]) -- chore(deps): bump async-compression from 0.4.18 to 0.4.19 [#15061](https://github.com/apache/datafusion/pull/15061) (dependabot[bot]) -- Handle columns in with_new_exprs with a Join [#15055](https://github.com/apache/datafusion/pull/15055) (delamarch3) -- Minor: Improve documentation of `need_handle_count_bug` [#15050](https://github.com/apache/datafusion/pull/15050) (suibianwanwank) -- Implement `tree` explain for `HashJoinExec` [#15079](https://github.com/apache/datafusion/pull/15079) (irenjj) -- Implement tree explain for PartialSortExec [#15066](https://github.com/apache/datafusion/pull/15066) (irenjj) -- Implement `tree` explain for `SortExec` [#15077](https://github.com/apache/datafusion/pull/15077) (irenjj) -- Minor: final `46.0.0` release tweaks: changelog + instructions [#15073](https://github.com/apache/datafusion/pull/15073) (alamb) -- Implement tree explain for `NestedLoopJoinExec`, `CrossJoinExec`, `So… [#15081](https://github.com/apache/datafusion/pull/15081) (irenjj) -- Implement `tree` explain for `BoundedWindowAggExec` and `WindowAggExec` [#15084](https://github.com/apache/datafusion/pull/15084) (irenjj) -- implement tree rendering for StreamingTableExec [#15085](https://github.com/apache/datafusion/pull/15085) (Standing-Man) -- chore(deps): bump semver from 1.0.25 to 1.0.26 [#15116](https://github.com/apache/datafusion/pull/15116) (dependabot[bot]) -- chore(deps): bump clap from 4.5.30 to 4.5.31 [#15115](https://github.com/apache/datafusion/pull/15115) (dependabot[bot]) -- implement tree explain for GlobalLimitExec [#15100](https://github.com/apache/datafusion/pull/15100) (zjregee) -- Minor: Cleanup useless/duplicated code in gen tools [#15113](https://github.com/apache/datafusion/pull/15113) (lewiszlw) -- Refactor EnforceDistribution test cases to demonstrate dependencies across optimizer runs. [#15074](https://github.com/apache/datafusion/pull/15074) (wiedld) -- Improve parsing `extra_info` in tree explain [#15125](https://github.com/apache/datafusion/pull/15125) (irenjj) -- Add tests for simplification and coercion of `SessionContext::create_physical_expr` [#15034](https://github.com/apache/datafusion/pull/15034) (alamb) -- Minor: Fix invalid query in test [#15131](https://github.com/apache/datafusion/pull/15131) (alamb) -- Do not display logical_plan win explain `tree` mode 🧹 [#15132](https://github.com/apache/datafusion/pull/15132) (alamb) -- Substrait support for propagating TableScan.filters to Substrait ReadRel.filter [#14194](https://github.com/apache/datafusion/pull/14194) (jamxia155) -- Fix wasm32 build on version 46 [#15102](https://github.com/apache/datafusion/pull/15102) (XiangpengHao) -- Fix broken `serde` feature [#15124](https://github.com/apache/datafusion/pull/15124) (vadimpiven) -- chore(deps): bump tempfile from 3.17.1 to 3.18.0 [#15146](https://github.com/apache/datafusion/pull/15146) (dependabot[bot]) -- chore(deps): bump syn from 2.0.98 to 2.0.100 [#15147](https://github.com/apache/datafusion/pull/15147) (dependabot[bot]) -- Implement tree explain for AggregateExec [#15103](https://github.com/apache/datafusion/pull/15103) (zebsme) -- Implement tree explain for `RepartitionExec` and `WorkTableExec` [#15137](https://github.com/apache/datafusion/pull/15137) (Standing-Man) -- Expand wildcard to actual expressions in `prepare_select_exprs` [#15090](https://github.com/apache/datafusion/pull/15090) (jayzhan211) -- fixed PushDownFilter bug [15047] [#15142](https://github.com/apache/datafusion/pull/15142) (Jiashu-Hu) -- Bump `env_logger` from `0.11.6` to `0.11.7` [#15148](https://github.com/apache/datafusion/pull/15148) (mbrobbel) -- Minor: fix extend sqllogical consistent with main test [#15145](https://github.com/apache/datafusion/pull/15145) (zhuqi-lucas) -- Implement tree rendering for `SortPreservingMergeExec` [#15140](https://github.com/apache/datafusion/pull/15140) (Standing-Man) -- Remove expand wildcard rule [#15170](https://github.com/apache/datafusion/pull/15170) (jayzhan211) -- chore: remove ScalarUDFImpl::return_type_from_exprs [#15130](https://github.com/apache/datafusion/pull/15130) (Blizzara) -- chore(deps): bump libc from 0.2.170 to 0.2.171 [#15176](https://github.com/apache/datafusion/pull/15176) (dependabot[bot]) -- chore(deps): bump serde_json from 1.0.139 to 1.0.140 [#15175](https://github.com/apache/datafusion/pull/15175) (dependabot[bot]) -- chore(deps): bump substrait from 0.53.2 to 0.54.0 [#15043](https://github.com/apache/datafusion/pull/15043) (dependabot[bot]) -- Minor: split EXPLAIN and ANALYZE planning into different functions [#15188](https://github.com/apache/datafusion/pull/15188) (alamb) -- Implement `tree` explain for `JsonSink` [#15185](https://github.com/apache/datafusion/pull/15185) (irenjj) -- Split out `datafusion-substrait` and `datafusion-proto` CI feature checks, increase coverage [#15156](https://github.com/apache/datafusion/pull/15156) (alamb) -- Remove unused wildcard expanding methods [#15180](https://github.com/apache/datafusion/pull/15180) (goldmedal) -- #15108 issue: "Non Panic Task error" is not an internal error [#15109](https://github.com/apache/datafusion/pull/15109) (Satyam018) -- Implement tree explain for LazyMemoryExec [#15187](https://github.com/apache/datafusion/pull/15187) (zebsme) -- implement tree explain for CoalesceBatchesExec [#15194](https://github.com/apache/datafusion/pull/15194) (Standing-Man) -- Implement `tree` explain for `CsvSink` [#15204](https://github.com/apache/datafusion/pull/15204) (irenjj) -- chore(deps): bump blake3 from 1.6.0 to 1.6.1 [#15198](https://github.com/apache/datafusion/pull/15198) (dependabot[bot]) -- chore(deps): bump clap from 4.5.31 to 4.5.32 [#15199](https://github.com/apache/datafusion/pull/15199) (dependabot[bot]) -- chore(deps): bump serde from 1.0.218 to 1.0.219 [#15197](https://github.com/apache/datafusion/pull/15197) (dependabot[bot]) -- Fix datafusion proto crate `json` feature [#15172](https://github.com/apache/datafusion/pull/15172) (Owen-CH-Leung) -- Add blog link to `EquivalenceProperties` docs [#15215](https://github.com/apache/datafusion/pull/15215) (alamb) -- Minor: split datafusion-cli testing into its own CI job [#15075](https://github.com/apache/datafusion/pull/15075) (alamb) -- Implement tree explain for InterleaveExec [#15219](https://github.com/apache/datafusion/pull/15219) (zebsme) -- Move catalog_common out of core [#15193](https://github.com/apache/datafusion/pull/15193) (logan-keede) -- chore(deps): bump tokio-util from 0.7.13 to 0.7.14 [#15223](https://github.com/apache/datafusion/pull/15223) (dependabot[bot]) -- chore(deps): bump aws-config from 1.5.18 to 1.6.0 [#15222](https://github.com/apache/datafusion/pull/15222) (dependabot[bot]) -- chore(deps): bump bzip2 from 0.5.1 to 0.5.2 [#15221](https://github.com/apache/datafusion/pull/15221) (dependabot[bot]) -- Document guidelines for physical operator yielding [#15030](https://github.com/apache/datafusion/pull/15030) (carols10cents) -- Implement `tree` explain for `ArrowFileSink`, fix original URL [#15206](https://github.com/apache/datafusion/pull/15206) (irenjj) -- Implement tree explain for `LocalLimitExec` [#15232](https://github.com/apache/datafusion/pull/15232) (shruti2522) -- Use insta for `DataFrame` tests [#15165](https://github.com/apache/datafusion/pull/15165) (blaginin) -- Re-enable github discussion [#15241](https://github.com/apache/datafusion/pull/15241) (2010YOUY01) -- Minor: exclude datafusion-cli testing for mac [#15240](https://github.com/apache/datafusion/pull/15240) (zhuqi-lucas) -- Implement tree explain for CoalescePartitionsExec [#15225](https://github.com/apache/datafusion/pull/15225) (Shreyaskr1409) -- Enable `used_underscore_binding` clippy lint [#15189](https://github.com/apache/datafusion/pull/15189) (Shreyaskr1409) -- Simpler to see expressions in explain `tree` mode [#15163](https://github.com/apache/datafusion/pull/15163) (irenjj) -- Fix invalid schema for unions in ViewTables [#15135](https://github.com/apache/datafusion/pull/15135) (Friede80) -- Make `ListingTableUrl::try_new` public [#15250](https://github.com/apache/datafusion/pull/15250) (linhr) -- Fix wildcard dataframe case [#15230](https://github.com/apache/datafusion/pull/15230) (jayzhan211) -- Simplify the printing of all plans containing `expr` in `tree` mode [#15249](https://github.com/apache/datafusion/pull/15249) (irenjj) -- Support utf8view datatype for window [#15257](https://github.com/apache/datafusion/pull/15257) (zhuqi-lucas) -- chore: remove deprecated variants of UDF's invoke (invoke, invoke_no_args, invoke_batch) [#15123](https://github.com/apache/datafusion/pull/15123) (Blizzara) -- Improve feature flag CI coverage `datafusion` and `datafusion-functions` [#15203](https://github.com/apache/datafusion/pull/15203) (alamb) -- Add debug logging for default catalog overwrite in SessionState build [#15251](https://github.com/apache/datafusion/pull/15251) (byte-sourcerer) -- Implement tree explain for PlaceholderRowExec [#15270](https://github.com/apache/datafusion/pull/15270) (zebsme) -- Implement tree explain for UnionExec [#15278](https://github.com/apache/datafusion/pull/15278) (zebsme) -- Migrate dataframe tests to `insta` [#15262](https://github.com/apache/datafusion/pull/15262) (jsai28) -- Minor: consistently apply `clippy::clone_on_ref_ptr` in all crates [#15284](https://github.com/apache/datafusion/pull/15284) (alamb) -- chore(deps): bump async-trait from 0.1.87 to 0.1.88 [#15294](https://github.com/apache/datafusion/pull/15294) (dependabot[bot]) -- chore(deps): bump uuid from 1.15.1 to 1.16.0 [#15292](https://github.com/apache/datafusion/pull/15292) (dependabot[bot]) -- Add CatalogProvider and SchemaProvider to FFI Crate [#15280](https://github.com/apache/datafusion/pull/15280) (timsaucer) -- Refactor file schema type coercions [#15268](https://github.com/apache/datafusion/pull/15268) (xudong963) -- chore(deps): bump rust_decimal from 1.36.0 to 1.37.0 [#15293](https://github.com/apache/datafusion/pull/15293) (dependabot[bot]) -- chore: Attach Diagnostic to "incompatible type in unary expression" error [#15209](https://github.com/apache/datafusion/pull/15209) (onlyjackfrost) -- Support logic optimize rule to pass the case that Utf8view datatype combined with Utf8 datatype [#15239](https://github.com/apache/datafusion/pull/15239) (zhuqi-lucas) -- Migrate user_defined tests to insta [#15255](https://github.com/apache/datafusion/pull/15255) (shruti2522) -- Remove inline table scan analyzer rule [#15201](https://github.com/apache/datafusion/pull/15201) (jayzhan211) -- CI Red: Fix union in view table test [#15300](https://github.com/apache/datafusion/pull/15300) (jayzhan211) -- refactor: Move view and stream from `datasource` to `catalog`, deprecate `View::try_new` [#15260](https://github.com/apache/datafusion/pull/15260) (logan-keede) -- chore(deps): bump substrait from 0.54.0 to 0.55.0 [#15305](https://github.com/apache/datafusion/pull/15305) (dependabot[bot]) -- chore(deps): bump half from 2.4.1 to 2.5.0 [#15303](https://github.com/apache/datafusion/pull/15303) (dependabot[bot]) -- chore(deps): bump mimalloc from 0.1.43 to 0.1.44 [#15304](https://github.com/apache/datafusion/pull/15304) (dependabot[bot]) -- Fix predicate pushdown for custom SchemaAdapters [#15263](https://github.com/apache/datafusion/pull/15263) (adriangb) -- Fix extended tests by restore datafusion-testing submodule [#15318](https://github.com/apache/datafusion/pull/15318) (alamb) -- Support Duration in min/max agg functions [#15310](https://github.com/apache/datafusion/pull/15310) (svranesevic) -- Migrate tests to insta [#15288](https://github.com/apache/datafusion/pull/15288) (jsai28) -- chore(deps): bump quote from 1.0.38 to 1.0.40 [#15332](https://github.com/apache/datafusion/pull/15332) (dependabot[bot]) -- chore(deps): bump blake3 from 1.6.1 to 1.7.0 [#15331](https://github.com/apache/datafusion/pull/15331) (dependabot[bot]) -- Simplify display format of `AggregateFunctionExpr`, add `Expr::sql_name` [#15253](https://github.com/apache/datafusion/pull/15253) (irenjj) -- chore(deps): bump indexmap from 2.7.1 to 2.8.0 [#15333](https://github.com/apache/datafusion/pull/15333) (dependabot[bot]) -- chore(deps): bump tokio from 1.43.0 to 1.44.1 [#15347](https://github.com/apache/datafusion/pull/15347) (dependabot[bot]) -- chore(deps): bump tempfile from 3.18.0 to 3.19.1 [#15346](https://github.com/apache/datafusion/pull/15346) (dependabot[bot]) -- Minor: Keep debug symbols for `release-nonlto` build [#15350](https://github.com/apache/datafusion/pull/15350) (2010YOUY01) -- Use `any` instead of `for_each` [#15289](https://github.com/apache/datafusion/pull/15289) (xudong963) -- refactor: move `CteWorkTable`, `default_table_source` a bunch of files out of core [#15316](https://github.com/apache/datafusion/pull/15316) (logan-keede) -- Fix empty aggregation function count() in Substrait [#15345](https://github.com/apache/datafusion/pull/15345) (gabotechs) -- Improved error for expand wildcard rule [#15287](https://github.com/apache/datafusion/pull/15287) (Jiashu-Hu) -- Added tests with are writing into parquet files in memory for issue #… [#15325](https://github.com/apache/datafusion/pull/15325) (pranavJibhakate) -- Migrate physical plan tests to `insta` (Part-1) [#15313](https://github.com/apache/datafusion/pull/15313) (Shreyaskr1409) -- Fix array_has_all and array_has_any with empty array [#15039](https://github.com/apache/datafusion/pull/15039) (LuQQiu) -- Update datafusion-testing pin to fix extended tests [#15368](https://github.com/apache/datafusion/pull/15368) (alamb) -- chore(deps): Update sqlparser to 0.55.0 [#15183](https://github.com/apache/datafusion/pull/15183) (PokIsemaine) -- Only unnest source for `EmptyRelation` [#15159](https://github.com/apache/datafusion/pull/15159) (blaginin) -- chore(deps): bump rust_decimal from 1.37.0 to 1.37.1 [#15378](https://github.com/apache/datafusion/pull/15378) (dependabot[bot]) -- chore(deps): bump chrono-tz from 0.10.1 to 0.10.2 [#15377](https://github.com/apache/datafusion/pull/15377) (dependabot[bot]) -- remove the duplicate test for unparser [#15385](https://github.com/apache/datafusion/pull/15385) (goldmedal) -- Minor: add average time for clickbench benchmark query [#15381](https://github.com/apache/datafusion/pull/15381) (zhuqi-lucas) -- include some BinaryOperator from sqlparser [#15327](https://github.com/apache/datafusion/pull/15327) (waynexia) -- Add "end to end parquet reading test" for WASM [#15362](https://github.com/apache/datafusion/pull/15362) (jsai28) -- Migrate physical plan tests to `insta` (Part-2) [#15364](https://github.com/apache/datafusion/pull/15364) (Shreyaskr1409) -- Migrate physical plan tests to `insta` (Part-3 / Final) [#15399](https://github.com/apache/datafusion/pull/15399) (Shreyaskr1409) -- Restore lazy evaluation of fallible CASE [#15390](https://github.com/apache/datafusion/pull/15390) (findepi) -- chore(deps): bump log from 0.4.26 to 0.4.27 [#15410](https://github.com/apache/datafusion/pull/15410) (dependabot[bot]) -- chore(deps): bump chrono-tz from 0.10.2 to 0.10.3 [#15412](https://github.com/apache/datafusion/pull/15412) (dependabot[bot]) -- Perf: Support Utf8View datatype single column comparisons for SortPreservingMergeStream [#15348](https://github.com/apache/datafusion/pull/15348) (zhuqi-lucas) -- Enforce JOIN plan to require condition [#15334](https://github.com/apache/datafusion/pull/15334) (goldmedal) -- Fix type coercion for unsigned and signed integers (`Int64` vs `UInt64`, etc) [#15341](https://github.com/apache/datafusion/pull/15341) (Omega359) -- simplify `array_has` UDF to `InList` expr when haystack is constant [#15354](https://github.com/apache/datafusion/pull/15354) (davidhewitt) -- Move `DataSink` to `datasource` and add session crate [#15371](https://github.com/apache/datafusion/pull/15371) (jayzhan-synnada) -- refactor: SpillManager into a separate file [#15407](https://github.com/apache/datafusion/pull/15407) (Weijun-H) -- Always use `PartitionMode::Auto` in planner [#15339](https://github.com/apache/datafusion/pull/15339) (Dandandan) -- Fix link to Volcano paper [#15437](https://github.com/apache/datafusion/pull/15437) (JackKelly) -- minor: Add new crates to labeler [#15426](https://github.com/apache/datafusion/pull/15426) (logan-keede) -- refactor: Use SpillManager for all spilling scenarios [#15405](https://github.com/apache/datafusion/pull/15405) (2010YOUY01) -- refactor(hash_join): Move JoinHashMap to separate mod [#15419](https://github.com/apache/datafusion/pull/15419) (ctsk) -- Migrate datasource tests to insta [#15258](https://github.com/apache/datafusion/pull/15258) (shruti2522) -- Add `downcast_to_source` method for `DataSourceExec` [#15416](https://github.com/apache/datafusion/pull/15416) (xudong963) -- refactor: use TypeSignature::Coercible for crypto functions [#14826](https://github.com/apache/datafusion/pull/14826) (Chen-Yuan-Lai) -- Minor: fix doc for `FileGroupPartitioner` [#15448](https://github.com/apache/datafusion/pull/15448) (xudong963) -- chore(deps): bump clap from 4.5.32 to 4.5.34 [#15452](https://github.com/apache/datafusion/pull/15452) (dependabot[bot]) -- Fix roundtrip bug with empty projection in DataSourceExec [#15449](https://github.com/apache/datafusion/pull/15449) (XiangpengHao) -- Triggering extended tests through PR comment: `Run extended tests` [#15101](https://github.com/apache/datafusion/pull/15101) (danila-b) -- Use `equals_datatype` to compare type when type coercion [#15366](https://github.com/apache/datafusion/pull/15366) (goldmedal) -- Fix no effect metrics bug in ParquetSource [#15460](https://github.com/apache/datafusion/pull/15460) (XiangpengHao) -- chore(deps): bump aws-config from 1.6.0 to 1.6.1 [#15470](https://github.com/apache/datafusion/pull/15470) (dependabot[bot]) -- minor: Allow to run TPCH bench for a specific query [#15467](https://github.com/apache/datafusion/pull/15467) (comphead) -- Migrate subtraits tests to insta, part1 [#15444](https://github.com/apache/datafusion/pull/15444) (qstommyshu) -- Add `FileScanConfigBuilder` [#15352](https://github.com/apache/datafusion/pull/15352) (blaginin) -- Update ClickBench queries to avoid to_timestamp_seconds [#15475](https://github.com/apache/datafusion/pull/15475) (acking-you) -- Remove CoalescePartitions insertion from HashJoinExec [#15476](https://github.com/apache/datafusion/pull/15476) (ctsk) -- Migrate-substrait-tests-to-insta, part2 [#15480](https://github.com/apache/datafusion/pull/15480) (qstommyshu) -- Revert #15476 to fix the datafusion-examples CI fail [#15496](https://github.com/apache/datafusion/pull/15496) (goldmedal) -- Migrate datafusion/sql tests to insta, part1 [#15497](https://github.com/apache/datafusion/pull/15497) (qstommyshu) -- Allow type coersion of zero input arrays to nullary [#15487](https://github.com/apache/datafusion/pull/15487) (timsaucer) -- Decimal type support for `to_timestamp` [#15486](https://github.com/apache/datafusion/pull/15486) (jatin510) -- refactor: Move `Memtable` to catalog [#15459](https://github.com/apache/datafusion/pull/15459) (logan-keede) -- Migrate optimizer tests to insta [#15446](https://github.com/apache/datafusion/pull/15446) (qstommyshu) -- FIX : some benchmarks are failing [#15367](https://github.com/apache/datafusion/pull/15367) (getChan) -- Add query to extended clickbench suite for "complex filter" [#15500](https://github.com/apache/datafusion/pull/15500) (acking-you) -- Extract tokio runtime creation from hot loop in benchmarks [#15508](https://github.com/apache/datafusion/pull/15508) (Omega359) -- chore(deps): bump blake3 from 1.7.0 to 1.8.0 [#15502](https://github.com/apache/datafusion/pull/15502) (dependabot[bot]) -- Minor: clone and debug for FileSinkConfig [#15516](https://github.com/apache/datafusion/pull/15516) (jayzhan211) -- use state machine to refactor the `get_files_with_limit` method [#15521](https://github.com/apache/datafusion/pull/15521) (xudong963) -- Migrate `datafusion/sql` tests to insta, part2 [#15499](https://github.com/apache/datafusion/pull/15499) (qstommyshu) -- Disable sccache action to fix gh cache issue [#15536](https://github.com/apache/datafusion/pull/15536) (Omega359) -- refactor: Cleanup unused `fetch` field inside `ExternalSorter` [#15525](https://github.com/apache/datafusion/pull/15525) (2010YOUY01) -- Fix duplicate unqualified Field name (schema error) on join queries [#15438](https://github.com/apache/datafusion/pull/15438) (LiaCastaneda) -- Add utf8view benchmark for aggregate topk [#15518](https://github.com/apache/datafusion/pull/15518) (zhuqi-lucas) -- ArraySort: support structs [#15527](https://github.com/apache/datafusion/pull/15527) (cht42) -- Migrate datafusion/sql tests to insta, part3 [#15533](https://github.com/apache/datafusion/pull/15533) (qstommyshu) -- Migrate datafusion/sql tests to insta, part4 [#15548](https://github.com/apache/datafusion/pull/15548) (qstommyshu) -- Add topk information into tree explain plans [#15547](https://github.com/apache/datafusion/pull/15547) (kumarlokesh) -- Minor: add Arc for statistics in FileGroup [#15564](https://github.com/apache/datafusion/pull/15564) (xudong963) -- Test: configuration fuzzer for (external) sort queries [#15501](https://github.com/apache/datafusion/pull/15501) (2010YOUY01) -- minor: Organize fields inside SortMergeJoinStream [#15557](https://github.com/apache/datafusion/pull/15557) (suibianwanwank) -- Minor: rm session downcast [#15575](https://github.com/apache/datafusion/pull/15575) (jayzhan211) -- Migrate datafusion/sql tests to insta, part5 [#15567](https://github.com/apache/datafusion/pull/15567) (qstommyshu) -- Add SQL logic tests for compound field access in JOIN conditions [#15556](https://github.com/apache/datafusion/pull/15556) (kosiew) -- Run audit CI check on all pushes to main [#15572](https://github.com/apache/datafusion/pull/15572) (alamb) -- Introduce load-balanced `split_groups_by_statistics` method [#15473](https://github.com/apache/datafusion/pull/15473) (xudong963) -- chore: update clickbench [#15574](https://github.com/apache/datafusion/pull/15574) (chenkovsky) -- Improve spill performance: Disable re-validation of spilled files [#15454](https://github.com/apache/datafusion/pull/15454) (zebsme) -- chore: rm duplicated `JoinOn` type [#15590](https://github.com/apache/datafusion/pull/15590) (jayzhan211) -- Chore: Call arrow's methods `row_count` and `skipped_row_count` [#15587](https://github.com/apache/datafusion/pull/15587) (jayzhan211) -- Actually run wasm test in ci [#15595](https://github.com/apache/datafusion/pull/15595) (XiangpengHao) -- Migrate datafusion/sql tests to insta, part6 [#15578](https://github.com/apache/datafusion/pull/15578) (qstommyshu) -- Add test case for new casting feature from date to tz-aware timestamps [#15609](https://github.com/apache/datafusion/pull/15609) (friendlymatthew) -- Remove CoalescePartitions insertion from Joins [#15570](https://github.com/apache/datafusion/pull/15570) (ctsk) -- fix doc and broken api [#15602](https://github.com/apache/datafusion/pull/15602) (logan-keede) -- Migrate datafusion/sql tests to insta, part7 [#15621](https://github.com/apache/datafusion/pull/15621) (qstommyshu) -- ignore security_audit CI check proc-macro-error warning [#15626](https://github.com/apache/datafusion/pull/15626) (Jiashu-Hu) -- chore(deps): bump tokio from 1.44.1 to 1.44.2 [#15627](https://github.com/apache/datafusion/pull/15627) (dependabot[bot]) -- Upgrade toolchain to Rust-1.86 [#15625](https://github.com/apache/datafusion/pull/15625) (jsai28) -- chore(deps): bump bigdecimal from 0.4.7 to 0.4.8 [#15523](https://github.com/apache/datafusion/pull/15523) (dependabot[bot]) -- chore(deps): bump the arrow-parquet group across 1 directory with 7 updates [#15593](https://github.com/apache/datafusion/pull/15593) (dependabot[bot]) -- chore: improve RepartitionExec display tree [#15606](https://github.com/apache/datafusion/pull/15606) (getChan) -- Move back schema not matching check and workaround [#15580](https://github.com/apache/datafusion/pull/15580) (LiaCastaneda) -- Minor: refine comments for statistics compution [#15647](https://github.com/apache/datafusion/pull/15647) (xudong963) -- Remove uneeded binary_op benchmarks [#15632](https://github.com/apache/datafusion/pull/15632) (alamb) -- chore(deps): bump blake3 from 1.8.0 to 1.8.1 [#15650](https://github.com/apache/datafusion/pull/15650) (dependabot[bot]) -- chore(deps): bump mimalloc from 0.1.44 to 0.1.46 [#15651](https://github.com/apache/datafusion/pull/15651) (dependabot[bot]) -- chore: avoid erroneuous warning for FFI table operation (only not default value) [#15579](https://github.com/apache/datafusion/pull/15579) (chenkovsky) -- Update datafusion-testing pin (to fix extended test on main) [#15655](https://github.com/apache/datafusion/pull/15655) (alamb) -- Ignore false positive only_used_in_recursion Clippy warning [#15635](https://github.com/apache/datafusion/pull/15635) (DerGut) -- chore: Rename protobuf Java package [#15658](https://github.com/apache/datafusion/pull/15658) (andygrove) -- Remove redundant `Precision` combination code in favor of `Precision::min/max/add` [#15659](https://github.com/apache/datafusion/pull/15659) (alamb) -- Introduce DynamicFilterSource and DynamicPhysicalExpr [#15568](https://github.com/apache/datafusion/pull/15568) (adriangb) -- Public some projected methods in `FileScanConfig` [#15671](https://github.com/apache/datafusion/pull/15671) (xudong963) -- fix decimal precision issue in simplify expression optimize rule [#15588](https://github.com/apache/datafusion/pull/15588) (jayzhan211) -- Implement Future for SpawnedTask. [#15653](https://github.com/apache/datafusion/pull/15653) (ashdnazg) -- chore(deps): bump crossbeam-channel from 0.5.14 to 0.5.15 [#15674](https://github.com/apache/datafusion/pull/15674) (dependabot[bot]) -- chore(deps): bump clap from 4.5.34 to 4.5.35 [#15668](https://github.com/apache/datafusion/pull/15668) (dependabot[bot]) -- [Minor] Use interleave_record_batch in TopK implementation [#15677](https://github.com/apache/datafusion/pull/15677) (Dandandan) -- Consolidate statistics merging code (try 2) [#15661](https://github.com/apache/datafusion/pull/15661) (alamb) -- Add Table Functions to FFI Crate [#15581](https://github.com/apache/datafusion/pull/15581) (timsaucer) -- Remove waits from blocking threads reading spill files. [#15654](https://github.com/apache/datafusion/pull/15654) (ashdnazg) -- chore(deps): bump sysinfo from 0.33.1 to 0.34.2 [#15682](https://github.com/apache/datafusion/pull/15682) (dependabot[bot]) -- Minor: add order by arg for last value [#15695](https://github.com/apache/datafusion/pull/15695) (jayzhan211) -- Upgrade to arrow/parquet 55, and `object_store` to `0.12.0` and pyo3 to `0.24.0` [#15466](https://github.com/apache/datafusion/pull/15466) (alamb) -- tests: only refresh the minimum sysinfo in mem limit tests. [#15702](https://github.com/apache/datafusion/pull/15702) (ashdnazg) -- ci: fix workflow triggering extended tests from pr comments. [#15704](https://github.com/apache/datafusion/pull/15704) (ashdnazg) -- chore(deps): bump flate2 from 1.1.0 to 1.1.1 [#15703](https://github.com/apache/datafusion/pull/15703) (dependabot[bot]) -- Fix internal error in sort when hitting memory limit [#15692](https://github.com/apache/datafusion/pull/15692) (DerGut) -- Update checked in Cargo.lock file to get clean CI [#15725](https://github.com/apache/datafusion/pull/15725) (alamb) -- chore(deps): bump indexmap from 2.8.0 to 2.9.0 [#15732](https://github.com/apache/datafusion/pull/15732) (dependabot[bot]) -- Minor: include output partition count of `RepartitionExec` to tree explain [#15717](https://github.com/apache/datafusion/pull/15717) (2010YOUY01) - -## Credits - -Thank you to everyone who contributed to this release. Here is a breakdown of commits (PRs merged) per contributor. - -``` - 48 dependabot[bot] - 34 Andrew Lamb - 16 xudong.w - 15 Jay Zhan - 15 Qi Zhu - 15 irenjj - 13 Chen Chongchen - 13 Yongting You - 10 Tommy shu - 7 Shruti Sharma - 6 Alan Tang - 6 Arttu - 6 Jiashu Hu - 6 Shreyas (Lua) - 6 logan-keede - 6 zeb - 5 Dmitrii Blaginin - 5 Geoffrey Claude - 5 Jax Liu - 5 YuNing Chen - 4 Bruce Ritchie - 4 Christian - 4 Eshed Schacham - 4 Xiangpeng Hao - 4 wiedld - 3 Adrian Garcia Badaracco - 3 Daniël Heres - 3 Gabriel - 3 LB7666 - 3 Namgung Chan - 3 Ruihang Xia - 3 Tim Saucer - 3 jsai28 - 3 kosiew - 3 suibianwanwan - 2 Bryce Mecum - 2 Carol (Nichols || Goulding) - 2 Heran Lin - 2 Jannik Steinmann - 2 Jyotir Sai - 2 Li-Lun Lin - 2 Lía Adriana - 2 Oleks V - 2 Raz Luvaton - 2 UBarney - 2 aditya singh rathore - 2 westhide - 2 zjregee - 1 @clflushopt - 1 Adam Gutglick - 1 Alex Huang - 1 Alex Wilcoxson - 1 Amos Aidoo - 1 Andy Grove - 1 Andy Yen - 1 Berkay Şahin - 1 Chang - 1 Danila Baklazhenko - 1 David Hewitt - 1 Emily Matheys - 1 Eren Avsarogullari - 1 Hari Varsha - 1 Ian Lai - 1 Jack Kelly - 1 Jagdish Parihar - 1 Joseph Koshakow - 1 Lokesh - 1 LuQQiu - 1 Matt Butrovich - 1 Matt Friede - 1 Matthew Kim - 1 Matthijs Brobbel - 1 Om Kenge - 1 Owen Leung - 1 Peter L - 1 Piotr Findeisen - 1 Rohan Krishnaswamy - 1 Satyam018 - 1 Sava Vranešević - 1 Siddhartha Sahu - 1 Sile Zhou - 1 Vadim Piven - 1 Zaki - 1 christophermcdermott - 1 cht42 - 1 cjw - 1 delamarch3 - 1 ding-young - 1 haruband - 1 jamxia155 - 1 oznur-synnada - 1 peasee - 1 pranavJibhakate - 1 张林伟 -``` - -Thank you also to everyone who contributed in other ways such as filing issues, reviewing PRs, and providing feedback on this release. diff --git a/dev/update_runtime_config_docs.sh b/dev/update_runtime_config_docs.sh deleted file mode 100755 index 0d9d0f1033236..0000000000000 --- a/dev/update_runtime_config_docs.sh +++ /dev/null @@ -1,76 +0,0 @@ -#!/bin/bash -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -set -e - -SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "${SOURCE_DIR}/../" && pwd - -TARGET_FILE="docs/source/user-guide/runtime_configs.md" -PRINT_CONFIG_DOCS_COMMAND="cargo run --manifest-path datafusion/core/Cargo.toml --bin print_runtime_config_docs" - -echo "Inserting header" -cat <<'EOF' > "$TARGET_FILE" - - - - -# Runtime Environment Configurations - -DataFusion runtime configurations can be set via SQL using the `SET` command. - -For example, to configure `datafusion.runtime.memory_limit`: - -```sql -SET datafusion.runtime.memory_limit = '2G'; -``` - -The following runtime configuration settings are available: - -EOF - -echo "Running CLI and inserting runtime config docs table" -$PRINT_CONFIG_DOCS_COMMAND >> "$TARGET_FILE" - -echo "Running prettier" -npx prettier@2.3.2 --write "$TARGET_FILE" - -echo "'$TARGET_FILE' successfully updated!" diff --git a/docs/source/index.rst b/docs/source/index.rst index e920a0f036cbe..0dc947fdea579 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -116,7 +116,6 @@ To get started, see user-guide/expressions user-guide/sql/index user-guide/configs - user-guide/runtime_configs user-guide/explain-usage user-guide/faq diff --git a/docs/source/library-user-guide/profiling.md b/docs/source/library-user-guide/profiling.md index 61e848a2b7d9b..40fae6f447056 100644 --- a/docs/source/library-user-guide/profiling.md +++ b/docs/source/library-user-guide/profiling.md @@ -21,7 +21,7 @@ The section contains examples how to perform CPU profiling for Apache DataFusion on different operating systems. -## Building a flame graph +## Building a flamegraph [Video: how to CPU profile DataFusion with a Flamegraph](https://youtu.be/2z11xtYw_xs) @@ -82,43 +82,6 @@ CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --root --bench sql_planner -- [Video: how to CPU profile DataFusion with XCode Instruments](https://youtu.be/P3dXH61Kr5U) -## Profiling using Samply cross platform profiler +## Linux -There is an opportunity to build flamegraphs, call trees and stack charts on any platform using -[Samply](https://github.com/mstange/samply) - -Install Samply profiler - -```shell -cargo install --locked samply -``` - -More Samply [installation options](https://github.com/mstange/samply?tab=readme-ov-file#installation) - -Run the profiler - -```shell -samply record --profile profiling ./my-application my-arguments -``` - -### Profile the benchmark - -[Set up benchmarks](https://github.com/apache/datafusion/blob/main/benchmarks/README.md#running-the-benchmarks) if not yet done - -Example: Profile Q22 query from TPC-H benchmark. -Note: `--profile profiling` to profile release optimized artifact with debug symbols - -```shell -cargo build --profile profiling --bin tpch -samply record ./target/profiling/tpch benchmark datafusion --iterations 5 --path datafusion/benchmarks/data/tpch_sf10 --prefer_hash_join true --format parquet -o datafusion/benchmarks/results/dev2/tpch_sf10.json --query 22 -``` - -After sampling has completed the Samply starts a local server and navigates to the profiler - -```shell -Local server listening at http://127.0.0.1:3000 -``` - -![img.png](samply_profiler.png) - -Note: The Firefox profiler cannot be opened in Safari, please use Chrome or Firefox instead +## Windows diff --git a/docs/source/library-user-guide/samply_profiler.png b/docs/source/library-user-guide/samply_profiler.png deleted file mode 100644 index 08b99074585682f7d86f0b98faffdc10ae39912f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 605887 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%0Dy;6$4Sgqv8ijJF?zw)&%aRIRC8SH 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 #$rQ9tzq2 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�zG1{yMp=X41OloFBDT41iW>8X=Ce9qiZM4`XEDS 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%0DEmQ% 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�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¥+*fynaN$9Mv9ASZ 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�#@Z62Q>yf^LM5_h>KNVL8D3KY5 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 E11kJmVgLXD diff --git a/docs/source/library-user-guide/upgrading.md b/docs/source/library-user-guide/upgrading.md index db5078d603f2f..11fd495665225 100644 --- a/docs/source/library-user-guide/upgrading.md +++ b/docs/source/library-user-guide/upgrading.md @@ -19,122 +19,6 @@ # Upgrade Guides -## DataFusion `47.0.0` - -This section calls out some of the major changes in the `47.0.0` release of DataFusion. - -Here are some example upgrade PRs that demonstrate changes required when upgrading from DataFusion 46.0.0: - -- [delta-rs Upgrade to `47.0.0`](https://github.com/delta-io/delta-rs/pull/3378) -- [DataFusion Comet Upgrade to `47.0.0`](https://github.com/apache/datafusion-comet/pull/1563) -- [Sail Upgrade to `47.0.0`](https://github.com/lakehq/sail/pull/434) - -### Upgrades to `arrow-rs` and `arrow-parquet` 55.0.0 and `object_store` 0.12.0 - -Several APIs are changed in the underlying arrow and parquet libraries to use a -`u64` instead of `usize` to better support WASM (See [#7371] and [#6961]) - -Additionally `ObjectStore::list` and `ObjectStore::list_with_offset` have been changed to return `static` lifetimes (See [#6619]) - -[#6619]: https://github.com/apache/arrow-rs/pull/6619 -[#7371]: https://github.com/apache/arrow-rs/pull/7371 -[#7328]: https://github.com/apache/arrow-rs/pull/6961 - -This requires converting from `usize` to `u64` occasionally as well as changes to `ObjectStore` implementations such as - -```rust -# /* comment to avoid running -impl Objectstore { - ... - // The range is now a u64 instead of usize - async fn get_range(&self, location: &Path, range: Range) -> ObjectStoreResult { - self.inner.get_range(location, range).await - } - ... - // the lifetime is now 'static instead of `_ (meaning the captured closure can't contain references) - // (this also applies to list_with_offset) - fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, ObjectStoreResult> { - self.inner.list(prefix) - } -} -# */ -``` - -The `ParquetObjectReader` has been updated to no longer require the object size -(it can be fetched using a single suffix request). See [#7334] for details - -[#7334]: https://github.com/apache/arrow-rs/pull/7334 - -Pattern in DataFusion `46.0.0`: - -```rust -# /* comment to avoid running -let meta: ObjectMeta = ...; -let reader = ParquetObjectReader::new(store, meta); -# */ -``` - -Pattern in DataFusion `47.0.0`: - -```rust -# /* comment to avoid running -let meta: ObjectMeta = ...; -let reader = ParquetObjectReader::new(store, location) - .with_file_size(meta.size); -# */ -``` - -### `DisplayFormatType::TreeRender` - -DataFusion now supports [`tree` style explain plans]. Implementations of -`Executionplan` must also provide a description in the -`DisplayFormatType::TreeRender` format. This can be the same as the existing -`DisplayFormatType::Default`. - -[`tree` style explain plans]: https://datafusion.apache.org/user-guide/sql/explain.html#tree-format-default - -### Removed Deprecated APIs - -Several APIs have been removed in this release. These were either deprecated -previously or were hard to use correctly such as the multiple different -`ScalarUDFImpl::invoke*` APIs. See [#15130], [#15123], and [#15027] for more -details. - -[#15130]: https://github.com/apache/datafusion/pull/15130 -[#15123]: https://github.com/apache/datafusion/pull/15123 -[#15027]: https://github.com/apache/datafusion/pull/15027 - -## `FileScanConfig` --> `FileScanConfigBuilder` - -Previously, `FileScanConfig::build()` directly created ExecutionPlans. In -DataFusion 47.0.0 this has been changed to use `FileScanConfigBuilder`. See -[#15352] for details. - -[#15352]: https://github.com/apache/datafusion/pull/15352 - -Pattern in DataFusion `46.0.0`: - -```rust -# /* comment to avoid running -let plan = FileScanConfig::new(url, schema, Arc::new(file_source)) - .with_statistics(stats) - ... - .build() -# */ -``` - -Pattern in DataFusion `47.0.0`: - -```rust -# /* comment to avoid running -let config = FileScanConfigBuilder::new(url, schema, Arc::new(file_source)) - .with_statistics(stats) - ... - .build(); -let scan = DataSourceExec::from_data_source(config); -# */ -``` - ## DataFusion `46.0.0` ### Use `invoke_with_args` instead of `invoke()` and `invoke_batch()` @@ -155,7 +39,7 @@ below. See [PR 14876] for an example. Given existing code like this: ```rust -# /* comment to avoid running +# /* impl ScalarUDFImpl for SparkConcat { ... fn invoke_batch(&self, args: &[ColumnarValue], number_rows: usize) -> Result { @@ -175,7 +59,7 @@ impl ScalarUDFImpl for SparkConcat { To ```rust -# /* comment to avoid running +# /* comment out so they don't run impl ScalarUDFImpl for SparkConcat { ... fn invoke_with_args(&self, args: ScalarFunctionArgs) -> Result { diff --git a/docs/source/user-guide/cli/datasources.md b/docs/source/user-guide/cli/datasources.md index 2e14f1f54c6c1..39172e94e5f80 100644 --- a/docs/source/user-guide/cli/datasources.md +++ b/docs/source/user-guide/cli/datasources.md @@ -95,7 +95,8 @@ additional configuration options. # `CREATE EXTERNAL TABLE` It is also possible to create a table backed by files or remote locations via -`CREATE EXTERNAL TABLE` as shown below. Note that DataFusion does not support wildcards (e.g. `*`) in file paths; instead, specify the directory path directly to read all compatible files in that directory. +`CREATE EXTERNAL TABLE` as shown below. Note that wildcards (e.g. `*`) are also +supported For example, to create a table `hits` backed by a local parquet file, use: @@ -125,32 +126,6 @@ select count(*) from hits; 1 row in set. Query took 0.344 seconds. ``` -**Why Wildcards Are Not Supported** - -Although wildcards (e.g., _.parquet or \*\*/_.parquet) may work for local filesystems in some cases, they are not officially supported by DataFusion. This is because wildcards are not universally applicable across all storage backends (e.g., S3, GCS). Instead, DataFusion expects the user to specify the directory path, and it will automatically read all compatible files within that directory. - -For example, the following usage is not supported: - -```sql -CREATE EXTERNAL TABLE test ( - message TEXT, - day DATE -) -STORED AS PARQUET -LOCATION 'gs://bucket/*.parquet'; -``` - -Instead, you should use: - -```sql -CREATE EXTERNAL TABLE test ( - message TEXT, - day DATE -) -STORED AS PARQUET -LOCATION 'gs://bucket/my_table'; -``` - # Formats ## Parquet @@ -174,6 +149,14 @@ STORED AS PARQUET LOCATION '/mnt/nyctaxi/'; ``` +Register a single folder parquet datasource by specifying a wildcard for files to read + +```sql +CREATE EXTERNAL TABLE taxi +STORED AS PARQUET +LOCATION '/mnt/nyctaxi/*.parquet'; +``` + ## CSV DataFusion will infer the CSV schema automatically or you can provide it explicitly. diff --git a/docs/source/user-guide/cli/usage.md b/docs/source/user-guide/cli/usage.md index 68b09d3199840..fb238dad10bb1 100644 --- a/docs/source/user-guide/cli/usage.md +++ b/docs/source/user-guide/cli/usage.md @@ -57,9 +57,6 @@ OPTIONS: --mem-pool-type Specify the memory pool type 'greedy' or 'fair', default to 'greedy' - -d, --disk-limit - Available disk space for spilling queries (e.g. '10g'), default to None (uses DataFusion's default value of '100g') - -p, --data-path Path to your data, default to current directory diff --git a/docs/source/user-guide/concepts-readings-events.md b/docs/source/user-guide/concepts-readings-events.md index ad444ef91c474..fef677dd3a621 100644 --- a/docs/source/user-guide/concepts-readings-events.md +++ b/docs/source/user-guide/concepts-readings-events.md @@ -37,10 +37,6 @@ This is a list of DataFusion related blog posts, articles, and other resources. Please open a PR to add any new resources you create or find -- **2025-03-21** [Blog: Efficient Filter Pushdown in Parquet](https://datafusion.apache.org/blog/2025/03/21/parquet-pushdown/) - -- **2025-03-20** [Blog: Parquet Pruning in DataFusion: Read Only What Matters](https://datafusion.apache.org/blog/2025/03/20/parquet-pruning/) - - **2025-02-12** [Video: Alex Kesling on Apache Arrow DataFusion - Papers We Love NYC ](https://www.youtube.com/watch?v=6A4vFRpSq3k) - **2025-01-30** [Video: Data & Drinks: Building Next-Gen Data Systems with Apache DataFusion](https://www.youtube.com/watch?v=GruBeVDoWq4) @@ -138,8 +134,6 @@ This is a list of DataFusion related blog posts, articles, and other resources. ## 📅 Release Notes & Updates -- **2025-03-24** [Apache DataFusion 46.0.0 Released](https://datafusion.apache.org/blog/2025/03/24/datafusion-46.0.0/) - - **2024-09-14** [Apache DataFusion Python 43.1.0 Released](https://datafusion.apache.org/blog/2024/12/14/datafusion-python-43.1.0/) - **2024-08-24** [Apache DataFusion Python 40.1.0 Released, Significant usability updates](https://datafusion.apache.org/blog/2024/08/20/python-datafusion-40.0.0/) diff --git a/docs/source/user-guide/configs.md b/docs/source/user-guide/configs.md index 7a46d59d893e6..68e21183938b1 100644 --- a/docs/source/user-guide/configs.md +++ b/docs/source/user-guide/configs.md @@ -58,7 +58,6 @@ Environment variables are read during `SessionConfig` initialisation so they mus | datafusion.execution.parquet.reorder_filters | false | (reading) If true, filter expressions evaluated during the parquet decoding operation will be reordered heuristically to minimize the cost of evaluation. If false, the filters are applied in the same order as written in the query | | datafusion.execution.parquet.schema_force_view_types | true | (reading) If true, parquet reader will read columns of `Utf8/Utf8Large` with `Utf8View`, and `Binary/BinaryLarge` with `BinaryView`. | | datafusion.execution.parquet.binary_as_string | false | (reading) If true, parquet reader will read columns of `Binary/LargeBinary` with `Utf8`, and `BinaryView` with `Utf8View`. Parquet files generated by some legacy writers do not correctly set the UTF8 flag for strings, causing string columns to be loaded as BLOB instead. | -| datafusion.execution.parquet.coerce_int96 | NULL | (reading) If true, parquet reader will read columns of physical type int96 as originating from a different resolution than nanosecond. This is useful for reading data from systems like Spark which stores microsecond resolution timestamps in an int96 allowing it to write values with a larger date range than 64-bit timestamps with nanosecond resolution. | | datafusion.execution.parquet.data_pagesize_limit | 1048576 | (writing) Sets best effort maximum size of data page in bytes | | datafusion.execution.parquet.write_batch_size | 1024 | (writing) Sets write_batch_size in bytes | | datafusion.execution.parquet.writer_version | 1.0 | (writing) Sets parquet writer version valid values are "1.0" and "2.0" | @@ -69,7 +68,7 @@ Environment variables are read during `SessionConfig` initialisation so they mus | datafusion.execution.parquet.statistics_enabled | page | (writing) Sets if statistics are enabled for any column Valid values are: "none", "chunk", and "page" These values are not case sensitive. If NULL, uses default parquet writer setting | | datafusion.execution.parquet.max_statistics_size | 4096 | (writing) Sets max statistics size for any column. If NULL, uses default parquet writer setting max_statistics_size is deprecated, currently it is not being used | | datafusion.execution.parquet.max_row_group_size | 1048576 | (writing) Target maximum number of rows in each row group (defaults to 1M rows). Writing larger row groups requires more memory to write, but can get better compression and be faster to read. | -| datafusion.execution.parquet.created_by | datafusion version 47.0.0 | (writing) Sets "created by" property | +| datafusion.execution.parquet.created_by | datafusion version 46.0.1 | (writing) Sets "created by" property | | datafusion.execution.parquet.column_index_truncate_length | 64 | (writing) Sets column index truncate length | | datafusion.execution.parquet.statistics_truncate_length | NULL | (writing) Sets statictics truncate length. If NULL, uses default parquet writer setting | | datafusion.execution.parquet.data_page_row_count_limit | 20000 | (writing) Sets best effort maximum number of rows in data page | diff --git a/docs/source/user-guide/introduction.md b/docs/source/user-guide/introduction.md index 1879221aa4d11..14d6ab177dc34 100644 --- a/docs/source/user-guide/introduction.md +++ b/docs/source/user-guide/introduction.md @@ -95,7 +95,6 @@ Here are some active projects using DataFusion: - [Arroyo](https://github.com/ArroyoSystems/arroyo) Distributed stream processing engine in Rust -- [ArkFlow](https://github.com/arkflow-rs/arkflow) High-performance Rust stream processing engine - [Ballista](https://github.com/apache/datafusion-ballista) Distributed SQL Query Engine - [Blaze](https://github.com/kwai/blaze) The Blaze accelerator for Apache Spark leverages native vectorized execution to accelerate query processing - [CnosDB](https://github.com/cnosdb/cnosdb) Open Source Distributed Time Series Database @@ -105,7 +104,6 @@ Here are some active projects using DataFusion: - [datafusion-dft](https://github.com/datafusion-contrib/datafusion-dft) Batteries included CLI, TUI, and server implementations for DataFusion. - [delta-rs](https://github.com/delta-io/delta-rs) Native Rust implementation of Delta Lake - [Exon](https://github.com/wheretrue/exon) Analysis toolkit for life-science applications -- [Feldera](https://github.com/feldera/feldera) Fast query engine for incremental computation - [Funnel](https://funnel.io/) Data Platform powering Marketing Intelligence applications. - [GlareDB](https://github.com/GlareDB/glaredb) Fast SQL database for querying and analyzing distributed data. - [GreptimeDB](https://github.com/GreptimeTeam/greptimedb) Open Source & Cloud Native Distributed Time Series Database diff --git a/docs/source/user-guide/runtime_configs.md b/docs/source/user-guide/runtime_configs.md deleted file mode 100644 index feef709db9929..0000000000000 --- a/docs/source/user-guide/runtime_configs.md +++ /dev/null @@ -1,40 +0,0 @@ - - - - -# Runtime Environment Configurations - -DataFusion runtime configurations can be set via SQL using the `SET` command. - -For example, to configure `datafusion.runtime.memory_limit`: - -```sql -SET datafusion.runtime.memory_limit = '2G'; -``` - -The following runtime configuration settings are available: - -| key | default | description | -| ------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| datafusion.runtime.memory_limit | NULL | Maximum memory limit for query execution. Supports suffixes K (kilobytes), M (megabytes), and G (gigabytes). Example: '2G' for 2 gigabytes. | diff --git a/docs/source/user-guide/sql/aggregate_functions.md b/docs/source/user-guide/sql/aggregate_functions.md index 774a4fae6bf32..c7f5c5f674424 100644 --- a/docs/source/user-guide/sql/aggregate_functions.md +++ b/docs/source/user-guide/sql/aggregate_functions.md @@ -371,10 +371,10 @@ min(expression) ### `string_agg` -Concatenates the values of string expressions and places separator values between them. If ordering is required, strings are concatenated in the specified order. This aggregation function can only mix DISTINCT and ORDER BY if the ordering expression is exactly the same as the first argument expression. +Concatenates the values of string expressions and places separator values between them. ```sql -string_agg([DISTINCT] expression, delimiter [ORDER BY expression]) +string_agg(expression, delimiter) ``` #### Arguments @@ -390,21 +390,7 @@ string_agg([DISTINCT] expression, delimiter [ORDER BY expression]) +--------------------------+ | names_list | +--------------------------+ -| Alice, Bob, Bob, Charlie | -+--------------------------+ -> SELECT string_agg(name, ', ' ORDER BY name DESC) AS names_list - FROM employee; -+--------------------------+ -| names_list | -+--------------------------+ -| Charlie, Bob, Bob, Alice | -+--------------------------+ -> SELECT string_agg(DISTINCT name, ', ' ORDER BY name DESC) AS names_list - FROM employee; -+--------------------------+ -| names_list | -+--------------------------+ -| Charlie, Bob, Alice | +| Alice, Bob, Charlie | +--------------------------+ ``` @@ -808,7 +794,7 @@ approx_distinct(expression) ### `approx_median` -Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(0.5) WITHIN GROUP (ORDER BY x)`. +Returns the approximate median (50th percentile) of input values. It is an alias of `approx_percentile_cont(x, 0.5)`. ```sql approx_median(expression) @@ -834,7 +820,7 @@ approx_median(expression) Returns the approximate percentile of input values using the t-digest algorithm. ```sql -approx_percentile_cont(percentile, centroids) WITHIN GROUP (ORDER BY expression) +approx_percentile_cont(expression, percentile, centroids) ``` #### Arguments @@ -846,12 +832,12 @@ approx_percentile_cont(percentile, centroids) WITHIN GROUP (ORDER BY expression) #### Example ```sql -> SELECT approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) FROM table_name; -+-----------------------------------------------------------------------+ -| approx_percentile_cont(0.75, 100) WITHIN GROUP (ORDER BY column_name) | -+-----------------------------------------------------------------------+ -| 65.0 | -+-----------------------------------------------------------------------+ +> SELECT approx_percentile_cont(column_name, 0.75, 100) FROM table_name; ++-------------------------------------------------+ +| approx_percentile_cont(column_name, 0.75, 100) | ++-------------------------------------------------+ +| 65.0 | ++-------------------------------------------------+ ``` ### `approx_percentile_cont_with_weight` @@ -859,7 +845,7 @@ approx_percentile_cont(percentile, centroids) WITHIN GROUP (ORDER BY expression) Returns the weighted approximate percentile of input values using the t-digest algorithm. ```sql -approx_percentile_cont_with_weight(weight, percentile) WITHIN GROUP (ORDER BY expression) +approx_percentile_cont_with_weight(expression, weight, percentile) ``` #### Arguments @@ -871,10 +857,10 @@ approx_percentile_cont_with_weight(weight, percentile) WITHIN GROUP (ORDER BY ex #### Example ```sql -> SELECT approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) FROM table_name; -+---------------------------------------------------------------------------------------------+ -| approx_percentile_cont_with_weight(weight_column, 0.90) WITHIN GROUP (ORDER BY column_name) | -+---------------------------------------------------------------------------------------------+ -| 78.5 | -+---------------------------------------------------------------------------------------------+ +> SELECT approx_percentile_cont_with_weight(column_name, weight_column, 0.90) FROM table_name; ++----------------------------------------------------------------------+ +| approx_percentile_cont_with_weight(column_name, weight_column, 0.90) | ++----------------------------------------------------------------------+ +| 78.5 | ++----------------------------------------------------------------------+ ``` diff --git a/docs/source/user-guide/sql/data_types.md b/docs/source/user-guide/sql/data_types.md index d977a4396e40d..18c95cdea70ed 100644 --- a/docs/source/user-guide/sql/data_types.md +++ b/docs/source/user-guide/sql/data_types.md @@ -60,20 +60,20 @@ select arrow_cast(now(), 'Timestamp(Second, None)'); ## Numeric Types -| SQL DataType | Arrow DataType | -| ------------------------------------ | :----------------------------- | -| `TINYINT` | `Int8` | -| `SMALLINT` | `Int16` | -| `INT` or `INTEGER` | `Int32` | -| `BIGINT` | `Int64` | -| `TINYINT UNSIGNED` | `UInt8` | -| `SMALLINT UNSIGNED` | `UInt16` | -| `INT UNSIGNED` or `INTEGER UNSIGNED` | `UInt32` | -| `BIGINT UNSIGNED` | `UInt64` | -| `FLOAT` | `Float32` | -| `REAL` | `Float32` | -| `DOUBLE` | `Float64` | -| `DECIMAL(precision, scale)` | `Decimal128(precision, scale)` | +| SQL DataType | Arrow DataType | Notes | +| ------------------------------------ | :----------------------------- | ----------------------------------------------------------------------------------------------------- | +| `TINYINT` | `Int8` | | +| `SMALLINT` | `Int16` | | +| `INT` or `INTEGER` | `Int32` | | +| `BIGINT` | `Int64` | | +| `TINYINT UNSIGNED` | `UInt8` | | +| `SMALLINT UNSIGNED` | `UInt16` | | +| `INT UNSIGNED` or `INTEGER UNSIGNED` | `UInt32` | | +| `BIGINT UNSIGNED` | `UInt64` | | +| `FLOAT` | `Float32` | | +| `REAL` | `Float32` | | +| `DOUBLE` | `Float64` | | +| `DECIMAL(precision, scale)` | `Decimal128(precision, scale)` | Decimal support is currently experimental ([#3523](https://github.com/apache/datafusion/issues/3523)) | ## Date/Time Types diff --git a/docs/source/user-guide/sql/ddl.md b/docs/source/user-guide/sql/ddl.md index fc18154becda6..71475cff9a39b 100644 --- a/docs/source/user-guide/sql/ddl.md +++ b/docs/source/user-guide/sql/ddl.md @@ -74,7 +74,7 @@ LOCATION := ( , ...) ``` -For a comprehensive list of format-specific options that can be specified in the `OPTIONS` clause, see [Format Options](format_options.md). +For a detailed list of write related options which can be passed in the OPTIONS key_value_list, see [Write Options](write_options). `file_type` is one of `CSV`, `ARROW`, `PARQUET`, `AVRO` or `JSON` diff --git a/docs/source/user-guide/sql/dml.md b/docs/source/user-guide/sql/dml.md index c29447f23cd9c..4eda59d6dea10 100644 --- a/docs/source/user-guide/sql/dml.md +++ b/docs/source/user-guide/sql/dml.md @@ -49,7 +49,7 @@ The output format is determined by the first match of the following rules: 1. Value of `STORED AS` 2. Filename extension (e.g. `foo.parquet` implies `PARQUET` format) -For a detailed list of valid OPTIONS, see [Format Options](format_options.md). +For a detailed list of valid OPTIONS, see [Write Options](write_options). ### Examples diff --git a/docs/source/user-guide/sql/explain.md b/docs/source/user-guide/sql/explain.md index 9984de147ecc5..f89e854ebffd5 100644 --- a/docs/source/user-guide/sql/explain.md +++ b/docs/source/user-guide/sql/explain.md @@ -39,7 +39,39 @@ the format from the [configuration value] `datafusion.explain.format`. [configuration value]: ../configs.md -### `tree` format (default) +### `indent` format (default) + +The `indent` format shows both the logical and physical plan, with one line for +each operator in the plan. Child plans are indented to show the hierarchy. + +See [Reading Explain Plans](../explain-usage.md) for more information on how to interpret these plans. + +```sql +> CREATE TABLE t(x int, b int) AS VALUES (1, 2), (2, 3); +0 row(s) fetched. +Elapsed 0.004 seconds. + +> EXPLAIN SELECT SUM(x) FROM t GROUP BY b; ++---------------+-------------------------------------------------------------------------------+ +| plan_type | plan | ++---------------+-------------------------------------------------------------------------------+ +| logical_plan | Projection: sum(t.x) | +| | Aggregate: groupBy=[[t.b]], aggr=[[sum(CAST(t.x AS Int64))]] | +| | TableScan: t projection=[x, b] | +| physical_plan | ProjectionExec: expr=[sum(t.x)@1 as sum(t.x)] | +| | AggregateExec: mode=FinalPartitioned, gby=[b@0 as b], aggr=[sum(t.x)] | +| | CoalesceBatchesExec: target_batch_size=8192 | +| | RepartitionExec: partitioning=Hash([b@0], 16), input_partitions=16 | +| | RepartitionExec: partitioning=RoundRobinBatch(16), input_partitions=1 | +| | AggregateExec: mode=Partial, gby=[b@1 as b], aggr=[sum(t.x)] | +| | DataSourceExec: partitions=1, partition_sizes=[1] | +| | | ++---------------+-------------------------------------------------------------------------------+ +2 row(s) fetched. +Elapsed 0.004 seconds. +``` + +### `tree` format The `tree` format is modeled after [DuckDB plans] and is designed to be easier to see the high level structure of the plan @@ -71,7 +103,7 @@ to see the high level structure of the plan | | ┌─────────────┴─────────────┐ | | | │ RepartitionExec │ | | | │ -------------------- │ | -| | │ input_partition_count: │ | +| | │ output_partition_count: │ | | | │ 16 │ | | | │ │ | | | │ partitioning_scheme: │ | @@ -80,7 +112,7 @@ to see the high level structure of the plan | | ┌─────────────┴─────────────┐ | | | │ RepartitionExec │ | | | │ -------------------- │ | -| | │ input_partition_count: │ | +| | │ output_partition_count: │ | | | │ 1 │ | | | │ │ | | | │ partitioning_scheme: │ | @@ -106,38 +138,6 @@ to see the high level structure of the plan Elapsed 0.016 seconds. ``` -### `indent` format - -The `indent` format shows both the logical and physical plan, with one line for -each operator in the plan. Child plans are indented to show the hierarchy. - -See [Reading Explain Plans](../explain-usage.md) for more information on how to interpret these plans. - -```sql -> CREATE TABLE t(x int, b int) AS VALUES (1, 2), (2, 3); -0 row(s) fetched. -Elapsed 0.004 seconds. - -> EXPLAIN SELECT SUM(x) FROM t GROUP BY b; -+---------------+-------------------------------------------------------------------------------+ -| plan_type | plan | -+---------------+-------------------------------------------------------------------------------+ -| logical_plan | Projection: sum(t.x) | -| | Aggregate: groupBy=[[t.b]], aggr=[[sum(CAST(t.x AS Int64))]] | -| | TableScan: t projection=[x, b] | -| physical_plan | ProjectionExec: expr=[sum(t.x)@1 as sum(t.x)] | -| | AggregateExec: mode=FinalPartitioned, gby=[b@0 as b], aggr=[sum(t.x)] | -| | CoalesceBatchesExec: target_batch_size=8192 | -| | RepartitionExec: partitioning=Hash([b@0], 16), input_partitions=16 | -| | RepartitionExec: partitioning=RoundRobinBatch(16), input_partitions=1 | -| | AggregateExec: mode=Partial, gby=[b@1 as b], aggr=[sum(t.x)] | -| | DataSourceExec: partitions=1, partition_sizes=[1] | -| | | -+---------------+-------------------------------------------------------------------------------+ -2 row(s) fetched. -Elapsed 0.004 seconds. -``` - ### `pgjson` format The `pgjson` format is modeled after [Postgres JSON] format. diff --git a/docs/source/user-guide/sql/format_options.md b/docs/source/user-guide/sql/format_options.md deleted file mode 100644 index e8008eafb166c..0000000000000 --- a/docs/source/user-guide/sql/format_options.md +++ /dev/null @@ -1,180 +0,0 @@ - - -# Format Options - -DataFusion supports customizing how data is read from or written to disk as a result of a `COPY`, `INSERT INTO`, or `CREATE EXTERNAL TABLE` statements. There are a few special options, file format (e.g., CSV or Parquet) specific options, and Parquet column-specific options. In some cases, Options can be specified in multiple ways with a set order of precedence. - -## Specifying Options and Order of Precedence - -Format-related options can be specified in three ways, in decreasing order of precedence: - -- `CREATE EXTERNAL TABLE` syntax -- `COPY` option tuples -- Session-level config defaults - -For a list of supported session-level config defaults, see [Configuration Settings](../configs). These defaults apply to all operations but have the lowest level of precedence. - -If creating an external table, table-specific format options can be specified when the table is created using the `OPTIONS` clause: - -```sql -CREATE EXTERNAL TABLE - my_table(a bigint, b bigint) - STORED AS csv - LOCATION '/tmp/my_csv_table/' - OPTIONS( - NULL_VALUE 'NAN', - 'has_header' 'true', - 'format.delimiter' ';' - ); -``` - -When running `INSERT INTO my_table ...`, the options from the `CREATE TABLE` will be respected (e.g., gzip compression, special delimiter, and header row included). Note that compression, header, and delimiter settings can also be specified within the `OPTIONS` tuple list. Dedicated syntax within the SQL statement always takes precedence over arbitrary option tuples, so if both are specified, the `OPTIONS` setting will be ignored. - -For example, with the table defined above, running the following command: - -```sql -INSERT INTO my_table VALUES(1,2); -``` - -Results in a new CSV file with the specified options: - -```shell -$ cat /tmp/my_csv_table/bmC8zWFvLMtWX68R_0.csv -a;b -1;2 -``` - -Finally, options can be passed when running a `COPY` command. - -```sql -COPY source_table - TO 'test/table_with_options' - PARTITIONED BY (column3, column4) - OPTIONS ( - format parquet, - compression snappy, - 'compression::column1' 'zstd(5)', - ) -``` - -In this example, we write the entire `source_table` out to a folder of Parquet files. One Parquet file will be written in parallel to the folder for each partition in the query. The next option `compression` set to `snappy` indicates that unless otherwise specified, all columns should use the snappy compression codec. The option `compression::col1` sets an override, so that the column `col1` in the Parquet file will use the ZSTD compression codec with compression level `5`. In general, Parquet options that support column-specific settings can be specified with the syntax `OPTION::COLUMN.NESTED.PATH`. - -# Available Options - -## JSON Format Options - -The following options are available when reading or writing JSON files. Note: If any unsupported option is specified, an error will be raised and the query will fail. - -| Option | Description | Default Value | -| ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| COMPRESSION | Sets the compression that should be applied to the entire JSON file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | - -**Example:** - -```sql -CREATE EXTERNAL TABLE t(a int) -STORED AS JSON -LOCATION '/tmp/foo/' -OPTIONS('COMPRESSION' 'gzip'); -``` - -## CSV Format Options - -The following options are available when reading or writing CSV files. Note: If any unsupported option is specified, an error will be raised and the query will fail. - -| Option | Description | Default Value | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -| COMPRESSION | Sets the compression that should be applied to the entire CSV file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | -| HAS_HEADER | Sets if the CSV file should include column headers. If not set, uses session or system default. | None | -| DELIMITER | Sets the character which should be used as the column delimiter within the CSV file. | `,` (comma) | -| QUOTE | Sets the character which should be used for quoting values within the CSV file. | `"` (double quote) | -| TERMINATOR | Sets the character which should be used as the line terminator within the CSV file. | None | -| ESCAPE | Sets the character which should be used for escaping special characters within the CSV file. | None | -| DOUBLE_QUOTE | Sets if quotes within quoted fields should be escaped by doubling them (e.g., `"aaa""bbb"`). | None | -| NEWLINES_IN_VALUES | Sets if newlines in quoted values are supported. If not set, uses session or system default. | None | -| DATE_FORMAT | Sets the format that dates should be encoded in within the CSV file. | None | -| DATETIME_FORMAT | Sets the format that datetimes should be encoded in within the CSV file. | None | -| TIMESTAMP_FORMAT | Sets the format that timestamps should be encoded in within the CSV file. | None | -| TIMESTAMP_TZ_FORMAT | Sets the format that timestamps with timezone should be encoded in within the CSV file. | None | -| TIME_FORMAT | Sets the format that times should be encoded in within the CSV file. | None | -| NULL_VALUE | Sets the string which should be used to indicate null values within the CSV file. | None | -| NULL_REGEX | Sets the regex pattern to match null values when loading CSVs. | None | -| SCHEMA_INFER_MAX_REC | Sets the maximum number of records to scan to infer the schema. | None | -| COMMENT | Sets the character which should be used to indicate comment lines in the CSV file. | None | - -**Example:** - -```sql -CREATE EXTERNAL TABLE t (col1 varchar, col2 int, col3 boolean) -STORED AS CSV -LOCATION '/tmp/foo/' -OPTIONS('DELIMITER' '|', 'HAS_HEADER' 'true', 'NEWLINES_IN_VALUES' 'true'); -``` - -## Parquet Format Options - -The following options are available when reading or writing Parquet files. If any unsupported option is specified, an error will be raised and the query will fail. If a column-specific option is specified for a column that does not exist, the option will be ignored without error. - -| Option | Can be Column Specific? | Description | OPTIONS Key | Default Value | -| ------------------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | ------------------------ | -| COMPRESSION | Yes | Sets the internal Parquet **compression codec** for data pages, optionally including the compression level. Applies globally if set without `::col`, or specifically to a column if set using `'compression::column_name'`. Valid values: `uncompressed`, `snappy`, `gzip(level)`, `lzo`, `brotli(level)`, `lz4`, `zstd(level)`, `lz4_raw`. | `'compression'` or `'compression::col'` | zstd(3) | -| ENCODING | Yes | Sets the **encoding** scheme for data pages. Valid values: `plain`, `plain_dictionary`, `rle`, `bit_packed`, `delta_binary_packed`, `delta_length_byte_array`, `delta_byte_array`, `rle_dictionary`, `byte_stream_split`. Use key `'encoding'` or `'encoding::col'` in OPTIONS. | `'encoding'` or `'encoding::col'` | None | -| DICTIONARY_ENABLED | Yes | Sets whether dictionary encoding should be enabled globally or for a specific column. | `'dictionary_enabled'` or `'dictionary_enabled::col'` | true | -| STATISTICS_ENABLED | Yes | Sets the level of statistics to write (`none`, `chunk`, `page`). | `'statistics_enabled'` or `'statistics_enabled::col'` | page | -| BLOOM_FILTER_ENABLED | Yes | Sets whether a bloom filter should be written for a specific column. | `'bloom_filter_enabled::column_name'` | None | -| BLOOM_FILTER_FPP | Yes | Sets bloom filter false positive probability (global or per column). | `'bloom_filter_fpp'` or `'bloom_filter_fpp::col'` | None | -| BLOOM_FILTER_NDV | Yes | Sets bloom filter number of distinct values (global or per column). | `'bloom_filter_ndv'` or `'bloom_filter_ndv::col'` | None | -| MAX_ROW_GROUP_SIZE | No | Sets the maximum number of rows per row group. Larger groups require more memory but can improve compression and scan efficiency. | `'max_row_group_size'` | 1048576 | -| ENABLE_PAGE_INDEX | No | If true, reads the Parquet data page level metadata (the Page Index), if present, to reduce I/O and decoding. | `'enable_page_index'` | true | -| PRUNING | No | If true, enables row group pruning based on min/max statistics. | `'pruning'` | true | -| SKIP_METADATA | No | If true, skips optional embedded metadata in the file schema. | `'skip_metadata'` | true | -| METADATA_SIZE_HINT | No | Sets the size hint (in bytes) for fetching Parquet file metadata. | `'metadata_size_hint'` | None | -| PUSHDOWN_FILTERS | No | If true, enables filter pushdown during Parquet decoding. | `'pushdown_filters'` | false | -| REORDER_FILTERS | No | If true, enables heuristic reordering of filters during Parquet decoding. | `'reorder_filters'` | false | -| SCHEMA_FORCE_VIEW_TYPES | No | If true, reads Utf8/Binary columns as view types. | `'schema_force_view_types'` | true | -| BINARY_AS_STRING | No | If true, reads Binary columns as strings. | `'binary_as_string'` | false | -| DATA_PAGESIZE_LIMIT | No | Sets best effort maximum size of data page in bytes. | `'data_pagesize_limit'` | 1048576 | -| DATA_PAGE_ROW_COUNT_LIMIT | No | Sets best effort maximum number of rows in data page. | `'data_page_row_count_limit'` | 20000 | -| DICTIONARY_PAGE_SIZE_LIMIT | No | Sets best effort maximum dictionary page size, in bytes. | `'dictionary_page_size_limit'` | 1048576 | -| WRITE_BATCH_SIZE | No | Sets write_batch_size in bytes. | `'write_batch_size'` | 1024 | -| WRITER_VERSION | No | Sets the Parquet writer version (`1.0` or `2.0`). | `'writer_version'` | 1.0 | -| SKIP_ARROW_METADATA | No | If true, skips writing Arrow schema information into the Parquet file metadata. | `'skip_arrow_metadata'` | false | -| CREATED_BY | No | Sets the "created by" string in the Parquet file metadata. | `'created_by'` | datafusion version X.Y.Z | -| COLUMN_INDEX_TRUNCATE_LENGTH | No | Sets the length (in bytes) to truncate min/max values in column indexes. | `'column_index_truncate_length'` | 64 | -| STATISTICS_TRUNCATE_LENGTH | No | Sets statistics truncate length. | `'statistics_truncate_length'` | None | -| BLOOM_FILTER_ON_WRITE | No | Sets whether bloom filters should be written for all columns by default (can be overridden per column). | `'bloom_filter_on_write'` | false | -| ALLOW_SINGLE_FILE_PARALLELISM | No | Enables parallel serialization of columns in a single file. | `'allow_single_file_parallelism'` | true | -| MAXIMUM_PARALLEL_ROW_GROUP_WRITERS | No | Maximum number of parallel row group writers. | `'maximum_parallel_row_group_writers'` | 1 | -| MAXIMUM_BUFFERED_RECORD_BATCHES_PER_STREAM | No | Maximum number of buffered record batches per stream. | `'maximum_buffered_record_batches_per_stream'` | 2 | -| KEY_VALUE_METADATA | No (Key is specific) | Adds custom key-value pairs to the file metadata. Use the format `'metadata::your_key_name' 'your_value'`. Multiple entries allowed. | `'metadata::key_name'` | None | - -**Example:** - -```sql -CREATE EXTERNAL TABLE t (id bigint, value double, category varchar) -STORED AS PARQUET -LOCATION '/tmp/parquet_data/' -OPTIONS( - 'COMPRESSION::user_id' 'snappy', - 'ENCODING::col_a' 'delta_binary_packed', - 'MAX_ROW_GROUP_SIZE' '1000000', - 'BLOOM_FILTER_ENABLED::id' 'true' -); -``` diff --git a/docs/source/user-guide/sql/index.rst b/docs/source/user-guide/sql/index.rst index a13d40334b639..8e3f51bf8b0bc 100644 --- a/docs/source/user-guide/sql/index.rst +++ b/docs/source/user-guide/sql/index.rst @@ -33,5 +33,5 @@ SQL Reference window_functions scalar_functions special_functions - format_options + write_options prepared_statements diff --git a/docs/source/user-guide/sql/window_functions.md b/docs/source/user-guide/sql/window_functions.md index 68a7003803123..1c02804f0deed 100644 --- a/docs/source/user-guide/sql/window_functions.md +++ b/docs/source/user-guide/sql/window_functions.md @@ -160,31 +160,12 @@ All [aggregate functions](aggregate_functions.md) can be used as window function ### `cume_dist` -Relative rank of the current row: (number of rows preceding or peer with the current row) / (total rows). +Relative rank of the current row: (number of rows preceding or peer with current row) / (total rows). ```sql cume_dist() ``` -#### Example - -```sql - --Example usage of the cume_dist window function: - SELECT salary, - cume_dist() OVER (ORDER BY salary) AS cume_dist - FROM employees; -``` - -```sql -+--------+-----------+ -| salary | cume_dist | -+--------+-----------+ -| 30000 | 0.33 | -| 50000 | 0.67 | -| 70000 | 1.00 | -+--------+-----------+ -``` - ### `dense_rank` Returns the rank of the current row without gaps. This function ranks rows in a dense manner, meaning consecutive ranks are assigned even for identical values. @@ -291,7 +272,7 @@ lead(expression, offset, default) ### `nth_value` -Returns the value evaluated at the nth row of the window frame (counting from 1). Returns NULL if no such row exists. +Returns value evaluated at the row that is the nth row of the window frame (counting from 1); null if no such row. ```sql nth_value(expression, n) @@ -299,37 +280,5 @@ nth_value(expression, n) #### Arguments -- **expression**: The column from which to retrieve the nth value. -- **n**: Integer. Specifies the row number (starting from 1) in the window frame. - -#### Example - -```sql --- Sample employees table: -CREATE TABLE employees (id INT, salary INT); -INSERT INTO employees (id, salary) VALUES -(1, 30000), -(2, 40000), -(3, 50000), -(4, 60000), -(5, 70000); - --- Example usage of nth_value: -SELECT nth_value(salary, 2) OVER ( - ORDER BY salary - ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -) AS nth_value -FROM employees; -``` - -```text -+-----------+ -| nth_value | -+-----------+ -| 40000 | -| 40000 | -| 40000 | -| 40000 | -| 40000 | -+-----------+ -``` +- **expression**: The name the column of which nth value to retrieve +- **n**: Integer. Specifies the n in nth diff --git a/docs/source/user-guide/sql/write_options.md b/docs/source/user-guide/sql/write_options.md new file mode 100644 index 0000000000000..521e29436212d --- /dev/null +++ b/docs/source/user-guide/sql/write_options.md @@ -0,0 +1,127 @@ + + +# Write Options + +DataFusion supports customizing how data is written out to disk as a result of a `COPY` or `INSERT INTO` query. There are a few special options, file format (e.g. CSV or parquet) specific options, and parquet column specific options. Options can also in some cases be specified in multiple ways with a set order of precedence. + +## Specifying Options and Order of Precedence + +Write related options can be specified in the following ways: + +- Session level config defaults +- `CREATE EXTERNAL TABLE` options +- `COPY` option tuples + +For a list of supported session level config defaults see [Configuration Settings](../configs). These defaults apply to all write operations but have the lowest level of precedence. + +If inserting to an external table, table specific write options can be specified when the table is created using the `OPTIONS` clause: + +```sql +CREATE EXTERNAL TABLE + my_table(a bigint, b bigint) + STORED AS csv + COMPRESSION TYPE gzip + LOCATION '/test/location/my_csv_table/' + OPTIONS( + NULL_VALUE 'NAN', + 'has_header' 'true', + 'format.delimiter' ';' + ) +``` + +When running `INSERT INTO my_table ...`, the options from the `CREATE TABLE` will be respected (gzip compression, special delimiter, and header row included). There will be a single output file if the output path doesn't have folder format, i.e. ending with a `\`. Note that compression, header, and delimiter settings can also be specified within the `OPTIONS` tuple list. Dedicated syntax within the SQL statement always takes precedence over arbitrary option tuples, so if both are specified the `OPTIONS` setting will be ignored. NULL_VALUE is a CSV format specific option that determines how null values should be encoded within the CSV file. + +Finally, options can be passed when running a `COPY` command. + + + +```sql +COPY source_table + TO 'test/table_with_options' + PARTITIONED BY (column3, column4) + OPTIONS ( + format parquet, + compression snappy, + 'compression::column1' 'zstd(5)', + ) +``` + +In this example, we write the entirety of `source_table` out to a folder of parquet files. One parquet file will be written in parallel to the folder for each partition in the query. The next option `compression` set to `snappy` indicates that unless otherwise specified all columns should use the snappy compression codec. The option `compression::col1` sets an override, so that the column `col1` in the parquet file will use `ZSTD` compression codec with compression level `5`. In general, parquet options which support column specific settings can be specified with the syntax `OPTION::COLUMN.NESTED.PATH`. + +## Available Options + +### Execution Specific Options + +The following options are available when executing a `COPY` query. + +| Option | Description | Default Value | +| ----------------------------------- | ---------------------------------------------------------------------------------- | ------------- | +| execution.keep_partition_by_columns | Flag to retain the columns in the output data when using `PARTITIONED BY` queries. | false | + +Note: `execution.keep_partition_by_columns` flag can also be enabled through `ExecutionOptions` within `SessionConfig`. + +### JSON Format Specific Options + +The following options are available when writing JSON files. Note: If any unsupported option is specified, an error will be raised and the query will fail. + +| Option | Description | Default Value | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| COMPRESSION | Sets the compression that should be applied to the entire JSON file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | + +### CSV Format Specific Options + +The following options are available when writing CSV files. Note: if any unsupported options is specified an error will be raised and the query will fail. + +| Option | Description | Default Value | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| COMPRESSION | Sets the compression that should be applied to the entire CSV file. Supported values are GZIP, BZIP2, XZ, ZSTD, and UNCOMPRESSED. | UNCOMPRESSED | +| HEADER | Sets if the CSV file should include column headers | false | +| DATE_FORMAT | Sets the format that dates should be encoded in within the CSV file | arrow-rs default | +| DATETIME_FORMAT | Sets the format that datetimes should be encoded in within the CSV file | arrow-rs default | +| TIME_FORMAT | Sets the format that times should be encoded in within the CSV file | arrow-rs default | +| RFC3339 | If true, uses RFC339 format for date and time encodings | arrow-rs default | +| NULL_VALUE | Sets the string which should be used to indicate null values within the CSV file. | arrow-rs default | +| DELIMITER | Sets the character which should be used as the column delimiter within the CSV file. | arrow-rs default | + +### Parquet Format Specific Options + +The following options are available when writing parquet files. If any unsupported option is specified an error will be raised and the query will fail. If a column specific option is specified for a column which does not exist, the option will be ignored without error. For default values, see: [Configuration Settings](https://datafusion.apache.org/user-guide/configs.html). + +| Option | Can be Column Specific? | Description | +| ---------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| COMPRESSION | Yes | Sets the compression codec and if applicable compression level to use | +| MAX_ROW_GROUP_SIZE | No | Sets the maximum number of rows that can be encoded in a single row group. Larger row groups require more memory to write and read. | +| DATA_PAGESIZE_LIMIT | No | Sets the best effort maximum page size in bytes | +| WRITE_BATCH_SIZE | No | Maximum number of rows written for each column in a single batch | +| WRITER_VERSION | No | Parquet writer version (1.0 or 2.0) | +| DICTIONARY_PAGE_SIZE_LIMIT | No | Sets best effort maximum dictionary page size in bytes | +| CREATED_BY | No | Sets the "created by" property in the parquet file | +| COLUMN_INDEX_TRUNCATE_LENGTH | No | Sets the max length of min/max value fields in the column index. | +| DATA_PAGE_ROW_COUNT_LIMIT | No | Sets best effort maximum number of rows in a data page. | +| BLOOM_FILTER_ENABLED | Yes | Sets whether a bloom filter should be written into the file. | +| ENCODING | Yes | Sets the encoding that should be used (e.g. PLAIN or RLE) | +| DICTIONARY_ENABLED | Yes | Sets if dictionary encoding is enabled. Use this instead of ENCODING to set dictionary encoding. | +| STATISTICS_ENABLED | Yes | Sets if statistics are enabled at PAGE or ROW_GROUP level. | +| MAX_STATISTICS_SIZE | Yes | Sets the maximum size in bytes that statistics can take up. | +| BLOOM_FILTER_FPP | Yes | Sets the false positive probability (fpp) for the bloom filter. Implicitly sets BLOOM_FILTER_ENABLED to true. | +| BLOOM_FILTER_NDV | Yes | Sets the number of distinct values (ndv) for the bloom filter. Implicitly sets bloom_filter_enabled to true. | diff --git a/parquet-testing b/parquet-testing index 6e851ddd768d6..f4d7ed772a62a 160000 --- a/parquet-testing +++ b/parquet-testing @@ -1 +1 @@ -Subproject commit 6e851ddd768d6af741c7b15dc594874399fc3cff +Subproject commit f4d7ed772a62a95111db50fbcad2460833e8c882 diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a85e6fa54299d..11f4fb798c376 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -19,5 +19,5 @@ # to compile this workspace and run CI jobs. [toolchain] -channel = "1.86.0" +channel = "1.85.0" components = ["rustfmt", "clippy"] diff --git a/test-utils/src/lib.rs b/test-utils/src/lib.rs index 47f23de4951e3..9db8920833ae5 100644 --- a/test-utils/src/lib.rs +++ b/test-utils/src/lib.rs @@ -67,9 +67,10 @@ pub fn add_empty_batches( .flat_map(|batch| { // insert 0, or 1 empty batches before and after the current batch let empty_batch = RecordBatch::new_empty(schema.clone()); - std::iter::repeat_n(empty_batch.clone(), rng.gen_range(0..2)) + std::iter::repeat(empty_batch.clone()) + .take(rng.gen_range(0..2)) .chain(std::iter::once(batch)) - .chain(std::iter::repeat_n(empty_batch, rng.gen_range(0..2))) + .chain(std::iter::repeat(empty_batch).take(rng.gen_range(0..2))) }) .collect() } From e1f16e4c62a67674a0874d5154683243ed14ae76 Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Mon, 18 Aug 2025 19:40:35 +0900 Subject: [PATCH 07/15] Fix submodules --- datafusion-testing | 2 +- parquet-testing | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/datafusion-testing b/datafusion-testing index 243047b9dd682..aed98a3bd7b7b 160000 --- a/datafusion-testing +++ b/datafusion-testing @@ -1 +1 @@ -Subproject commit 243047b9dd682be688628539c604daaddfe640f9 +Subproject commit aed98a3bd7b7b9dc82da514ec876e8fe6fa7e10e diff --git a/parquet-testing b/parquet-testing index f4d7ed772a62a..107b36603e051 160000 --- a/parquet-testing +++ b/parquet-testing @@ -1 +1 @@ -Subproject commit f4d7ed772a62a95111db50fbcad2460833e8c882 +Subproject commit 107b36603e051aee26bd93e04b871034f6c756c0 From 6c952bf0b65b056093f4381bbe55398c25801563 Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Mon, 18 Aug 2025 20:27:43 +0900 Subject: [PATCH 08/15] Fix broken import --- datafusion/functions-aggregate/src/sum.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafusion/functions-aggregate/src/sum.rs b/datafusion/functions-aggregate/src/sum.rs index 06694d65cb8f3..872cd38b5dc7b 100644 --- a/datafusion/functions-aggregate/src/sum.rs +++ b/datafusion/functions-aggregate/src/sum.rs @@ -17,6 +17,7 @@ //! Defines `SUM` and `SUM DISTINCT` aggregate accumulators +use ahash::RandomState; use datafusion_expr::utils::AggregateOrderSensitivity; use std::any::Any; use std::mem::size_of_val; @@ -24,7 +25,6 @@ use std::mem::size_of_val; use arrow::array::Array; use arrow::array::ArrowNativeTypeOp; use arrow::array::{ArrowNumericType, AsArray}; -use arrow::datatypes::ArrowPrimitiveType; use arrow::datatypes::{ArrowNativeType, FieldRef}; use arrow::datatypes::{ DataType, Decimal128Type, Decimal256Type, Float64Type, Int64Type, UInt64Type, From af66e5fcf132ff812c3c4b2a1c36cf74b3a1c1af Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Tue, 19 Aug 2025 20:24:48 +0900 Subject: [PATCH 09/15] Fix state_fields for distinct avg --- .../src/aggregate/avg_distinct/numeric.rs | 22 ++++----- .../src/aggregate/sum_distinct/numeric.rs | 6 +-- datafusion/functions-aggregate/src/average.rs | 46 +++++++++++-------- datafusion/functions-aggregate/src/sum.rs | 2 +- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs index c9fb14fb10691..bb43acc2614f9 100644 --- a/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs +++ b/datafusion/functions-aggregate-common/src/aggregate/avg_distinct/numeric.rs @@ -19,7 +19,7 @@ use std::fmt::Debug; use arrow::array::ArrayRef; use arrow::datatypes::{DataType, Float64Type}; -use datafusion_common::ScalarValue; +use datafusion_common::{Result, ScalarValue}; use datafusion_expr_common::accumulator::Accumulator; use crate::aggregate::sum_distinct::DistinctSumAccumulator; @@ -32,30 +32,30 @@ pub struct Float64DistinctAvgAccumulator { sum_accumulator: DistinctSumAccumulator, } -impl Float64DistinctAvgAccumulator { - pub fn new() -> datafusion_common::Result { - Ok(Self { - sum_accumulator: DistinctSumAccumulator::::try_new( +impl Default for Float64DistinctAvgAccumulator { + fn default() -> Self { + Self { + sum_accumulator: DistinctSumAccumulator::::new( &DataType::Float64, - )?, - }) + ), + } } } impl Accumulator for Float64DistinctAvgAccumulator { - fn state(&mut self) -> datafusion_common::Result> { + fn state(&mut self) -> Result> { self.sum_accumulator.state() } - fn update_batch(&mut self, values: &[ArrayRef]) -> datafusion_common::Result<()> { + fn update_batch(&mut self, values: &[ArrayRef]) -> Result<()> { self.sum_accumulator.update_batch(values) } - fn merge_batch(&mut self, states: &[ArrayRef]) -> datafusion_common::Result<()> { + fn merge_batch(&mut self, states: &[ArrayRef]) -> Result<()> { self.sum_accumulator.merge_batch(states) } - fn evaluate(&mut self) -> datafusion_common::Result { + fn evaluate(&mut self) -> Result { // Get the sum from the DistinctSumAccumulator let sum_result = self.sum_accumulator.evaluate()?; diff --git a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs index 859c82d95660b..3021783a2a79c 100644 --- a/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs +++ b/datafusion/functions-aggregate-common/src/aggregate/sum_distinct/numeric.rs @@ -49,11 +49,11 @@ impl Debug for DistinctSumAccumulator { } impl DistinctSumAccumulator { - pub fn try_new(data_type: &DataType) -> Result { - Ok(Self { + pub fn new(data_type: &DataType) -> Self { + Self { values: HashSet::default(), data_type: data_type.clone(), - }) + } } pub fn distinct_count(&self) -> usize { diff --git a/datafusion/functions-aggregate/src/average.rs b/datafusion/functions-aggregate/src/average.rs index bbe3503fa7a5c..309ebd950dfba 100644 --- a/datafusion/functions-aggregate/src/average.rs +++ b/datafusion/functions-aggregate/src/average.rs @@ -118,15 +118,14 @@ impl AggregateUDFImpl for Avg { let data_type = acc_args.exprs[0].data_type(acc_args.schema)?; use DataType::*; + // instantiate specialized accumulator based for the type if acc_args.is_distinct { - // instantiate specialized accumulator based for the type match &data_type { // Numeric types are converted to Float64 via `coerce_avg_type` during logical plan creation - Float64 => Ok(Box::new(Float64DistinctAvgAccumulator::new()?)), + Float64 => Ok(Box::new(Float64DistinctAvgAccumulator::default())), _ => exec_err!("AVG(DISTINCT) for {} not supported", data_type), } } else { - // instantiate specialized accumulator based for the type match (&data_type, acc_args.return_field.data_type()) { (Float64, Float64) => Ok(Box::::default()), ( @@ -172,21 +171,32 @@ impl AggregateUDFImpl for Avg { } fn state_fields(&self, args: StateFieldsArgs) -> Result> { - Ok(vec![ - Field::new( - format_state_name(args.name, "count"), - DataType::UInt64, - true, - ), - Field::new( - format_state_name(args.name, "sum"), - args.input_fields[0].data_type().clone(), - true, - ), - ] - .into_iter() - .map(Arc::new) - .collect()) + if args.is_distinct { + // Copied from datafusion_functions_aggregate::sum::Sum::state_fields + // since the accumulator uses DistinctSumAccumulator internally. + Ok(vec![Field::new_list( + format_state_name(args.name, "sum distinct"), + Field::new_list_field(args.return_type().clone(), true), + false, + ) + .into()]) + } else { + Ok(vec![ + Field::new( + format_state_name(args.name, "count"), + DataType::UInt64, + true, + ), + Field::new( + format_state_name(args.name, "sum"), + args.input_fields[0].data_type().clone(), + true, + ), + ] + .into_iter() + .map(Arc::new) + .collect()) + } } fn groups_accumulator_supported(&self, args: AccumulatorArgs) -> bool { diff --git a/datafusion/functions-aggregate/src/sum.rs b/datafusion/functions-aggregate/src/sum.rs index 872cd38b5dc7b..445c7dfe6b7af 100644 --- a/datafusion/functions-aggregate/src/sum.rs +++ b/datafusion/functions-aggregate/src/sum.rs @@ -185,7 +185,7 @@ impl AggregateUDFImpl for Sum { if args.is_distinct { macro_rules! helper { ($t:ty, $dt:expr) => { - Ok(Box::new(DistinctSumAccumulator::<$t>::try_new(&$dt)?)) + Ok(Box::new(DistinctSumAccumulator::<$t>::new(&$dt))) }; } downcast_sum!(args, helper) From 228679b0283f7890126cc8caff260d0381f92414 Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Wed, 20 Aug 2025 12:17:24 +0900 Subject: [PATCH 10/15] Update SLT test files --- .../sqllogictest/test_files/aggregate.slt | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/datafusion/sqllogictest/test_files/aggregate.slt b/datafusion/sqllogictest/test_files/aggregate.slt index f3a5e2fad476d..3737d32bbaf09 100644 --- a/datafusion/sqllogictest/test_files/aggregate.slt +++ b/datafusion/sqllogictest/test_files/aggregate.slt @@ -7270,38 +7270,6 @@ SELECT a, median(b), arrow_typeof(median(b)) FROM group_median_all_nulls GROUP B group0 NULL Int32 group1 NULL Int32 -statement ok -create table t_decimal (c decimal(10, 4)) as values (100.00), (125.00), (175.00), (200.00), (200.00), (300.00), (null), (null); - -# Test avg_distinct for Decimal128 -query RT -select avg(distinct c), arrow_typeof(avg(distinct c)) from t_decimal; ----- -180 Decimal128(14, 8) - -statement ok -drop table t_decimal; - -# Test avg_distinct for Decimal256 -statement ok -create table t_decimal256 (c decimal(50, 2)) as values - (100.00), - (125.00), - (175.00), - (200.00), - (200.00), - (300.00), - (null), - (null); - -query RT -select avg(distinct c), arrow_typeof(avg(distinct c)) from t_decimal256; ----- -180 Decimal256(54, 6) - -statement ok -drop table t_decimal256; - query I with test AS (SELECT i as c1, i + 1 as c2 FROM generate_series(1, 10) t(i)) select count(*) from test WHERE 1 = 1; @@ -7421,3 +7389,35 @@ FROM (VALUES ('a'), ('d'), ('c'), ('a')) t(a_varchar); query error Error during planning: ORDER BY and WITHIN GROUP clauses cannot be used together in the same aggregate function SELECT array_agg(a_varchar order by a_varchar) WITHIN GROUP (ORDER BY a_varchar) FROM (VALUES ('a'), ('d'), ('c'), ('a')) t(a_varchar); + +# distinct average +statement ok +create table distinct_avg (a int, b int) as values + (null, null), + (1, 1), + (2, 2), + (3, 3), + (4, 4), + (5, 5), + (5, 5), + (5, 5), + (5, 5), + (5, 5), + (5, 5) +; + +# Need two columns to ensure single_distinct_to_group_by rule doesn't kick in, so we know our actual avg(distinct) code is being tested +query RTRTR +select + avg(distinct a), + arrow_typeof(avg(distinct a)), + avg(distinct b), + arrow_typeof(avg(distinct b)), + avg(a) +from distinct_avg; +---- +3 Float64 3 Float64 4 + +statement ok +drop table distinct_avg; + From 7b7ffbf4b4ff5d3837835053c52103dd2b9e627c Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Wed, 20 Aug 2025 12:24:35 +0900 Subject: [PATCH 11/15] Added null case for SLT test --- datafusion/sqllogictest/test_files/aggregate.slt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/datafusion/sqllogictest/test_files/aggregate.slt b/datafusion/sqllogictest/test_files/aggregate.slt index 3737d32bbaf09..4fa6c24a9c33a 100644 --- a/datafusion/sqllogictest/test_files/aggregate.slt +++ b/datafusion/sqllogictest/test_files/aggregate.slt @@ -7418,6 +7418,15 @@ from distinct_avg; ---- 3 Float64 3 Float64 4 +query RR +select + avg(distinct a), + avg(distinct b) +from distinct_avg +where a is null; +---- +NULL NULL + statement ok drop table distinct_avg; From bc121fbf46639bc6dde7791658cb2f624fc3ea8c Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Wed, 20 Aug 2025 17:56:56 +0900 Subject: [PATCH 12/15] Disable group accumulator support for avg(distinct) and add group by test case --- datafusion/functions-aggregate/src/average.rs | 2 +- datafusion/sqllogictest/test_files/aggregate.slt | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/datafusion/functions-aggregate/src/average.rs b/datafusion/functions-aggregate/src/average.rs index 309ebd950dfba..2520c60bee309 100644 --- a/datafusion/functions-aggregate/src/average.rs +++ b/datafusion/functions-aggregate/src/average.rs @@ -203,7 +203,7 @@ impl AggregateUDFImpl for Avg { matches!( args.return_field.data_type(), DataType::Float64 | DataType::Decimal128(_, _) | DataType::Duration(_) - ) + ) && !args.is_distinct } fn create_groups_accumulator( diff --git a/datafusion/sqllogictest/test_files/aggregate.slt b/datafusion/sqllogictest/test_files/aggregate.slt index 4fa6c24a9c33a..439f54143df45 100644 --- a/datafusion/sqllogictest/test_files/aggregate.slt +++ b/datafusion/sqllogictest/test_files/aggregate.slt @@ -7418,6 +7418,20 @@ from distinct_avg; ---- 3 Float64 3 Float64 4 +query RR rowsort +select + avg(distinct a), + avg(distinct b) +from distinct_avg +group by b; +---- +1 1 +2 2 +3 3 +4 4 +5 5 +NULL NULL + query RR select avg(distinct a), From b9c458f0a62791eaa6dca6120d9fc087b20cbe61 Mon Sep 17 00:00:00 2001 From: Jeffrey Vo Date: Sat, 23 Aug 2025 14:18:49 +1000 Subject: [PATCH 13/15] Fix state field name for avg distinct Co-authored-by: Andrew Lamb --- datafusion/functions-aggregate/src/average.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafusion/functions-aggregate/src/average.rs b/datafusion/functions-aggregate/src/average.rs index 2520c60bee309..f7cb74fd55a25 100644 --- a/datafusion/functions-aggregate/src/average.rs +++ b/datafusion/functions-aggregate/src/average.rs @@ -175,7 +175,7 @@ impl AggregateUDFImpl for Avg { // Copied from datafusion_functions_aggregate::sum::Sum::state_fields // since the accumulator uses DistinctSumAccumulator internally. Ok(vec![Field::new_list( - format_state_name(args.name, "sum distinct"), + format_state_name(args.name, "avg distinct"), Field::new_list_field(args.return_type().clone(), true), false, ) From 3abb4b729b87c80a6c6083caf2c68f221d97b96c Mon Sep 17 00:00:00 2001 From: Jefffrey Date: Sat, 23 Aug 2025 18:54:30 +0900 Subject: [PATCH 14/15] Mix up tests --- .../sqllogictest/test_files/aggregate.slt | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/datafusion/sqllogictest/test_files/aggregate.slt b/datafusion/sqllogictest/test_files/aggregate.slt index 439f54143df45..35b2a6c03b399 100644 --- a/datafusion/sqllogictest/test_files/aggregate.slt +++ b/datafusion/sqllogictest/test_files/aggregate.slt @@ -7392,31 +7392,33 @@ FROM (VALUES ('a'), ('d'), ('c'), ('a')) t(a_varchar); # distinct average statement ok -create table distinct_avg (a int, b int) as values - (null, null), - (1, 1), - (2, 2), - (3, 3), - (4, 4), - (5, 5), - (5, 5), - (5, 5), - (5, 5), - (5, 5), - (5, 5) +create table distinct_avg (a int, b double) as values + (3, null), + (2, null), + (5, 100.5), + (5, 1.0), + (5, 44.112), + (null, 1.0), + (5, 100.5), + (1, 4.09), + (5, 100.5), + (5, 100.5), + (4, null), + (null, null) ; # Need two columns to ensure single_distinct_to_group_by rule doesn't kick in, so we know our actual avg(distinct) code is being tested -query RTRTR +query RTRTRR select avg(distinct a), arrow_typeof(avg(distinct a)), avg(distinct b), arrow_typeof(avg(distinct b)), - avg(a) + avg(a), + avg(b) from distinct_avg; ---- -3 Float64 3 Float64 4 +3 Float64 37.4255 Float64 4 56.52525 query RR rowsort select @@ -7425,19 +7427,18 @@ select from distinct_avg group by b; ---- -1 1 -2 2 -3 3 -4 4 -5 5 -NULL NULL +1 4.09 +3 NULL +5 1 +5 100.5 +5 44.112 query RR select avg(distinct a), avg(distinct b) from distinct_avg -where a is null; +where a is null and b is null; ---- NULL NULL From 2fd3a33c08ea1eb07ba9ae7f0de22b94805a4b52 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Sat, 23 Aug 2025 07:36:38 -0400 Subject: [PATCH 15/15] Update datafusion-tesitng pin --- datafusion-testing | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/datafusion-testing b/datafusion-testing index aed98a3bd7b7b..f72ac4075ada5 160000 --- a/datafusion-testing +++ b/datafusion-testing @@ -1 +1 @@ -Subproject commit aed98a3bd7b7b9dc82da514ec876e8fe6fa7e10e +Subproject commit f72ac4075ada5ea9810551bc0c3e3161c61204a2