blob: 98804e424b407db5ec5ff23d4bb1ef24b1218e2e [file]
// 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.
//! [`DataFrame`] API for building and executing query plans.
#[cfg(feature = "parquet")]
mod parquet;
use crate::arrow::record_batch::RecordBatch;
use crate::arrow::util::pretty;
use crate::datasource::file_format::csv::CsvFormatFactory;
use crate::datasource::file_format::format_as_file_type;
use crate::datasource::file_format::json::JsonFormatFactory;
use crate::datasource::{
provider_as_source, DefaultTableSource, MemTable, TableProvider,
};
use crate::error::Result;
use crate::execution::context::{SessionState, TaskContext};
use crate::execution::FunctionRegistry;
use crate::logical_expr::utils::find_window_exprs;
use crate::logical_expr::{
col, ident, Expr, JoinType, LogicalPlan, LogicalPlanBuilder,
LogicalPlanBuilderOptions, Partitioning, TableType,
};
use crate::physical_plan::{
collect, collect_partitioned, execute_stream, execute_stream_partitioned,
ExecutionPlan, SendableRecordBatchStream,
};
use crate::prelude::SessionContext;
use std::any::Any;
use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use arrow::array::{Array, ArrayRef, Int64Array, StringArray};
use arrow::compute::{cast, concat};
use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
use datafusion_common::config::{CsvOptions, JsonOptions};
use datafusion_common::{
exec_err, internal_datafusion_err, not_impl_err, plan_datafusion_err, plan_err,
Column, DFSchema, DataFusionError, ParamValues, ScalarValue, SchemaError,
TableReference, UnnestOptions,
};
use datafusion_expr::select_expr::SelectExpr;
use datafusion_expr::{
case,
dml::InsertOp,
expr::{Alias, ScalarFunction},
is_null, lit,
utils::COUNT_STAR_EXPANSION,
ExplainOption, SortExpr, TableProviderFilterPushDown, UNNAMED_TABLE,
};
use datafusion_functions::core::coalesce;
use datafusion_functions_aggregate::expr_fn::{
avg, count, max, median, min, stddev, sum,
};
use async_trait::async_trait;
use datafusion_catalog::Session;
/// Contains options that control how data is
/// written out from a DataFrame
pub struct DataFrameWriteOptions {
/// Controls how new data should be written to the table, determining whether
/// to append, overwrite, or replace existing data.
insert_op: InsertOp,
/// Controls if all partitions should be coalesced into a single output file
/// Generally will have slower performance when set to true.
single_file_output: bool,
/// Sets which columns should be used for hive-style partitioned writes by name.
/// Can be set to empty vec![] for non-partitioned writes.
partition_by: Vec<String>,
/// Sets which columns should be used for sorting the output by name.
/// Can be set to empty vec![] for non-sorted writes.
sort_by: Vec<SortExpr>,
}
impl DataFrameWriteOptions {
/// Create a new DataFrameWriteOptions with default values
pub fn new() -> Self {
DataFrameWriteOptions {
insert_op: InsertOp::Append,
single_file_output: false,
partition_by: vec![],
sort_by: vec![],
}
}
/// Set the insert operation
pub fn with_insert_operation(mut self, insert_op: InsertOp) -> Self {
self.insert_op = insert_op;
self
}
/// Set the single_file_output value to true or false
pub fn with_single_file_output(mut self, single_file_output: bool) -> Self {
self.single_file_output = single_file_output;
self
}
/// Sets the partition_by columns for output partitioning
pub fn with_partition_by(mut self, partition_by: Vec<String>) -> Self {
self.partition_by = partition_by;
self
}
/// Sets the sort_by columns for output sorting
pub fn with_sort_by(mut self, sort_by: Vec<SortExpr>) -> Self {
self.sort_by = sort_by;
self
}
}
impl Default for DataFrameWriteOptions {
fn default() -> Self {
Self::new()
}
}
/// Represents a logical set of rows with the same named columns.
///
/// Similar to a [Pandas DataFrame] or [Spark DataFrame], a DataFusion DataFrame
/// represents a 2 dimensional table of rows and columns.
///
/// The typical workflow using DataFrames looks like
///
/// 1. Create a DataFrame via methods on [SessionContext], such as [`read_csv`]
/// and [`read_parquet`].
///
/// 2. Build a desired calculation by calling methods such as [`filter`],
/// [`select`], [`aggregate`], and [`limit`]
///
/// 3. Execute into [`RecordBatch`]es by calling [`collect`]
///
/// A `DataFrame` is a wrapper around a [`LogicalPlan`] and the [`SessionState`]
/// required for execution.
///
/// DataFrames are "lazy" in the sense that most methods do not actually compute
/// anything, they just build up a plan. Calling [`collect`] executes the plan
/// using the same DataFusion planning and execution process used to execute SQL
/// and other queries.
///
/// [Pandas DataFrame]: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.html
/// [Spark DataFrame]: https://spark.apache.org/docs/latest/sql-programming-guide.html
/// [`read_csv`]: SessionContext::read_csv
/// [`read_parquet`]: SessionContext::read_parquet
/// [`filter`]: DataFrame::filter
/// [`select`]: DataFrame::select
/// [`aggregate`]: DataFrame::aggregate
/// [`limit`]: DataFrame::limit
/// [`collect`]: DataFrame::collect
///
/// # Example
/// ```
/// # use std::sync::Arc;
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion::functions_aggregate::expr_fn::min;
/// # use datafusion::arrow::array::{Int32Array, RecordBatch, StringArray};
/// # use datafusion::arrow::datatypes::{DataType, Field, Schema};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// // Read the data from a csv file
/// let df = ctx.read_csv("tests/data/example.csv", CsvReadOptions::new()).await?;
/// // create a new dataframe that computes the equivalent of
/// // `SELECT a, MIN(b) FROM df WHERE a <= b GROUP BY a LIMIT 100;`
/// let df = df.filter(col("a").lt_eq(col("b")))?
/// .aggregate(vec![col("a")], vec![min(col("b"))])?
/// .limit(0, Some(100))?;
/// // Perform the actual computation
/// let results = df.collect();
///
/// // Create a new dataframe with in-memory data
/// let schema = Schema::new(vec![
/// Field::new("id", DataType::Int32, true),
/// Field::new("name", DataType::Utf8, true),
/// ]);
/// let batch = RecordBatch::try_new(
/// Arc::new(schema),
/// vec![
/// Arc::new(Int32Array::from(vec![1, 2, 3])),
/// Arc::new(StringArray::from(vec!["foo", "bar", "baz"])),
/// ],
/// )?;
/// let df = ctx.read_batch(batch)?;
/// df.show().await?;
///
/// // Create a new dataframe with in-memory data using macro
/// let df = dataframe!(
/// "id" => [1, 2, 3],
/// "name" => ["foo", "bar", "baz"]
/// )?;
/// df.show().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct DataFrame {
// Box the (large) SessionState to reduce the size of DataFrame on the stack
session_state: Box<SessionState>,
plan: LogicalPlan,
// Whether projection ops can skip validation or not. This flag if false
// allows for an optimization in `with_column` and `with_column_renamed` functions
// where the recursive work required to columnize and normalize expressions can
// be skipped if set to false. Since these function calls are often chained or
// called many times in dataframe operations this can result in a significant
// performance gain.
//
// The conditions where this can be set to false is when the dataframe function
// call results in the last operation being a
// `LogicalPlanBuilder::from(plan).project(fields)?.build()` or
// `LogicalPlanBuilder::from(plan).project_with_validation(fields)?.build()`
// call. This requirement guarantees that the plan has had all columnization
// and normalization applied to existing expressions and only new expressions
// will require that work. Any operation that update the plan in any way
// via anything other than a `project` call should set this to true.
projection_requires_validation: bool,
}
impl DataFrame {
/// Create a new `DataFrame ` based on an existing `LogicalPlan`
///
/// This is a low-level method and is not typically used by end users. See
/// [`SessionContext::read_csv`] and other methods for creating a
/// `DataFrame` from an existing datasource.
pub fn new(session_state: SessionState, plan: LogicalPlan) -> Self {
Self {
session_state: Box::new(session_state),
plan,
projection_requires_validation: true,
}
}
/// Creates logical expression from a SQL query text.
/// The expression is created and processed against the current schema.
///
/// # Example: Parsing SQL queries
/// ```
/// # use arrow::datatypes::{DataType, Field, Schema};
/// # use datafusion::prelude::*;
/// # use datafusion_common::{DFSchema, Result};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// // datafusion will parse number as i64 first.
/// let sql = "a > 1 and b in (1, 10)";
/// let expected = col("a")
/// .gt(lit(1 as i64))
/// .and(col("b").in_list(vec![lit(1 as i64), lit(10 as i64)], false));
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let expr = df.parse_sql_expr(sql)?;
/// assert_eq!(expected, expr);
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "sql")]
pub fn parse_sql_expr(&self, sql: &str) -> Result<Expr> {
let df_schema = self.schema();
self.session_state.create_logical_expr(sql, df_schema)
}
/// Consume the DataFrame and produce a physical plan
pub async fn create_physical_plan(self) -> Result<Arc<dyn ExecutionPlan>> {
self.session_state.create_physical_plan(&self.plan).await
}
/// Filter the DataFrame by column. Returns a new DataFrame only containing the
/// specified columns.
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.select_columns(&["a", "b"])?;
/// let expected = vec![
/// "+---+---+",
/// "| a | b |",
/// "+---+---+",
/// "| 1 | 2 |",
/// "+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn select_columns(self, columns: &[&str]) -> Result<DataFrame> {
let fields = columns
.iter()
.flat_map(|name| {
self.plan
.schema()
.qualified_fields_with_unqualified_name(name)
})
.collect::<Vec<_>>();
let expr: Vec<Expr> = fields
.into_iter()
.map(|(qualifier, field)| Expr::Column(Column::from((qualifier, field))))
.collect();
self.select(expr)
}
/// Project arbitrary list of expression strings into a new `DataFrame`.
/// Method will parse string expressions into logical plan expressions.
///
/// The output `DataFrame` has one column for each element in `exprs`.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let df: DataFrame = df.select_exprs(&["a * b", "c"])?;
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "sql")]
pub fn select_exprs(self, exprs: &[&str]) -> Result<DataFrame> {
let expr_list = exprs
.iter()
.map(|e| self.parse_sql_expr(e))
.collect::<Result<Vec<_>>>()?;
self.select(expr_list)
}
/// Project arbitrary expressions (like SQL SELECT expressions) into a new
/// `DataFrame`.
///
/// The output `DataFrame` has one column for each element in `expr_list`.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.select(vec![col("a"), col("b") * col("c")])?;
/// let expected = vec![
/// "+---+-----------------------+",
/// "| a | ?table?.b * ?table?.c |",
/// "+---+-----------------------+",
/// "| 1 | 6 |",
/// "+---+-----------------------+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn select(
self,
expr_list: impl IntoIterator<Item = impl Into<SelectExpr>>,
) -> Result<DataFrame> {
let expr_list: Vec<SelectExpr> =
expr_list.into_iter().map(|e| e.into()).collect::<Vec<_>>();
let expressions = expr_list.iter().filter_map(|e| match e {
SelectExpr::Expression(expr) => Some(expr),
_ => None,
});
let window_func_exprs = find_window_exprs(expressions);
let plan = if window_func_exprs.is_empty() {
self.plan
} else {
LogicalPlanBuilder::window_plan(self.plan, window_func_exprs)?
};
let project_plan = LogicalPlanBuilder::from(plan).project(expr_list)?.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan: project_plan,
projection_requires_validation: false,
})
}
/// Returns a new DataFrame containing all columns except the specified columns.
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// // +----+----+----+
/// // | a | b | c |
/// // +----+----+----+
/// // | 1 | 2 | 3 |
/// // +----+----+----+
/// let df = df.drop_columns(&["a"])?;
/// let expected = vec![
/// "+---+---+",
/// "| b | c |",
/// "+---+---+",
/// "| 2 | 3 |",
/// "+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn drop_columns(self, columns: &[&str]) -> Result<DataFrame> {
let fields_to_drop = columns
.iter()
.flat_map(|name| {
self.plan
.schema()
.qualified_fields_with_unqualified_name(name)
})
.collect::<Vec<_>>();
let expr: Vec<Expr> = self
.plan
.schema()
.fields()
.into_iter()
.enumerate()
.map(|(idx, _)| self.plan.schema().qualified_field(idx))
.filter(|(qualifier, f)| !fields_to_drop.contains(&(*qualifier, f)))
.map(|(qualifier, field)| Expr::Column(Column::from((qualifier, field))))
.collect();
self.select(expr)
}
/// Expand multiple list/struct columns into a set of rows and new columns.
///
/// See also: [`UnnestOptions`] documentation for the behavior of `unnest`
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx.read_json("tests/data/unnest.json", NdJsonReadOptions::default()).await?;
/// // expand into multiple columns if it's json array, flatten field name if it's nested structure
/// let df = df.unnest_columns(&["b","c","d"])?;
/// let expected = vec![
/// "+---+------+-------+-----+-----+",
/// "| a | b | c | d.e | d.f |",
/// "+---+------+-------+-----+-----+",
/// "| 1 | 2.0 | false | 1 | 2 |",
/// "| 1 | 1.3 | true | 1 | 2 |",
/// "| 1 | -6.1 | | 1 | 2 |",
/// "| 2 | 3.0 | false | | |",
/// "| 2 | 2.3 | true | | |",
/// "| 2 | -7.1 | | | |",
/// "+---+------+-------+-----+-----+"
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn unnest_columns(self, columns: &[&str]) -> Result<DataFrame> {
self.unnest_columns_with_options(columns, UnnestOptions::new())
}
/// Expand multiple list columns into a set of rows, with
/// behavior controlled by [`UnnestOptions`].
///
/// Please see the documentation on [`UnnestOptions`] for more
/// details about the meaning of unnest.
pub fn unnest_columns_with_options(
self,
columns: &[&str],
options: UnnestOptions,
) -> Result<DataFrame> {
let columns = columns.iter().map(|c| Column::from(*c)).collect();
let plan = LogicalPlanBuilder::from(self.plan)
.unnest_columns_with_options(columns, options)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Return a DataFrame with only rows for which `predicate` evaluates to
/// `true`.
///
/// Rows for which `predicate` evaluates to `false` or `null`
/// are filtered out.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.filter(col("a").lt_eq(col("b")))?;
/// // all rows where a <= b are returned
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "| 4 | 5 | 6 |",
/// "| 7 | 8 | 9 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn filter(self, predicate: Expr) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.filter(predicate)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Return a new `DataFrame` that aggregates the rows of the current
/// `DataFrame`, first optionally grouping by the given expressions.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion::functions_aggregate::expr_fn::min;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
///
/// // The following use is the equivalent of "SELECT MIN(b) GROUP BY a"
/// let df1 = df.clone().aggregate(vec![col("a")], vec![min(col("b"))])?;
/// let expected1 = vec![
/// "+---+----------------+",
/// "| a | min(?table?.b) |",
/// "+---+----------------+",
/// "| 1 | 2 |",
/// "| 4 | 5 |",
/// "| 7 | 8 |",
/// "+---+----------------+",
/// ];
/// assert_batches_sorted_eq!(expected1, &df1.collect().await?);
/// // The following use is the equivalent of "SELECT MIN(b)"
/// let df2 = df.aggregate(vec![], vec![min(col("b"))])?;
/// let expected2 = vec![
/// "+----------------+",
/// "| min(?table?.b) |",
/// "+----------------+",
/// "| 2 |",
/// "+----------------+",
/// ];
/// # assert_batches_sorted_eq!(expected2, &df2.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn aggregate(
self,
group_expr: Vec<Expr>,
aggr_expr: Vec<Expr>,
) -> Result<DataFrame> {
let is_grouping_set = matches!(group_expr.as_slice(), [Expr::GroupingSet(_)]);
let aggr_expr_len = aggr_expr.len();
let options =
LogicalPlanBuilderOptions::new().with_add_implicit_group_by_exprs(true);
let plan = LogicalPlanBuilder::from(self.plan)
.with_options(options)
.aggregate(group_expr, aggr_expr)?
.build()?;
let plan = if is_grouping_set {
let grouping_id_pos = plan.schema().fields().len() - 1 - aggr_expr_len;
// For grouping sets we do a project to not expose the internal grouping id
let exprs = plan
.schema()
.columns()
.into_iter()
.enumerate()
.filter(|(idx, _)| *idx != grouping_id_pos)
.map(|(_, column)| Expr::Column(column))
.collect::<Vec<_>>();
LogicalPlanBuilder::from(plan).project(exprs)?.build()?
} else {
plan
};
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: !is_grouping_set,
})
}
/// Return a new DataFrame that adds the result of evaluating one or more
/// window functions ([`Expr::WindowFunction`]) to the existing columns
pub fn window(self, window_exprs: Vec<Expr>) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.window(window_exprs)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Returns a new `DataFrame` with a limited number of rows.
///
/// # Arguments
/// `skip` - Number of rows to skip before fetch any row
/// `fetch` - Maximum number of rows to return, after skipping `skip` rows.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.limit(1, Some(2))?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 4 | 5 | 6 |",
/// "| 7 | 8 | 9 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn limit(self, skip: usize, fetch: Option<usize>) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.limit(skip, fetch)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
})
}
/// Calculate the union of two [`DataFrame`]s, preserving duplicate rows.
///
/// The two [`DataFrame`]s must have exactly the same schema
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = df.clone();
/// let df = df.union(d2)?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "| 1 | 2 | 3 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn union(self, dataframe: DataFrame) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.union(dataframe.plan)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Calculate the union of two [`DataFrame`]s using column names, preserving duplicate rows.
///
/// The two [`DataFrame`]s are combined using column names rather than position,
/// filling missing columns with null.
///
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = df
/// .clone()
/// .select_columns(&["b", "c", "a"])?
/// .with_column("d", lit("77"))?;
/// let df = df.union_by_name(d2)?;
/// let expected = vec![
/// "+---+---+---+----+",
/// "| a | b | c | d |",
/// "+---+---+---+----+",
/// "| 1 | 2 | 3 | |",
/// "| 1 | 2 | 3 | 77 |",
/// "+---+---+---+----+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn union_by_name(self, dataframe: DataFrame) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.union_by_name(dataframe.plan)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Calculate the distinct union of two [`DataFrame`]s.
///
/// The two [`DataFrame`]s must have exactly the same schema. Any duplicate
/// rows are discarded.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = df.clone();
/// let df = df.union_distinct(d2)?;
/// // df2 are duplicate of df
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn union_distinct(self, dataframe: DataFrame) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.union_distinct(dataframe.plan)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Calculate the union of two [`DataFrame`]s using column names with all duplicated rows removed.
///
/// The two [`DataFrame`]s are combined using column names rather than position,
/// filling missing columns with null.
///
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = df.clone().select_columns(&["b", "c", "a"])?;
/// let df = df.union_by_name_distinct(d2)?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn union_by_name_distinct(self, dataframe: DataFrame) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.union_by_name_distinct(dataframe.plan)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Return a new `DataFrame` with all duplicated rows removed.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.distinct()?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn distinct(self) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan).distinct()?.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Return a new `DataFrame` with duplicated rows removed as per the specified expression list
/// according to the provided sorting expressions grouped by the `DISTINCT ON` clause
/// expressions.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?
/// // Return a single row (a, b) for each distinct value of a
/// .distinct_on(vec![col("a")], vec![col("a"), col("b")], None)?;
/// let expected = vec![
/// "+---+---+",
/// "| a | b |",
/// "+---+---+",
/// "| 1 | 2 |",
/// "+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn distinct_on(
self,
on_expr: Vec<Expr>,
select_expr: Vec<Expr>,
sort_expr: Option<Vec<SortExpr>>,
) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.distinct_on(on_expr, select_expr, sort_expr)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Return a new `DataFrame` that has statistics for a DataFrame.
///
/// Only summarizes numeric datatypes at the moment and returns nulls for
/// non numeric datatypes. The output format is modeled after pandas
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use arrow::util::pretty;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx.read_csv("tests/tpch-csv/customer.csv", CsvReadOptions::new()).await?;
/// let stat = df.describe().await?;
/// # // some output column are ignored
/// let expected = vec![
/// "+------------+--------------------+--------------------+------------------------------------+--------------------+-----------------+--------------------+--------------+----------------------------------------------------------------------------------------------------------+",
/// "| describe | c_custkey | c_name | c_address | c_nationkey | c_phone | c_acctbal | c_mktsegment | c_comment |",
/// "+------------+--------------------+--------------------+------------------------------------+--------------------+-----------------+--------------------+--------------+----------------------------------------------------------------------------------------------------------+",
/// "| count | 9.0 | 9 | 9 | 9.0 | 9 | 9.0 | 9 | 9 |",
/// "| max | 10.0 | Customer#000000010 | xKiAFTjUsCuxfeleNqefumTrjS | 20.0 | 30-114-968-4951 | 9561.95 | MACHINERY | tions. even deposits boost according to the slyly bold packages. final accounts cajole requests. furious |",
/// "| mean | 6.0 | null | null | 9.88888888888889 | null | 5153.2155555555555 | null | null |",
/// "| median | 6.0 | null | null | 8.0 | null | 6819.74 | null | null |",
/// "| min | 2.0 | Customer#000000002 | 6LrEaV6KR6PLVcgl2ArL Q3rqzLzcT1 v2 | 1.0 | 11-719-748-3364 | 121.65 | AUTOMOBILE | deposits eat slyly ironic, even instructions. express foxes detect slyly. blithely even accounts abov |",
/// "| null_count | 0.0 | 0 | 0 | 0.0 | 0 | 0.0 | 0 | 0 |",
/// "| std | 2.7386127875258306 | null | null | 7.2188026092359046 | null | 3522.169804254585 | null | null |",
/// "+------------+--------------------+--------------------+------------------------------------+--------------------+-----------------+--------------------+--------------+----------------------------------------------------------------------------------------------------------+"];
/// assert_batches_sorted_eq!(expected, &stat.collect().await?);
/// # Ok(())
/// # }
/// ```
pub async fn describe(self) -> Result<Self> {
//the functions now supported
let supported_describe_functions =
vec!["count", "null_count", "mean", "std", "min", "max", "median"];
let original_schema_fields = self.schema().fields().iter();
//define describe column
let mut describe_schemas = vec![Field::new("describe", DataType::Utf8, false)];
describe_schemas.extend(original_schema_fields.clone().map(|field| {
if field.data_type().is_numeric() {
Field::new(field.name(), DataType::Float64, true)
} else {
Field::new(field.name(), DataType::Utf8, true)
}
}));
//collect recordBatch
let describe_record_batch = [
// count aggregation
self.clone().aggregate(
vec![],
original_schema_fields
.clone()
.map(|f| count(ident(f.name())).alias(f.name()))
.collect::<Vec<_>>(),
),
// null_count aggregation
self.clone().aggregate(
vec![],
original_schema_fields
.clone()
.map(|f| {
sum(case(is_null(ident(f.name())))
.when(lit(true), lit(1))
.otherwise(lit(0))
.unwrap())
.alias(f.name())
})
.collect::<Vec<_>>(),
),
// mean aggregation
self.clone().aggregate(
vec![],
original_schema_fields
.clone()
.filter(|f| f.data_type().is_numeric())
.map(|f| avg(ident(f.name())).alias(f.name()))
.collect::<Vec<_>>(),
),
// std aggregation
self.clone().aggregate(
vec![],
original_schema_fields
.clone()
.filter(|f| f.data_type().is_numeric())
.map(|f| stddev(ident(f.name())).alias(f.name()))
.collect::<Vec<_>>(),
),
// min aggregation
self.clone().aggregate(
vec![],
original_schema_fields
.clone()
.filter(|f| {
!matches!(f.data_type(), DataType::Binary | DataType::Boolean)
})
.map(|f| min(ident(f.name())).alias(f.name()))
.collect::<Vec<_>>(),
),
// max aggregation
self.clone().aggregate(
vec![],
original_schema_fields
.clone()
.filter(|f| {
!matches!(f.data_type(), DataType::Binary | DataType::Boolean)
})
.map(|f| max(ident(f.name())).alias(f.name()))
.collect::<Vec<_>>(),
),
// median aggregation
self.clone().aggregate(
vec![],
original_schema_fields
.clone()
.filter(|f| f.data_type().is_numeric())
.map(|f| median(ident(f.name())).alias(f.name()))
.collect::<Vec<_>>(),
),
];
// first column with function names
let mut array_ref_vec: Vec<ArrayRef> = vec![Arc::new(StringArray::from(
supported_describe_functions.clone(),
))];
for field in original_schema_fields {
let mut array_datas = vec![];
for result in describe_record_batch.iter() {
let array_ref = match result {
Ok(df) => {
let batches = df.clone().collect().await;
match batches {
Ok(batches)
if batches.len() == 1
&& batches[0]
.column_by_name(field.name())
.is_some() =>
{
let column =
batches[0].column_by_name(field.name()).unwrap();
if column.data_type().is_null() {
Arc::new(StringArray::from(vec!["null"]))
} else if field.data_type().is_numeric() {
cast(column, &DataType::Float64)?
} else {
cast(column, &DataType::Utf8)?
}
}
_ => Arc::new(StringArray::from(vec!["null"])),
}
}
//Handling error when only boolean/binary column, and in other cases
Err(err)
if err.to_string().contains(
"Error during planning: \
Aggregate requires at least one grouping \
or aggregate expression",
) =>
{
Arc::new(StringArray::from(vec!["null"]))
}
Err(e) => return exec_err!("{}", e),
};
array_datas.push(array_ref);
}
array_ref_vec.push(concat(
array_datas
.iter()
.map(|af| af.as_ref())
.collect::<Vec<_>>()
.as_slice(),
)?);
}
let describe_record_batch =
RecordBatch::try_new(Arc::new(Schema::new(describe_schemas)), array_ref_vec)?;
let provider = MemTable::try_new(
describe_record_batch.schema(),
vec![vec![describe_record_batch]],
)?;
let plan = LogicalPlanBuilder::scan(
UNNAMED_TABLE,
provider_as_source(Arc::new(provider)),
None,
)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
})
}
/// Apply a sort by provided expressions with default direction
pub fn sort_by(self, expr: Vec<Expr>) -> Result<DataFrame> {
self.sort(
expr.into_iter()
.map(|e| e.sort(true, false))
.collect::<Vec<SortExpr>>(),
)
}
/// Sort the DataFrame by the specified sorting expressions.
///
/// Note that any expression can be turned into
/// a sort expression by calling its [sort](Expr::sort) method.
///
/// # Example
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.sort(vec![
/// col("a").sort(false, true), // a DESC, nulls first
/// col("b").sort(true, false), // b ASC, nulls last
/// ])?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "| 4 | 5 | 6 |",
/// "| 7 | 8 | 9 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn sort(self, expr: Vec<SortExpr>) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan).sort(expr)?.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
})
}
/// Join this `DataFrame` with another `DataFrame` using explicitly specified
/// columns and an optional filter expression.
///
/// See [`join_on`](Self::join_on) for a more concise way to specify the
/// join condition. Since DataFusion will automatically identify and
/// optimize equality predicates there is no performance difference between
/// this function and `join_on`
///
/// `left_cols` and `right_cols` are used to form "equijoin" predicates (see
/// example below), which are then combined with the optional `filter`
/// expression. If `left_cols` and `right_cols` contain ambiguous column
/// references, they will be disambiguated by prioritizing the left relation
/// for `left_cols` and the right relation for `right_cols`.
///
/// Note that in case of outer join, the `filter` is applied to only matched rows.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let left = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let right = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?
/// .select(vec![
/// col("a").alias("a2"),
/// col("b").alias("b2"),
/// col("c").alias("c2"),
/// ])?;
/// // Perform the equivalent of `left INNER JOIN right ON (a = a2 AND b = b2)`
/// // finding all pairs of rows from `left` and `right` where `a = a2` and `b = b2`.
/// let join = left.join(right, JoinType::Inner, &["a", "b"], &["a2", "b2"], None)?;
/// let expected = vec![
/// "+---+---+---+----+----+----+",
/// "| a | b | c | a2 | b2 | c2 |",
/// "+---+---+---+----+----+----+",
/// "| 1 | 2 | 3 | 1 | 2 | 3 |",
/// "+---+---+---+----+----+----+",
/// ];
/// assert_batches_sorted_eq!(expected, &join.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn join(
self,
right: DataFrame,
join_type: JoinType,
left_cols: &[&str],
right_cols: &[&str],
filter: Option<Expr>,
) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.join(
right.plan,
join_type,
(left_cols.to_vec(), right_cols.to_vec()),
filter,
)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Join this `DataFrame` with another `DataFrame` using the specified
/// expressions.
///
/// Note that DataFusion automatically optimizes joins, including
/// identifying and optimizing equality predicates.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let left = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let right = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?
/// .select(vec![
/// col("a").alias("a2"),
/// col("b").alias("b2"),
/// col("c").alias("c2"),
/// ])?;
///
/// // Perform the equivalent of `left INNER JOIN right ON (a != a2 AND b != b2)`
/// // finding all pairs of rows from `left` and `right` where
/// // where `a != a2` and `b != b2`.
/// let join_on = left.join_on(
/// right,
/// JoinType::Inner,
/// [col("a").not_eq(col("a2")), col("b").not_eq(col("b2"))],
/// )?;
/// let expected = vec![
/// "+---+---+---+----+----+----+",
/// "| a | b | c | a2 | b2 | c2 |",
/// "+---+---+---+----+----+----+",
/// "+---+---+---+----+----+----+",
/// ];
/// # assert_batches_sorted_eq!(expected, &join_on.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn join_on(
self,
right: DataFrame,
join_type: JoinType,
on_exprs: impl IntoIterator<Item = Expr>,
) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.join_on(right.plan, join_type, on_exprs)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Repartition a DataFrame based on a logical partitioning scheme.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let df1 = df.repartition(Partitioning::RoundRobinBatch(4))?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "| 4 | 5 | 6 |",
/// "| 7 | 8 | 9 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df1.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn repartition(self, partitioning_scheme: Partitioning) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan)
.repartition(partitioning_scheme)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Return the total number of rows in this `DataFrame`.
///
/// Note that this method will actually run a plan to calculate the count,
/// which may be slow for large or complicated DataFrames.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let count = df.count().await?; // 1
/// # assert_eq!(count, 1);
/// # Ok(())
/// # }
/// ```
pub async fn count(self) -> Result<usize> {
let rows = self
.aggregate(
vec![],
vec![count(Expr::Literal(COUNT_STAR_EXPANSION, None))],
)?
.collect()
.await?;
let len = *rows
.first()
.and_then(|r| r.columns().first())
.and_then(|c| c.as_any().downcast_ref::<Int64Array>())
.and_then(|a| a.values().first())
.ok_or_else(|| {
internal_datafusion_err!("Unexpected output when collecting for count()")
})? as usize;
Ok(len)
}
/// Execute this `DataFrame` and buffer all resulting `RecordBatch`es into memory.
///
/// Prior to calling `collect`, modifying a DataFrame simply updates a plan
/// (no actual computation is performed). `collect` triggers the computation.
///
/// See [`Self::execute_stream`] to execute a DataFrame without buffering.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let batches = df.collect().await?;
/// # Ok(())
/// # }
/// ```
pub async fn collect(self) -> Result<Vec<RecordBatch>> {
let task_ctx = Arc::new(self.task_ctx());
let plan = self.create_physical_plan().await?;
collect(plan, task_ctx).await
}
/// Execute the `DataFrame` and print the results to the console.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// df.show().await?;
/// # Ok(())
/// # }
/// ```
pub async fn show(self) -> Result<()> {
println!("{}", self.to_string().await?);
Ok(())
}
/// Execute the `DataFrame` and return a string representation of the results.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion::execution::SessionStateBuilder;
///
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let cfg = SessionConfig::new()
/// .set_str("datafusion.format.null", "no-value");
/// let session_state = SessionStateBuilder::new()
/// .with_config(cfg)
/// .with_default_features()
/// .build();
/// let ctx = SessionContext::new_with_state(session_state);
/// let df = ctx.sql("select null as 'null-column'").await?;
/// let result = df.to_string().await?;
/// assert_eq!(result,
/// "+-------------+
/// | null-column |
/// +-------------+
/// | no-value |
/// +-------------+"
/// );
/// # Ok(())
/// # }
pub async fn to_string(self) -> Result<String> {
let options = self.session_state.config().options().format.clone();
let arrow_options: arrow::util::display::FormatOptions = (&options).try_into()?;
let results = self.collect().await?;
Ok(
pretty::pretty_format_batches_with_options(&results, &arrow_options)?
.to_string(),
)
}
/// Execute the `DataFrame` and print only the first `num` rows of the
/// result to the console.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// df.show_limit(10).await?;
/// # Ok(())
/// # }
/// ```
pub async fn show_limit(self, num: usize) -> Result<()> {
let results = self.limit(0, Some(num))?.collect().await?;
Ok(pretty::print_batches(&results)?)
}
/// Return a new [`TaskContext`] which would be used to execute this DataFrame
pub fn task_ctx(&self) -> TaskContext {
TaskContext::from(self.session_state.as_ref())
}
/// Executes this DataFrame and returns a stream over a single partition
///
/// See [Self::collect] to buffer the `RecordBatch`es in memory.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let stream = df.execute_stream().await?;
/// # Ok(())
/// # }
/// ```
///
/// # Aborting Execution
///
/// Dropping the stream will abort the execution of the query, and free up
/// any allocated resources
pub async fn execute_stream(self) -> Result<SendableRecordBatchStream> {
let task_ctx = Arc::new(self.task_ctx());
let plan = self.create_physical_plan().await?;
execute_stream(plan, task_ctx)
}
/// Executes this DataFrame and collects all results into a vector of vector of RecordBatch
/// maintaining the input partitioning.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let batches = df.collect_partitioned().await?;
/// # Ok(())
/// # }
/// ```
pub async fn collect_partitioned(self) -> Result<Vec<Vec<RecordBatch>>> {
let task_ctx = Arc::new(self.task_ctx());
let plan = self.create_physical_plan().await?;
collect_partitioned(plan, task_ctx).await
}
/// Executes this DataFrame and returns one stream per partition.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let batches = df.execute_stream_partitioned().await?;
/// # Ok(())
/// # }
/// ```
/// # Aborting Execution
///
/// Dropping the stream will abort the execution of the query, and free up
/// any allocated resources
pub async fn execute_stream_partitioned(
self,
) -> Result<Vec<SendableRecordBatchStream>> {
let task_ctx = Arc::new(self.task_ctx());
let plan = self.create_physical_plan().await?;
execute_stream_partitioned(plan, task_ctx)
}
/// Returns the `DFSchema` describing the output of this DataFrame.
///
/// The output `DFSchema` contains information on the name, data type, and
/// nullability for each column.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let schema = df.schema();
/// # Ok(())
/// # }
/// ```
pub fn schema(&self) -> &DFSchema {
self.plan.schema()
}
/// Return a reference to the unoptimized [`LogicalPlan`] that comprises
/// this DataFrame.
///
/// See [`Self::into_unoptimized_plan`] for more details.
pub fn logical_plan(&self) -> &LogicalPlan {
&self.plan
}
/// Returns both the [`LogicalPlan`] and [`SessionState`] that comprise this [`DataFrame`]
pub fn into_parts(self) -> (SessionState, LogicalPlan) {
(*self.session_state, self.plan)
}
/// Return the [`LogicalPlan`] represented by this DataFrame without running
/// any optimizers
///
/// Note: This method should not be used outside testing, as it loses the
/// snapshot of the [`SessionState`] attached to this [`DataFrame`] and
/// consequently subsequent operations may take place against a different
/// state (e.g. a different value of `now()`)
///
/// See [`Self::into_parts`] to retrieve the owned [`LogicalPlan`] and
/// corresponding [`SessionState`].
pub fn into_unoptimized_plan(self) -> LogicalPlan {
self.plan
}
/// Return the optimized [`LogicalPlan`] represented by this DataFrame.
///
/// Note: This method should not be used outside testing -- see
/// [`Self::into_unoptimized_plan`] for more details.
pub fn into_optimized_plan(self) -> Result<LogicalPlan> {
// Optimize the plan first for better UX
self.session_state.optimize(&self.plan)
}
/// Converts this [`DataFrame`] into a [`TableProvider`] that can be registered
/// as a table view using [`SessionContext::register_table`].
///
/// Note: This discards the [`SessionState`] associated with this
/// [`DataFrame`] in favour of the one passed to [`TableProvider::scan`]
pub fn into_view(self) -> Arc<dyn TableProvider> {
Arc::new(DataFrameTableProvider {
plan: self.plan,
table_type: TableType::Temporary,
})
}
/// See [`Self::into_view`]. The returned [`TableProvider`] will
/// create a transient table.
pub fn into_temporary_view(self) -> Arc<dyn TableProvider> {
Arc::new(DataFrameTableProvider {
plan: self.plan,
table_type: TableType::Temporary,
})
}
/// Return a DataFrame with the explanation of its plan so far.
///
/// if `analyze` is specified, runs the plan and reports metrics
/// if `verbose` is true, prints out additional details.
/// The default format is Indent format.
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let batches = df
/// .limit(0, Some(100))?
/// .explain(false, false)?
/// .collect()
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn explain(self, verbose: bool, analyze: bool) -> Result<DataFrame> {
// Set the default format to Indent to keep the previous behavior
let opts = ExplainOption::default()
.with_verbose(verbose)
.with_analyze(analyze);
self.explain_with_options(opts)
}
/// Return a DataFrame with the explanation of its plan so far.
///
/// `opt` is used to specify the options for the explain operation.
/// Details of the options can be found in [`ExplainOption`].
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// use datafusion_expr::{Explain, ExplainOption};
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let batches = df
/// .limit(0, Some(100))?
/// .explain_with_options(
/// ExplainOption::default()
/// .with_verbose(false)
/// .with_analyze(false),
/// )?
/// .collect()
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn explain_with_options(
self,
explain_option: ExplainOption,
) -> Result<DataFrame> {
if matches!(self.plan, LogicalPlan::Explain(_)) {
return plan_err!("Nested EXPLAINs are not supported");
}
let plan = LogicalPlanBuilder::from(self.plan)
.explain_option_format(explain_option)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
})
}
/// Return a `FunctionRegistry` used to plan udf's calls
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let f = df.registry();
/// // use f.udf("name", vec![...]) to use the udf
/// # Ok(())
/// # }
/// ```
pub fn registry(&self) -> &dyn FunctionRegistry {
self.session_state.as_ref()
}
/// Calculate the intersection of two [`DataFrame`]s. The two [`DataFrame`]s must have exactly the same schema
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.intersect(d2)?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn intersect(self, dataframe: DataFrame) -> Result<DataFrame> {
let left_plan = self.plan;
let right_plan = dataframe.plan;
let plan = LogicalPlanBuilder::intersect(left_plan, right_plan, true)?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Calculate the distinct intersection of two [`DataFrame`]s. The two [`DataFrame`]s must have exactly the same schema
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.intersect_distinct(d2)?;
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 1 | 2 | 3 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &df.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn intersect_distinct(self, dataframe: DataFrame) -> Result<DataFrame> {
let left_plan = self.plan;
let right_plan = dataframe.plan;
let plan = LogicalPlanBuilder::intersect(left_plan, right_plan, false)?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Calculate the exception of two [`DataFrame`]s. The two [`DataFrame`]s must have exactly the same schema
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let result = df.except(d2)?;
/// // those columns are not in example.csv, but in example_long.csv
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 4 | 5 | 6 |",
/// "| 7 | 8 | 9 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &result.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn except(self, dataframe: DataFrame) -> Result<DataFrame> {
let left_plan = self.plan;
let right_plan = dataframe.plan;
let plan = LogicalPlanBuilder::except(left_plan, right_plan, true)?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Calculate the distinct exception of two [`DataFrame`]s. The two [`DataFrame`]s must have exactly the same schema
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::assert_batches_sorted_eq;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example_long.csv", CsvReadOptions::new())
/// .await?;
/// let d2 = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let result = df.except_distinct(d2)?;
/// // those columns are not in example.csv, but in example_long.csv
/// let expected = vec![
/// "+---+---+---+",
/// "| a | b | c |",
/// "+---+---+---+",
/// "| 4 | 5 | 6 |",
/// "| 7 | 8 | 9 |",
/// "+---+---+---+",
/// ];
/// # assert_batches_sorted_eq!(expected, &result.collect().await?);
/// # Ok(())
/// # }
/// ```
pub fn except_distinct(self, dataframe: DataFrame) -> Result<DataFrame> {
let left_plan = self.plan;
let right_plan = dataframe.plan;
let plan = LogicalPlanBuilder::except(left_plan, right_plan, false)?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: true,
})
}
/// Execute this `DataFrame` and write the results to `table_name`.
///
/// Returns a single [RecordBatch] containing a single column and
/// row representing the count of total rows written.
///
/// Unlike most other `DataFrame` methods, this method executes eagerly.
/// Data is written to the table using the [`TableProvider::insert_into`]
/// method. This is the same underlying implementation used by SQL `INSERT
/// INTO` statements.
pub async fn write_table(
self,
table_name: &str,
write_options: DataFrameWriteOptions,
) -> Result<Vec<RecordBatch>, DataFusionError> {
let plan = if write_options.sort_by.is_empty() {
self.plan
} else {
LogicalPlanBuilder::from(self.plan)
.sort(write_options.sort_by)?
.build()?
};
let table_ref: TableReference = table_name.into();
let table_schema = self.session_state.schema_for_ref(table_ref.clone())?;
let target = match table_schema.table(table_ref.table()).await? {
Some(ref provider) => Ok(Arc::clone(provider)),
_ => plan_err!("No table named '{table_name}'"),
}?;
let target = Arc::new(DefaultTableSource::new(target));
let plan = LogicalPlanBuilder::insert_into(
plan,
table_ref,
target,
write_options.insert_op,
)?
.build()?;
DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
}
.collect()
.await
}
/// Execute the `DataFrame` and write the results to CSV file(s).
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use std::fs;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// use datafusion::dataframe::DataFrameWriteOptions;
/// let ctx = SessionContext::new();
/// // Sort the data by column "b" and write it to a new location
/// ctx.read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?
/// .sort(vec![col("b").sort(true, true)])? // sort by b asc, nulls first
/// .write_csv(
/// "output.csv",
/// DataFrameWriteOptions::new(),
/// None, // can also specify CSV writing options here
/// )
/// .await?;
/// # fs::remove_file("output.csv")?;
/// # Ok(())
/// # }
/// ```
pub async fn write_csv(
self,
path: &str,
options: DataFrameWriteOptions,
writer_options: Option<CsvOptions>,
) -> Result<Vec<RecordBatch>, DataFusionError> {
if options.insert_op != InsertOp::Append {
return not_impl_err!(
"{} is not implemented for DataFrame::write_csv.",
options.insert_op
);
}
let format = if let Some(csv_opts) = writer_options {
Arc::new(CsvFormatFactory::new_with_options(csv_opts))
} else {
Arc::new(CsvFormatFactory::new())
};
let file_type = format_as_file_type(format);
let plan = if options.sort_by.is_empty() {
self.plan
} else {
LogicalPlanBuilder::from(self.plan)
.sort(options.sort_by)?
.build()?
};
let plan = LogicalPlanBuilder::copy_to(
plan,
path.into(),
file_type,
HashMap::new(),
options.partition_by,
)?
.build()?;
DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
}
.collect()
.await
}
/// Execute the `DataFrame` and write the results to JSON file(s).
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use std::fs;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// use datafusion::dataframe::DataFrameWriteOptions;
/// let ctx = SessionContext::new();
/// // Sort the data by column "b" and write it to a new location
/// ctx.read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?
/// .sort(vec![col("b").sort(true, true)])? // sort by b asc, nulls first
/// .write_json("output.json", DataFrameWriteOptions::new(), None)
/// .await?;
/// # fs::remove_file("output.json")?;
/// # Ok(())
/// # }
/// ```
pub async fn write_json(
self,
path: &str,
options: DataFrameWriteOptions,
writer_options: Option<JsonOptions>,
) -> Result<Vec<RecordBatch>, DataFusionError> {
if options.insert_op != InsertOp::Append {
return not_impl_err!(
"{} is not implemented for DataFrame::write_json.",
options.insert_op
);
}
let format = if let Some(json_opts) = writer_options {
Arc::new(JsonFormatFactory::new_with_options(json_opts))
} else {
Arc::new(JsonFormatFactory::new())
};
let file_type = format_as_file_type(format);
let plan = if options.sort_by.is_empty() {
self.plan
} else {
LogicalPlanBuilder::from(self.plan)
.sort(options.sort_by)?
.build()?
};
let plan = LogicalPlanBuilder::copy_to(
plan,
path.into(),
file_type,
Default::default(),
options.partition_by,
)?
.build()?;
DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
}
.collect()
.await
}
/// Add or replace a column in the DataFrame.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.with_column("ab_sum", col("a") + col("b"))?;
/// # Ok(())
/// # }
/// ```
pub fn with_column(self, name: &str, expr: Expr) -> Result<DataFrame> {
let window_func_exprs = find_window_exprs([&expr]);
let original_names: HashSet<String> = self
.plan
.schema()
.iter()
.map(|(_, f)| f.name().clone())
.collect();
// Maybe build window plan
let plan = if window_func_exprs.is_empty() {
self.plan
} else {
LogicalPlanBuilder::window_plan(self.plan, window_func_exprs)?
};
let new_column = expr.alias(name);
let mut col_exists = false;
let mut fields: Vec<(Expr, bool)> = plan
.schema()
.iter()
.filter_map(|(qualifier, field)| {
// Skip new fields introduced by window_plan
if !original_names.contains(field.name()) {
return None;
}
if field.name() == name {
col_exists = true;
Some((new_column.clone(), true))
} else {
let e = col(Column::from((qualifier, field)));
Some((e, self.projection_requires_validation))
}
})
.collect();
if !col_exists {
fields.push((new_column, true));
}
let project_plan = LogicalPlanBuilder::from(plan)
.project_with_validation(fields)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan: project_plan,
projection_requires_validation: false,
})
}
/// Rename one column by applying a new projection. This is a no-op if the column to be
/// renamed does not exist.
///
/// The method supports case sensitive rename with wrapping column name into one of following symbols ( " or ' or ` )
///
/// Alternatively setting DataFusion param `datafusion.sql_parser.enable_ident_normalization` to `false` will enable
/// case sensitive rename without need to wrap column name into special symbols
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.with_column_renamed("ab_sum", "total")?;
///
/// # Ok(())
/// # }
/// ```
pub fn with_column_renamed(
self,
old_name: impl Into<String>,
new_name: &str,
) -> Result<DataFrame> {
let ident_opts = self
.session_state
.config_options()
.sql_parser
.enable_ident_normalization;
let old_column: Column = if ident_opts {
Column::from_qualified_name(old_name)
} else {
Column::from_qualified_name_ignore_case(old_name)
};
let (qualifier_rename, field_rename) =
match self.plan.schema().qualified_field_from_column(&old_column) {
Ok(qualifier_and_field) => qualifier_and_field,
// no-op if field not found
Err(DataFusionError::SchemaError(e, _))
if matches!(*e, SchemaError::FieldNotFound { .. }) =>
{
return Ok(self);
}
Err(err) => return Err(err),
};
let projection = self
.plan
.schema()
.iter()
.map(|(qualifier, field)| {
if qualifier.eq(&qualifier_rename) && field.as_ref() == field_rename {
(
col(Column::from((qualifier, field)))
.alias_qualified(qualifier.cloned(), new_name),
false,
)
} else {
(col(Column::from((qualifier, field))), false)
}
})
.collect::<Vec<_>>();
let project_plan = LogicalPlanBuilder::from(self.plan)
.project_with_validation(projection)?
.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan: project_plan,
projection_requires_validation: false,
})
}
/// Replace all parameters in logical plan with the specified
/// values, in preparation for execution.
///
/// # Example
///
/// ```
/// use datafusion::prelude::*;
/// # use datafusion::{error::Result, assert_batches_eq};
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// # use datafusion_common::ScalarValue;
/// let ctx = SessionContext::new();
/// # ctx.register_csv("example", "tests/data/example.csv", CsvReadOptions::new()).await?;
/// let results = ctx
/// .sql("SELECT a FROM example WHERE b = $1")
/// .await?
/// // replace $1 with value 2
/// .with_param_values(vec![
/// // value at index 0 --> $1
/// ScalarValue::from(2i64)
/// ])?
/// .collect()
/// .await?;
/// assert_batches_eq!(
/// &[
/// "+---+",
/// "| a |",
/// "+---+",
/// "| 1 |",
/// "+---+",
/// ],
/// &results
/// );
/// // Note you can also provide named parameters
/// let results = ctx
/// .sql("SELECT a FROM example WHERE b = $my_param")
/// .await?
/// // replace $my_param with value 2
/// // Note you can also use a HashMap as well
/// .with_param_values(vec![
/// ("my_param", ScalarValue::from(2i64))
/// ])?
/// .collect()
/// .await?;
/// assert_batches_eq!(
/// &[
/// "+---+",
/// "| a |",
/// "+---+",
/// "| 1 |",
/// "+---+",
/// ],
/// &results
/// );
/// # Ok(())
/// # }
/// ```
pub fn with_param_values(self, query_values: impl Into<ParamValues>) -> Result<Self> {
let plan = self.plan.with_param_values(query_values)?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
})
}
/// Cache DataFrame as a memory table.
///
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// let df = df.cache().await?;
/// # Ok(())
/// # }
/// ```
pub async fn cache(self) -> Result<DataFrame> {
let context = SessionContext::new_with_state((*self.session_state).clone());
// The schema is consistent with the output
let plan = self.clone().create_physical_plan().await?;
let schema = plan.schema();
let task_ctx = Arc::new(self.task_ctx());
let partitions = collect_partitioned(plan, task_ctx).await?;
let mem_table = MemTable::try_new(schema, partitions)?;
context.read_table(Arc::new(mem_table))
}
/// Apply an alias to the DataFrame.
///
/// This method replaces the qualifiers of output columns with the given alias.
pub fn alias(self, alias: &str) -> Result<DataFrame> {
let plan = LogicalPlanBuilder::from(self.plan).alias(alias)?.build()?;
Ok(DataFrame {
session_state: self.session_state,
plan,
projection_requires_validation: self.projection_requires_validation,
})
}
/// Fill null values in specified columns with a given value
/// If no columns are specified (empty vector), applies to all columns
/// Only fills if the value can be cast to the column's type
///
/// # Arguments
/// * `value` - Value to fill nulls with
/// * `columns` - List of column names to fill. If empty, fills all columns.
///
/// # Example
/// ```
/// # use datafusion::prelude::*;
/// # use datafusion::error::Result;
/// # use datafusion_common::ScalarValue;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let ctx = SessionContext::new();
/// let df = ctx
/// .read_csv("tests/data/example.csv", CsvReadOptions::new())
/// .await?;
/// // Fill nulls in only columns "a" and "c":
/// let df = df.fill_null(ScalarValue::from(0), vec!["a".to_owned(), "c".to_owned()])?;
/// // Fill nulls across all columns:
/// let df = df.fill_null(ScalarValue::from(0), vec![])?;
/// # Ok(())
/// # }
/// ```
pub fn fill_null(
&self,
value: ScalarValue,
columns: Vec<String>,
) -> Result<DataFrame> {
let cols = if columns.is_empty() {
self.logical_plan()
.schema()
.fields()
.iter()
.map(|f| f.as_ref().clone())
.collect()
} else {
self.find_columns(&columns)?
};
// Create projections for each column
let projections = self
.logical_plan()
.schema()
.fields()
.iter()
.map(|field| {
if cols.contains(field) {
// Try to cast fill value to column type. If the cast fails, fallback to the original column.
match value.clone().cast_to(field.data_type()) {
Ok(fill_value) => Expr::Alias(Alias {
expr: Box::new(Expr::ScalarFunction(ScalarFunction {
func: coalesce(),
args: vec![col(field.name()), lit(fill_value)],
})),
relation: None,
name: field.name().to_string(),
metadata: None,
}),
Err(_) => col(field.name()),
}
} else {
col(field.name())
}
})
.collect::<Vec<_>>();
self.clone().select(projections)
}
// Helper to find columns from names
fn find_columns(&self, names: &[String]) -> Result<Vec<Field>> {
let schema = self.logical_plan().schema();
names
.iter()
.map(|name| {
schema
.field_with_name(None, name)
.cloned()
.map_err(|_| plan_datafusion_err!("Column '{}' not found", name))
})
.collect()
}
/// Helper for creating DataFrame.
/// # Example
/// ```
/// use arrow::array::{ArrayRef, Int32Array, StringArray};
/// use datafusion::prelude::DataFrame;
/// use std::sync::Arc;
/// let id: ArrayRef = Arc::new(Int32Array::from(vec![1, 2, 3]));
/// let name: ArrayRef = Arc::new(StringArray::from(vec!["foo", "bar", "baz"]));
/// let df = DataFrame::from_columns(vec![("id", id), ("name", name)]).unwrap();
/// // +----+------+,
/// // | id | name |,
/// // +----+------+,
/// // | 1 | foo |,
/// // | 2 | bar |,
/// // | 3 | baz |,
/// // +----+------+,
/// ```
pub fn from_columns(columns: Vec<(&str, ArrayRef)>) -> Result<Self> {
let fields = columns
.iter()
.map(|(name, array)| Field::new(*name, array.data_type().clone(), true))
.collect::<Vec<_>>();
let arrays = columns
.into_iter()
.map(|(_, array)| array)
.collect::<Vec<_>>();
let schema = Arc::new(Schema::new(fields));
let batch = RecordBatch::try_new(schema, arrays)?;
let ctx = SessionContext::new();
let df = ctx.read_batch(batch)?;
Ok(df)
}
}
/// Macro for creating DataFrame.
/// # Example
/// ```
/// use datafusion::prelude::dataframe;
/// # use datafusion::error::Result;
/// # #[tokio::main]
/// # async fn main() -> Result<()> {
/// let df = dataframe!(
/// "id" => [1, 2, 3],
/// "name" => ["foo", "bar", "baz"]
/// )?;
/// df.show().await?;
/// // +----+------+,
/// // | id | name |,
/// // +----+------+,
/// // | 1 | foo |,
/// // | 2 | bar |,
/// // | 3 | baz |,
/// // +----+------+,
/// let df_empty = dataframe!()?; // empty DataFrame
/// assert_eq!(df_empty.schema().fields().len(), 0);
/// assert_eq!(df_empty.count().await?, 0);
/// # Ok(())
/// # }
/// ```
#[macro_export]
macro_rules! dataframe {
() => {{
use std::sync::Arc;
use datafusion::prelude::SessionContext;
use datafusion::arrow::array::RecordBatch;
use datafusion::arrow::datatypes::Schema;
let ctx = SessionContext::new();
let batch = RecordBatch::new_empty(Arc::new(Schema::empty()));
ctx.read_batch(batch)
}};
($($name:expr => $data:expr),+ $(,)?) => {{
use datafusion::prelude::DataFrame;
use datafusion::common::test_util::IntoArrayRef;
let columns = vec![
$(
($name, $data.into_array_ref()),
)+
];
DataFrame::from_columns(columns)
}};
}
#[derive(Debug)]
struct DataFrameTableProvider {
plan: LogicalPlan,
table_type: TableType,
}
#[async_trait]
impl TableProvider for DataFrameTableProvider {
fn as_any(&self) -> &dyn Any {
self
}
fn get_logical_plan(&self) -> Option<Cow<'_, LogicalPlan>> {
Some(Cow::Borrowed(&self.plan))
}
fn supports_filters_pushdown(
&self,
filters: &[&Expr],
) -> Result<Vec<TableProviderFilterPushDown>> {
// A filter is added on the DataFrame when given
Ok(vec![TableProviderFilterPushDown::Exact; filters.len()])
}
fn schema(&self) -> SchemaRef {
Arc::clone(self.plan.schema().inner())
}
fn table_type(&self) -> TableType {
self.table_type
}
async fn scan(
&self,
state: &dyn Session,
projection: Option<&Vec<usize>>,
filters: &[Expr],
limit: Option<usize>,
) -> Result<Arc<dyn ExecutionPlan>> {
let mut expr = LogicalPlanBuilder::from(self.plan.clone());
// Add filter when given
let filter = filters.iter().cloned().reduce(|acc, new| acc.and(new));
if let Some(filter) = filter {
expr = expr.filter(filter)?
}
if let Some(p) = projection {
expr = expr.select(p.iter().copied())?
}
// add a limit if given
if let Some(l) = limit {
expr = expr.limit(0, Some(l))?
}
let plan = expr.build()?;
state.create_physical_plan(&plan).await
}
}
// see tests in datafusion/core/tests/dataframe/mod.rs:2816