| // 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. |
| |
| // Include tests in dataframe_functions |
| mod dataframe_functions; |
| mod describe; |
| |
| use arrow::array::{ |
| record_batch, Array, ArrayRef, BooleanArray, DictionaryArray, FixedSizeListArray, |
| FixedSizeListBuilder, Float32Array, Float64Array, Int32Array, Int32Builder, |
| Int8Array, LargeListArray, ListArray, ListBuilder, RecordBatch, StringArray, |
| StringBuilder, StructBuilder, UInt32Array, UInt32Builder, UnionArray, |
| }; |
| use arrow::buffer::ScalarBuffer; |
| use arrow::datatypes::{ |
| DataType, Field, Float32Type, Int32Type, Schema, UInt64Type, UnionFields, UnionMode, |
| }; |
| use arrow::error::ArrowError; |
| use arrow::util::pretty::pretty_format_batches; |
| use arrow_schema::{SortOptions, TimeUnit}; |
| use datafusion::{assert_batches_eq, dataframe}; |
| use datafusion_common::metadata::FieldMetadata; |
| use datafusion_functions_aggregate::count::{count_all, count_all_window}; |
| use datafusion_functions_aggregate::expr_fn::{ |
| array_agg, avg, avg_distinct, count, count_distinct, max, median, min, sum, |
| sum_distinct, |
| }; |
| use datafusion_functions_nested::make_array::make_array_udf; |
| use datafusion_functions_window::expr_fn::{first_value, lead, row_number}; |
| use insta::assert_snapshot; |
| use object_store::local::LocalFileSystem; |
| use std::collections::HashMap; |
| use std::fs; |
| use std::path::Path; |
| use std::sync::Arc; |
| use tempfile::TempDir; |
| use url::Url; |
| |
| use datafusion::dataframe::{DataFrame, DataFrameWriteOptions}; |
| use datafusion::datasource::MemTable; |
| use datafusion::error::Result; |
| use datafusion::execution::context::SessionContext; |
| use datafusion::execution::session_state::SessionStateBuilder; |
| use datafusion::logical_expr::{ColumnarValue, Volatility}; |
| use datafusion::prelude::{ |
| CsvReadOptions, JoinType, NdJsonReadOptions, ParquetReadOptions, |
| }; |
| use datafusion::test_util::{ |
| parquet_test_data, populate_csv_partitions, register_aggregate_csv, test_table, |
| test_table_with_name, |
| }; |
| use datafusion_catalog::TableProvider; |
| use datafusion_common::test_util::{batches_to_sort_string, batches_to_string}; |
| use datafusion_common::{ |
| assert_contains, internal_datafusion_err, Constraint, Constraints, DFSchema, |
| DataFusionError, ScalarValue, TableReference, UnnestOptions, |
| }; |
| use datafusion_common_runtime::SpawnedTask; |
| use datafusion_datasource::file_format::format_as_file_type; |
| use datafusion_execution::config::SessionConfig; |
| use datafusion_execution::runtime_env::RuntimeEnv; |
| use datafusion_expr::expr::{GroupingSet, NullTreatment, Sort, WindowFunction}; |
| use datafusion_expr::var_provider::{VarProvider, VarType}; |
| use datafusion_expr::{ |
| cast, col, create_udf, exists, in_subquery, lit, out_ref_col, placeholder, |
| scalar_subquery, when, wildcard, Expr, ExprFunctionExt, ExprSchemable, LogicalPlan, |
| LogicalPlanBuilder, ScalarFunctionImplementation, SortExpr, TableType, WindowFrame, |
| WindowFrameBound, WindowFrameUnits, WindowFunctionDefinition, |
| }; |
| use datafusion_physical_expr::aggregate::AggregateExprBuilder; |
| use datafusion_physical_expr::expressions::Column; |
| use datafusion_physical_expr::Partitioning; |
| use datafusion_physical_expr_common::physical_expr::PhysicalExpr; |
| use datafusion_physical_expr_common::sort_expr::PhysicalSortExpr; |
| use datafusion_physical_plan::aggregates::{ |
| AggregateExec, AggregateMode, PhysicalGroupBy, |
| }; |
| use datafusion_physical_plan::empty::EmptyExec; |
| use datafusion_physical_plan::{displayable, ExecutionPlan, ExecutionPlanProperties}; |
| |
| use datafusion::error::Result as DataFusionResult; |
| use datafusion_functions_window::expr_fn::lag; |
| |
| // Get string representation of the plan |
| async fn physical_plan_to_string(df: &DataFrame) -> String { |
| let physical_plan = df |
| .clone() |
| .create_physical_plan() |
| .await |
| .expect("Error creating physical plan"); |
| |
| let formatted = displayable(physical_plan.as_ref()).indent(true); |
| formatted.to_string() |
| } |
| |
| pub fn table_with_constraints() -> Arc<dyn TableProvider> { |
| let dual_schema = Arc::new(Schema::new(vec![ |
| Field::new("id", DataType::Int32, false), |
| Field::new("name", DataType::Utf8, false), |
| ])); |
| let batch = RecordBatch::try_new( |
| dual_schema.clone(), |
| vec![ |
| Arc::new(Int32Array::from(vec![1])), |
| Arc::new(StringArray::from(vec!["a"])), |
| ], |
| ) |
| .unwrap(); |
| let provider = MemTable::try_new(dual_schema, vec![vec![batch]]) |
| .unwrap() |
| .with_constraints(Constraints::new_unverified(vec![Constraint::PrimaryKey( |
| vec![0], |
| )])); |
| Arc::new(provider) |
| } |
| |
| async fn assert_logical_expr_schema_eq_physical_expr_schema(df: DataFrame) -> Result<()> { |
| let logical_expr_schema = Arc::clone(df.schema().inner()); |
| let batches = df.collect().await?; |
| let physical_expr_schema = batches[0].schema(); |
| assert_eq!(logical_expr_schema, physical_expr_schema); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_array_agg_ord_schema() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let create_table_query = r#" |
| CREATE TABLE test_table ( |
| "double_field" DOUBLE, |
| "string_field" VARCHAR |
| ) AS VALUES |
| (1.0, 'a'), |
| (2.0, 'b'), |
| (3.0, 'c') |
| "#; |
| ctx.sql(create_table_query).await?; |
| |
| let query = r#"SELECT |
| array_agg("double_field" ORDER BY "string_field") as "double_field", |
| array_agg("string_field" ORDER BY "string_field") as "string_field" |
| FROM test_table"#; |
| |
| let result = ctx.sql(query).await?; |
| assert_logical_expr_schema_eq_physical_expr_schema(result).await?; |
| Ok(()) |
| } |
| |
| type WindowFnCase = (fn() -> Expr, &'static str); |
| |
| #[tokio::test] |
| async fn with_column_window_functions() -> DataFusionResult<()> { |
| let schema = Schema::new(vec![Field::new("a", DataType::Int32, true)]); |
| |
| let batch = RecordBatch::try_new( |
| Arc::new(schema.clone()), |
| vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5]))], |
| )?; |
| |
| let ctx = SessionContext::new(); |
| |
| let provider = MemTable::try_new(Arc::new(schema), vec![vec![batch]])?; |
| ctx.register_table("t", Arc::new(provider))?; |
| |
| // Define test cases: (expr builder, alias name) |
| let test_cases: Vec<WindowFnCase> = vec![ |
| (|| lag(col("a"), Some(1), None), "lag_val"), |
| (|| lead(col("a"), Some(1), None), "lead_val"), |
| (row_number, "row_num"), |
| ]; |
| |
| for (make_expr, alias) in test_cases { |
| let df = ctx.table("t").await?; |
| let expr = make_expr(); |
| let df_with = df.with_column(alias, expr)?; |
| let df_schema = df_with.schema().clone(); |
| |
| assert!( |
| df_schema.has_column_with_unqualified_name(alias), |
| "Schema does not contain expected column {alias}", |
| ); |
| |
| assert_eq!(2, df_schema.columns().len()); |
| } |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_coalesce_schema() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let query = r#"SELECT COALESCE(null, 5)"#; |
| |
| let result = ctx.sql(query).await?; |
| assert_logical_expr_schema_eq_physical_expr_schema(result).await?; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_coalesce_from_values_schema() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let query = r#"SELECT COALESCE(column1, column2) FROM VALUES (null, 1.2)"#; |
| |
| let result = ctx.sql(query).await?; |
| assert_logical_expr_schema_eq_physical_expr_schema(result).await?; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_coalesce_from_values_schema_multiple_rows() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let query = r#"SELECT COALESCE(column1, column2) |
| FROM VALUES |
| (null, 1.2), |
| (1.1, null), |
| (2, 5);"#; |
| |
| let result = ctx.sql(query).await?; |
| assert_logical_expr_schema_eq_physical_expr_schema(result).await?; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_array_agg_schema() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let create_table_query = r#" |
| CREATE TABLE test_table ( |
| "double_field" DOUBLE, |
| "string_field" VARCHAR |
| ) AS VALUES |
| (1.0, 'a'), |
| (2.0, 'b'), |
| (3.0, 'c') |
| "#; |
| ctx.sql(create_table_query).await?; |
| |
| let query = r#"SELECT |
| array_agg("double_field") as "double_field", |
| array_agg("string_field") as "string_field" |
| FROM test_table"#; |
| |
| let result = ctx.sql(query).await?; |
| assert_logical_expr_schema_eq_physical_expr_schema(result).await?; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_array_agg_distinct_schema() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let create_table_query = r#" |
| CREATE TABLE test_table ( |
| "double_field" DOUBLE, |
| "string_field" VARCHAR |
| ) AS VALUES |
| (1.0, 'a'), |
| (2.0, 'b'), |
| (2.0, 'a') |
| "#; |
| ctx.sql(create_table_query).await?; |
| |
| let query = r#"SELECT |
| array_agg(distinct "double_field") as "double_field", |
| array_agg(distinct "string_field") as "string_field" |
| FROM test_table"#; |
| |
| let result = ctx.sql(query).await?; |
| assert_logical_expr_schema_eq_physical_expr_schema(result).await?; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_columns() -> Result<()> { |
| // build plan using Table API |
| |
| let t = test_table().await?; |
| let t2 = t.select_columns(&["c1", "c2", "c11"])?; |
| let plan = t2.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = create_plan("SELECT c1, c2, c11 FROM aggregate_test_100").await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_expr() -> Result<()> { |
| // build plan using Table API |
| let t = test_table().await?; |
| let t2 = t.select(vec![col("c1"), col("c2"), col("c11")])?; |
| let plan = t2.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = create_plan("SELECT c1, c2, c11 FROM aggregate_test_100").await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_all() -> Result<()> { |
| let t = test_table().await?; |
| let plan = t.select([wildcard()])?.logical_plan().clone(); |
| let sql_plan = create_plan("SELECT * FROM aggregate_test_100").await?; |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_exprs() -> Result<()> { |
| // build plan using `select_expr`` |
| let t = test_table().await?; |
| let plan = t |
| .clone() |
| .select_exprs(&["c1", "c2", "c11", "c2 * c11"])? |
| .logical_plan() |
| .clone(); |
| |
| // build plan using select |
| let expected_plan = t |
| .select(vec![ |
| col("c1"), |
| col("c2"), |
| col("c11"), |
| col("c2") * col("c11"), |
| ])? |
| .logical_plan() |
| .clone(); |
| |
| assert_same_plan(&expected_plan, &plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_with_window_exprs() -> Result<()> { |
| // build plan using Table API |
| let t = test_table().await?; |
| let first_row = first_value(col("aggregate_test_100.c1")) |
| .partition_by(vec![col("aggregate_test_100.c2")]) |
| .build() |
| .unwrap(); |
| let t2 = t.select(vec![col("c1"), first_row])?; |
| let plan = t2.logical_plan().clone(); |
| |
| let sql_plan = create_plan( |
| "select c1, first_value(c1) over (partition by c2) from aggregate_test_100", |
| ) |
| .await?; |
| |
| assert_same_plan(&plan, &sql_plan); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_with_periods() -> Result<()> { |
| // define data with a column name that has a "." in it: |
| let array: Int32Array = [1, 10].into_iter().collect(); |
| let batch = RecordBatch::try_from_iter(vec![("f.c1", Arc::new(array) as _)])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch)?; |
| |
| let df = ctx.table("t").await?.select_columns(&["f.c1"])?; |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +------+ |
| | f.c1 | |
| +------+ |
| | 1 | |
| | 10 | |
| +------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_columns_duplicated_names_from_different_qualifiers() -> Result<()> { |
| let t1 = test_table_with_name("t1") |
| .await? |
| .select_columns(&["c1"])? |
| .limit(0, Some(3))?; |
| let t2 = test_table_with_name("t2") |
| .await? |
| .select_columns(&["c1"])? |
| .limit(3, Some(3))?; |
| let t3 = test_table_with_name("t3") |
| .await? |
| .select_columns(&["c1"])? |
| .limit(6, Some(3))?; |
| |
| let join_res = t1 |
| .join(t2, JoinType::Left, &["t1.c1"], &["t2.c1"], None)? |
| .join(t3, JoinType::Left, &["t1.c1"], &["t3.c1"], None)?; |
| assert_snapshot!( |
| batches_to_sort_string(&join_res.clone().collect().await.unwrap()), |
| @r" |
| +----+----+----+ |
| | c1 | c1 | c1 | |
| +----+----+----+ |
| | b | b | | |
| | b | b | | |
| | c | | | |
| | d | | d | |
| +----+----+----+ |
| " |
| ); |
| |
| let select_res = join_res.select_columns(&["c1"])?; |
| assert_snapshot!( |
| batches_to_sort_string(&select_res.clone().collect().await.unwrap()), |
| @r" |
| +----+----+----+ |
| | c1 | c1 | c1 | |
| +----+----+----+ |
| | b | b | | |
| | b | b | | |
| | c | | | |
| | d | | d | |
| +----+----+----+ |
| " |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn drop_columns() -> Result<()> { |
| // build plan using Table API |
| let t = test_table().await?; |
| let t2 = t.drop_columns(&["c2", "c11"])?; |
| let plan = t2.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = |
| create_plan("SELECT c1,c3,c4,c5,c6,c7,c8,c9,c10,c12,c13 FROM aggregate_test_100") |
| .await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn drop_columns_with_duplicates() -> Result<()> { |
| // build plan using Table API |
| let t = test_table().await?; |
| let t2 = t.drop_columns(&["c2", "c11", "c2", "c2"])?; |
| let plan = t2.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = |
| create_plan("SELECT c1,c3,c4,c5,c6,c7,c8,c9,c10,c12,c13 FROM aggregate_test_100") |
| .await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn drop_columns_with_nonexistent_columns() -> Result<()> { |
| // build plan using Table API |
| let t = test_table().await?; |
| let t2 = t.drop_columns(&["canada", "c2", "rocks"])?; |
| let plan = t2.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = create_plan( |
| "SELECT c1,c3,c4,c5,c6,c7,c8,c9,c10,c11,c12,c13 FROM aggregate_test_100", |
| ) |
| .await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn drop_columns_with_empty_array() -> Result<()> { |
| // build plan using Table API |
| let t = test_table().await?; |
| let t2 = t.drop_columns(&[])?; |
| let plan = t2.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = create_plan( |
| "SELECT c1,c2,c3,c4,c5,c6,c7,c8,c9,c10,c11,c12,c13 FROM aggregate_test_100", |
| ) |
| .await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn drop_with_quotes() -> Result<()> { |
| // define data with a column name that has a "." in it: |
| let array1: Int32Array = [1, 10].into_iter().collect(); |
| let array2: Int32Array = [2, 11].into_iter().collect(); |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("f\"c1", Arc::new(array1) as _), |
| ("f\"c2", Arc::new(array2) as _), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch)?; |
| |
| let df = ctx.table("t").await?.drop_columns(&["f\"c1"])?; |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +------+ |
| | f"c2 | |
| +------+ |
| | 11 | |
| | 2 | |
| +------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn drop_with_periods() -> Result<()> { |
| // define data with a column name that has a "." in it: |
| let array1: Int32Array = [1, 10].into_iter().collect(); |
| let array2: Int32Array = [2, 11].into_iter().collect(); |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("f.c1", Arc::new(array1) as _), |
| ("f.c2", Arc::new(array2) as _), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch)?; |
| |
| let df = ctx.table("t").await?.drop_columns(&["f.c1"])?; |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +------+ |
| | f.c2 | |
| +------+ |
| | 11 | |
| | 2 | |
| +------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn drop_columns_duplicated_names_from_different_qualifiers() -> Result<()> { |
| let t1 = test_table_with_name("t1") |
| .await? |
| .select_columns(&["c1"])? |
| .limit(0, Some(3))?; |
| let t2 = test_table_with_name("t2") |
| .await? |
| .select_columns(&["c1"])? |
| .limit(3, Some(3))?; |
| let t3 = test_table_with_name("t3") |
| .await? |
| .select_columns(&["c1"])? |
| .limit(6, Some(3))?; |
| |
| let join_res = t1 |
| .join(t2, JoinType::LeftMark, &["c1"], &["c1"], None)? |
| .join(t3, JoinType::LeftMark, &["c1"], &["c1"], None)?; |
| assert_snapshot!( |
| batches_to_sort_string(&join_res.clone().collect().await.unwrap()), |
| @r" |
| +----+-------+-------+ |
| | c1 | mark | mark | |
| +----+-------+-------+ |
| | b | true | false | |
| | c | false | false | |
| | d | false | true | |
| +----+-------+-------+ |
| " |
| ); |
| |
| let drop_res = join_res.drop_columns(&["mark"])?; |
| assert_snapshot!( |
| batches_to_sort_string(&drop_res.clone().collect().await.unwrap()), |
| @r" |
| +----+ |
| | c1 | |
| +----+ |
| | b | |
| | c | |
| | d | |
| +----+ |
| " |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn aggregate() -> Result<()> { |
| // build plan using DataFrame API |
| // union so some of the distincts have a clearly distinct result |
| let df = test_table().await?.union(test_table().await?)?; |
| let group_expr = vec![col("c1")]; |
| let aggr_expr = vec![ |
| min(col("c4")).alias("min(c4)"), |
| max(col("c4")).alias("max(c4)"), |
| avg(col("c4")).alias("avg(c4)"), |
| avg_distinct(col("c4")).alias("avg_distinct(c4)"), |
| sum(col("c4")).alias("sum(c4)"), |
| sum_distinct(col("c4")).alias("sum_distinct(c4)"), |
| count(col("c4")).alias("count(c4)"), |
| count_distinct(col("c4")).alias("count_distinct(c4)"), |
| ]; |
| |
| let df: Vec<RecordBatch> = df.aggregate(group_expr, aggr_expr)?.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df), |
| @r" |
| +----+---------+---------+---------------------+---------------------+---------+------------------+-----------+--------------------+ |
| | c1 | min(c4) | max(c4) | avg(c4) | avg_distinct(c4) | sum(c4) | sum_distinct(c4) | count(c4) | count_distinct(c4) | |
| +----+---------+---------+---------------------+---------------------+---------+------------------+-----------+--------------------+ |
| | a | -28462 | 32064 | 306.04761904761904 | 306.04761904761904 | 12854 | 6427 | 42 | 21 | |
| | b | -28070 | 25286 | 7732.315789473684 | 7732.315789473684 | 293828 | 146914 | 38 | 19 | |
| | c | -30508 | 29106 | -1320.5238095238096 | -1320.5238095238096 | -55462 | -27731 | 42 | 21 | |
| | d | -24558 | 31106 | 10890.111111111111 | 10890.111111111111 | 392044 | 196022 | 36 | 18 | |
| | e | -31500 | 32514 | -4268.333333333333 | -4268.333333333333 | -179270 | -89635 | 42 | 21 | |
| +----+---------+---------+---------------------+---------------------+---------+------------------+-----------+--------------------+ |
| " |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn aggregate_assert_no_empty_batches() -> Result<()> { |
| // build plan using DataFrame API |
| let df = test_table().await?; |
| let group_expr = vec![col("c1")]; |
| let aggr_expr = vec![ |
| min(col("c12")), |
| max(col("c12")), |
| avg(col("c12")), |
| avg_distinct(col("c12")), |
| sum(col("c12")), |
| sum_distinct(col("c12")), |
| count(col("c12")), |
| count_distinct(col("c12")), |
| median(col("c12")), |
| ]; |
| |
| let df: Vec<RecordBatch> = df.aggregate(group_expr, aggr_expr)?.collect().await?; |
| // Empty batches should not be produced |
| for batch in df { |
| assert!(batch.num_rows() > 0); |
| } |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_with_pk() -> Result<()> { |
| // create the dataframe |
| let config = SessionConfig::new().with_target_partitions(1); |
| let ctx = SessionContext::new_with_config(config); |
| |
| let df = ctx.read_table(table_with_constraints())?; |
| |
| // GROUP BY id |
| let group_expr = vec![col("id")]; |
| let aggr_expr = vec![]; |
| let df = df.aggregate(group_expr, aggr_expr)?; |
| |
| // Since id and name are functionally dependant, we can use name among |
| // expression even if it is not part of the group by expression and can |
| // select "name" column even though it wasn't explicitly grouped |
| let df = df.select(vec![col("id"), col("name")])?; |
| |
| assert_snapshot!( |
| physical_plan_to_string(&df).await, |
| @r###" |
| AggregateExec: mode=Single, gby=[id@0 as id, name@1 as name], aggr=[] |
| DataSourceExec: partitions=1, partition_sizes=[1] |
| "### |
| ); |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+------+ |
| | id | name | |
| +----+------+ |
| | 1 | a | |
| +----+------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_with_pk2() -> Result<()> { |
| // create the dataframe |
| let config = SessionConfig::new().with_target_partitions(1); |
| let ctx = SessionContext::new_with_config(config); |
| |
| let df = ctx.read_table(table_with_constraints())?; |
| |
| // GROUP BY id |
| let group_expr = vec![col("id")]; |
| let aggr_expr = vec![]; |
| let df = df.aggregate(group_expr, aggr_expr)?; |
| |
| // Predicate refers to id, and name fields: |
| // id = 1 AND name = 'a' |
| let predicate = col("id").eq(lit(1i32)).and(col("name").eq(lit("a"))); |
| let df = df.filter(predicate)?; |
| assert_snapshot!( |
| physical_plan_to_string(&df).await, |
| @r" |
| AggregateExec: mode=Single, gby=[id@0 as id, name@1 as name], aggr=[], ordering_mode=Sorted |
| CoalesceBatchesExec: target_batch_size=8192 |
| FilterExec: id@0 = 1 AND name@1 = a |
| DataSourceExec: partitions=1, partition_sizes=[1] |
| " |
| ); |
| |
| // Since id and name are functionally dependant, we can use name among expression |
| // even if it is not part of the group by expression. |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+------+ |
| | id | name | |
| +----+------+ |
| | 1 | a | |
| +----+------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_with_pk3() -> Result<()> { |
| // create the dataframe |
| let config = SessionConfig::new().with_target_partitions(1); |
| let ctx = SessionContext::new_with_config(config); |
| |
| let df = ctx.read_table(table_with_constraints())?; |
| |
| // GROUP BY id |
| let group_expr = vec![col("id")]; |
| let aggr_expr = vec![]; |
| // group by id, |
| let df = df.aggregate(group_expr, aggr_expr)?; |
| |
| // Predicate refers to id field |
| // id = 1 |
| let predicate = col("id").eq(lit(1i32)); |
| let df = df.filter(predicate)?; |
| // Select expression refers to id, and name columns. |
| // id, name |
| let df = df.select(vec![col("id"), col("name")])?; |
| assert_snapshot!( |
| physical_plan_to_string(&df).await, |
| @r" |
| AggregateExec: mode=Single, gby=[id@0 as id, name@1 as name], aggr=[], ordering_mode=PartiallySorted([0]) |
| CoalesceBatchesExec: target_batch_size=8192 |
| FilterExec: id@0 = 1 |
| DataSourceExec: partitions=1, partition_sizes=[1] |
| " |
| ); |
| |
| // Since id and name are functionally dependant, we can use name among expression |
| // even if it is not part of the group by expression. |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+------+ |
| | id | name | |
| +----+------+ |
| | 1 | a | |
| +----+------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_with_pk4() -> Result<()> { |
| // create the dataframe |
| let config = SessionConfig::new().with_target_partitions(1); |
| let ctx = SessionContext::new_with_config(config); |
| |
| let df = ctx.read_table(table_with_constraints())?; |
| |
| // GROUP BY id |
| let group_expr = vec![col("id")]; |
| let aggr_expr = vec![]; |
| let df = df.aggregate(group_expr, aggr_expr)?; |
| |
| // Predicate refers to id field |
| // id = 1 |
| let predicate = col("id").eq(lit(1i32)); |
| let df = df.filter(predicate)?; |
| // Select expression refers to id column. |
| // id |
| let df = df.select(vec![col("id")])?; |
| |
| // In this case aggregate shouldn't be expanded, since these |
| // columns are not used. |
| assert_snapshot!( |
| physical_plan_to_string(&df).await, |
| @r" |
| AggregateExec: mode=Single, gby=[id@0 as id], aggr=[], ordering_mode=Sorted |
| CoalesceBatchesExec: target_batch_size=8192 |
| FilterExec: id@0 = 1 |
| DataSourceExec: partitions=1, partition_sizes=[1] |
| " |
| ); |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+ |
| | id | |
| +----+ |
| | 1 | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_alias() -> Result<()> { |
| let df = test_table().await?; |
| |
| let df = df |
| // GROUP BY `c2 + 1` |
| .aggregate(vec![col("c2") + lit(1)], vec![])? |
| // SELECT `c2 + 1` as c2 |
| .select(vec![(col("c2") + lit(1)).alias("c2")])? |
| // GROUP BY c2 as "c2" (alias in expr is not supported by SQL) |
| .aggregate(vec![col("c2").alias("c2")], vec![])?; |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+ |
| | c2 | |
| +----+ |
| | 2 | |
| | 3 | |
| | 4 | |
| | 5 | |
| | 6 | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_with_union() -> Result<()> { |
| let df = test_table().await?; |
| |
| let df1 = df |
| .clone() |
| // GROUP BY `c1` |
| .aggregate(vec![col("c1")], vec![min(col("c2"))])? |
| // SELECT `c1` , min(c2) as `result` |
| .select(vec![col("c1"), min(col("c2")).alias("result")])?; |
| let df2 = df |
| .clone() |
| // GROUP BY `c1` |
| .aggregate(vec![col("c1")], vec![max(col("c3"))])? |
| // SELECT `c1` , max(c3) as `result` |
| .select(vec![col("c1"), max(col("c3")).alias("result")])?; |
| |
| let df_union = df1.union(df2)?; |
| let df = df_union |
| // GROUP BY `c1` |
| .aggregate( |
| vec![col("c1")], |
| vec![sum(col("result")).alias("sum_result")], |
| )? |
| // SELECT `c1`, sum(result) as `sum_result` |
| .select(vec![(col("c1")), col("sum_result")])?; |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+------------+ |
| | c1 | sum_result | |
| +----+------------+ |
| | a | 84 | |
| | b | 69 | |
| | c | 124 | |
| | d | 126 | |
| | e | 121 | |
| +----+------------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_subexpr() -> Result<()> { |
| let df = test_table().await?; |
| |
| let group_expr = col("c2") + lit(1); |
| let aggr_expr = sum(col("c3") + lit(2)); |
| |
| let df = df |
| // GROUP BY `c2 + 1` |
| .aggregate(vec![group_expr.clone()], vec![aggr_expr.clone()])? |
| // SELECT `c2 + 1` as c2 + 10, sum(c3 + 2) + 20 |
| // SELECT expressions contain aggr_expr and group_expr as subexpressions |
| .select(vec![ |
| group_expr.alias("c2") + lit(10), |
| (aggr_expr + lit(20)).alias("sum"), |
| ])?; |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----------------+------+ |
| | c2 + Int32(10) | sum | |
| +----------------+------+ |
| | 12 | 431 | |
| | 13 | 248 | |
| | 14 | 453 | |
| | 15 | 95 | |
| | 16 | -146 | |
| +----------------+------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_aggregate_name_collision() -> Result<()> { |
| let df = test_table().await?; |
| |
| let collided_alias = "aggregate_test_100.c2 + aggregate_test_100.c3"; |
| let group_expr = lit(1).alias(collided_alias); |
| |
| let df = df |
| // GROUP BY 1 |
| .aggregate(vec![group_expr], vec![])? |
| // SELECT `aggregate_test_100.c2 + aggregate_test_100.c3` |
| .select(vec![ |
| (col("aggregate_test_100.c2") + col("aggregate_test_100.c3")), |
| ]) |
| // The select expr has the same display_name as the group_expr, |
| // but since they are different expressions, it should fail. |
| .expect_err("Expected error"); |
| assert_snapshot!(df.strip_backtrace(), @r###"Schema error: No field named aggregate_test_100.c2. Valid fields are "aggregate_test_100.c2 + aggregate_test_100.c3"."###); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn window_using_aggregates() -> Result<()> { |
| // build plan using DataFrame API |
| let df = test_table().await?.filter(col("c1").eq(lit("a")))?; |
| let mut aggr_expr = vec![ |
| ( |
| datafusion_functions_aggregate::first_last::first_value_udaf(), |
| "first_value", |
| ), |
| ( |
| datafusion_functions_aggregate::first_last::last_value_udaf(), |
| "last_val", |
| ), |
| ( |
| datafusion_functions_aggregate::approx_distinct::approx_distinct_udaf(), |
| "approx_distinct", |
| ), |
| ( |
| datafusion_functions_aggregate::approx_median::approx_median_udaf(), |
| "approx_median", |
| ), |
| ( |
| datafusion_functions_aggregate::median::median_udaf(), |
| "median", |
| ), |
| (datafusion_functions_aggregate::min_max::max_udaf(), "max"), |
| (datafusion_functions_aggregate::min_max::min_udaf(), "min"), |
| ] |
| .into_iter() |
| .map(|(func, name)| { |
| let w = WindowFunction::new( |
| WindowFunctionDefinition::AggregateUDF(func), |
| vec![col("c3")], |
| ); |
| |
| Expr::from(w) |
| .null_treatment(NullTreatment::IgnoreNulls) |
| .order_by(vec![col("c2").sort(true, true), col("c3").sort(true, true)]) |
| .window_frame(WindowFrame::new_bounds( |
| WindowFrameUnits::Rows, |
| WindowFrameBound::Preceding(ScalarValue::UInt64(None)), |
| WindowFrameBound::Preceding(ScalarValue::UInt64(Some(1))), |
| )) |
| .build() |
| .unwrap() |
| .alias(name) |
| }) |
| .collect::<Vec<_>>(); |
| aggr_expr.extend_from_slice(&[col("c2"), col("c3")]); |
| |
| let df: Vec<RecordBatch> = df.select(aggr_expr)?.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df), |
| @r###" |
| +-------------+----------+-----------------+---------------+--------+-----+------+----+------+ |
| | first_value | last_val | approx_distinct | approx_median | median | max | min | c2 | c3 | |
| +-------------+----------+-----------------+---------------+--------+-----+------+----+------+ |
| | | | | | | | | 1 | -85 | |
| | -85 | -101 | 14 | -12 | -101 | 83 | -101 | 4 | -54 | |
| | -85 | -101 | 17 | -25 | -101 | 83 | -101 | 5 | -31 | |
| | -85 | -12 | 10 | -32 | -12 | 83 | -85 | 3 | 13 | |
| | -85 | -25 | 3 | -56 | -25 | -25 | -85 | 1 | -5 | |
| | -85 | -31 | 18 | -29 | -31 | 83 | -101 | 5 | 36 | |
| | -85 | -38 | 16 | -25 | -38 | 83 | -101 | 4 | 65 | |
| | -85 | -43 | 7 | -43 | -43 | 83 | -85 | 2 | 45 | |
| | -85 | -48 | 6 | -35 | -48 | 83 | -85 | 2 | -43 | |
| | -85 | -5 | 4 | -37 | -5 | -5 | -85 | 1 | 83 | |
| | -85 | -54 | 15 | -17 | -54 | 83 | -101 | 4 | -38 | |
| | -85 | -56 | 2 | -70 | -56 | -56 | -85 | 1 | -25 | |
| | -85 | -72 | 9 | -43 | -72 | 83 | -85 | 3 | -12 | |
| | -85 | -85 | 1 | -85 | -85 | -85 | -85 | 1 | -56 | |
| | -85 | 13 | 11 | -17 | 13 | 83 | -85 | 3 | 14 | |
| | -85 | 13 | 11 | -25 | 13 | 83 | -85 | 3 | 13 | |
| | -85 | 14 | 12 | -12 | 14 | 83 | -85 | 3 | 17 | |
| | -85 | 17 | 13 | -11 | 17 | 83 | -85 | 4 | -101 | |
| | -85 | 45 | 8 | -34 | 45 | 83 | -85 | 3 | -72 | |
| | -85 | 65 | 17 | -17 | 65 | 83 | -101 | 5 | -101 | |
| | -85 | 83 | 5 | -25 | 83 | 83 | -85 | 2 | -48 | |
| +-------------+----------+-----------------+---------------+--------+-----+------+----+------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn window_aggregates_with_filter() -> Result<()> { |
| // Define a small in-memory table to make expected values clear |
| let ts: Int32Array = [1, 2, 3, 4, 5].into_iter().collect(); |
| let val: Int32Array = [-3, -2, 1, 4, -1].into_iter().collect(); |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("ts", Arc::new(ts) as _), |
| ("val", Arc::new(val) as _), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch)?; |
| |
| let df = ctx.table("t").await?; |
| |
| // Build filtered window aggregates over ORDER BY ts ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW |
| let mut exprs = vec![ |
| (datafusion_functions_aggregate::sum::sum_udaf(), "sum_pos"), |
| ( |
| datafusion_functions_aggregate::average::avg_udaf(), |
| "avg_pos", |
| ), |
| ( |
| datafusion_functions_aggregate::min_max::min_udaf(), |
| "min_pos", |
| ), |
| ( |
| datafusion_functions_aggregate::min_max::max_udaf(), |
| "max_pos", |
| ), |
| ( |
| datafusion_functions_aggregate::count::count_udaf(), |
| "cnt_pos", |
| ), |
| ] |
| .into_iter() |
| .map(|(func, alias)| { |
| let w = WindowFunction::new( |
| WindowFunctionDefinition::AggregateUDF(func), |
| vec![col("val")], |
| ); |
| |
| Expr::from(w) |
| .order_by(vec![col("ts").sort(true, true)]) |
| .window_frame(WindowFrame::new_bounds( |
| WindowFrameUnits::Rows, |
| WindowFrameBound::Preceding(ScalarValue::UInt64(None)), |
| WindowFrameBound::CurrentRow, |
| )) |
| .filter(col("val").gt(lit(0))) |
| .build() |
| .unwrap() |
| .alias(alias) |
| }) |
| .collect::<Vec<_>>(); |
| exprs.extend_from_slice(&[col("ts"), col("val")]); |
| |
| let results = df.select(exprs)?.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +---------+---------+---------+---------+---------+----+-----+ |
| | sum_pos | avg_pos | min_pos | max_pos | cnt_pos | ts | val | |
| +---------+---------+---------+---------+---------+----+-----+ |
| | | | | | 0 | 1 | -3 | |
| | | | | | 0 | 2 | -2 | |
| | 1 | 1.0 | 1 | 1 | 1 | 3 | 1 | |
| | 5 | 2.5 | 1 | 4 | 2 | 4 | 4 | |
| | 5 | 2.5 | 1 | 4 | 2 | 5 | -1 | |
| +---------+---------+---------+---------+---------+----+-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| // Test issue: https://github.com/apache/datafusion/issues/10346 |
| #[tokio::test] |
| async fn test_select_over_aggregate_schema() -> Result<()> { |
| let df = test_table() |
| .await? |
| .with_column("c", col("c1"))? |
| .aggregate(vec![], vec![array_agg(col("c")).alias("c")])? |
| .select(vec![col("c")])?; |
| |
| assert_eq!(df.schema().fields().len(), 1); |
| let field = df.schema().field(0); |
| // There are two columns named 'c', one from the input of the aggregate and the other from the output. |
| // Select should return the column from the output of the aggregate, which is a list. |
| assert!(matches!(field.data_type(), DataType::List(_))); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_distinct() -> Result<()> { |
| let t = test_table().await?; |
| let plan = t |
| .select(vec![col("c1")]) |
| .unwrap() |
| .distinct() |
| .unwrap() |
| .logical_plan() |
| .clone(); |
| |
| let sql_plan = create_plan("select distinct c1 from aggregate_test_100").await?; |
| |
| assert_same_plan(&plan, &sql_plan); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_distinct_sort_by() -> Result<()> { |
| let t = test_table().await?; |
| let plan = t |
| .select(vec![col("c1")]) |
| .unwrap() |
| .distinct() |
| .unwrap() |
| .sort(vec![col("c1").sort(true, true)]) |
| .unwrap(); |
| |
| let df_results = plan.clone().collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+ |
| | c1 | |
| +----+ |
| | a | |
| | b | |
| | c | |
| | d | |
| | e | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_distinct_sort_by_unprojected() -> Result<()> { |
| let t = test_table().await?; |
| let err = t |
| .select(vec![col("c1")]) |
| .unwrap() |
| .distinct() |
| .unwrap() |
| // try to sort on some value not present in input to distinct |
| .sort(vec![col("c2").sort(true, true)]) |
| .unwrap_err(); |
| assert_snapshot!(err.strip_backtrace(), @"Error during planning: For SELECT DISTINCT, ORDER BY expressions c2 must appear in select list"); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_distinct_on() -> Result<()> { |
| let t = test_table().await?; |
| let plan = t |
| .distinct_on(vec![col("c1")], vec![col("aggregate_test_100.c1")], None) |
| .unwrap(); |
| |
| let sql_plan = |
| create_plan("select distinct on (c1) c1 from aggregate_test_100").await?; |
| |
| assert_same_plan(&plan.logical_plan().clone(), &sql_plan); |
| |
| let df_results = plan.clone().collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+ |
| | c1 | |
| +----+ |
| | a | |
| | b | |
| | c | |
| | d | |
| | e | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_distinct_on_sort_by() -> Result<()> { |
| let t = test_table().await?; |
| let plan = t |
| .select(vec![col("c1")]) |
| .unwrap() |
| .distinct_on( |
| vec![col("c1")], |
| vec![col("c1")], |
| Some(vec![col("c1").sort(true, true)]), |
| ) |
| .unwrap() |
| .sort(vec![col("c1").sort(true, true)]) |
| .unwrap(); |
| |
| let df_results = plan.clone().collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+ |
| | c1 | |
| +----+ |
| | a | |
| | b | |
| | c | |
| | d | |
| | e | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_distinct_on_sort_by_unprojected() -> Result<()> { |
| let t = test_table().await?; |
| let err = t |
| .select(vec![col("c1")]) |
| .unwrap() |
| .distinct_on( |
| vec![col("c1")], |
| vec![col("c1")], |
| Some(vec![col("c1").sort(true, true)]), |
| ) |
| .unwrap() |
| // try to sort on some value not present in input to distinct |
| .sort(vec![col("c2").sort(true, true)]) |
| .unwrap_err(); |
| assert_snapshot!(err.strip_backtrace(), @"Error during planning: For SELECT DISTINCT, ORDER BY expressions c2 must appear in select list"); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn join() -> Result<()> { |
| let left = test_table().await?.select_columns(&["c1", "c2"])?; |
| let right = test_table_with_name("c2") |
| .await? |
| .select_columns(&["c1", "c3"])?; |
| let left_rows = left.clone().collect().await?; |
| let right_rows = right.clone().collect().await?; |
| let join = left.join(right, JoinType::Inner, &["c1"], &["c1"], None)?; |
| let join_rows = join.collect().await?; |
| assert_eq!(100, left_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| assert_eq!(100, right_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| assert_eq!(2008, join_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn join_coercion_unnamed() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| // Test that join will coerce column types when necessary |
| // even when the relations don't have unique names |
| let left = ctx.read_batch(record_batch!( |
| ("id", Int32, [1, 2, 3]), |
| ("name", Utf8, ["a", "b", "c"]) |
| )?)?; |
| let right = ctx.read_batch(record_batch!( |
| ("id", Int32, [10, 3]), |
| ("name", Utf8View, ["d", "c"]) // Utf8View is a different type |
| )?)?; |
| let cols = vec!["name", "id"]; |
| |
| let filter = None; |
| let join = right.join(left, JoinType::LeftAnti, &cols, &cols, filter)?; |
| let results = join.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----+------+ |
| | id | name | |
| +----+------+ |
| | 10 | d | |
| +----+------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn join_on() -> Result<()> { |
| let left = test_table_with_name("a") |
| .await? |
| .select_columns(&["c1", "c2"])?; |
| let right = test_table_with_name("b") |
| .await? |
| .select_columns(&["c1", "c2"])?; |
| let join = left.join_on( |
| right, |
| JoinType::Inner, |
| [col("a.c1").not_eq(col("b.c1")), col("a.c2").eq(col("b.c2"))], |
| )?; |
| |
| assert_snapshot!(join.logical_plan(), @r###" |
| Inner Join: Filter: a.c1 != b.c1 AND a.c2 = b.c2 |
| Projection: a.c1, a.c2 |
| TableScan: a |
| Projection: b.c1, b.c2 |
| TableScan: b |
| "###); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn join_on_filter_datatype() -> Result<()> { |
| let left = test_table_with_name("a").await?.select_columns(&["c1"])?; |
| let right = test_table_with_name("b").await?.select_columns(&["c1"])?; |
| |
| // JOIN ON untyped NULL |
| let join = left.clone().join_on( |
| right.clone(), |
| JoinType::Inner, |
| Some(Expr::Literal(ScalarValue::Null, None)), |
| )?; |
| assert_snapshot!(join.into_optimized_plan().unwrap(), @"EmptyRelation: rows=0"); |
| |
| // JOIN ON expression must be boolean type |
| let join = left.join_on(right, JoinType::Inner, Some(lit("TRUE")))?; |
| let err = join.into_optimized_plan().unwrap_err(); |
| assert_snapshot!( |
| err.strip_backtrace(), |
| @"type_coercion\ncaused by\nError during planning: Join condition must be boolean type, but got Utf8" |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn join_ambiguous_filter() -> Result<()> { |
| let left = test_table_with_name("a") |
| .await? |
| .select_columns(&["c1", "c2"])?; |
| let right = test_table_with_name("b") |
| .await? |
| .select_columns(&["c1", "c2"])?; |
| |
| let join = left |
| .join_on(right, JoinType::Inner, [col("c1").eq(col("c1"))]) |
| .expect_err("join didn't fail check"); |
| assert_snapshot!(join.strip_backtrace(), @"Schema error: Ambiguous reference to unqualified field c1"); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn limit() -> Result<()> { |
| // build query using Table API |
| let t = test_table().await?; |
| let t2 = t.select_columns(&["c1", "c2", "c11"])?.limit(0, Some(10))?; |
| let plan = t2.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = |
| create_plan("SELECT c1, c2, c11 FROM aggregate_test_100 LIMIT 10").await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn df_count() -> Result<()> { |
| let count = test_table().await?.count().await?; |
| assert_eq!(100, count); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn explain() -> Result<()> { |
| // build query using Table API |
| let df = test_table().await?; |
| let df = df |
| .select_columns(&["c1", "c2", "c11"])? |
| .limit(0, Some(10))? |
| .explain(false, false)?; |
| let plan = df.logical_plan().clone(); |
| |
| // build query using SQL |
| let sql_plan = |
| create_plan("EXPLAIN SELECT c1, c2, c11 FROM aggregate_test_100 LIMIT 10") |
| .await?; |
| |
| // the two plans should be identical |
| assert_same_plan(&plan, &sql_plan); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn registry() -> Result<()> { |
| let ctx = SessionContext::new(); |
| register_aggregate_csv(&ctx, "aggregate_test_100").await?; |
| |
| // declare the udf |
| let my_fn: ScalarFunctionImplementation = |
| Arc::new(|_: &[ColumnarValue]| unimplemented!("my_fn is not implemented")); |
| |
| // create and register the udf |
| ctx.register_udf(create_udf( |
| "my_fn", |
| vec![DataType::Float64], |
| DataType::Float64, |
| Volatility::Immutable, |
| my_fn, |
| )); |
| |
| // build query with a UDF using DataFrame API |
| let df = ctx.table("aggregate_test_100").await?; |
| |
| let expr = df.registry().udf("my_fn")?.call(vec![col("c12")]); |
| let df = df.select(vec![expr])?; |
| |
| // build query using SQL |
| let sql_plan = ctx.sql("SELECT my_fn(c12) FROM aggregate_test_100").await?; |
| |
| // the two plans should be identical |
| assert_same_plan(df.logical_plan(), sql_plan.logical_plan()); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn sendable() { |
| let df = test_table().await.unwrap(); |
| // dataframes should be sendable between threads/tasks |
| let task = SpawnedTask::spawn(async move { |
| df.select_columns(&["c1"]) |
| .expect("should be usable in a task") |
| }); |
| task.join().await.expect("task completed successfully"); |
| } |
| |
| #[tokio::test] |
| async fn intersect() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c3"])?; |
| let d2 = df.clone(); |
| let plan = df.intersect(d2)?; |
| let result = plan.logical_plan().clone(); |
| let expected = create_plan( |
| "SELECT c1, c3 FROM aggregate_test_100 |
| INTERSECT ALL SELECT c1, c3 FROM aggregate_test_100", |
| ) |
| .await?; |
| assert_same_plan(&result, &expected); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn except() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c3"])?; |
| let d2 = df.clone(); |
| let plan = df.except(d2)?; |
| let result = plan.logical_plan().clone(); |
| let expected = create_plan( |
| "SELECT c1, c3 FROM aggregate_test_100 |
| EXCEPT ALL SELECT c1, c3 FROM aggregate_test_100", |
| ) |
| .await?; |
| assert_same_plan(&result, &expected); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn except_distinct() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c3"])?; |
| let d2 = df.clone(); |
| let plan = df.except_distinct(d2)?; |
| let result = plan.logical_plan().clone(); |
| let expected = create_plan( |
| "SELECT c1, c3 FROM aggregate_test_100 |
| EXCEPT DISTINCT SELECT c1, c3 FROM aggregate_test_100", |
| ) |
| .await?; |
| assert_same_plan(&result, &expected); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn intersect_distinct() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c3"])?; |
| let d2 = df.clone(); |
| let plan = df.intersect_distinct(d2)?; |
| let result = plan.logical_plan().clone(); |
| let expected = create_plan( |
| "SELECT c1, c3 FROM aggregate_test_100 |
| INTERSECT DISTINCT SELECT c1, c3 FROM aggregate_test_100", |
| ) |
| .await?; |
| assert_same_plan(&result, &expected); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn register_table() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c12"])?; |
| let ctx = SessionContext::new(); |
| let df_impl = DataFrame::new(ctx.state(), df.logical_plan().clone()); |
| |
| // register a dataframe as a table |
| ctx.register_table("test_table", df_impl.clone().into_view())?; |
| |
| // pull the table out |
| let table = ctx.table("test_table").await?; |
| |
| let group_expr = vec![col("c1")]; |
| let aggr_expr = vec![sum(col("c12"))]; |
| |
| // check that we correctly read from the table |
| let df_results = df_impl |
| .aggregate(group_expr.clone(), aggr_expr.clone())? |
| .collect() |
| .await?; |
| let table_results = &table.aggregate(group_expr, aggr_expr)?.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+-----------------------------+ |
| | c1 | sum(aggregate_test_100.c12) | |
| +----+-----------------------------+ |
| | a | 10.238448667882977 | |
| | b | 7.797734760124923 | |
| | c | 13.860958726523545 | |
| | d | 8.793968289758968 | |
| | e | 10.206140546981722 | |
| +----+-----------------------------+ |
| "### |
| ); |
| |
| // the results are the same as the results from the view, modulo the leaf table name |
| assert_snapshot!( |
| batches_to_sort_string(table_results), |
| @r###" |
| +----+---------------------+ |
| | c1 | sum(test_table.c12) | |
| +----+---------------------+ |
| | a | 10.238448667882977 | |
| | b | 7.797734760124923 | |
| | c | 13.860958726523545 | |
| | d | 8.793968289758968 | |
| | e | 10.206140546981722 | |
| +----+---------------------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn register_temporary_table() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c12"])?; |
| let ctx = SessionContext::new(); |
| let df_impl = DataFrame::new(ctx.state(), df.logical_plan().clone()); |
| |
| let df_table_provider = df_impl.clone().into_temporary_view(); |
| |
| // check that we set the correct table_type |
| assert_eq!(df_table_provider.table_type(), TableType::Temporary); |
| |
| // check that we can register a dataframe as a temporary table |
| ctx.register_table("test_table", df_table_provider)?; |
| |
| Ok(()) |
| } |
| |
| /// Compare the formatted string representation of two plans for equality |
| fn assert_same_plan(plan1: &LogicalPlan, plan2: &LogicalPlan) { |
| assert_eq!(format!("{plan1:?}"), format!("{plan2:?}")); |
| } |
| |
| /// Create a logical plan from a SQL query |
| async fn create_plan(sql: &str) -> Result<LogicalPlan> { |
| let ctx = SessionContext::new(); |
| register_aggregate_csv(&ctx, "aggregate_test_100").await?; |
| Ok(ctx.sql(sql).await?.into_unoptimized_plan()) |
| } |
| |
| #[tokio::test] |
| async fn with_column() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c2", "c3"])?; |
| let ctx = SessionContext::new(); |
| let df_impl = DataFrame::new(ctx.state(), df.logical_plan().clone()); |
| |
| let df = df_impl |
| .filter(col("c2").eq(lit(3)).and(col("c1").eq(lit("a"))))? |
| .with_column("sum", col("c2") + col("c3"))?; |
| |
| // check that new column added |
| let df_results = df.clone().collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+----+-----+-----+ |
| | c1 | c2 | c3 | sum | |
| +----+----+-----+-----+ |
| | a | 3 | -12 | -9 | |
| | a | 3 | -72 | -69 | |
| | a | 3 | 13 | 16 | |
| | a | 3 | 13 | 16 | |
| | a | 3 | 14 | 17 | |
| | a | 3 | 17 | 20 | |
| +----+----+-----+-----+ |
| "### |
| ); |
| |
| // check that col with the same name overwritten |
| let df_results_overwrite = df |
| .clone() |
| .with_column("c1", col("c2") + col("c3"))? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results_overwrite), |
| @r###" |
| +-----+----+-----+-----+ |
| | c1 | c2 | c3 | sum | |
| +-----+----+-----+-----+ |
| | -69 | 3 | -72 | -69 | |
| | -9 | 3 | -12 | -9 | |
| | 16 | 3 | 13 | 16 | |
| | 16 | 3 | 13 | 16 | |
| | 17 | 3 | 14 | 17 | |
| | 20 | 3 | 17 | 20 | |
| +-----+----+-----+-----+ |
| "### |
| ); |
| |
| // check that col with the same name overwritten using same name as reference |
| let df_results_overwrite_self = df |
| .clone() |
| .with_column("c2", col("c2") + lit(1))? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results_overwrite_self), |
| @r###" |
| +----+----+-----+-----+ |
| | c1 | c2 | c3 | sum | |
| +----+----+-----+-----+ |
| | a | 4 | -12 | -9 | |
| | a | 4 | -72 | -69 | |
| | a | 4 | 13 | 16 | |
| | a | 4 | 13 | 16 | |
| | a | 4 | 14 | 17 | |
| | a | 4 | 17 | 20 | |
| +----+----+-----+-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| // Test issues: https://github.com/apache/datafusion/issues/11982 |
| // and https://github.com/apache/datafusion/issues/12425 |
| // Window function was creating unwanted projection when using with_column() method. |
| #[tokio::test] |
| async fn test_window_function_with_column() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c2", "c3"])?; |
| let ctx = SessionContext::new(); |
| let df_impl = DataFrame::new(ctx.state(), df.logical_plan().clone()); |
| let func = row_number().alias("row_num"); |
| |
| // This first `with_column` results in a column without a `qualifier` |
| let df_impl = df_impl.with_column("s", col("c2") + col("c3"))?; |
| |
| // This second `with_column` should only alias `func` as `"r"` |
| let df = df_impl.with_column("r", func)?.limit(0, Some(2))?; |
| |
| df.clone().show().await?; |
| assert_eq!(5, df.schema().fields().len()); |
| |
| let df_results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+----+-----+-----+---+ |
| | c1 | c2 | c3 | s | r | |
| +----+----+-----+-----+---+ |
| | c | 2 | 1 | 3 | 1 | |
| | d | 5 | -40 | -35 | 2 | |
| +----+----+-----+-----+---+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| // Test issue: https://github.com/apache/datafusion/issues/7790 |
| // The join operation outputs two identical column names, but they belong to different relations. |
| #[tokio::test] |
| async fn with_column_join_same_columns() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1"])?; |
| let ctx = SessionContext::new(); |
| |
| let table = df.into_view(); |
| ctx.register_table("t1", table.clone())?; |
| ctx.register_table("t2", table)?; |
| let df = ctx |
| .table("t1") |
| .await? |
| .join( |
| ctx.table("t2").await?, |
| JoinType::Inner, |
| &["c1"], |
| &["c1"], |
| None, |
| )? |
| .sort(vec![ |
| // make the test deterministic |
| col("t1.c1").sort(true, true), |
| ])? |
| .limit(0, Some(1))?; |
| |
| let df_results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+----+ |
| | c1 | c1 | |
| +----+----+ |
| | a | a | |
| +----+----+ |
| "### |
| ); |
| |
| let df_with_column = df.clone().with_column("new_column", lit(true))?; |
| |
| assert_snapshot!( |
| df_with_column.logical_plan(), |
| @r" |
| Projection: t1.c1, t2.c1, Boolean(true) AS new_column |
| Limit: skip=0, fetch=1 |
| Sort: t1.c1 ASC NULLS FIRST |
| Inner Join: t1.c1 = t2.c1 |
| SubqueryAlias: t1 |
| Projection: aggregate_test_100.c1 |
| TableScan: aggregate_test_100 |
| SubqueryAlias: t2 |
| Projection: aggregate_test_100.c1 |
| TableScan: aggregate_test_100 |
| " |
| ); |
| |
| assert_snapshot!( |
| df_with_column.clone().into_optimized_plan().unwrap(), |
| @r###" |
| Projection: t1.c1, t2.c1, Boolean(true) AS new_column |
| Sort: t1.c1 ASC NULLS FIRST, fetch=1 |
| Inner Join: t1.c1 = t2.c1 |
| SubqueryAlias: t1 |
| TableScan: aggregate_test_100 projection=[c1] |
| SubqueryAlias: t2 |
| TableScan: aggregate_test_100 projection=[c1] |
| "### |
| ); |
| |
| let df_results = df_with_column.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+----+------------+ |
| | c1 | c1 | new_column | |
| +----+----+------------+ |
| | a | a | true | |
| +----+----+------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn with_column_renamed() -> Result<()> { |
| let df = test_table() |
| .await? |
| .select_columns(&["c1", "c2", "c3"])? |
| .filter(col("c2").eq(lit(3)).and(col("c1").eq(lit("a"))))? |
| .sort(vec![ |
| // make the test deterministic |
| col("c1").sort(true, true), |
| col("c2").sort(true, true), |
| col("c3").sort(true, true), |
| ])? |
| .limit(0, Some(1))? |
| .with_column("sum", col("c2") + col("c3"))?; |
| |
| let df_sum_renamed = df |
| .with_column_renamed("sum", "total")? |
| // table qualifier optional |
| .with_column_renamed("c1", "one")? |
| // accepts table qualifier |
| .with_column_renamed("aggregate_test_100.c2", "two")? |
| // no-op for missing column |
| .with_column_renamed("c4", "boom")?; |
| |
| let references: Vec<_> = df_sum_renamed |
| .schema() |
| .iter() |
| .map(|(a, _)| a.cloned()) |
| .collect(); |
| |
| assert_eq!( |
| references, |
| vec![ |
| Some(TableReference::bare("aggregate_test_100")), // table name is preserved |
| Some(TableReference::bare("aggregate_test_100")), |
| Some(TableReference::bare("aggregate_test_100")), |
| None // total column |
| ] |
| ); |
| |
| let batches = &df_sum_renamed.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(batches), |
| @r###" |
| +-----+-----+-----+-------+ |
| | one | two | c3 | total | |
| +-----+-----+-----+-------+ |
| | a | 3 | -72 | -69 | |
| +-----+-----+-----+-------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn with_column_renamed_ambiguous() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c2", "c3"])?; |
| let ctx = SessionContext::new(); |
| |
| let table = df.into_view(); |
| ctx.register_table("t1", table.clone())?; |
| ctx.register_table("t2", table)?; |
| |
| let actual_err = ctx |
| .table("t1") |
| .await? |
| .join( |
| ctx.table("t2").await?, |
| JoinType::Inner, |
| &["c1"], |
| &["c1"], |
| None, |
| )? |
| // can be t1.c2 or t2.c2 |
| .with_column_renamed("c2", "AAA") |
| .unwrap_err(); |
| assert_snapshot!(actual_err.strip_backtrace(), @"Schema error: Ambiguous reference to unqualified field c2"); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn with_column_renamed_join() -> Result<()> { |
| let df = test_table().await?.select_columns(&["c1", "c2", "c3"])?; |
| let ctx = SessionContext::new(); |
| |
| let table = df.into_view(); |
| ctx.register_table("t1", table.clone())?; |
| ctx.register_table("t2", table)?; |
| let df = ctx |
| .table("t1") |
| .await? |
| .join( |
| ctx.table("t2").await?, |
| JoinType::Inner, |
| &["c1"], |
| &["c1"], |
| None, |
| )? |
| .sort(vec![ |
| // make the test deterministic |
| col("t1.c1").sort(true, true), |
| col("t1.c2").sort(true, true), |
| col("t1.c3").sort(true, true), |
| col("t2.c1").sort(true, true), |
| col("t2.c2").sort(true, true), |
| col("t2.c3").sort(true, true), |
| ])? |
| .limit(0, Some(1))?; |
| |
| let df_results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+----+-----+----+----+-----+ |
| | c1 | c2 | c3 | c1 | c2 | c3 | |
| +----+----+-----+----+----+-----+ |
| | a | 1 | -85 | a | 1 | -85 | |
| +----+----+-----+----+----+-----+ |
| "### |
| ); |
| |
| let df_renamed = df.clone().with_column_renamed("t1.c1", "AAA")?; |
| |
| assert_snapshot!( |
| df_renamed.logical_plan(), |
| @r" |
| Projection: t1.c1 AS AAA, t1.c2, t1.c3, t2.c1, t2.c2, t2.c3 |
| Limit: skip=0, fetch=1 |
| Sort: t1.c1 ASC NULLS FIRST, t1.c2 ASC NULLS FIRST, t1.c3 ASC NULLS FIRST, t2.c1 ASC NULLS FIRST, t2.c2 ASC NULLS FIRST, t2.c3 ASC NULLS FIRST |
| Inner Join: t1.c1 = t2.c1 |
| SubqueryAlias: t1 |
| Projection: aggregate_test_100.c1, aggregate_test_100.c2, aggregate_test_100.c3 |
| TableScan: aggregate_test_100 |
| SubqueryAlias: t2 |
| Projection: aggregate_test_100.c1, aggregate_test_100.c2, aggregate_test_100.c3 |
| TableScan: aggregate_test_100 |
| " |
| ); |
| |
| assert_snapshot!( |
| df_renamed.clone().into_optimized_plan().unwrap(), |
| @r###" |
| Projection: t1.c1 AS AAA, t1.c2, t1.c3, t2.c1, t2.c2, t2.c3 |
| Sort: t1.c1 ASC NULLS FIRST, t1.c2 ASC NULLS FIRST, t1.c3 ASC NULLS FIRST, t2.c1 ASC NULLS FIRST, t2.c2 ASC NULLS FIRST, t2.c3 ASC NULLS FIRST, fetch=1 |
| Inner Join: t1.c1 = t2.c1 |
| SubqueryAlias: t1 |
| TableScan: aggregate_test_100 projection=[c1, c2, c3] |
| SubqueryAlias: t2 |
| TableScan: aggregate_test_100 projection=[c1, c2, c3] |
| "### |
| ); |
| |
| let df_results = df_renamed.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +-----+----+-----+----+----+-----+ |
| | AAA | c2 | c3 | c1 | c2 | c3 | |
| +-----+----+-----+----+----+-----+ |
| | a | 1 | -85 | a | 1 | -85 | |
| +-----+----+-----+----+----+-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn with_column_renamed_case_sensitive() -> Result<()> { |
| let config = SessionConfig::from_string_hash_map(&HashMap::from([( |
| "datafusion.sql_parser.enable_ident_normalization".to_owned(), |
| "false".to_owned(), |
| )]))?; |
| let ctx = SessionContext::new_with_config(config); |
| let name = "aggregate_test_100"; |
| register_aggregate_csv(&ctx, name).await?; |
| let df = ctx.table(name); |
| |
| let df = df |
| .await? |
| .filter(col("c2").eq(lit(3)).and(col("c1").eq(lit("a"))))? |
| .limit(0, Some(1))? |
| .sort(vec![ |
| // make the test deterministic |
| col("c1").sort(true, true), |
| col("c2").sort(true, true), |
| col("c3").sort(true, true), |
| ])? |
| .select_columns(&["c1"])?; |
| |
| let df_renamed = df.clone().with_column_renamed("c1", "CoLuMn1")?; |
| |
| let res = &df_renamed.clone().collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(res), |
| @r###" |
| +---------+ |
| | CoLuMn1 | |
| +---------+ |
| | a | |
| +---------+ |
| "### |
| ); |
| |
| let df_renamed = df_renamed |
| .with_column_renamed("CoLuMn1", "c1")? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_renamed), |
| @r###" |
| +----+ |
| | c1 | |
| +----+ |
| | a | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn describe_lookup_via_quoted_identifier() -> Result<()> { |
| let ctx = SessionContext::new(); |
| let name = "aggregate_test_100"; |
| register_aggregate_csv(&ctx, name).await?; |
| let df = ctx.table(name); |
| |
| let df = df |
| .await? |
| .filter(col("c2").eq(lit(3)).and(col("c1").eq(lit("a"))))? |
| .limit(0, Some(1))? |
| .sort(vec![ |
| // make the test deterministic |
| col("c1").sort(true, true), |
| col("c2").sort(true, true), |
| col("c3").sort(true, true), |
| ])? |
| .select_columns(&["c1"])?; |
| |
| let df_renamed = df.clone().with_column_renamed("c1", "CoLu.Mn[\"1\"]")?; |
| |
| let describe_result = df_renamed.describe().await?; |
| describe_result |
| .clone() |
| .sort(vec![ |
| col("describe").sort(true, true), |
| col("CoLu.Mn[\"1\"]").sort(true, true), |
| ])? |
| .show() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&describe_result.clone().collect().await?), |
| @r###" |
| +------------+--------------+ |
| | describe | CoLu.Mn["1"] | |
| +------------+--------------+ |
| | count | 1 | |
| | max | a | |
| | mean | null | |
| | median | null | |
| | min | a | |
| | null_count | 0 | |
| | std | null | |
| +------------+--------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn cast_expr_test() -> Result<()> { |
| let df = test_table() |
| .await? |
| .select_columns(&["c2", "c3"])? |
| .limit(0, Some(1))? |
| .with_column("sum", cast(col("c2") + col("c3"), DataType::Int64))?; |
| |
| let df_results = df.clone().collect().await?; |
| df.clone().show().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +----+----+-----+ |
| | c2 | c3 | sum | |
| +----+----+-----+ |
| | 2 | 1 | 3 | |
| +----+----+-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn row_writer_resize_test() -> Result<()> { |
| let schema = Arc::new(Schema::new(vec![Field::new( |
| "column_1", |
| DataType::Utf8, |
| false, |
| )])); |
| |
| let data = RecordBatch::try_new( |
| schema, |
| vec![ |
| Arc::new(StringArray::from(vec![ |
| Some("2a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), |
| Some("3a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800"), |
| ])) |
| ], |
| )?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("test", data)?; |
| |
| let sql = r#" |
| SELECT |
| count(1) |
| FROM |
| test |
| GROUP BY |
| column_1"#; |
| |
| let df = ctx.sql(sql).await?; |
| df.show_limit(10).await?; |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn with_column_name() -> Result<()> { |
| // define data with a column name that has a "." in it: |
| let array: Int32Array = [1, 10].into_iter().collect(); |
| let batch = RecordBatch::try_from_iter(vec![("f.c1", Arc::new(array) as _)])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch)?; |
| |
| let df = ctx |
| .table("t") |
| .await? |
| // try and create a column with a '.' in it |
| .with_column("f.c2", lit("hello"))?; |
| |
| let df_results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df_results), |
| @r###" |
| +------+-------+ |
| | f.c1 | f.c2 | |
| +------+-------+ |
| | 1 | hello | |
| | 10 | hello | |
| +------+-------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_cache_mismatch() -> Result<()> { |
| let ctx = SessionContext::new(); |
| let df = ctx |
| .sql("SELECT CASE WHEN true THEN NULL ELSE 1 END") |
| .await?; |
| let cache_df = df.cache().await; |
| assert!(cache_df.is_ok()); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn cache_test() -> Result<()> { |
| let df = test_table() |
| .await? |
| .select_columns(&["c2", "c3"])? |
| .limit(0, Some(1))? |
| .with_column("sum", cast(col("c2") + col("c3"), DataType::Int64))?; |
| |
| let cached_df = df.clone().cache().await?; |
| |
| assert_snapshot!( |
| cached_df.clone().into_optimized_plan().unwrap(), |
| @"TableScan: ?table? projection=[c2, c3, sum]" |
| ); |
| |
| let df_results = df.collect().await?; |
| let cached_df_results = cached_df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&cached_df_results), |
| @r###" |
| +----+----+-----+ |
| | c2 | c3 | sum | |
| +----+----+-----+ |
| | 2 | 1 | 3 | |
| +----+----+-----+ |
| "### |
| ); |
| |
| assert_eq!(&df_results, &cached_df_results); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn partition_aware_union() -> Result<()> { |
| let left = test_table().await?.select_columns(&["c1", "c2"])?; |
| let right = test_table_with_name("c2") |
| .await? |
| .select_columns(&["c1", "c3"])? |
| .with_column_renamed("c2.c1", "c2_c1")?; |
| |
| let left_rows = left.clone().collect().await?; |
| let right_rows = right.clone().collect().await?; |
| let join1 = |
| left.clone() |
| .join(right.clone(), JoinType::Inner, &["c1"], &["c2_c1"], None)?; |
| let join2 = left.join(right, JoinType::Inner, &["c1"], &["c2_c1"], None)?; |
| |
| let union = join1.union(join2)?; |
| |
| let union_rows = union.clone().collect().await?; |
| |
| assert_eq!(100, left_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| assert_eq!(100, right_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| assert_eq!(4016, union_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| |
| let physical_plan = union.create_physical_plan().await?; |
| let default_partition_count = SessionConfig::new().target_partitions(); |
| |
| // For partition aware union, the output partition count should not be changed. |
| assert_eq!( |
| physical_plan.output_partitioning().partition_count(), |
| default_partition_count |
| ); |
| // For partition aware union, the output partition is the same with the union's inputs |
| for child in physical_plan.children() { |
| assert_eq!( |
| physical_plan.output_partitioning(), |
| child.output_partitioning() |
| ); |
| } |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn non_partition_aware_union() -> Result<()> { |
| let left = test_table().await?.select_columns(&["c1", "c2"])?; |
| let right = test_table_with_name("c2") |
| .await? |
| .select_columns(&["c1", "c2"])? |
| .with_column_renamed("c2.c1", "c2_c1")? |
| .with_column_renamed("c2.c2", "c2_c2")?; |
| |
| let left_rows = left.clone().collect().await?; |
| let right_rows = right.clone().collect().await?; |
| let join1 = left.clone().join( |
| right.clone(), |
| JoinType::Inner, |
| &["c1", "c2"], |
| &["c2_c1", "c2_c2"], |
| None, |
| )?; |
| |
| // join key ordering is different |
| let join2 = left.join( |
| right, |
| JoinType::Inner, |
| &["c2", "c1"], |
| &["c2_c2", "c2_c1"], |
| None, |
| )?; |
| |
| let union = join1.union(join2)?; |
| |
| let union_rows = union.clone().collect().await?; |
| |
| assert_eq!(100, left_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| assert_eq!(100, right_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| assert_eq!(916, union_rows.iter().map(|x| x.num_rows()).sum::<usize>()); |
| |
| let physical_plan = union.create_physical_plan().await?; |
| let default_partition_count = SessionConfig::new().target_partitions(); |
| |
| // For non-partition aware union, the output partitioning count should be the combination of all output partitions count |
| assert!(matches!( |
| physical_plan.output_partitioning(), |
| Partitioning::UnknownPartitioning(partition_count) if *partition_count == default_partition_count * 2)); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn verify_join_output_partitioning() -> Result<()> { |
| let left = test_table().await?.select_columns(&["c1", "c2"])?; |
| let right = test_table_with_name("c2") |
| .await? |
| .select_columns(&["c1", "c2"])? |
| .with_column_renamed("c2.c1", "c2_c1")? |
| .with_column_renamed("c2.c2", "c2_c2")?; |
| |
| let all_join_types = vec![ |
| JoinType::Inner, |
| JoinType::Left, |
| JoinType::Right, |
| JoinType::Full, |
| JoinType::LeftSemi, |
| JoinType::RightSemi, |
| JoinType::LeftAnti, |
| JoinType::RightAnti, |
| JoinType::LeftMark, |
| JoinType::RightMark, |
| ]; |
| |
| let default_partition_count = SessionConfig::new().target_partitions(); |
| |
| for join_type in all_join_types { |
| let join = left.clone().join( |
| right.clone(), |
| join_type, |
| &["c1", "c2"], |
| &["c2_c1", "c2_c2"], |
| None, |
| )?; |
| let physical_plan = join.create_physical_plan().await?; |
| let out_partitioning = physical_plan.output_partitioning(); |
| let join_schema = physical_plan.schema(); |
| |
| match join_type { |
| JoinType::Left |
| | JoinType::LeftSemi |
| | JoinType::LeftAnti |
| | JoinType::LeftMark => { |
| let left_exprs: Vec<Arc<dyn PhysicalExpr>> = vec![ |
| Arc::new(Column::new_with_schema("c1", &join_schema)?), |
| Arc::new(Column::new_with_schema("c2", &join_schema)?), |
| ]; |
| assert_eq!( |
| out_partitioning, |
| &Partitioning::Hash(left_exprs, default_partition_count) |
| ); |
| } |
| JoinType::Inner |
| | JoinType::Right |
| | JoinType::RightSemi |
| | JoinType::RightAnti |
| | JoinType::RightMark => { |
| let right_exprs: Vec<Arc<dyn PhysicalExpr>> = vec![ |
| Arc::new(Column::new_with_schema("c2_c1", &join_schema)?), |
| Arc::new(Column::new_with_schema("c2_c2", &join_schema)?), |
| ]; |
| assert_eq!( |
| out_partitioning, |
| &Partitioning::Hash(right_exprs, default_partition_count) |
| ); |
| } |
| JoinType::Full => { |
| assert!(matches!( |
| out_partitioning, |
| &Partitioning::UnknownPartitioning(partition_count) if partition_count == default_partition_count)); |
| } |
| } |
| } |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_except_nested_struct() -> Result<()> { |
| use arrow::array::StructArray; |
| |
| let nested_schema = Arc::new(Schema::new(vec![ |
| Field::new("id", DataType::Int32, true), |
| Field::new("lat", DataType::Int32, true), |
| Field::new("long", DataType::Int32, true), |
| ])); |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("value", DataType::Int32, true), |
| Field::new( |
| "nested", |
| DataType::Struct(nested_schema.fields.clone()), |
| true, |
| ), |
| ])); |
| let batch = RecordBatch::try_new( |
| Arc::clone(&schema), |
| vec![ |
| Arc::new(Int32Array::from(vec![Some(1), Some(2), Some(3)])), |
| Arc::new(StructArray::from(vec![ |
| ( |
| Arc::new(Field::new("id", DataType::Int32, true)), |
| Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef, |
| ), |
| ( |
| Arc::new(Field::new("lat", DataType::Int32, true)), |
| Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef, |
| ), |
| ( |
| Arc::new(Field::new("long", DataType::Int32, true)), |
| Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef, |
| ), |
| ])), |
| ], |
| ) |
| .unwrap(); |
| |
| let updated_batch = RecordBatch::try_new( |
| Arc::clone(&schema), |
| vec![ |
| Arc::new(Int32Array::from(vec![Some(1), Some(12), Some(3)])), |
| Arc::new(StructArray::from(vec![ |
| ( |
| Arc::new(Field::new("id", DataType::Int32, true)), |
| Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef, |
| ), |
| ( |
| Arc::new(Field::new("lat", DataType::Int32, true)), |
| Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef, |
| ), |
| ( |
| Arc::new(Field::new("long", DataType::Int32, true)), |
| Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef, |
| ), |
| ])), |
| ], |
| ) |
| .unwrap(); |
| |
| let ctx = SessionContext::new(); |
| let before = ctx.read_batch(batch).expect("Failed to make DataFrame"); |
| let after = ctx |
| .read_batch(updated_batch) |
| .expect("Failed to make DataFrame"); |
| |
| let diff = before |
| .except(after) |
| .expect("Failed to except") |
| .collect() |
| .await?; |
| assert_eq!(diff.len(), 1); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn nested_explain_should_fail() -> Result<()> { |
| let ctx = SessionContext::new(); |
| // must be error |
| let mut result = ctx.sql("explain select 1").await?.explain(false, false); |
| assert!(result.is_err()); |
| // must be error |
| result = ctx.sql("explain explain select 1").await; |
| assert!(result.is_err()); |
| Ok(()) |
| } |
| |
| // Test issue: https://github.com/apache/datafusion/issues/12065 |
| #[tokio::test] |
| async fn filtered_aggr_with_param_values() -> Result<()> { |
| let cfg = SessionConfig::new().set( |
| "datafusion.sql_parser.dialect", |
| &ScalarValue::from("PostgreSQL"), |
| ); |
| let ctx = SessionContext::new_with_config(cfg); |
| register_aggregate_csv(&ctx, "table1").await?; |
| |
| let df = ctx |
| .sql("select count (c2) filter (where c3 > $1) from table1") |
| .await? |
| .with_param_values(vec![ScalarValue::from(10u64)]); |
| |
| let df_results = df?.collect().await?; |
| assert_snapshot!( |
| batches_to_string(&df_results), |
| @r###" |
| +------------------------------------------------+ |
| | count(table1.c2) FILTER (WHERE table1.c3 > $1) | |
| +------------------------------------------------+ |
| | 54 | |
| +------------------------------------------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| // Test issue: https://github.com/apache/datafusion/issues/13873 |
| #[tokio::test] |
| async fn write_parquet_with_order() -> Result<()> { |
| let tmp_dir = TempDir::new()?; |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::Int32, true), |
| Field::new("b", DataType::Int32, true), |
| ])); |
| |
| let ctx = SessionContext::new(); |
| let write_df = ctx.read_batch(RecordBatch::try_new( |
| schema.clone(), |
| vec![ |
| Arc::new(Int32Array::from(vec![1, 5, 7, 3, 2])), |
| Arc::new(Int32Array::from(vec![2, 3, 4, 5, 6])), |
| ], |
| )?)?; |
| |
| let test_path = tmp_dir.path().join("test.parquet"); |
| |
| write_df |
| .clone() |
| .write_parquet( |
| test_path.to_str().unwrap(), |
| DataFrameWriteOptions::new().with_sort_by(vec![col("a").sort(true, true)]), |
| None, |
| ) |
| .await?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_parquet( |
| "data", |
| test_path.to_str().unwrap(), |
| ParquetReadOptions::default(), |
| ) |
| .await?; |
| |
| let df = ctx.sql("SELECT * FROM data").await?; |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +---+---+ |
| | a | b | |
| +---+---+ |
| | 1 | 2 | |
| | 2 | 6 | |
| | 3 | 5 | |
| | 5 | 3 | |
| | 7 | 4 | |
| +---+---+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| // Test issue: https://github.com/apache/datafusion/issues/13873 |
| #[tokio::test] |
| async fn write_csv_with_order() -> Result<()> { |
| let tmp_dir = TempDir::new()?; |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::Int32, true), |
| Field::new("b", DataType::Int32, true), |
| ])); |
| |
| let ctx = SessionContext::new(); |
| let write_df = ctx.read_batch(RecordBatch::try_new( |
| schema.clone(), |
| vec![ |
| Arc::new(Int32Array::from(vec![1, 5, 7, 3, 2])), |
| Arc::new(Int32Array::from(vec![2, 3, 4, 5, 6])), |
| ], |
| )?)?; |
| |
| let test_path = tmp_dir.path().join("test.csv"); |
| |
| write_df |
| .clone() |
| .write_csv( |
| test_path.to_str().unwrap(), |
| DataFrameWriteOptions::new().with_sort_by(vec![col("a").sort(true, true)]), |
| None, |
| ) |
| .await?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_csv( |
| "data", |
| test_path.to_str().unwrap(), |
| CsvReadOptions::new().schema(&schema), |
| ) |
| .await?; |
| |
| let df = ctx.sql("SELECT * FROM data").await?; |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +---+---+ |
| | a | b | |
| +---+---+ |
| | 1 | 2 | |
| | 2 | 6 | |
| | 3 | 5 | |
| | 5 | 3 | |
| | 7 | 4 | |
| +---+---+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| // Test issue: https://github.com/apache/datafusion/issues/13873 |
| #[tokio::test] |
| async fn write_json_with_order() -> Result<()> { |
| let tmp_dir = TempDir::new()?; |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::Int32, true), |
| Field::new("b", DataType::Int32, true), |
| ])); |
| |
| let ctx = SessionContext::new(); |
| let write_df = ctx.read_batch(RecordBatch::try_new( |
| schema.clone(), |
| vec![ |
| Arc::new(Int32Array::from(vec![1, 5, 7, 3, 2])), |
| Arc::new(Int32Array::from(vec![2, 3, 4, 5, 6])), |
| ], |
| )?)?; |
| |
| let test_path = tmp_dir.path().join("test.json"); |
| |
| write_df |
| .clone() |
| .write_json( |
| test_path.to_str().unwrap(), |
| DataFrameWriteOptions::new().with_sort_by(vec![col("a").sort(true, true)]), |
| None, |
| ) |
| .await?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_json( |
| "data", |
| test_path.to_str().unwrap(), |
| NdJsonReadOptions::default().schema(&schema), |
| ) |
| .await?; |
| |
| let df = ctx.sql("SELECT * FROM data").await?; |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +---+---+ |
| | a | b | |
| +---+---+ |
| | 1 | 2 | |
| | 2 | 6 | |
| | 3 | 5 | |
| | 5 | 3 | |
| | 7 | 4 | |
| +---+---+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| // Test issue: https://github.com/apache/datafusion/issues/13873 |
| #[tokio::test] |
| async fn write_table_with_order() -> Result<()> { |
| let tmp_dir = TempDir::new()?; |
| let ctx = SessionContext::new(); |
| let location = tmp_dir.path().join("test_table/"); |
| |
| let mut write_df = ctx |
| .sql("values ('z'), ('x'), ('a'), ('b'), ('c')") |
| .await |
| .unwrap(); |
| |
| // Ensure the column names and types match the target table |
| write_df = write_df |
| .with_column_renamed("column1", "tablecol1") |
| .unwrap(); |
| |
| // Ensure the column type matches the target table |
| write_df = |
| write_df.with_column("tablecol1", cast(col("tablecol1"), DataType::Utf8View))?; |
| |
| let sql_str = |
| "create external table data(tablecol1 varchar) stored as parquet location '" |
| .to_owned() |
| + location.to_str().unwrap() |
| + "'"; |
| |
| ctx.sql(sql_str.as_str()).await?.collect().await?; |
| |
| // This is equivalent to INSERT INTO test. |
| write_df |
| .clone() |
| .write_table( |
| "data", |
| DataFrameWriteOptions::new() |
| .with_sort_by(vec![col("tablecol1").sort(true, true)]), |
| ) |
| .await?; |
| |
| let df = ctx.sql("SELECT * FROM data").await?; |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-----------+ |
| | tablecol1 | |
| +-----------+ |
| | a | |
| | b | |
| | c | |
| | x | |
| | z | |
| +-----------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_count_wildcard_on_sort() -> Result<()> { |
| let ctx = create_join_context()?; |
| |
| let sql_results = ctx |
| .sql("select b, count(*) from t1 group by b order by count(*)") |
| .await? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| let df_results = ctx |
| .table("t1") |
| .await? |
| .aggregate(vec![col("b")], vec![count_all()])? |
| .sort(vec![count_all().sort(true, false)])? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&sql_results).unwrap(), |
| @r###" |
| +---------------+------------------------------------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+------------------------------------------------------------------------------------------------------------+ |
| | logical_plan | Projection: t1.b, count(*) | |
| | | Sort: count(Int64(1)) AS count(*) AS count(*) ASC NULLS LAST | |
| | | Projection: t1.b, count(Int64(1)) AS count(*), count(Int64(1)) | |
| | | Aggregate: groupBy=[[t1.b]], aggr=[[count(Int64(1))]] | |
| | | TableScan: t1 projection=[b] | |
| | physical_plan | ProjectionExec: expr=[b@0 as b, count(*)@1 as count(*)] | |
| | | SortPreservingMergeExec: [count(Int64(1))@2 ASC NULLS LAST] | |
| | | SortExec: expr=[count(*)@1 ASC NULLS LAST], preserve_partitioning=[true] | |
| | | ProjectionExec: expr=[b@0 as b, count(Int64(1))@1 as count(*), count(Int64(1))@1 as count(Int64(1))] | |
| | | AggregateExec: mode=FinalPartitioned, gby=[b@0 as b], aggr=[count(Int64(1))] | |
| | | CoalesceBatchesExec: target_batch_size=8192 | |
| | | RepartitionExec: partitioning=Hash([b@0], 4), input_partitions=4 | |
| | | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 | |
| | | AggregateExec: mode=Partial, gby=[b@0 as b], aggr=[count(Int64(1))] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+------------------------------------------------------------------------------------------------------------+ |
| "### |
| ); |
| |
| assert_snapshot!( |
| pretty_format_batches(&df_results).unwrap(), |
| @r###" |
| +---------------+--------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+--------------------------------------------------------------------------------+ |
| | logical_plan | Sort: count(*) ASC NULLS LAST | |
| | | Aggregate: groupBy=[[t1.b]], aggr=[[count(Int64(1)) AS count(*)]] | |
| | | TableScan: t1 projection=[b] | |
| | physical_plan | SortPreservingMergeExec: [count(*)@1 ASC NULLS LAST] | |
| | | SortExec: expr=[count(*)@1 ASC NULLS LAST], preserve_partitioning=[true] | |
| | | AggregateExec: mode=FinalPartitioned, gby=[b@0 as b], aggr=[count(*)] | |
| | | CoalesceBatchesExec: target_batch_size=8192 | |
| | | RepartitionExec: partitioning=Hash([b@0], 4), input_partitions=4 | |
| | | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 | |
| | | AggregateExec: mode=Partial, gby=[b@0 as b], aggr=[count(*)] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+--------------------------------------------------------------------------------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_count_wildcard_on_where_in() -> Result<()> { |
| let ctx = create_join_context()?; |
| let sql_results = ctx |
| .sql("SELECT a, b FROM t1 WHERE a in (SELECT count(*) FROM t2)") |
| .await? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&sql_results).unwrap(), |
| @r" |
| +---------------+------------------------------------------------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+------------------------------------------------------------------------------------------------------------------------+ |
| | logical_plan | LeftSemi Join: CAST(t1.a AS Int64) = __correlated_sq_1.count(*) | |
| | | TableScan: t1 projection=[a, b] | |
| | | SubqueryAlias: __correlated_sq_1 | |
| | | Projection: count(Int64(1)) AS count(*) | |
| | | Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] | |
| | | TableScan: t2 projection=[] | |
| | physical_plan | CoalesceBatchesExec: target_batch_size=8192 | |
| | | HashJoinExec: mode=CollectLeft, join_type=RightSemi, on=[(count(*)@0, CAST(t1.a AS Int64)@2)], projection=[a@0, b@1] | |
| | | ProjectionExec: expr=[4 as count(*)] | |
| | | PlaceholderRowExec | |
| | | ProjectionExec: expr=[a@0 as a, b@1 as b, CAST(a@0 AS Int64) as CAST(t1.a AS Int64)] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+------------------------------------------------------------------------------------------------------------------------+ |
| " |
| ); |
| |
| // In the same SessionContext, AliasGenerator will increase subquery_alias id by 1 |
| // https://github.com/apache/datafusion/blame/cf45eb9020092943b96653d70fafb143cc362e19/datafusion/optimizer/src/alias.rs#L40-L43 |
| // for compare difference between sql and df logical plan, we need to create a new SessionContext here |
| let ctx = create_join_context()?; |
| let df_results = ctx |
| .table("t1") |
| .await? |
| .filter(in_subquery( |
| col("a"), |
| Arc::new( |
| ctx.table("t2") |
| .await? |
| .aggregate(vec![], vec![count_all()])? |
| .select(vec![count_all()])? |
| .into_optimized_plan()?, |
| ), |
| ))? |
| .select(vec![col("a"), col("b")])? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| // make sure sql plan same with df plan |
| assert_snapshot!( |
| pretty_format_batches(&df_results).unwrap(), |
| @r" |
| +---------------+------------------------------------------------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+------------------------------------------------------------------------------------------------------------------------+ |
| | logical_plan | LeftSemi Join: CAST(t1.a AS Int64) = __correlated_sq_1.count(*) | |
| | | TableScan: t1 projection=[a, b] | |
| | | SubqueryAlias: __correlated_sq_1 | |
| | | Aggregate: groupBy=[[]], aggr=[[count(Int64(1)) AS count(*)]] | |
| | | TableScan: t2 projection=[] | |
| | physical_plan | CoalesceBatchesExec: target_batch_size=8192 | |
| | | HashJoinExec: mode=CollectLeft, join_type=RightSemi, on=[(count(*)@0, CAST(t1.a AS Int64)@2)], projection=[a@0, b@1] | |
| | | ProjectionExec: expr=[4 as count(*)] | |
| | | PlaceholderRowExec | |
| | | ProjectionExec: expr=[a@0 as a, b@1 as b, CAST(a@0 AS Int64) as CAST(t1.a AS Int64)] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+------------------------------------------------------------------------------------------------------------------------+ |
| " |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_count_wildcard_on_where_exist() -> Result<()> { |
| let ctx = create_join_context()?; |
| let sql_results = ctx |
| .sql("SELECT a, b FROM t1 WHERE EXISTS (SELECT count(*) FROM t2)") |
| .await? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&sql_results).unwrap(), |
| @r" |
| +---------------+-----------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+-----------------------------------------------------+ |
| | logical_plan | LeftSemi Join: | |
| | | TableScan: t1 projection=[a, b] | |
| | | SubqueryAlias: __correlated_sq_1 | |
| | | EmptyRelation: rows=1 | |
| | physical_plan | NestedLoopJoinExec: join_type=RightSemi | |
| | | PlaceholderRowExec | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+-----------------------------------------------------+ |
| " |
| ); |
| |
| let df_results = ctx |
| .table("t1") |
| .await? |
| .filter(exists(Arc::new( |
| ctx.table("t2") |
| .await? |
| .aggregate(vec![], vec![count_all()])? |
| .select(vec![count_all()])? |
| .into_unoptimized_plan(), |
| // Usually, into_optimized_plan() should be used here, but due to |
| // https://github.com/apache/datafusion/issues/5771, |
| // subqueries in SQL cannot be optimized, resulting in differences in logical_plan. Therefore, into_unoptimized_plan() is temporarily used here. |
| )))? |
| .select(vec![col("a"), col("b")])? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&df_results).unwrap(), |
| @r" |
| +---------------+-----------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+-----------------------------------------------------+ |
| | logical_plan | LeftSemi Join: | |
| | | TableScan: t1 projection=[a, b] | |
| | | SubqueryAlias: __correlated_sq_1 | |
| | | EmptyRelation: rows=1 | |
| | physical_plan | NestedLoopJoinExec: join_type=RightSemi | |
| | | PlaceholderRowExec | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+-----------------------------------------------------+ |
| " |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_count_wildcard_on_window() -> Result<()> { |
| let ctx = create_join_context()?; |
| |
| let sql_results = ctx |
| .sql("select count(*) OVER(ORDER BY a DESC RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING) from t1") |
| .await? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&sql_results).unwrap(), |
| @r#" |
| +---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| | logical_plan | Projection: count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING AS count(*) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING | |
| | | WindowAggr: windowExpr=[[count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING]] | |
| | | TableScan: t1 projection=[a] | |
| | physical_plan | ProjectionExec: expr=[count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING@1 as count(*) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING] | |
| | | BoundedWindowAggExec: wdw=[count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING: Field { "count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING": Int64 }, frame: RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING], mode=[Sorted] | |
| | | SortExec: expr=[a@0 DESC], preserve_partitioning=[false] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| "# |
| ); |
| |
| let df_results = ctx |
| .table("t1") |
| .await? |
| .select(vec![count_all_window() |
| .order_by(vec![Sort::new(col("a"), false, true)]) |
| .window_frame(WindowFrame::new_bounds( |
| WindowFrameUnits::Range, |
| WindowFrameBound::Preceding(ScalarValue::UInt32(Some(6))), |
| WindowFrameBound::Following(ScalarValue::UInt32(Some(2))), |
| )) |
| .build() |
| .unwrap()])? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&df_results).unwrap(), |
| @r#" |
| +---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| | logical_plan | Projection: count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING | |
| | | WindowAggr: windowExpr=[[count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING]] | |
| | | TableScan: t1 projection=[a] | |
| | physical_plan | ProjectionExec: expr=[count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING@1 as count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING] | |
| | | BoundedWindowAggExec: wdw=[count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING: Field { "count(Int64(1)) ORDER BY [t1.a DESC NULLS FIRST] RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING": Int64 }, frame: RANGE BETWEEN 6 PRECEDING AND 2 FOLLOWING], mode=[Sorted] | |
| | | SortExec: expr=[a@0 DESC], preserve_partitioning=[false] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| "# |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| // Test with `repartition_sorts` disabled, causing a full resort of the data |
| async fn union_with_mix_of_presorted_and_explicitly_resorted_inputs_with_repartition_sorts_false( |
| ) -> Result<()> { |
| assert_snapshot!( |
| union_with_mix_of_presorted_and_explicitly_resorted_inputs_impl(false).await?, |
| @r#" |
| AggregateExec: mode=Final, gby=[id@0 as id], aggr=[], ordering_mode=Sorted |
| SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[false] |
| CoalescePartitionsExec |
| AggregateExec: mode=Partial, gby=[id@0 as id], aggr=[] |
| UnionExec |
| DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], output_ordering=[id@0 ASC NULLS LAST], file_type=parquet |
| DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], file_type=parquet |
| "#); |
| Ok(()) |
| } |
| |
| #[ignore] // See https://github.com/apache/datafusion/issues/18380 |
| #[tokio::test] |
| // Test with `repartition_sorts` enabled to preserve pre-sorted partitions and avoid resorting |
| async fn union_with_mix_of_presorted_and_explicitly_resorted_inputs_with_repartition_sorts_true( |
| ) -> Result<()> { |
| assert_snapshot!( |
| union_with_mix_of_presorted_and_explicitly_resorted_inputs_impl(true).await?, |
| @r#" |
| AggregateExec: mode=Final, gby=[id@0 as id], aggr=[], ordering_mode=Sorted |
| SortPreservingMergeExec: [id@0 ASC NULLS LAST] |
| AggregateExec: mode=Partial, gby=[id@0 as id], aggr=[], ordering_mode=Sorted |
| UnionExec |
| DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], output_ordering=[id@0 ASC NULLS LAST], file_type=parquet |
| SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[false] |
| DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], file_type=parquet |
| "#); |
| |
| // 💥 Doesn't pass, and generates this plan: |
| // |
| // AggregateExec: mode=Final, gby=[id@0 as id], aggr=[], ordering_mode=Sorted |
| // SortPreservingMergeExec: [id@0 ASC NULLS LAST] |
| // SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[true] |
| // AggregateExec: mode=Partial, gby=[id@0 as id], aggr=[] |
| // UnionExec |
| // DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], output_ordering=[id@0 ASC NULLS LAST], file_type=parquet |
| // DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], file_type=parquet |
| // |
| // |
| // === Excerpt from the verbose explain === |
| // |
| // +------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| // | plan_type | plan | |
| // +------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| // | initial_physical_plan | AggregateExec: mode=Final, gby=[id@0 as id], aggr=[], ordering_mode=Sorted | |
| // | | AggregateExec: mode=Partial, gby=[id@0 as id], aggr=[], ordering_mode=Sorted | |
| // | | UnionExec | |
| // | | DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], output_ordering=[id@0 ASC NULLS LAST], file_type=parquet | |
| // | | SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[false] | |
| // | | DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], file_type=parquet | |
| // ... |
| // | physical_plan after EnforceDistribution | OutputRequirementExec: order_by=[], dist_by=Unspecified | |
| // | | AggregateExec: mode=Final, gby=[id@0 as id], aggr=[], ordering_mode=Sorted | |
| // | | SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[false] | |
| // | | CoalescePartitionsExec | |
| // | | AggregateExec: mode=Partial, gby=[id@0 as id], aggr=[], ordering_mode=Sorted | |
| // | | UnionExec | |
| // | | DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], output_ordering=[id@0 ASC NULLS LAST], file_type=parquet | |
| // | | SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[false] | |
| // | | DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], file_type=parquet | |
| // | | | |
| // | physical_plan after CombinePartialFinalAggregate | SAME TEXT AS ABOVE |
| // | | | |
| // | physical_plan after EnforceSorting | OutputRequirementExec: order_by=[], dist_by=Unspecified | |
| // | | AggregateExec: mode=Final, gby=[id@0 as id], aggr=[], ordering_mode=Sorted | |
| // | | SortPreservingMergeExec: [id@0 ASC NULLS LAST] | |
| // | | SortExec: expr=[id@0 ASC NULLS LAST], preserve_partitioning=[true] | |
| // | | AggregateExec: mode=Partial, gby=[id@0 as id], aggr=[] | |
| // | | UnionExec | |
| // | | DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], output_ordering=[id@0 ASC NULLS LAST], file_type=parquet | |
| // | | DataSourceExec: file_groups={1 group: [[{testdata}/alltypes_tiny_pages.parquet]]}, projection=[id], file_type=parquet | |
| // ... |
| // +------------------------------------------------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ |
| |
| Ok(()) |
| } |
| |
| async fn union_with_mix_of_presorted_and_explicitly_resorted_inputs_impl( |
| repartition_sorts: bool, |
| ) -> Result<String> { |
| let config = SessionConfig::default() |
| .with_target_partitions(1) |
| .with_repartition_sorts(repartition_sorts); |
| let ctx = SessionContext::new_with_config(config); |
| |
| let testdata = parquet_test_data(); |
| |
| // Register "sorted" table, that is sorted |
| ctx.register_parquet( |
| "sorted", |
| &format!("{testdata}/alltypes_tiny_pages.parquet"), |
| ParquetReadOptions::default() |
| .file_sort_order(vec![vec![col("id").sort(true, false)]]), |
| ) |
| .await?; |
| |
| // Register "unsorted" table |
| ctx.register_parquet( |
| "unsorted", |
| &format!("{testdata}/alltypes_tiny_pages.parquet"), |
| ParquetReadOptions::default(), |
| ) |
| .await?; |
| |
| let source_sorted = ctx |
| .table("sorted") |
| .await |
| .unwrap() |
| .select(vec![col("id")]) |
| .unwrap(); |
| |
| let source_unsorted = ctx |
| .table("unsorted") |
| .await |
| .unwrap() |
| .select(vec![col("id")]) |
| .unwrap(); |
| |
| let source_unsorted_resorted = |
| source_unsorted.sort(vec![col("id").sort(true, false)])?; |
| |
| let union = source_sorted.union(source_unsorted_resorted)?; |
| |
| let agg = union.aggregate(vec![col("id")], vec![])?; |
| |
| let df = agg; |
| |
| // To be able to remove user specific paths from the plan, for stable assertions |
| let testdata_clean = Path::new(&testdata).canonicalize()?.display().to_string(); |
| let testdata_clean = testdata_clean.strip_prefix("/").unwrap_or(&testdata_clean); |
| |
| // Use displayable() rather than explain().collect() to avoid table formatting issues. We need |
| // to replace machine-specific paths with variable lengths, which breaks table alignment and |
| // causes snapshot mismatches. |
| let physical_plan = df.create_physical_plan().await?; |
| let displayable_plan = displayable(physical_plan.as_ref()) |
| .indent(true) |
| .to_string() |
| .replace(testdata_clean, "{testdata}"); |
| |
| Ok(displayable_plan) |
| } |
| |
| #[tokio::test] |
| async fn test_count_wildcard_on_aggregate() -> Result<()> { |
| let ctx = create_join_context()?; |
| register_alltypes_tiny_pages_parquet(&ctx).await?; |
| |
| let sql_results = ctx |
| .sql("select count(*) from t1") |
| .await? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&sql_results).unwrap(), |
| @r###" |
| +---------------+-----------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+-----------------------------------------------------+ |
| | logical_plan | Projection: count(Int64(1)) AS count(*) | |
| | | Aggregate: groupBy=[[]], aggr=[[count(Int64(1))]] | |
| | | TableScan: t1 projection=[] | |
| | physical_plan | ProjectionExec: expr=[4 as count(*)] | |
| | | PlaceholderRowExec | |
| | | | |
| +---------------+-----------------------------------------------------+ |
| "### |
| ); |
| |
| // add `.select(vec![count_wildcard()])?` to make sure we can analyze all node instead of just top node. |
| let df_results = ctx |
| .table("t1") |
| .await? |
| .aggregate(vec![], vec![count_all()])? |
| .select(vec![count_all()])? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&df_results).unwrap(), |
| @r###" |
| +---------------+---------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+---------------------------------------------------------------+ |
| | logical_plan | Aggregate: groupBy=[[]], aggr=[[count(Int64(1)) AS count(*)]] | |
| | | TableScan: t1 projection=[] | |
| | physical_plan | ProjectionExec: expr=[4 as count(*)] | |
| | | PlaceholderRowExec | |
| | | | |
| +---------------+---------------------------------------------------------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_count_wildcard_on_where_scalar_subquery() -> Result<()> { |
| let ctx = create_join_context()?; |
| |
| let sql_results = ctx |
| .sql("select a,b from t1 where (select count(*) from t2 where t1.a = t2.a)>0;") |
| .await? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&sql_results).unwrap(), |
| @r" |
| +---------------+---------------------------------------------------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+---------------------------------------------------------------------------------------------------------------------------+ |
| | logical_plan | Projection: t1.a, t1.b | |
| | | Filter: CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(0) ELSE __scalar_sq_1.count(*) END > Int64(0) | |
| | | Projection: t1.a, t1.b, __scalar_sq_1.count(*), __scalar_sq_1.__always_true | |
| | | Left Join: t1.a = __scalar_sq_1.a | |
| | | TableScan: t1 projection=[a, b] | |
| | | SubqueryAlias: __scalar_sq_1 | |
| | | Projection: count(Int64(1)) AS count(*), t2.a, Boolean(true) AS __always_true | |
| | | Aggregate: groupBy=[[t2.a]], aggr=[[count(Int64(1))]] | |
| | | TableScan: t2 projection=[a] | |
| | physical_plan | CoalesceBatchesExec: target_batch_size=8192 | |
| | | FilterExec: CASE WHEN __always_true@3 IS NULL THEN 0 ELSE count(*)@2 END > 0, projection=[a@0, b@1] | |
| | | CoalesceBatchesExec: target_batch_size=8192 | |
| | | HashJoinExec: mode=CollectLeft, join_type=Left, on=[(a@0, a@1)], projection=[a@0, b@1, count(*)@2, __always_true@4] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | ProjectionExec: expr=[count(Int64(1))@1 as count(*), a@0 as a, true as __always_true] | |
| | | AggregateExec: mode=FinalPartitioned, gby=[a@0 as a], aggr=[count(Int64(1))] | |
| | | CoalesceBatchesExec: target_batch_size=8192 | |
| | | RepartitionExec: partitioning=Hash([a@0], 4), input_partitions=4 | |
| | | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 | |
| | | AggregateExec: mode=Partial, gby=[a@0 as a], aggr=[count(Int64(1))] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+---------------------------------------------------------------------------------------------------------------------------+ |
| " |
| ); |
| |
| // In the same SessionContext, AliasGenerator will increase subquery_alias id by 1 |
| // https://github.com/apache/datafusion/blame/cf45eb9020092943b96653d70fafb143cc362e19/datafusion/optimizer/src/alias.rs#L40-L43 |
| // for compare difference between sql and df logical plan, we need to create a new SessionContext here |
| let ctx = create_join_context()?; |
| let agg_expr = count_all(); |
| let agg_expr_col = col(agg_expr.schema_name().to_string()); |
| let df_results = ctx |
| .table("t1") |
| .await? |
| .filter( |
| scalar_subquery(Arc::new( |
| ctx.table("t2") |
| .await? |
| .filter(out_ref_col(DataType::UInt32, "t1.a").eq(col("t2.a")))? |
| .aggregate(vec![], vec![agg_expr])? |
| .select(vec![agg_expr_col])? |
| .into_unoptimized_plan(), |
| )) |
| .gt(lit(ScalarValue::UInt8(Some(0)))), |
| )? |
| .select(vec![col("t1.a"), col("t1.b")])? |
| .explain(false, false)? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| pretty_format_batches(&df_results).unwrap(), |
| @r" |
| +---------------+---------------------------------------------------------------------------------------------------------------------------+ |
| | plan_type | plan | |
| +---------------+---------------------------------------------------------------------------------------------------------------------------+ |
| | logical_plan | Projection: t1.a, t1.b | |
| | | Filter: CASE WHEN __scalar_sq_1.__always_true IS NULL THEN Int64(0) ELSE __scalar_sq_1.count(*) END > Int64(0) | |
| | | Projection: t1.a, t1.b, __scalar_sq_1.count(*), __scalar_sq_1.__always_true | |
| | | Left Join: t1.a = __scalar_sq_1.a | |
| | | TableScan: t1 projection=[a, b] | |
| | | SubqueryAlias: __scalar_sq_1 | |
| | | Projection: count(*), t2.a, Boolean(true) AS __always_true | |
| | | Aggregate: groupBy=[[t2.a]], aggr=[[count(Int64(1)) AS count(*)]] | |
| | | TableScan: t2 projection=[a] | |
| | physical_plan | CoalesceBatchesExec: target_batch_size=8192 | |
| | | FilterExec: CASE WHEN __always_true@3 IS NULL THEN 0 ELSE count(*)@2 END > 0, projection=[a@0, b@1] | |
| | | CoalesceBatchesExec: target_batch_size=8192 | |
| | | HashJoinExec: mode=CollectLeft, join_type=Left, on=[(a@0, a@1)], projection=[a@0, b@1, count(*)@2, __always_true@4] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | ProjectionExec: expr=[count(*)@1 as count(*), a@0 as a, true as __always_true] | |
| | | AggregateExec: mode=FinalPartitioned, gby=[a@0 as a], aggr=[count(*)] | |
| | | CoalesceBatchesExec: target_batch_size=8192 | |
| | | RepartitionExec: partitioning=Hash([a@0], 4), input_partitions=4 | |
| | | RepartitionExec: partitioning=RoundRobinBatch(4), input_partitions=1 | |
| | | AggregateExec: mode=Partial, gby=[a@0 as a], aggr=[count(*)] | |
| | | DataSourceExec: partitions=1, partition_sizes=[1] | |
| | | | |
| +---------------+---------------------------------------------------------------------------------------------------------------------------+ |
| " |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn join2() -> Result<()> { |
| let schema1 = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::Utf8, false), |
| Field::new("b", DataType::Int32, false), |
| ])); |
| let schema2 = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::Utf8, false), |
| Field::new("c", DataType::Int32, false), |
| ])); |
| |
| // define data. |
| let batch1 = RecordBatch::try_new( |
| schema1.clone(), |
| vec![ |
| Arc::new(StringArray::from(vec!["a", "b", "c", "d"])), |
| Arc::new(Int32Array::from(vec![1, 10, 10, 100])), |
| ], |
| )?; |
| // define data. |
| let batch2 = RecordBatch::try_new( |
| schema2.clone(), |
| vec![ |
| Arc::new(StringArray::from(vec!["a", "b", "c", "d"])), |
| Arc::new(Int32Array::from(vec![1, 10, 10, 100])), |
| ], |
| )?; |
| |
| let ctx = SessionContext::new(); |
| |
| ctx.register_batch("aa", batch1)?; |
| |
| let df1 = ctx.table("aa").await?; |
| |
| ctx.register_batch("aaa", batch2)?; |
| |
| let df2 = ctx.table("aaa").await?; |
| |
| let a = df1.join(df2, JoinType::Inner, &["a"], &["a"], None)?; |
| |
| let batches = a.collect().await?; |
| |
| assert_eq!(batches.iter().map(|b| b.num_rows()).sum::<usize>(), 4); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn sort_on_unprojected_columns() -> Result<()> { |
| let schema = Schema::new(vec![ |
| Field::new("a", DataType::Int32, false), |
| Field::new("b", DataType::Int32, false), |
| ]); |
| |
| let batch = RecordBatch::try_new( |
| Arc::new(schema.clone()), |
| vec![ |
| Arc::new(Int32Array::from(vec![1, 10, 10, 100])), |
| Arc::new(Int32Array::from(vec![2, 12, 12, 120])), |
| ], |
| ) |
| .unwrap(); |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch).unwrap(); |
| |
| let df = ctx |
| .table("t") |
| .await |
| .unwrap() |
| .select(vec![col("a")]) |
| .unwrap() |
| .sort(vec![Sort::new(col("b"), false, true)]) |
| .unwrap(); |
| let results = df.collect().await.unwrap(); |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-----+ |
| | a | |
| +-----+ |
| | 100 | |
| | 10 | |
| | 10 | |
| | 1 | |
| +-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn sort_on_distinct_columns() -> Result<()> { |
| let schema = Schema::new(vec![ |
| Field::new("a", DataType::Int32, false), |
| Field::new("b", DataType::Int32, false), |
| ]); |
| |
| let batch = RecordBatch::try_new( |
| Arc::new(schema.clone()), |
| vec![ |
| Arc::new(Int32Array::from(vec![1, 10, 10, 100])), |
| Arc::new(Int32Array::from(vec![2, 3, 4, 5])), |
| ], |
| ) |
| .unwrap(); |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch).unwrap(); |
| let df = ctx |
| .table("t") |
| .await |
| .unwrap() |
| .select(vec![col("a")]) |
| .unwrap() |
| .distinct() |
| .unwrap() |
| .sort(vec![Sort::new(col("a"), false, true)]) |
| .unwrap(); |
| let results = df.collect().await.unwrap(); |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-----+ |
| | a | |
| +-----+ |
| | 100 | |
| | 10 | |
| | 1 | |
| +-----+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn sort_on_distinct_unprojected_columns() -> Result<()> { |
| let schema = Schema::new(vec![ |
| Field::new("a", DataType::Int32, false), |
| Field::new("b", DataType::Int32, false), |
| ]); |
| |
| let batch = RecordBatch::try_new( |
| Arc::new(schema.clone()), |
| vec![ |
| Arc::new(Int32Array::from(vec![1, 10, 10, 100])), |
| Arc::new(Int32Array::from(vec![2, 3, 4, 5])), |
| ], |
| )?; |
| |
| // Cannot sort on a column after distinct that would add a new column |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch)?; |
| let err = ctx |
| .table("t") |
| .await? |
| .select(vec![col("a")])? |
| .distinct()? |
| .sort(vec![Sort::new(col("b"), false, true)]) |
| .unwrap_err(); |
| assert_snapshot!(err.strip_backtrace(), @"Error during planning: For SELECT DISTINCT, ORDER BY expressions b must appear in select list"); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn sort_on_ambiguous_column() -> Result<()> { |
| let err = create_test_table("t1") |
| .await? |
| .join( |
| create_test_table("t2").await?, |
| JoinType::Inner, |
| &["a"], |
| &["a"], |
| None, |
| )? |
| .sort(vec![col("b").sort(true, true)]) |
| .unwrap_err(); |
| |
| assert_snapshot!(err.strip_backtrace(), @"Schema error: Ambiguous reference to unqualified field b"); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn group_by_ambiguous_column() -> Result<()> { |
| let err = create_test_table("t1") |
| .await? |
| .join( |
| create_test_table("t2").await?, |
| JoinType::Inner, |
| &["a"], |
| &["a"], |
| None, |
| )? |
| .aggregate(vec![col("b")], vec![max(col("a"))]) |
| .unwrap_err(); |
| |
| assert_snapshot!(err.strip_backtrace(), @"Schema error: Ambiguous reference to unqualified field b"); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn filter_on_ambiguous_column() -> Result<()> { |
| let err = create_test_table("t1") |
| .await? |
| .join( |
| create_test_table("t2").await?, |
| JoinType::Inner, |
| &["a"], |
| &["a"], |
| None, |
| )? |
| .filter(col("b").eq(lit(1))) |
| .unwrap_err(); |
| |
| assert_snapshot!(err.strip_backtrace(), @"Schema error: Ambiguous reference to unqualified field b"); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_ambiguous_column() -> Result<()> { |
| let err = create_test_table("t1") |
| .await? |
| .join( |
| create_test_table("t2").await?, |
| JoinType::Inner, |
| &["a"], |
| &["a"], |
| None, |
| )? |
| .select(vec![col("b")]) |
| .unwrap_err(); |
| |
| assert_snapshot!(err.strip_backtrace(), @"Schema error: Ambiguous reference to unqualified field b"); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn filter_with_alias_overwrite() -> Result<()> { |
| let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]); |
| |
| let batch = RecordBatch::try_new( |
| Arc::new(schema.clone()), |
| vec![Arc::new(Int32Array::from(vec![1, 10, 10, 100]))], |
| ) |
| .unwrap(); |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch).unwrap(); |
| |
| let df = ctx |
| .table("t") |
| .await |
| .unwrap() |
| .select(vec![(col("a").eq(lit(10))).alias("a")]) |
| .unwrap() |
| .filter(col("a")) |
| .unwrap(); |
| let results = df.collect().await.unwrap(); |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +------+ |
| | a | |
| +------+ |
| | true | |
| | true | |
| +------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn select_with_alias_overwrite() -> Result<()> { |
| let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]); |
| |
| let batch = RecordBatch::try_new( |
| Arc::new(schema.clone()), |
| vec![Arc::new(Int32Array::from(vec![1, 10, 10, 100]))], |
| )?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("t", batch)?; |
| |
| let df = ctx |
| .table("t") |
| .await? |
| .select(vec![col("a").alias("a")])? |
| .select(vec![(col("a").eq(lit(10))).alias("a")])? |
| .select(vec![col("a")])?; |
| |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-------+ |
| | a | |
| +-------+ |
| | false | |
| | true | |
| | true | |
| | false | |
| +-------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_grouping_sets() -> Result<()> { |
| let grouping_set_expr = Expr::GroupingSet(GroupingSet::GroupingSets(vec![ |
| vec![col("a")], |
| vec![col("b")], |
| vec![col("a"), col("b")], |
| ])); |
| |
| let df = create_test_table("test") |
| .await? |
| .aggregate(vec![grouping_set_expr], vec![count(col("a"))])? |
| .sort(vec![ |
| Sort::new(col("a"), false, true), |
| Sort::new(col("b"), false, true), |
| ])?; |
| |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-----------+-----+---------------+ |
| | a | b | count(test.a) | |
| +-----------+-----+---------------+ |
| | | 100 | 1 | |
| | | 10 | 2 | |
| | | 1 | 1 | |
| | abcDEF | | 1 | |
| | abcDEF | 1 | 1 | |
| | abc123 | | 1 | |
| | abc123 | 10 | 1 | |
| | CBAdef | | 1 | |
| | CBAdef | 10 | 1 | |
| | 123AbcDef | | 1 | |
| | 123AbcDef | 100 | 1 | |
| +-----------+-----+---------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_grouping_sets_count() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let grouping_set_expr = Expr::GroupingSet(GroupingSet::GroupingSets(vec![ |
| vec![col("c1")], |
| vec![col("c2")], |
| ])); |
| |
| let df = aggregates_table(&ctx) |
| .await? |
| .aggregate(vec![grouping_set_expr], vec![count(lit(1))])? |
| .sort(vec![ |
| Sort::new(col("c1"), false, true), |
| Sort::new(col("c2"), false, true), |
| ])?; |
| |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +----+----+-----------------+ |
| | c1 | c2 | count(Int32(1)) | |
| +----+----+-----------------+ |
| | | 5 | 14 | |
| | | 4 | 23 | |
| | | 3 | 19 | |
| | | 2 | 22 | |
| | | 1 | 22 | |
| | e | | 21 | |
| | d | | 18 | |
| | c | | 21 | |
| | b | | 19 | |
| | a | | 21 | |
| +----+----+-----------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_grouping_set_array_agg_with_overflow() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| let grouping_set_expr = Expr::GroupingSet(GroupingSet::GroupingSets(vec![ |
| vec![col("c1")], |
| vec![col("c2")], |
| vec![col("c1"), col("c2")], |
| ])); |
| |
| let df = aggregates_table(&ctx) |
| .await? |
| .aggregate( |
| vec![grouping_set_expr], |
| vec![ |
| sum(col("c3")).alias("sum_c3"), |
| avg(col("c3")).alias("avg_c3"), |
| ], |
| )? |
| .sort(vec![ |
| Sort::new(col("c1"), false, true), |
| Sort::new(col("c2"), false, true), |
| ])?; |
| |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +----+----+--------+---------------------+ |
| | c1 | c2 | sum_c3 | avg_c3 | |
| +----+----+--------+---------------------+ |
| | | 5 | -194 | -13.857142857142858 | |
| | | 4 | 29 | 1.2608695652173914 | |
| | | 3 | 395 | 20.789473684210527 | |
| | | 2 | 184 | 8.363636363636363 | |
| | | 1 | 367 | 16.681818181818183 | |
| | e | | 847 | 40.333333333333336 | |
| | e | 5 | -22 | -11.0 | |
| | e | 4 | 261 | 37.285714285714285 | |
| | e | 3 | 192 | 48.0 | |
| | e | 2 | 189 | 37.8 | |
| | e | 1 | 227 | 75.66666666666667 | |
| | d | | 458 | 25.444444444444443 | |
| | d | 5 | -99 | -49.5 | |
| | d | 4 | 162 | 54.0 | |
| | d | 3 | 124 | 41.333333333333336 | |
| | d | 2 | 328 | 109.33333333333333 | |
| | d | 1 | -57 | -8.142857142857142 | |
| | c | | -28 | -1.3333333333333333 | |
| | c | 5 | 24 | 12.0 | |
| | c | 4 | -43 | -10.75 | |
| | c | 3 | 190 | 47.5 | |
| | c | 2 | -389 | -55.57142857142857 | |
| | c | 1 | 190 | 47.5 | |
| | b | | -111 | -5.842105263157895 | |
| | b | 5 | -1 | -0.2 | |
| | b | 4 | -223 | -44.6 | |
| | b | 3 | -84 | -42.0 | |
| | b | 2 | 102 | 25.5 | |
| | b | 1 | 95 | 31.666666666666668 | |
| | a | | -385 | -18.333333333333332 | |
| | a | 5 | -96 | -32.0 | |
| | a | 4 | -128 | -32.0 | |
| | a | 3 | -27 | -4.5 | |
| | a | 2 | -46 | -15.333333333333334 | |
| | a | 1 | -88 | -17.6 | |
| +----+----+--------+---------------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn join_with_alias_filter() -> Result<()> { |
| let join_ctx = create_join_context()?; |
| let t1 = join_ctx.table("t1").await?; |
| let t2 = join_ctx.table("t2").await?; |
| let t1_schema = t1.schema().clone(); |
| let t2_schema = t2.schema().clone(); |
| |
| // filter: t1.a + CAST(Int64(1), UInt32) = t2.a + CAST(Int64(2), UInt32) as t1.a + 1 = t2.a + 2 |
| let filter = Expr::eq( |
| col("t1.a") + lit(3i64).cast_to(&DataType::UInt32, &t1_schema)?, |
| col("t2.a") + lit(1i32).cast_to(&DataType::UInt32, &t2_schema)?, |
| ) |
| .alias("t1.b + 1 = t2.a + 2"); |
| |
| let df = t1 |
| .join(t2, JoinType::Inner, &[], &[], Some(filter))? |
| .select(vec![ |
| col("t1.a"), |
| col("t2.a"), |
| col("t1.b"), |
| col("t1.c"), |
| col("t2.b"), |
| col("t2.c"), |
| ])?; |
| let optimized_plan = df.clone().into_optimized_plan()?; |
| |
| let formatted = optimized_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| assert_snapshot!( |
| actual, |
| @r###" |
| Projection: t1.a, t2.a, t1.b, t1.c, t2.b, t2.c [a:UInt32, a:UInt32, b:Utf8, c:Int32, b:Utf8, c:Int32] |
| Inner Join: t1.a + UInt32(3) = t2.a + UInt32(1) [a:UInt32, b:Utf8, c:Int32, a:UInt32, b:Utf8, c:Int32] |
| TableScan: t1 projection=[a, b, c] [a:UInt32, b:Utf8, c:Int32] |
| TableScan: t2 projection=[a, b, c] [a:UInt32, b:Utf8, c:Int32] |
| "### |
| ); |
| |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----+----+---+----+---+---+ |
| | a | a | b | c | b | c | |
| +----+----+---+----+---+---+ |
| | 1 | 3 | a | 10 | a | 1 | |
| | 11 | 13 | c | 30 | c | 3 | |
| +----+----+---+----+---+---+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn right_semi_with_alias_filter() -> Result<()> { |
| let join_ctx = create_join_context()?; |
| let t1 = join_ctx.table("t1").await?; |
| let t2 = join_ctx.table("t2").await?; |
| |
| // t1.a = t2.a and t1.c > 1 and t2.c > 1 |
| let filter = col("t1.a") |
| .eq(col("t2.a")) |
| .and(col("t1.c").gt(lit(1u32))) |
| .and(col("t2.c").gt(lit(1u32))); |
| |
| let df = t1 |
| .join(t2, JoinType::RightSemi, &[], &[], Some(filter))? |
| .select(vec![col("t2.a"), col("t2.b"), col("t2.c")])?; |
| let optimized_plan = df.clone().into_optimized_plan()?; |
| |
| let formatted = optimized_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| assert_snapshot!( |
| actual, |
| @r###" |
| RightSemi Join: t1.a = t2.a [a:UInt32, b:Utf8, c:Int32] |
| Projection: t1.a [a:UInt32] |
| Filter: t1.c > Int32(1) [a:UInt32, c:Int32] |
| TableScan: t1 projection=[a, c] [a:UInt32, c:Int32] |
| Filter: t2.c > Int32(1) [a:UInt32, b:Utf8, c:Int32] |
| TableScan: t2 projection=[a, b, c] [a:UInt32, b:Utf8, c:Int32] |
| "### |
| ); |
| |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +-----+---+---+ |
| | a | b | c | |
| +-----+---+---+ |
| | 10 | b | 2 | |
| | 100 | d | 4 | |
| +-----+---+---+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn right_anti_filter_push_down() -> Result<()> { |
| let join_ctx = create_join_context()?; |
| let t1 = join_ctx.table("t1").await?; |
| let t2 = join_ctx.table("t2").await?; |
| |
| // t1.a = t2.a and t1.c > 1 and t2.c > 1 |
| let filter = col("t1.a") |
| .eq(col("t2.a")) |
| .and(col("t1.c").gt(lit(1u32))) |
| .and(col("t2.c").gt(lit(1u32))); |
| |
| let df = t1 |
| .join(t2, JoinType::RightAnti, &[], &[], Some(filter))? |
| .select(vec![col("t2.a"), col("t2.b"), col("t2.c")])?; |
| let optimized_plan = df.clone().into_optimized_plan()?; |
| |
| let formatted = optimized_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| assert_snapshot!( |
| actual, |
| @r###" |
| RightAnti Join: t1.a = t2.a Filter: t2.c > Int32(1) [a:UInt32, b:Utf8, c:Int32] |
| Projection: t1.a [a:UInt32] |
| Filter: t1.c > Int32(1) [a:UInt32, c:Int32] |
| TableScan: t1 projection=[a, c] [a:UInt32, c:Int32] |
| TableScan: t2 projection=[a, b, c] [a:UInt32, b:Utf8, c:Int32] |
| "### |
| ); |
| |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----+---+---+ |
| | a | b | c | |
| +----+---+---+ |
| | 13 | c | 3 | |
| | 3 | a | 1 | |
| +----+---+---+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_columns() -> Result<()> { |
| const NUM_ROWS: usize = 4; |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+---------------------------------+--------------------------+ |
| | shape_id | points | tags | |
| +----------+---------------------------------+--------------------------+ |
| | 1 | [{x: 5, y: -8}, {x: -3, y: -4}] | [tag1] | |
| | 2 | [{x: 6, y: 2}, {x: -2, y: -8}] | [tag1] | |
| | 3 | [{x: -9, y: -7}, {x: -2, y: 5}] | [tag1, tag2, tag3, tag4] | |
| | 4 | | [tag1, tag2, tag3] | |
| +----------+---------------------------------+--------------------------+ |
| "###); |
| |
| // Unnest tags |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let results = df.unnest_columns(&["tags"])?.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+---------------------------------+------+ |
| | shape_id | points | tags | |
| +----------+---------------------------------+------+ |
| | 1 | [{x: 5, y: -8}, {x: -3, y: -4}] | tag1 | |
| | 2 | [{x: 6, y: 2}, {x: -2, y: -8}] | tag1 | |
| | 3 | [{x: -9, y: -7}, {x: -2, y: 5}] | tag1 | |
| | 3 | [{x: -9, y: -7}, {x: -2, y: 5}] | tag2 | |
| | 3 | [{x: -9, y: -7}, {x: -2, y: 5}] | tag3 | |
| | 3 | [{x: -9, y: -7}, {x: -2, y: 5}] | tag4 | |
| | 4 | | tag1 | |
| | 4 | | tag2 | |
| | 4 | | tag3 | |
| +----------+---------------------------------+------+ |
| "###); |
| |
| // Test aggregate results for tags. |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let count = df.unnest_columns(&["tags"])?.count().await?; |
| assert_eq!(count, results.iter().map(|r| r.num_rows()).sum::<usize>()); |
| |
| // Unnest points |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let results = df.unnest_columns(&["points"])?.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+----------------+--------------------------+ |
| | shape_id | points | tags | |
| +----------+----------------+--------------------------+ |
| | 1 | {x: -3, y: -4} | [tag1] | |
| | 1 | {x: 5, y: -8} | [tag1] | |
| | 2 | {x: -2, y: -8} | [tag1] | |
| | 2 | {x: 6, y: 2} | [tag1] | |
| | 3 | {x: -2, y: 5} | [tag1, tag2, tag3, tag4] | |
| | 3 | {x: -9, y: -7} | [tag1, tag2, tag3, tag4] | |
| | 4 | | [tag1, tag2, tag3] | |
| +----------+----------------+--------------------------+ |
| "###); |
| |
| // Test aggregate results for points. |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let count = df.unnest_columns(&["points"])?.count().await?; |
| assert_eq!(count, results.iter().map(|r| r.num_rows()).sum::<usize>()); |
| |
| // Unnest both points and tags. |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let results = df |
| .unnest_columns(&["points"])? |
| .unnest_columns(&["tags"])? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+----------------+------+ |
| | shape_id | points | tags | |
| +----------+----------------+------+ |
| | 1 | {x: -3, y: -4} | tag1 | |
| | 1 | {x: 5, y: -8} | tag1 | |
| | 2 | {x: -2, y: -8} | tag1 | |
| | 2 | {x: 6, y: 2} | tag1 | |
| | 3 | {x: -2, y: 5} | tag1 | |
| | 3 | {x: -2, y: 5} | tag2 | |
| | 3 | {x: -2, y: 5} | tag3 | |
| | 3 | {x: -2, y: 5} | tag4 | |
| | 3 | {x: -9, y: -7} | tag1 | |
| | 3 | {x: -9, y: -7} | tag2 | |
| | 3 | {x: -9, y: -7} | tag3 | |
| | 3 | {x: -9, y: -7} | tag4 | |
| | 4 | | tag1 | |
| | 4 | | tag2 | |
| | 4 | | tag3 | |
| +----------+----------------+------+ |
| "###); |
| |
| // Test aggregate results for points and tags. |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let count = df |
| .unnest_columns(&["points"])? |
| .unnest_columns(&["tags"])? |
| .count() |
| .await?; |
| assert_eq!(count, results.iter().map(|r| r.num_rows()).sum::<usize>()); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_dict_encoded_columns() -> Result<()> { |
| let strings = vec!["x", "y", "z"]; |
| let keys = Int32Array::from_iter(0..strings.len() as i32); |
| |
| let utf8_values = StringArray::from(strings.clone()); |
| let utf8_dict = DictionaryArray::new(keys.clone(), Arc::new(utf8_values)); |
| |
| let make_array_udf_expr1 = make_array_udf().call(vec![col("column1")]); |
| let batch = |
| RecordBatch::try_from_iter(vec![("column1", Arc::new(utf8_dict) as ArrayRef)])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("test", batch)?; |
| let df = ctx |
| .table("test") |
| .await? |
| .select(vec![ |
| make_array_udf_expr1.alias("make_array_expr"), |
| col("column1"), |
| ])? |
| .unnest_columns(&["make_array_expr"])?; |
| |
| let results = df.collect().await.unwrap(); |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-----------------+---------+ |
| | make_array_expr | column1 | |
| +-----------------+---------+ |
| | x | x | |
| | y | y | |
| | z | z | |
| +-----------------+---------+ |
| "### |
| ); |
| |
| // make_array(dict_encoded_string,literal string) |
| let make_array_udf_expr2 = make_array_udf().call(vec![ |
| col("column1"), |
| lit(ScalarValue::new_utf8("fixed_string")), |
| ]); |
| let df = ctx |
| .table("test") |
| .await? |
| .select(vec![ |
| make_array_udf_expr2.alias("make_array_expr"), |
| col("column1"), |
| ])? |
| .unnest_columns(&["make_array_expr"])?; |
| |
| let results = df.collect().await.unwrap(); |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-----------------+---------+ |
| | make_array_expr | column1 | |
| +-----------------+---------+ |
| | x | x | |
| | fixed_string | x | |
| | y | y | |
| | fixed_string | y | |
| | z | z | |
| | fixed_string | z | |
| +-----------------+---------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_column_nulls() -> Result<()> { |
| let df = table_with_lists_and_nulls().await?; |
| let results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +--------+----+ |
| | list | id | |
| +--------+----+ |
| | [1, 2] | A | |
| | | B | |
| | [] | C | |
| | [3] | D | |
| +--------+----+ |
| "### |
| ); |
| |
| // Unnest, preserving nulls (row with B is preserved) |
| let options = UnnestOptions::new().with_preserve_nulls(true); |
| |
| let results = df |
| .clone() |
| .unnest_columns_with_options(&["list"], options)? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +------+----+ |
| | list | id | |
| +------+----+ |
| | 1 | A | |
| | 2 | A | |
| | | B | |
| | 3 | D | |
| +------+----+ |
| "### |
| ); |
| |
| let options = UnnestOptions::new().with_preserve_nulls(false); |
| let results = df |
| .unnest_columns_with_options(&["list"], options)? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +------+----+ |
| | list | id | |
| +------+----+ |
| | 1 | A | |
| | 2 | A | |
| | 3 | D | |
| +------+----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_fixed_list() -> Result<()> { |
| let batch = get_fixed_list_batch()?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| let df = ctx.table("shapes").await?; |
| |
| let results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+----------------+ |
| | shape_id | tags | |
| +----------+----------------+ |
| | 1 | | |
| | 2 | [tag21, tag22] | |
| | 3 | [tag31, tag32] | |
| | 4 | | |
| | 5 | [tag51, tag52] | |
| | 6 | [tag61, tag62] | |
| +----------+----------------+ |
| "### |
| ); |
| |
| let options = UnnestOptions::new().with_preserve_nulls(true); |
| |
| let results = df |
| .unnest_columns_with_options(&["tags"], options)? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+-------+ |
| | shape_id | tags | |
| +----------+-------+ |
| | 1 | | |
| | 2 | tag21 | |
| | 2 | tag22 | |
| | 3 | tag31 | |
| | 3 | tag32 | |
| | 4 | | |
| | 5 | tag51 | |
| | 5 | tag52 | |
| | 6 | tag61 | |
| | 6 | tag62 | |
| +----------+-------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_fixed_list_drop_nulls() -> Result<()> { |
| let batch = get_fixed_list_batch()?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| let df = ctx.table("shapes").await?; |
| |
| let results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+----------------+ |
| | shape_id | tags | |
| +----------+----------------+ |
| | 1 | | |
| | 2 | [tag21, tag22] | |
| | 3 | [tag31, tag32] | |
| | 4 | | |
| | 5 | [tag51, tag52] | |
| | 6 | [tag61, tag62] | |
| +----------+----------------+ |
| "### |
| ); |
| |
| let options = UnnestOptions::new().with_preserve_nulls(false); |
| |
| let results = df |
| .unnest_columns_with_options(&["tags"], options)? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+-------+ |
| | shape_id | tags | |
| +----------+-------+ |
| | 2 | tag21 | |
| | 2 | tag22 | |
| | 3 | tag31 | |
| | 3 | tag32 | |
| | 5 | tag51 | |
| | 5 | tag52 | |
| | 6 | tag61 | |
| | 6 | tag62 | |
| +----------+-------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_fixed_list_non_null() -> Result<()> { |
| let mut shape_id_builder = UInt32Builder::new(); |
| let mut tags_builder = FixedSizeListBuilder::new(StringBuilder::new(), 2); |
| |
| for idx in 0..6 { |
| // Append shape id. |
| shape_id_builder.append_value(idx as u32 + 1); |
| |
| tags_builder |
| .values() |
| .append_value(format!("tag{}1", idx + 1)); |
| tags_builder |
| .values() |
| .append_value(format!("tag{}2", idx + 1)); |
| tags_builder.append(true); |
| } |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("shape_id", Arc::new(shape_id_builder.finish()) as ArrayRef), |
| ("tags", Arc::new(tags_builder.finish()) as ArrayRef), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| let df = ctx.table("shapes").await?; |
| |
| let results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+----------------+ |
| | shape_id | tags | |
| +----------+----------------+ |
| | 1 | [tag11, tag12] | |
| | 2 | [tag21, tag22] | |
| | 3 | [tag31, tag32] | |
| | 4 | [tag41, tag42] | |
| | 5 | [tag51, tag52] | |
| | 6 | [tag61, tag62] | |
| +----------+----------------+ |
| "### |
| ); |
| |
| let options = UnnestOptions::new().with_preserve_nulls(true); |
| let results = df |
| .unnest_columns_with_options(&["tags"], options)? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+-------+ |
| | shape_id | tags | |
| +----------+-------+ |
| | 1 | tag11 | |
| | 1 | tag12 | |
| | 2 | tag21 | |
| | 2 | tag22 | |
| | 3 | tag31 | |
| | 3 | tag32 | |
| | 4 | tag41 | |
| | 4 | tag42 | |
| | 5 | tag51 | |
| | 5 | tag52 | |
| | 6 | tag61 | |
| | 6 | tag62 | |
| +----------+-------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_aggregate_columns() -> Result<()> { |
| const NUM_ROWS: usize = 5; |
| |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let results = df.select_columns(&["tags"])?.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +--------------------------+ |
| | tags | |
| +--------------------------+ |
| | [tag1, tag2, tag3, tag4] | |
| | [tag1, tag2, tag3] | |
| | [tag1, tag2] | |
| | [tag1] | |
| | [tag1] | |
| +--------------------------+ |
| "### |
| ); |
| |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let results = df |
| .unnest_columns(&["tags"])? |
| .aggregate(vec![], vec![count(col("tags"))])? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +-------------+ |
| | count(tags) | |
| +-------------+ |
| | 11 | |
| +-------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_no_empty_batches() -> Result<()> { |
| let mut shape_id_builder = UInt32Builder::new(); |
| let mut tag_id_builder = UInt32Builder::new(); |
| |
| for shape_id in 1..=10 { |
| for tag_id in 1..=10 { |
| shape_id_builder.append_value(shape_id as u32); |
| tag_id_builder.append_value((shape_id * 10 + tag_id) as u32); |
| } |
| } |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("shape_id", Arc::new(shape_id_builder.finish()) as ArrayRef), |
| ("tag_id", Arc::new(tag_id_builder.finish()) as ArrayRef), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| let df = ctx.table("shapes").await?; |
| |
| let results = df |
| .clone() |
| .aggregate( |
| vec![col("shape_id")], |
| vec![array_agg(col("tag_id")).alias("tag_id")], |
| )? |
| .collect() |
| .await?; |
| |
| // Assert that there are no empty batches in result |
| for rb in results { |
| assert!(rb.num_rows() > 0); |
| } |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_array_agg() -> Result<()> { |
| let mut shape_id_builder = UInt32Builder::new(); |
| let mut tag_id_builder = UInt32Builder::new(); |
| |
| for shape_id in 1..=3 { |
| for tag_id in 1..=3 { |
| shape_id_builder.append_value(shape_id as u32); |
| tag_id_builder.append_value((shape_id * 10 + tag_id) as u32); |
| } |
| } |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("shape_id", Arc::new(shape_id_builder.finish()) as ArrayRef), |
| ("tag_id", Arc::new(tag_id_builder.finish()) as ArrayRef), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| let df = ctx.table("shapes").await?; |
| |
| let results = df.clone().collect().await?; |
| |
| // Assert that there are no empty batches in result |
| for rb in results.clone() { |
| assert!(rb.num_rows() > 0); |
| } |
| |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+--------+ |
| | shape_id | tag_id | |
| +----------+--------+ |
| | 1 | 11 | |
| | 1 | 12 | |
| | 1 | 13 | |
| | 2 | 21 | |
| | 2 | 22 | |
| | 2 | 23 | |
| | 3 | 31 | |
| | 3 | 32 | |
| | 3 | 33 | |
| +----------+--------+ |
| "### |
| ); |
| |
| // Doing an `array_agg` by `shape_id` produces: |
| let results = df |
| .clone() |
| .aggregate( |
| vec![col("shape_id")], |
| vec![array_agg(col("tag_id")).alias("tag_id")], |
| )? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+--------------+ |
| | shape_id | tag_id | |
| +----------+--------------+ |
| | 1 | [11, 12, 13] | |
| | 2 | [21, 22, 23] | |
| | 3 | [31, 32, 33] | |
| +----------+--------------+ |
| "### |
| ); |
| |
| // Unnesting again should produce the original batch. |
| let results = ctx |
| .table("shapes") |
| .await? |
| .aggregate( |
| vec![col("shape_id")], |
| vec![array_agg(col("tag_id")).alias("tag_id")], |
| )? |
| .unnest_columns(&["tag_id"])? |
| .collect() |
| .await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+--------+ |
| | shape_id | tag_id | |
| +----------+--------+ |
| | 1 | 11 | |
| | 1 | 12 | |
| | 1 | 13 | |
| | 2 | 21 | |
| | 2 | 22 | |
| | 2 | 23 | |
| | 3 | 31 | |
| | 3 | 32 | |
| | 3 | 33 | |
| +----------+--------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_with_redundant_columns() -> Result<()> { |
| let mut shape_id_builder = UInt32Builder::new(); |
| let mut tag_id_builder = UInt32Builder::new(); |
| |
| for shape_id in 1..=3 { |
| for tag_id in 1..=3 { |
| shape_id_builder.append_value(shape_id as u32); |
| tag_id_builder.append_value((shape_id * 10 + tag_id) as u32); |
| } |
| } |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("shape_id", Arc::new(shape_id_builder.finish()) as ArrayRef), |
| ("tag_id", Arc::new(tag_id_builder.finish()) as ArrayRef), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| let df = ctx.table("shapes").await?; |
| |
| let results = df.clone().collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+--------+ |
| | shape_id | tag_id | |
| +----------+--------+ |
| | 1 | 11 | |
| | 1 | 12 | |
| | 1 | 13 | |
| | 2 | 21 | |
| | 2 | 22 | |
| | 2 | 23 | |
| | 3 | 31 | |
| | 3 | 32 | |
| | 3 | 33 | |
| +----------+--------+ |
| "### |
| ); |
| |
| // Doing an `array_agg` by `shape_id` produces: |
| let df = df |
| .clone() |
| .aggregate( |
| vec![col("shape_id")], |
| vec![array_agg(col("shape_id")).alias("shape_id2")], |
| )? |
| .unnest_columns(&["shape_id2"])? |
| .select(vec![col("shape_id")])?; |
| |
| let optimized_plan = df.clone().into_optimized_plan()?; |
| |
| let formatted = optimized_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| assert_snapshot!( |
| actual, |
| @r" |
| Projection: shapes.shape_id [shape_id:UInt32] |
| Unnest: lists[shape_id2|depth=1] structs[] [shape_id:UInt32, shape_id2:UInt32;N] |
| Aggregate: groupBy=[[shapes.shape_id]], aggr=[[array_agg(shapes.shape_id) AS shape_id2]] [shape_id:UInt32, shape_id2:List(Field { data_type: UInt32, nullable: true });N] |
| TableScan: shapes projection=[shape_id] [shape_id:UInt32] |
| " |
| ); |
| |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----------+ |
| | shape_id | |
| +----------+ |
| | 1 | |
| | 1 | |
| | 1 | |
| | 2 | |
| | 2 | |
| | 2 | |
| | 3 | |
| | 3 | |
| | 3 | |
| +----------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_analyze_metrics() -> Result<()> { |
| const NUM_ROWS: usize = 5; |
| |
| let df = table_with_nested_types(NUM_ROWS).await?; |
| let results = df |
| .unnest_columns(&["tags"])? |
| .explain(false, true)? |
| .collect() |
| .await?; |
| let formatted = pretty_format_batches(&results).unwrap().to_string(); |
| assert_contains!(&formatted, "elapsed_compute="); |
| assert_contains!(&formatted, "input_batches=1"); |
| assert_contains!(&formatted, "input_rows=5"); |
| assert_contains!(&formatted, "output_rows=11"); |
| assert_contains!(&formatted, "output_batches=1"); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn unnest_multiple_columns() -> Result<()> { |
| let df = table_with_mixed_lists().await?; |
| // Default behavior is to preserve nulls. |
| let results = df |
| .clone() |
| .unnest_columns(&["list", "large_list", "fixed_list"])? |
| .collect() |
| .await?; |
| // list: [1,2,3], null, [null], null, |
| // large_list: [null, 1.1], [2.2, 3.3, 4.4], null, [], |
| // fixed_list: null, [1,2], [3,4], null |
| // string: a, b, c, d |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +------+------------+------------+--------+ |
| | list | large_list | fixed_list | string | |
| +------+------------+------------+--------+ |
| | 1 | | | a | |
| | 2 | 1.1 | | a | |
| | 3 | | | a | |
| | | 2.2 | 1 | b | |
| | | 3.3 | 2 | b | |
| | | 4.4 | | b | |
| | | | 3 | c | |
| | | | 4 | c | |
| | | | | d | |
| +------+------------+------------+--------+ |
| "### |
| ); |
| |
| // Test with `preserve_nulls = false`` |
| let results = df |
| .unnest_columns_with_options( |
| &["list", "large_list", "fixed_list"], |
| UnnestOptions::new().with_preserve_nulls(false), |
| )? |
| .collect() |
| .await?; |
| // list: [1,2,3], null, [null], null, |
| // large_list: [null, 1.1], [2.2, 3.3, 4.4], null, [], |
| // fixed_list: null, [1,2], [3,4], null |
| // string: a, b, c, d |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +------+------------+------------+--------+ |
| | list | large_list | fixed_list | string | |
| +------+------------+------------+--------+ |
| | 1 | | | a | |
| | 2 | 1.1 | | a | |
| | 3 | | | a | |
| | | 2.2 | 1 | b | |
| | | 3.3 | 2 | b | |
| | | 4.4 | | b | |
| | | | 3 | c | |
| | | | 4 | c | |
| +------+------------+------------+--------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| /// Test unnesting a non-nullable list. |
| #[tokio::test] |
| async fn unnest_non_nullable_list() -> Result<()> { |
| let list_array = ListArray::from_iter_primitive::<Int32Type, _, _>(vec![ |
| Some(vec![Some(1), Some(2)]), |
| Some(vec![None]), |
| ]); |
| let schema = Arc::new(Schema::new(vec![Field::new( |
| "c1", |
| DataType::new_list(DataType::Int32, true), |
| false, |
| )])); |
| |
| let batch = RecordBatch::try_new(schema, vec![Arc::new(list_array)])?; |
| let ctx = SessionContext::new(); |
| let results = ctx |
| .read_batches(vec![batch])? |
| .unnest_columns(&["c1"])? |
| .collect() |
| .await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +----+ |
| | c1 | |
| +----+ |
| | 1 | |
| | 2 | |
| | | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_read_batches() -> Result<()> { |
| let config = SessionConfig::new(); |
| let runtime = Arc::new(RuntimeEnv::default()); |
| let state = SessionStateBuilder::new() |
| .with_config(config) |
| .with_runtime_env(runtime) |
| .with_default_features() |
| .build(); |
| let ctx = SessionContext::new_with_state(state); |
| |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("id", DataType::Int32, false), |
| Field::new("number", DataType::Float32, false), |
| ])); |
| |
| let batches = vec![ |
| RecordBatch::try_new( |
| schema.clone(), |
| vec![ |
| Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])), |
| Arc::new(Float32Array::from(vec![1.12, 3.40, 2.33, 9.10, 6.66])), |
| ], |
| ) |
| .unwrap(), |
| RecordBatch::try_new( |
| schema.clone(), |
| vec![ |
| Arc::new(Int32Array::from(vec![3, 4, 5])), |
| Arc::new(Float32Array::from(vec![1.11, 2.22, 3.33])), |
| ], |
| ) |
| .unwrap(), |
| ]; |
| let df = ctx.read_batches(batches).unwrap(); |
| df.clone().show().await.unwrap(); |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----+--------+ |
| | id | number | |
| +----+--------+ |
| | 1 | 1.12 | |
| | 2 | 3.4 | |
| | 3 | 1.11 | |
| | 3 | 2.33 | |
| | 4 | 2.22 | |
| | 4 | 9.1 | |
| | 5 | 3.33 | |
| | 5 | 6.66 | |
| +----+--------+ |
| "### |
| ); |
| Ok(()) |
| } |
| #[tokio::test] |
| async fn test_read_batches_empty() -> Result<()> { |
| let config = SessionConfig::new(); |
| let runtime = Arc::new(RuntimeEnv::default()); |
| let state = SessionStateBuilder::new() |
| .with_config(config) |
| .with_runtime_env(runtime) |
| .with_default_features() |
| .build(); |
| let ctx = SessionContext::new_with_state(state); |
| |
| let batches = vec![]; |
| let df = ctx.read_batches(batches).unwrap(); |
| df.clone().show().await.unwrap(); |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| ++ |
| ++ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn consecutive_projection_same_schema() -> Result<()> { |
| let state = SessionStateBuilder::new().with_default_features().build(); |
| let ctx = SessionContext::new_with_state(state); |
| |
| let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)])); |
| |
| let batch = |
| RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![0, 1]))]) |
| .unwrap(); |
| |
| let df = ctx.read_batch(batch).unwrap(); |
| df.clone().show().await.unwrap(); |
| |
| // Add `t` column full of nulls |
| let df = df |
| .with_column( |
| "t", |
| cast(Expr::Literal(ScalarValue::Null, None), DataType::Int32), |
| ) |
| .unwrap(); |
| df.clone().show().await.unwrap(); |
| |
| let df = df |
| // (case when id = 1 then 10 else t) as t |
| .with_column( |
| "t", |
| when(col("id").eq(lit(1)), lit(10)) |
| .otherwise(col("t")) |
| .unwrap(), |
| ) |
| .unwrap() |
| // (case when id = 1 then 10 else t) as t2 |
| .with_column( |
| "t2", |
| when(col("id").eq(lit(1)), lit(10)) |
| .otherwise(col("t")) |
| .unwrap(), |
| ) |
| .unwrap(); |
| |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +----+----+----+ |
| | id | t | t2 | |
| +----+----+----+ |
| | 0 | | | |
| | 1 | 10 | 10 | |
| +----+----+----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| async fn create_test_table(name: &str) -> Result<DataFrame> { |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::Utf8, false), |
| Field::new("b", DataType::Int32, false), |
| ])); |
| |
| // define data. |
| let batch = RecordBatch::try_new( |
| schema, |
| vec![ |
| Arc::new(StringArray::from(vec![ |
| "abcDEF", |
| "abc123", |
| "CBAdef", |
| "123AbcDef", |
| ])), |
| Arc::new(Int32Array::from(vec![1, 10, 10, 100])), |
| ], |
| )?; |
| |
| let ctx = SessionContext::new(); |
| |
| ctx.register_batch(name, batch)?; |
| |
| ctx.table(name).await |
| } |
| |
| async fn aggregates_table(ctx: &SessionContext) -> Result<DataFrame> { |
| let testdata = datafusion::test_util::arrow_test_data(); |
| |
| ctx.read_csv( |
| format!("{testdata}/csv/aggregate_test_100.csv"), |
| CsvReadOptions::default(), |
| ) |
| .await |
| } |
| |
| fn create_join_context() -> Result<SessionContext> { |
| let t1 = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::UInt32, false), |
| Field::new("b", DataType::Utf8, false), |
| Field::new("c", DataType::Int32, false), |
| ])); |
| let t2 = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::UInt32, false), |
| Field::new("b", DataType::Utf8, false), |
| Field::new("c", DataType::Int32, false), |
| ])); |
| |
| // define data. |
| let batch1 = RecordBatch::try_new( |
| t1, |
| vec![ |
| Arc::new(UInt32Array::from(vec![1, 10, 11, 100])), |
| Arc::new(StringArray::from(vec!["a", "b", "c", "d"])), |
| Arc::new(Int32Array::from(vec![10, 20, 30, 40])), |
| ], |
| )?; |
| // define data. |
| let batch2 = RecordBatch::try_new( |
| t2, |
| vec![ |
| Arc::new(UInt32Array::from(vec![3, 10, 13, 100])), |
| Arc::new(StringArray::from(vec!["a", "b", "c", "d"])), |
| Arc::new(Int32Array::from(vec![1, 2, 3, 4])), |
| ], |
| )?; |
| |
| let config = SessionConfig::new().with_target_partitions(4); |
| let ctx = SessionContext::new_with_config(config); |
| // let ctx = SessionContext::new(); |
| |
| ctx.register_batch("t1", batch1)?; |
| ctx.register_batch("t2", batch2)?; |
| |
| Ok(ctx) |
| } |
| |
| /// Create a data frame that contains nested types. |
| /// |
| /// Create a data frame with nested types, each row contains: |
| /// - shape_id an integer primary key |
| /// - points A list of points structs {x, y} |
| /// - A list of tags. |
| async fn table_with_nested_types(n: usize) -> Result<DataFrame> { |
| use rand::prelude::*; |
| |
| let mut shape_id_builder = UInt32Builder::new(); |
| let mut points_builder = ListBuilder::new(StructBuilder::from_fields( |
| vec![ |
| Field::new("x", DataType::Int32, false), |
| Field::new("y", DataType::Int32, false), |
| ], |
| 5, |
| )); |
| let mut tags_builder = ListBuilder::new(StringBuilder::new()); |
| |
| let mut rng = StdRng::seed_from_u64(197); |
| |
| for idx in 0..n { |
| // Append shape id. |
| shape_id_builder.append_value(idx as u32 + 1); |
| |
| // Add a random number of points |
| let num_points: usize = rng.random_range(0..4); |
| if num_points > 0 { |
| for _ in 0..num_points.max(2) { |
| // Add x value |
| points_builder |
| .values() |
| .field_builder::<Int32Builder>(0) |
| .unwrap() |
| .append_value(rng.random_range(-10..10)); |
| // Add y value |
| points_builder |
| .values() |
| .field_builder::<Int32Builder>(1) |
| .unwrap() |
| .append_value(rng.random_range(-10..10)); |
| points_builder.values().append(true); |
| } |
| } |
| |
| // Append null if num points is 0. |
| points_builder.append(num_points > 0); |
| |
| // Append tags. |
| let num_tags: usize = rng.random_range(0..5); |
| for id in 0..num_tags { |
| tags_builder.values().append_value(format!("tag{}", id + 1)); |
| } |
| tags_builder.append(num_tags > 0); |
| } |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("shape_id", Arc::new(shape_id_builder.finish()) as ArrayRef), |
| ("points", Arc::new(points_builder.finish()) as ArrayRef), |
| ("tags", Arc::new(tags_builder.finish()) as ArrayRef), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| ctx.table("shapes").await |
| } |
| |
| fn get_fixed_list_batch() -> Result<RecordBatch, ArrowError> { |
| let mut shape_id_builder = UInt32Builder::new(); |
| let mut tags_builder = FixedSizeListBuilder::new(StringBuilder::new(), 2); |
| |
| for idx in 0..6 { |
| // Append shape id. |
| shape_id_builder.append_value(idx as u32 + 1); |
| |
| if idx % 3 != 0 { |
| tags_builder |
| .values() |
| .append_value(format!("tag{}1", idx + 1)); |
| tags_builder |
| .values() |
| .append_value(format!("tag{}2", idx + 1)); |
| tags_builder.append(true); |
| } else { |
| tags_builder.values().append_null(); |
| tags_builder.values().append_null(); |
| tags_builder.append(false); |
| } |
| } |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("shape_id", Arc::new(shape_id_builder.finish()) as ArrayRef), |
| ("tags", Arc::new(tags_builder.finish()) as ArrayRef), |
| ])?; |
| |
| Ok(batch) |
| } |
| |
| /// Create a table with different types of list columns and a string column. |
| async fn table_with_mixed_lists() -> Result<DataFrame> { |
| let list_array = ListArray::from_iter_primitive::<Int32Type, _, _>(vec![ |
| Some(vec![Some(1), Some(2), Some(3)]), |
| None, |
| Some(vec![None]), |
| None, |
| ]); |
| |
| let large_list_array = |
| LargeListArray::from_iter_primitive::<Float32Type, _, _>(vec![ |
| Some(vec![None, Some(1.1)]), |
| Some(vec![Some(2.2), Some(3.3), Some(4.4)]), |
| None, |
| Some(vec![]), |
| ]); |
| |
| let fixed_list_array = FixedSizeListArray::from_iter_primitive::<UInt64Type, _, _>( |
| vec![ |
| None, |
| Some(vec![Some(1), Some(2)]), |
| Some(vec![Some(3), Some(4)]), |
| None, |
| ], |
| 2, |
| ); |
| |
| let string_array = StringArray::from(vec!["a", "b", "c", "d"]); |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("list", Arc::new(list_array) as ArrayRef), |
| ("large_list", Arc::new(large_list_array) as ArrayRef), |
| ("fixed_list", Arc::new(fixed_list_array) as ArrayRef), |
| ("string", Arc::new(string_array) as ArrayRef), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("mixed_lists", batch)?; |
| ctx.table("mixed_lists").await |
| } |
| |
| /// A a data frame that a list of integers and string IDs |
| async fn table_with_lists_and_nulls() -> Result<DataFrame> { |
| let mut list_builder = ListBuilder::new(UInt32Builder::new()); |
| let mut id_builder = StringBuilder::new(); |
| |
| // [1, 2], A |
| list_builder.values().append_value(1); |
| list_builder.values().append_value(2); |
| list_builder.append(true); |
| id_builder.append_value("A"); |
| |
| // NULL, B |
| list_builder.append(false); |
| id_builder.append_value("B"); |
| |
| // [], C |
| list_builder.append(true); |
| id_builder.append_value("C"); |
| |
| // [3], D |
| list_builder.values().append_value(3); |
| list_builder.append(true); |
| id_builder.append_value("D"); |
| |
| let batch = RecordBatch::try_from_iter(vec![ |
| ("list", Arc::new(list_builder.finish()) as ArrayRef), |
| ("id", Arc::new(id_builder.finish()) as ArrayRef), |
| ])?; |
| |
| let ctx = SessionContext::new(); |
| ctx.register_batch("shapes", batch)?; |
| ctx.table("shapes").await |
| } |
| |
| pub async fn register_alltypes_tiny_pages_parquet(ctx: &SessionContext) -> Result<()> { |
| let testdata = parquet_test_data(); |
| ctx.register_parquet( |
| "alltypes_tiny_pages", |
| &format!("{testdata}/alltypes_tiny_pages.parquet"), |
| ParquetReadOptions::default(), |
| ) |
| .await?; |
| Ok(()) |
| } |
| #[derive(Debug)] |
| struct HardcodedIntProvider {} |
| |
| impl VarProvider for HardcodedIntProvider { |
| fn get_value(&self, _var_names: Vec<String>) -> Result<ScalarValue, DataFusionError> { |
| Ok(ScalarValue::Int64(Some(1234))) |
| } |
| |
| fn get_type(&self, _: &[String]) -> Option<DataType> { |
| Some(DataType::Int64) |
| } |
| } |
| |
| #[tokio::test] |
| async fn use_var_provider() -> Result<()> { |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("foo", DataType::Int64, false), |
| Field::new("bar", DataType::Int64, false), |
| ])); |
| |
| let mem_table = Arc::new(MemTable::try_new(schema, vec![vec![]])?); |
| |
| let config = SessionConfig::new() |
| .with_target_partitions(4) |
| .set_bool("datafusion.optimizer.skip_failed_rules", false); |
| let ctx = SessionContext::new_with_config(config); |
| |
| ctx.register_table("csv_table", mem_table)?; |
| ctx.register_variable(VarType::UserDefined, Arc::new(HardcodedIntProvider {})); |
| |
| let dataframe = ctx |
| .sql("SELECT foo FROM csv_table WHERE bar > @var") |
| .await?; |
| dataframe.collect().await?; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_array_agg() -> Result<()> { |
| let df = create_test_table("test") |
| .await? |
| .aggregate(vec![], vec![array_agg(col("a"))])?; |
| |
| let results = df.collect().await?; |
| |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-------------------------------------+ |
| | array_agg(test.a) | |
| +-------------------------------------+ |
| | [abcDEF, abc123, CBAdef, 123AbcDef] | |
| +-------------------------------------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_dataframe_placeholder_missing_param_values() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| // Creating LogicalPlans with placeholders should work. |
| let df = ctx |
| .read_empty() |
| .unwrap() |
| .with_column("a", lit(1)) |
| .unwrap() |
| .filter(col("a").eq(placeholder("$0"))) |
| .unwrap(); |
| |
| let logical_plan = df.logical_plan(); |
| let formatted = logical_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| |
| assert_snapshot!( |
| actual, |
| @r" |
| Filter: a = $0 [a:Int32] |
| Projection: Int32(1) AS a [a:Int32] |
| EmptyRelation: rows=1 [] |
| " |
| ); |
| |
| // Executing LogicalPlans with placeholders that don't have bound values |
| // should fail. |
| let results = df.collect().await; |
| let err_msg = results.unwrap_err().strip_backtrace(); |
| assert_snapshot!( |
| err_msg, |
| @"Execution error: Placeholder '$0' was not provided a value for execution." |
| ); |
| |
| // Providing a parameter value should resolve the error |
| let df = ctx |
| .read_empty() |
| .unwrap() |
| .with_column("a", lit(1)) |
| .unwrap() |
| .filter(col("a").eq(placeholder("$0"))) |
| .unwrap() |
| .with_param_values(vec![("0", ScalarValue::from(3i32))]) // <-- provide parameter value |
| .unwrap(); |
| |
| let logical_plan = df.logical_plan(); |
| let formatted = logical_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| |
| assert_snapshot!( |
| actual, |
| @r" |
| Filter: a = Int32(3) [a:Int32] |
| Projection: Int32(1) AS a [a:Int32] |
| EmptyRelation: rows=1 [] |
| " |
| ); |
| |
| // N.B., the test is basically `SELECT 1 as a WHERE a = 3;` which returns no results. |
| assert_snapshot!( |
| batches_to_string(&df.collect().await.unwrap()), |
| @r###" |
| ++ |
| ++ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_dataframe_placeholder_column_parameter() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| // Creating LogicalPlans with placeholders should work |
| let df = ctx.read_empty().unwrap().select_exprs(&["$1"]).unwrap(); |
| let logical_plan = df.logical_plan(); |
| let formatted = logical_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| |
| assert_snapshot!( |
| actual, |
| @r" |
| Projection: $1 [$1:Null;N] |
| EmptyRelation: rows=1 [] |
| " |
| ); |
| |
| // Executing LogicalPlans with placeholders that don't have bound values |
| // should fail. |
| let results = df.collect().await; |
| let err_msg = results.unwrap_err().strip_backtrace(); |
| assert_snapshot!( |
| err_msg, |
| @"Execution error: Placeholder '$1' was not provided a value for execution." |
| ); |
| |
| // Providing a parameter value should resolve the error |
| let df = ctx |
| .read_empty() |
| .unwrap() |
| .select_exprs(&["$1"]) |
| .unwrap() |
| .with_param_values(vec![("1", ScalarValue::from(3i32))]) |
| .unwrap(); |
| |
| let logical_plan = df.logical_plan(); |
| let formatted = logical_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| |
| assert_snapshot!( |
| actual, |
| @r" |
| Projection: Int32(3) AS $1 [$1:Null;N] |
| EmptyRelation: rows=1 [] |
| " |
| ); |
| |
| assert_snapshot!( |
| batches_to_string(&df.collect().await.unwrap()), |
| @r###" |
| +----+ |
| | $1 | |
| +----+ |
| | 3 | |
| +----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_dataframe_placeholder_like_expression() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| // Creating LogicalPlans with placeholders should work |
| let df = ctx |
| .read_empty() |
| .unwrap() |
| .with_column("a", lit("foo")) |
| .unwrap() |
| .filter(col("a").like(placeholder("$1"))) |
| .unwrap(); |
| |
| let logical_plan = df.logical_plan(); |
| let formatted = logical_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| |
| assert_snapshot!( |
| actual, |
| @r#" |
| Filter: a LIKE $1 [a:Utf8] |
| Projection: Utf8("foo") AS a [a:Utf8] |
| EmptyRelation: rows=1 [] |
| "# |
| ); |
| |
| // Executing LogicalPlans with placeholders that don't have bound values |
| // should fail. |
| let results = df.collect().await; |
| let err_msg = results.unwrap_err().strip_backtrace(); |
| assert_snapshot!( |
| err_msg, |
| @"Execution error: Placeholder '$1' was not provided a value for execution." |
| ); |
| |
| // Providing a parameter value should resolve the error |
| let df = ctx |
| .read_empty() |
| .unwrap() |
| .with_column("a", lit("foo")) |
| .unwrap() |
| .filter(col("a").like(placeholder("$1"))) |
| .unwrap() |
| .with_param_values(vec![("1", ScalarValue::from("f%"))]) |
| .unwrap(); |
| |
| let logical_plan = df.logical_plan(); |
| let formatted = logical_plan.display_indent_schema().to_string(); |
| let actual = formatted.trim(); |
| |
| assert_snapshot!( |
| actual, |
| @r#" |
| Filter: a LIKE Utf8("f%") [a:Utf8] |
| Projection: Utf8("foo") AS a [a:Utf8] |
| EmptyRelation: rows=1 [] |
| "# |
| ); |
| |
| assert_snapshot!( |
| batches_to_string(&df.collect().await.unwrap()), |
| @r###" |
| +-----+ |
| | a | |
| +-----+ |
| | foo | |
| +-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn write_partitioned_parquet_results() -> Result<()> { |
| // create partitioned input file and context |
| let tmp_dir = TempDir::new()?; |
| |
| let ctx = SessionContext::new(); |
| |
| // Create an in memory table with schema C1 and C2, both strings |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("c1", DataType::Utf8, false), |
| Field::new("c2", DataType::Utf8, false), |
| ])); |
| |
| let record_batch = RecordBatch::try_new( |
| schema.clone(), |
| vec![ |
| Arc::new(StringArray::from(vec!["abc", "def"])), |
| Arc::new(StringArray::from(vec!["123", "456"])), |
| ], |
| )?; |
| |
| let mem_table = Arc::new(MemTable::try_new(schema, vec![vec![record_batch]])?); |
| |
| // Register the table in the context |
| ctx.register_table("test", mem_table)?; |
| |
| let local = Arc::new(LocalFileSystem::new_with_prefix(&tmp_dir)?); |
| let local_url = Url::parse("file://local").unwrap(); |
| ctx.register_object_store(&local_url, local); |
| |
| // execute a simple query and write the results to parquet |
| let out_dir = tmp_dir.as_ref().to_str().unwrap().to_string() + "/out/"; |
| let out_dir_url = format!("file://{out_dir}"); |
| |
| // Write the results to parquet with partitioning |
| let df = ctx.sql("SELECT c1, c2 FROM test").await?; |
| let df_write_options = |
| DataFrameWriteOptions::new().with_partition_by(vec![String::from("c2")]); |
| |
| df.write_parquet(&out_dir_url, df_write_options, None) |
| .await?; |
| |
| // Explicitly read the parquet file at c2=123 to verify the physical files are partitioned |
| let partitioned_file = format!("{out_dir}/c2=123"); |
| let filter_df = ctx |
| .read_parquet(&partitioned_file, ParquetReadOptions::default()) |
| .await?; |
| |
| // Check that the c2 column is gone and that c1 is abc. |
| let results = filter_df.collect().await?; |
| assert_snapshot!( |
| batches_to_string(&results), |
| @r###" |
| +-----+ |
| | c1 | |
| +-----+ |
| | abc | |
| +-----+ |
| "### |
| ); |
| |
| // Read the entire set of parquet files |
| let df = ctx |
| .read_parquet( |
| &out_dir_url, |
| ParquetReadOptions::default() |
| .table_partition_cols(vec![(String::from("c2"), DataType::Utf8)]), |
| ) |
| .await?; |
| |
| // Check that the df has the entire set of data |
| let results = df.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +-----+-----+ |
| | c1 | c2 | |
| +-----+-----+ |
| | abc | 123 | |
| | def | 456 | |
| +-----+-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn write_parquet_results() -> Result<()> { |
| // create partitioned input file and context |
| let tmp_dir = TempDir::new()?; |
| // let mut ctx = create_ctx(&tmp_dir, 4).await?; |
| let ctx = |
| SessionContext::new_with_config(SessionConfig::new().with_target_partitions(8)); |
| let schema = populate_csv_partitions(&tmp_dir, 4, ".csv")?; |
| // register csv file with the execution context |
| ctx.register_csv( |
| "test", |
| tmp_dir.path().to_str().unwrap(), |
| CsvReadOptions::new().schema(&schema), |
| ) |
| .await?; |
| |
| // register a local file system object store for /tmp directory |
| let local = Arc::new(LocalFileSystem::new_with_prefix(&tmp_dir)?); |
| let local_url = Url::parse("file://local").unwrap(); |
| ctx.register_object_store(&local_url, local); |
| |
| // execute a simple query and write the results to parquet |
| let out_dir = tmp_dir.as_ref().to_str().unwrap().to_string() + "/out/"; |
| let out_dir_url = "file://local/out/"; |
| let df = ctx.sql("SELECT c1, c2 FROM test").await?; |
| df.write_parquet(out_dir_url, DataFrameWriteOptions::new(), None) |
| .await?; |
| // write_parquet(&mut ctx, "SELECT c1, c2 FROM test", &out_dir, None).await?; |
| |
| // create a new context and verify that the results were saved to a partitioned parquet file |
| let ctx = SessionContext::new(); |
| |
| // get write_id |
| let mut paths = fs::read_dir(&out_dir).unwrap(); |
| let path = paths.next(); |
| let name = path |
| .unwrap()? |
| .path() |
| .file_name() |
| .expect("Should be a file name") |
| .to_str() |
| .expect("Should be a str") |
| .to_owned(); |
| let (parsed_id, _) = name.split_once('_').expect("File should contain _ !"); |
| let write_id = parsed_id.to_owned(); |
| |
| // register each partition as well as the top level dir |
| ctx.register_parquet( |
| "part0", |
| &format!("{out_dir}/{write_id}_0.parquet"), |
| ParquetReadOptions::default(), |
| ) |
| .await?; |
| |
| ctx.register_parquet("allparts", &out_dir, ParquetReadOptions::default()) |
| .await?; |
| |
| let part0 = ctx.sql("SELECT c1, c2 FROM part0").await?.collect().await?; |
| let allparts = ctx |
| .sql("SELECT c1, c2 FROM allparts") |
| .await? |
| .collect() |
| .await?; |
| |
| let allparts_count: usize = allparts.iter().map(|batch| batch.num_rows()).sum(); |
| |
| assert_eq!(part0[0].schema(), allparts[0].schema()); |
| |
| assert_eq!(allparts_count, 40); |
| |
| Ok(()) |
| } |
| |
| fn union_fields() -> UnionFields { |
| [ |
| (0, Arc::new(Field::new("A", DataType::Int32, true))), |
| (1, Arc::new(Field::new("B", DataType::Float64, true))), |
| (2, Arc::new(Field::new("C", DataType::Utf8, true))), |
| ] |
| .into_iter() |
| .collect() |
| } |
| |
| #[tokio::test] |
| async fn union_literal_is_null_and_not_null() -> Result<()> { |
| let str_array_1 = StringArray::from(vec![None::<String>]); |
| 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(internal_datafusion_err!( |
| "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=}] |
| let int_array = Int32Array::from(vec![Some(1), None, None, None, None, None]); |
| let float_array = Float64Array::from(vec![None, None, Some(3.2), None, None, None]); |
| let str_array = StringArray::from(vec![None, None, None, None, Some("a"), None]); |
| let type_ids = [0, 0, 1, 1, 2, 2].into_iter().collect::<ScalarBuffer<i8>>(); |
| |
| let children = vec![ |
| Arc::new(int_array) as Arc<dyn Array>, |
| Arc::new(float_array), |
| Arc::new(str_array), |
| ]; |
| |
| let array = UnionArray::try_new(union_fields(), type_ids, None, children).unwrap(); |
| |
| let field = Field::new( |
| "my_union", |
| DataType::Union(union_fields(), UnionMode::Sparse), |
| true, |
| ); |
| let schema = Arc::new(Schema::new(vec![field])); |
| |
| let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap(); |
| |
| let ctx = SessionContext::new(); |
| |
| ctx.register_batch("union_batch", batch).unwrap(); |
| |
| let df = ctx.table("union_batch").await.unwrap(); |
| |
| // view_all |
| assert_snapshot!( |
| batches_to_sort_string(&df.clone().collect().await.unwrap()), |
| @r###" |
| +----------+ |
| | my_union | |
| +----------+ |
| | {A=1} | |
| | {A=} | |
| | {B=3.2} | |
| | {B=} | |
| | {C=a} | |
| | {C=} | |
| +----------+ |
| "### |
| ); |
| |
| // filter where is null |
| let result_df = df.clone().filter(col("my_union").is_null()).unwrap(); |
| assert_snapshot!( |
| batches_to_sort_string(&result_df.collect().await.unwrap()), |
| @r###" |
| +----------+ |
| | my_union | |
| +----------+ |
| | {A=} | |
| | {B=} | |
| | {C=} | |
| +----------+ |
| "### |
| ); |
| |
| // filter where is not null |
| let result_df = df.filter(col("my_union").is_not_null()).unwrap(); |
| assert_snapshot!( |
| batches_to_sort_string(&result_df.collect().await.unwrap()), |
| @r###" |
| +----------+ |
| | my_union | |
| +----------+ |
| | {A=1} | |
| | {B=3.2} | |
| | {C=a} | |
| +----------+ |
| "### |
| ); |
| } |
| |
| #[tokio::test] |
| async fn dense_union_is_null() { |
| // union of [{A=1}, null, {B=3.2}, {A=34}] |
| let int_array = Int32Array::from(vec![Some(1), None]); |
| let float_array = Float64Array::from(vec![Some(3.2), None]); |
| let str_array = StringArray::from(vec![Some("a"), None]); |
| let type_ids = [0, 0, 1, 1, 2, 2].into_iter().collect::<ScalarBuffer<i8>>(); |
| let offsets = [0, 1, 0, 1, 0, 1] |
| .into_iter() |
| .collect::<ScalarBuffer<i32>>(); |
| |
| let children = vec![ |
| Arc::new(int_array) as Arc<dyn Array>, |
| Arc::new(float_array), |
| Arc::new(str_array), |
| ]; |
| |
| let array = |
| UnionArray::try_new(union_fields(), type_ids, Some(offsets), children).unwrap(); |
| |
| let field = Field::new( |
| "my_union", |
| DataType::Union(union_fields(), UnionMode::Dense), |
| true, |
| ); |
| let schema = Arc::new(Schema::new(vec![field])); |
| |
| let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap(); |
| |
| let ctx = SessionContext::new(); |
| |
| ctx.register_batch("union_batch", batch).unwrap(); |
| |
| let df = ctx.table("union_batch").await.unwrap(); |
| |
| // view_all |
| assert_snapshot!( |
| batches_to_sort_string(&df.clone().collect().await.unwrap()), |
| @r###" |
| +----------+ |
| | my_union | |
| +----------+ |
| | {A=1} | |
| | {A=} | |
| | {B=3.2} | |
| | {B=} | |
| | {C=a} | |
| | {C=} | |
| +----------+ |
| "### |
| ); |
| |
| // filter where is null |
| let result_df = df.clone().filter(col("my_union").is_null()).unwrap(); |
| assert_snapshot!( |
| batches_to_sort_string(&result_df.collect().await.unwrap()), |
| @r###" |
| +----------+ |
| | my_union | |
| +----------+ |
| | {A=} | |
| | {B=} | |
| | {C=} | |
| +----------+ |
| "### |
| ); |
| |
| // filter where is not null |
| let result_df = df.filter(col("my_union").is_not_null()).unwrap(); |
| assert_snapshot!( |
| batches_to_sort_string(&result_df.collect().await.unwrap()), |
| @r###" |
| +----------+ |
| | my_union | |
| +----------+ |
| | {A=1} | |
| | {B=3.2} | |
| | {C=a} | |
| +----------+ |
| "### |
| ); |
| } |
| |
| #[tokio::test] |
| async fn boolean_dictionary_as_filter() { |
| let values = vec![Some(true), Some(false), None, Some(true)]; |
| let keys = vec![0, 0, 1, 2, 1, 3, 1]; |
| let values_array = BooleanArray::from(values); |
| let keys_array = Int8Array::from(keys); |
| let array = |
| DictionaryArray::new(keys_array, Arc::new(values_array) as Arc<dyn Array>); |
| let array = Arc::new(array); |
| |
| let field = Field::new( |
| "my_dict", |
| DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Boolean)), |
| true, |
| ); |
| let schema = Arc::new(Schema::new(vec![field])); |
| |
| let batch = RecordBatch::try_new(schema, vec![array.clone()]).unwrap(); |
| |
| let ctx = SessionContext::new(); |
| |
| ctx.register_batch("dict_batch", batch).unwrap(); |
| |
| let df = ctx.table("dict_batch").await.unwrap(); |
| |
| // view_all |
| assert_snapshot!( |
| batches_to_string(&df.clone().collect().await.unwrap()), |
| @r###" |
| +---------+ |
| | my_dict | |
| +---------+ |
| | true | |
| | true | |
| | false | |
| | | |
| | false | |
| | true | |
| | false | |
| +---------+ |
| "### |
| ); |
| |
| let result_df = df.clone().filter(col("my_dict")).unwrap(); |
| |
| assert_snapshot!( |
| batches_to_string(&result_df.collect().await.unwrap()), |
| @r###" |
| +---------+ |
| | my_dict | |
| +---------+ |
| | true | |
| | true | |
| | true | |
| +---------+ |
| "### |
| ); |
| |
| // test nested dictionary |
| let keys = vec![0, 2]; // 0 -> true, 2 -> false |
| let keys_array = Int8Array::from(keys); |
| let nested_array = DictionaryArray::new(keys_array, array); |
| |
| let field = Field::new( |
| "my_nested_dict", |
| DataType::Dictionary( |
| Box::new(DataType::Int8), |
| Box::new(DataType::Dictionary( |
| Box::new(DataType::Int8), |
| Box::new(DataType::Boolean), |
| )), |
| ), |
| true, |
| ); |
| |
| let schema = Arc::new(Schema::new(vec![field])); |
| |
| let batch = RecordBatch::try_new(schema, vec![Arc::new(nested_array)]).unwrap(); |
| |
| ctx.register_batch("nested_dict_batch", batch).unwrap(); |
| |
| let df = ctx.table("nested_dict_batch").await.unwrap(); |
| |
| // view_all |
| assert_snapshot!( |
| batches_to_string(&df.clone().collect().await.unwrap()), |
| @r###" |
| +----------------+ |
| | my_nested_dict | |
| +----------------+ |
| | true | |
| | false | |
| +----------------+ |
| "### |
| ); |
| |
| let result_df = df.clone().filter(col("my_nested_dict")).unwrap(); |
| assert_snapshot!( |
| batches_to_string(&result_df.collect().await.unwrap()), |
| @r###" |
| +----------------+ |
| | my_nested_dict | |
| +----------------+ |
| | true | |
| +----------------+ |
| "### |
| ); |
| } |
| |
| #[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") |
| .await? |
| .select(vec![col("a"), col("test.b"), lit(1).alias("one")])? |
| .alias("table_alias")?; |
| // All output column qualifiers are changed to "table_alias" |
| df.schema().columns().iter().for_each(|c| { |
| assert_eq!(c.relation, Some("table_alias".into())); |
| }); |
| |
| let plan = df |
| .clone() |
| .into_unoptimized_plan() |
| .display_indent_schema() |
| .to_string(); |
| assert_snapshot!(plan, @r###" |
| SubqueryAlias: table_alias [a:Utf8, b:Int32, one:Int32] |
| Projection: test.a, test.b, Int32(1) AS one [a:Utf8, b:Int32, one:Int32] |
| TableScan: test [a:Utf8, b:Int32] |
| "###); |
| |
| // Select over the aliased DataFrame |
| let df = df.select(vec![ |
| col("table_alias.a"), |
| col("b") + col("table_alias.one"), |
| ])?; |
| assert_snapshot!( |
| batches_to_sort_string(&df.collect().await.unwrap()), |
| @r###" |
| +-----------+---------------------------------+ |
| | a | table_alias.b + table_alias.one | |
| +-----------+---------------------------------+ |
| | 123AbcDef | 101 | |
| | CBAdef | 11 | |
| | abc123 | 11 | |
| | abcDEF | 2 | |
| +-----------+---------------------------------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_alias_with_metadata() -> Result<()> { |
| let mut metadata = HashMap::new(); |
| metadata.insert(String::from("k"), String::from("v")); |
| let metadata = FieldMetadata::from(metadata); |
| let df = create_test_table("test") |
| .await? |
| .select(vec![col("a").alias_with_metadata("b", Some(metadata))])? |
| .alias("table_alias")?; |
| let df = df.select(vec![col("table_alias.b")])?; |
| let schema = df.schema(); |
| let metadata = schema.field(0).metadata(); |
| assert_eq!(metadata.get("k"), Some(&String::from("v"))); |
| Ok(()) |
| } |
| |
| // Use alias to perform a self-join |
| // Issue: https://github.com/apache/datafusion/issues/14112 |
| #[tokio::test] |
| async fn test_alias_self_join() -> Result<()> { |
| let left = create_test_table("t1").await?; |
| let right = left.clone().alias("t2")?; |
| let joined = left.join(right, JoinType::Full, &["a"], &["a"], None)?; |
| assert_snapshot!( |
| batches_to_sort_string(&joined.collect().await.unwrap()), |
| @r###" |
| +-----------+-----+-----------+-----+ |
| | a | b | a | b | |
| +-----------+-----+-----------+-----+ |
| | 123AbcDef | 100 | 123AbcDef | 100 | |
| | CBAdef | 10 | CBAdef | 10 | |
| | abc123 | 10 | abc123 | 10 | |
| | abcDEF | 1 | abcDEF | 1 | |
| +-----------+-----+-----------+-----+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_alias_empty() -> Result<()> { |
| let df = create_test_table("test").await?.alias("")?; |
| let plan = df |
| .clone() |
| .into_unoptimized_plan() |
| .display_indent_schema() |
| .to_string(); |
| assert_snapshot!(plan, @r###" |
| SubqueryAlias: [a:Utf8, b:Int32] |
| TableScan: test [a:Utf8, b:Int32] |
| "###); |
| |
| assert_snapshot!( |
| batches_to_sort_string(&df.select(vec![col("a"), col("b")])?.collect().await.unwrap()), |
| @r###" |
| +-----------+-----+ |
| | a | b | |
| +-----------+-----+ |
| | 123AbcDef | 100 | |
| | CBAdef | 10 | |
| | abc123 | 10 | |
| | abcDEF | 1 | |
| +-----------+-----+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_alias_nested() -> Result<()> { |
| let df = create_test_table("test") |
| .await? |
| .select(vec![col("a"), col("test.b"), lit(1).alias("one")])? |
| .alias("alias1")? |
| .alias("alias2")?; |
| |
| let plan = df |
| .clone() |
| .into_optimized_plan()? |
| .display_indent_schema() |
| .to_string(); |
| assert_snapshot!(plan, @r###" |
| SubqueryAlias: alias2 [a:Utf8, b:Int32, one:Int32] |
| SubqueryAlias: alias1 [a:Utf8, b:Int32, one:Int32] |
| Projection: test.a, test.b, Int32(1) AS one [a:Utf8, b:Int32, one:Int32] |
| TableScan: test projection=[a, b] [a:Utf8, b:Int32] |
| "###); |
| |
| // Select over the aliased DataFrame |
| let select1 = df |
| .clone() |
| .select(vec![col("alias2.a"), col("b") + col("alias2.one")])?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&select1.collect().await.unwrap()), |
| @r###" |
| +-----------+-----------------------+ |
| | a | alias2.b + alias2.one | |
| +-----------+-----------------------+ |
| | 123AbcDef | 101 | |
| | CBAdef | 11 | |
| | abc123 | 11 | |
| | abcDEF | 2 | |
| +-----------+-----------------------+ |
| "### |
| ); |
| |
| // Only the outermost alias is visible |
| let select2 = df.select(vec![col("alias1.a")]); |
| assert_snapshot!( |
| select2.unwrap_err().strip_backtrace(), |
| @"Schema error: No field named alias1.a. Valid fields are alias2.a, alias2.b, alias2.one." |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn register_non_json_file() { |
| let ctx = SessionContext::new(); |
| let err = ctx |
| .register_json( |
| "data", |
| "tests/data/test_binary.parquet", |
| NdJsonReadOptions::default(), |
| ) |
| .await; |
| assert_contains!( |
| err.unwrap_err().to_string(), |
| "test_binary.parquet' does not match the expected extension '.json'" |
| ); |
| } |
| |
| #[tokio::test] |
| async fn register_non_csv_file() { |
| let ctx = SessionContext::new(); |
| let err = ctx |
| .register_csv( |
| "data", |
| "tests/data/test_binary.parquet", |
| CsvReadOptions::default(), |
| ) |
| .await; |
| assert_contains!( |
| err.unwrap_err().to_string(), |
| "test_binary.parquet' does not match the expected extension '.csv'" |
| ); |
| } |
| |
| #[cfg(feature = "avro")] |
| #[tokio::test] |
| async fn register_non_avro_file() { |
| let ctx = SessionContext::new(); |
| let err = ctx |
| .register_avro( |
| "data", |
| "tests/data/test_binary.parquet", |
| datafusion::prelude::AvroReadOptions::default(), |
| ) |
| .await; |
| assert_contains!( |
| err.unwrap_err().to_string(), |
| "test_binary.parquet' does not match the expected extension '.avro'" |
| ); |
| } |
| |
| #[tokio::test] |
| async fn register_non_parquet_file() { |
| let ctx = SessionContext::new(); |
| let err = ctx |
| .register_parquet("data", "tests/data/1.json", ParquetReadOptions::default()) |
| .await; |
| assert_contains!( |
| err.unwrap_err().to_string(), |
| "1.json' does not match the expected extension '.parquet'" |
| ); |
| } |
| |
| // Test inserting into checking. |
| #[tokio::test] |
| async fn test_insert_into_checking() -> Result<()> { |
| // Create a new schema with one field called "a" of type Int64, and setting nullable to false |
| let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, false)])); |
| |
| let session_ctx = SessionContext::new(); |
| |
| // Create and register the initial table with the provided schema and data |
| let initial_table = Arc::new(MemTable::try_new(schema.clone(), vec![vec![]])?); |
| session_ctx.register_table("t", initial_table.clone())?; |
| |
| // There are two cases we need to check |
| // 1. The len of the schema of the plan and the schema of the table should be the same |
| // 2. The datatype of the schema of the plan and the schema of the table should be the same |
| |
| // Test case 1: |
| let write_df = session_ctx.sql("values (1, 2), (3, 4)").await.unwrap(); |
| |
| let e = write_df |
| .write_table("t", DataFrameWriteOptions::new()) |
| .await |
| .unwrap_err(); |
| |
| assert_contains!( |
| e.to_string(), |
| "Inserting query must have the same schema length as the table." |
| ); |
| |
| // Setting nullable to true |
| // Make sure the nullable check go through |
| let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)])); |
| |
| let session_ctx = SessionContext::new(); |
| |
| // Create and register the initial table with the provided schema and data |
| let initial_table = Arc::new(MemTable::try_new(schema.clone(), vec![vec![]])?); |
| session_ctx.register_table("t", initial_table.clone())?; |
| |
| // Test case 2: |
| let write_df = session_ctx.sql("values ('a123'), ('b456')").await.unwrap(); |
| |
| let e = write_df |
| .write_table("t", DataFrameWriteOptions::new()) |
| .await |
| .unwrap_err(); |
| |
| assert_contains!(e.to_string(), "Inserting query schema mismatch: Expected table field 'a' with type Int64, but got 'column1' with type Utf8"); |
| |
| Ok(()) |
| } |
| |
| async fn create_null_table() -> Result<DataFrame> { |
| // create a DataFrame with null values |
| // "+---+----+", |
| // "| a | b |", |
| // "+---+---+", |
| // "| 1 | x |", |
| // "| | |", |
| // "| 3 | z |", |
| // "+---+---+", |
| let schema = Arc::new(Schema::new(vec![ |
| Field::new("a", DataType::Int32, true), |
| Field::new("b", DataType::Utf8, true), |
| ])); |
| let a_values = Int32Array::from(vec![Some(1), None, Some(3)]); |
| let b_values = StringArray::from(vec![Some("x"), None, Some("z")]); |
| let batch = RecordBatch::try_new( |
| schema.clone(), |
| vec![Arc::new(a_values), Arc::new(b_values)], |
| )?; |
| |
| let ctx = SessionContext::new(); |
| let table = MemTable::try_new(schema.clone(), vec![vec![batch]])?; |
| ctx.register_table("t_null", Arc::new(table))?; |
| let df = ctx.table("t_null").await?; |
| Ok(df) |
| } |
| |
| #[tokio::test] |
| async fn test_fill_null() -> Result<()> { |
| let df = create_null_table().await?; |
| |
| // Use fill_null to replace nulls on each column. |
| let df_filled = df |
| .fill_null(ScalarValue::Int32(Some(0)), vec!["a".to_string()])? |
| .fill_null( |
| ScalarValue::Utf8(Some("default".to_string())), |
| vec!["b".to_string()], |
| )?; |
| |
| let results = df_filled.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +---+---------+ |
| | a | b | |
| +---+---------+ |
| | 0 | default | |
| | 1 | x | |
| | 3 | z | |
| +---+---------+ |
| "### |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_fill_null_all_columns() -> Result<()> { |
| let df = create_null_table().await?; |
| |
| // Use fill_null to replace nulls on all columns. |
| // Only column "b" will be replaced since ScalarValue::Utf8(Some("default".to_string())) |
| // can be cast to Utf8. |
| let df_filled = |
| df.fill_null(ScalarValue::Utf8(Some("default".to_string())), vec![])?; |
| |
| let results = df_filled.clone().collect().await?; |
| |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +---+---------+ |
| | a | b | |
| +---+---------+ |
| | | default | |
| | 1 | x | |
| | 3 | z | |
| +---+---------+ |
| "### |
| ); |
| |
| // Fill column "a" null values with a value that cannot be cast to Int32. |
| let df_filled = df_filled.fill_null(ScalarValue::Int32(Some(0)), vec![])?; |
| |
| let results = df_filled.collect().await?; |
| assert_snapshot!( |
| batches_to_sort_string(&results), |
| @r###" |
| +---+---------+ |
| | a | b | |
| +---+---------+ |
| | 0 | default | |
| | 1 | x | |
| | 3 | z | |
| +---+---------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_insert_into_casting_support() -> Result<()> { |
| // Testing case1: |
| // Inserting query schema mismatch: Expected table field 'a' with type Float16, but got 'a' with type Utf8. |
| // And the cast is not supported from Utf8 to Float16. |
| |
| // Create a new schema with one field called "a" of type Float16, and setting nullable to false |
| let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Float16, false)])); |
| |
| let session_ctx = SessionContext::new(); |
| |
| // Create and register the initial table with the provided schema and data |
| let initial_table = Arc::new(MemTable::try_new(schema.clone(), vec![vec![]])?); |
| session_ctx.register_table("t", initial_table.clone())?; |
| |
| let mut write_df = session_ctx.sql("values ('a123'), ('b456')").await.unwrap(); |
| |
| write_df = write_df |
| .clone() |
| .with_column_renamed("column1", "a") |
| .unwrap(); |
| |
| let e = write_df |
| .write_table("t", DataFrameWriteOptions::new()) |
| .await |
| .unwrap_err(); |
| |
| assert_contains!(e.to_string(), "Inserting query schema mismatch: Expected table field 'a' with type Float16, but got 'a' with type Utf8."); |
| |
| // Testing case2: |
| // Inserting query schema mismatch: Expected table field 'a' with type Utf8View, but got 'a' with type Utf8. |
| // And the cast is supported from Utf8 to Utf8View. |
| |
| // Create a new schema with one field called "a" of type Utf8View, and setting nullable to false |
| let schema = Arc::new(Schema::new(vec![Field::new( |
| "a", |
| DataType::Utf8View, |
| false, |
| )])); |
| |
| let initial_table = Arc::new(MemTable::try_new(schema.clone(), vec![vec![]])?); |
| |
| session_ctx.register_table("t2", initial_table.clone())?; |
| |
| let mut write_df = session_ctx.sql("values ('a123'), ('b456')").await.unwrap(); |
| |
| write_df = write_df |
| .clone() |
| .with_column_renamed("column1", "a") |
| .unwrap(); |
| |
| write_df |
| .write_table("t2", DataFrameWriteOptions::new()) |
| .await?; |
| |
| let res = session_ctx |
| .sql("select * from t2") |
| .await |
| .unwrap() |
| .collect() |
| .await |
| .unwrap(); |
| |
| assert_snapshot!( |
| batches_to_string(&res), |
| @r###" |
| +------+ |
| | a | |
| +------+ |
| | a123 | |
| | b456 | |
| +------+ |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_dataframe_from_columns() -> Result<()> { |
| let a: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3])); |
| let b: ArrayRef = Arc::new(BooleanArray::from(vec![true, true, false])); |
| let c: ArrayRef = Arc::new(StringArray::from(vec![Some("foo"), Some("bar"), None])); |
| let df = DataFrame::from_columns(vec![("a", a), ("b", b), ("c", c)])?; |
| |
| assert_eq!(df.schema().fields().len(), 3); |
| assert_eq!(df.clone().count().await?, 3); |
| |
| let rows = df.sort(vec![col("a").sort(true, true)])?; |
| assert_batches_eq!( |
| &[ |
| "+---+-------+-----+", |
| "| a | b | c |", |
| "+---+-------+-----+", |
| "| 1 | true | foo |", |
| "| 2 | true | bar |", |
| "| 3 | false | |", |
| "+---+-------+-----+", |
| ], |
| &rows.collect().await? |
| ); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_dataframe_macro() -> Result<()> { |
| let df = dataframe!( |
| "a" => [1, 2, 3], |
| "b" => [true, true, false], |
| "c" => [Some("foo"), Some("bar"), None] |
| )?; |
| |
| assert_eq!(df.schema().fields().len(), 3); |
| assert_eq!(df.clone().count().await?, 3); |
| |
| let rows = df.sort(vec![col("a").sort(true, true)])?; |
| assert_batches_eq!( |
| &[ |
| "+---+-------+-----+", |
| "| a | b | c |", |
| "+---+-------+-----+", |
| "| 1 | true | foo |", |
| "| 2 | true | bar |", |
| "| 3 | false | |", |
| "+---+-------+-----+", |
| ], |
| &rows.collect().await? |
| ); |
| |
| let df_empty = dataframe!()?; |
| assert_eq!(df_empty.schema().fields().len(), 0); |
| assert_eq!(df_empty.count().await?, 0); |
| |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_copy_schema() -> Result<()> { |
| let tmp_dir = TempDir::new()?; |
| |
| let session_state = SessionStateBuilder::new_with_default_features().build(); |
| |
| let session_ctx = SessionContext::new_with_state(session_state); |
| |
| let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int64, true)])); |
| |
| // Create and register the source table with the provided schema and data |
| let source_table = Arc::new(MemTable::try_new(schema.clone(), vec![vec![]])?); |
| session_ctx.register_table("source_table", source_table.clone())?; |
| |
| let target_path = tmp_dir.path().join("target.csv"); |
| |
| let query = format!( |
| "COPY source_table TO '{}' STORED AS csv", |
| target_path.to_str().unwrap() |
| ); |
| |
| let result = session_ctx.sql(&query).await?; |
| assert_logical_expr_schema_eq_physical_expr_schema(result).await?; |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_copy_to_preserves_order() -> Result<()> { |
| let tmp_dir = TempDir::new()?; |
| |
| let session_state = SessionStateBuilder::new_with_default_features().build(); |
| let session_ctx = SessionContext::new_with_state(session_state); |
| |
| let target_path = tmp_dir.path().join("target_ordered.csv"); |
| let csv_file_format = session_ctx |
| .state() |
| .get_file_format_factory("csv") |
| .map(format_as_file_type) |
| .unwrap(); |
| |
| let ordered_select_plan = LogicalPlanBuilder::values(vec![ |
| vec![lit(1u64)], |
| vec![lit(10u64)], |
| vec![lit(20u64)], |
| vec![lit(100u64)], |
| ])? |
| .sort(vec![SortExpr::new(col("column1"), false, true)])? |
| .build()?; |
| |
| let copy_to_plan = LogicalPlanBuilder::copy_to( |
| ordered_select_plan, |
| target_path.to_str().unwrap().to_string(), |
| csv_file_format, |
| HashMap::new(), |
| vec![], |
| )? |
| .build()?; |
| |
| let union_side_branch = LogicalPlanBuilder::values(vec![vec![lit(1u64)]])?.build()?; |
| let union_plan = LogicalPlanBuilder::from(copy_to_plan) |
| .union(union_side_branch)? |
| .build()?; |
| |
| let frame = session_ctx.execute_logical_plan(union_plan).await?; |
| let physical_plan = frame.create_physical_plan().await?; |
| |
| let physical_plan_format = |
| displayable(physical_plan.as_ref()).indent(true).to_string(); |
| |
| // Expect that input to the DataSinkExec is sorted correctly |
| assert_snapshot!( |
| physical_plan_format, |
| @r###" |
| UnionExec |
| DataSinkExec: sink=CsvSink(file_groups=[]) |
| SortExec: expr=[column1@0 DESC], preserve_partitioning=[false] |
| DataSourceExec: partitions=1, partition_sizes=[1] |
| DataSourceExec: partitions=1, partition_sizes=[1] |
| "### |
| ); |
| Ok(()) |
| } |
| |
| #[tokio::test] |
| async fn test_duplicate_state_fields_for_dfschema_construct() -> Result<()> { |
| let ctx = SessionContext::new(); |
| |
| // Simple schema with just the fields we need |
| let file_schema = Arc::new(Schema::new(vec![ |
| Field::new( |
| "timestamp", |
| DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())), |
| true, |
| ), |
| Field::new("ticker", DataType::Utf8, true), |
| Field::new("value", DataType::Float64, true), |
| Field::new("date", DataType::Utf8, false), |
| ])); |
| |
| let df_schema = DFSchema::try_from(file_schema.clone())?; |
| |
| let timestamp = col("timestamp"); |
| let value = col("value"); |
| let ticker = col("ticker"); |
| let date = col("date"); |
| |
| let mock_exec = Arc::new(EmptyExec::new(file_schema.clone())); |
| |
| // Build first_value aggregate |
| let first_value = Arc::new( |
| AggregateExprBuilder::new( |
| datafusion_functions_aggregate::first_last::first_value_udaf(), |
| vec![ctx.create_physical_expr(value.clone(), &df_schema)?], |
| ) |
| .alias("first_value(value)") |
| .order_by(vec![PhysicalSortExpr::new( |
| ctx.create_physical_expr(timestamp.clone(), &df_schema)?, |
| SortOptions::new(false, false), |
| )]) |
| .schema(file_schema.clone()) |
| .build() |
| .expect("Failed to build first_value"), |
| ); |
| |
| // Build last_value aggregate |
| let last_value = Arc::new( |
| AggregateExprBuilder::new( |
| datafusion_functions_aggregate::first_last::last_value_udaf(), |
| vec![ctx.create_physical_expr(value.clone(), &df_schema)?], |
| ) |
| .alias("last_value(value)") |
| .order_by(vec![PhysicalSortExpr::new( |
| ctx.create_physical_expr(timestamp.clone(), &df_schema)?, |
| SortOptions::new(false, false), |
| )]) |
| .schema(file_schema.clone()) |
| .build() |
| .expect("Failed to build last_value"), |
| ); |
| |
| let partial_agg = AggregateExec::try_new( |
| AggregateMode::Partial, |
| PhysicalGroupBy::new_single(vec![ |
| ( |
| ctx.create_physical_expr(date.clone(), &df_schema)?, |
| "date".to_string(), |
| ), |
| ( |
| ctx.create_physical_expr(ticker.clone(), &df_schema)?, |
| "ticker".to_string(), |
| ), |
| ]), |
| vec![first_value, last_value], |
| vec![None, None], |
| mock_exec, |
| file_schema, |
| ) |
| .expect("Failed to build partial agg"); |
| |
| // Assert that the schema field names match the expected names |
| let expected_field_names = vec![ |
| "date", |
| "ticker", |
| "first_value(value)[first_value]", |
| "timestamp@0", |
| "first_value(value)[first_value_is_set]", |
| "last_value(value)[last_value]", |
| "timestamp@0", |
| "last_value(value)[last_value_is_set]", |
| ]; |
| |
| let binding = partial_agg.schema(); |
| let actual_field_names: Vec<_> = binding.fields().iter().map(|f| f.name()).collect(); |
| assert_eq!(actual_field_names, expected_field_names); |
| |
| // Ensure that DFSchema::try_from does not fail |
| let partial_agg_exec_schema = DFSchema::try_from(partial_agg.schema()); |
| assert!( |
| partial_agg_exec_schema.is_ok(), |
| "Expected get AggregateExec schema to succeed with duplicate state fields" |
| ); |
| |
| Ok(()) |
| } |