blob: e9bf77141953cae7ced6bfe412d8c2735d2ff041 [file]
//! DataFrame with Reader/Writer repesentation
use std::collections::HashMap;
use crate::column::Column;
use crate::plan::LogicalPlanBuilder;
pub use crate::readwriter::{DataFrameReader, DataFrameWriter};
use crate::session::SparkSession;
use crate::spark;
use spark::relation::RelType;
use arrow::error::ArrowError;
use arrow::record_batch::RecordBatch;
use arrow::util::pretty;
/// DataFrame is composed of a `spark_session` connecting to a remote
/// Spark Connect enabled cluster, and a `logical_plan` which represents
/// the `Plan` to be submitted to the cluster when an action is called
#[derive(Clone, Debug)]
pub struct DataFrame {
/// Global [SparkSession] connecting to the remote cluster
pub spark_session: SparkSession,
/// Logical Plan representing the unresolved Relation
/// which will be submitted to the remote cluster
pub logical_plan: LogicalPlanBuilder,
}
impl DataFrame {
/// create default DataFrame based on a spark session and initial logical plan
pub fn new(spark_session: SparkSession, logical_plan: LogicalPlanBuilder) -> DataFrame {
DataFrame {
spark_session,
logical_plan,
}
}
/// Projects a set of expressions and returns a new [DataFrame]
///
/// # Arguments:
///
/// * `cols` is a vector of `&str` which resolve to a specific column
///
/// # Example:
/// ```rust
/// async {
/// df.select(vec![col("age"), col("name")]).collect().await?;
/// }
/// ```
pub fn select(&mut self, cols: Vec<Column>) -> DataFrame {
DataFrame::new(self.spark_session.clone(), self.logical_plan.select(cols))
}
/// Project a set of SQL expressions and returns a new [DataFrame]
///
/// This is a variant of `select` that accepts SQL Expressions
///
/// # Example:
/// ```rust
/// async {
/// df.selectExpr(vec!["id * 2", "abs(id)"]).collect().await?;
/// }
/// ```
#[allow(non_snake_case)]
pub fn selectExpr(&mut self, cols: Vec<&str>) -> DataFrame {
DataFrame::new(
self.spark_session.clone(),
self.logical_plan.select_expr(cols),
)
}
/// Filters rows using a given conditions and returns a new [DataFrame]
///
/// # Example:
/// ```rust
/// async {
/// df.filter("salary > 4000").collect().await?;
/// }
/// ```
pub fn filter(&mut self, condition: &str) -> DataFrame {
DataFrame::new(
self.spark_session.clone(),
self.logical_plan.filter(condition),
)
}
pub fn sort(&mut self, cols: Vec<&str>, ascending: Option<Vec<bool>>) -> DataFrame {
DataFrame::new(
self.spark_session.clone(),
self.logical_plan.sort(cols, ascending),
)
}
/// Limits the result count o thte number specified and returns a new [DataFrame]
///
/// # Example:
/// ```rust
/// async {
/// df.limit(10).collect().await?;
/// }
/// ```
pub fn limit(&mut self, limit: i32) -> DataFrame {
DataFrame::new(self.spark_session.clone(), self.logical_plan.limit(limit))
}
/// Return a new [DataFrame] with duplicate rows removed,
/// optionally only considering certain columns from a `Vec<String>`
///
/// If no columns are supplied then it all columns are used
///
/// Alias for `dropDuplciates`
///
pub fn drop_duplicates(&mut self, cols: Option<Vec<&str>>) -> DataFrame {
DataFrame::new(
self.spark_session.clone(),
self.logical_plan.drop_duplicates(cols),
)
}
#[allow(non_snake_case)]
pub fn dropDuplicates(&mut self, cols: Option<Vec<&str>>) -> DataFrame {
self.drop_duplicates(cols)
}
/// Returns a new [DataFrame] by renaming multiple columns from a
/// `HashMap<String, String>` containing the `existing` as the key
/// and the `new` as the value.
///
#[allow(non_snake_case)]
pub fn withColumnsRenamed(&mut self, cols: HashMap<String, String>) -> DataFrame {
DataFrame::new(
self.spark_session.clone(),
self.logical_plan.with_columns_renamed(cols),
)
}
/// Returns a new [DataFrame] without the specified columns
pub fn drop(&mut self, cols: Vec<String>) -> DataFrame {
DataFrame::new(self.spark_session.clone(), self.logical_plan.drop(cols))
}
/// Returns a sampled subset of this [DataFrame]
pub fn sample(
&mut self,
lower_bound: f64,
upper_bound: f64,
with_replacement: Option<bool>,
seed: Option<i64>,
) -> DataFrame {
DataFrame::new(
self.spark_session.clone(),
self.logical_plan
.sample(lower_bound, upper_bound, with_replacement, seed),
)
}
/// Returns a new [DataFrame] partitioned by the given partition number and shuffle
/// option
///
/// # Arguments
///
/// * `num_partitions`: the target number of partitions
/// * (optional) `shuffle`: to induce a shuffle. Default is `false`
///
pub fn repartition(&mut self, num_partitions: i32, shuffle: Option<bool>) -> DataFrame {
DataFrame::new(
self.spark_session.clone(),
self.logical_plan.repartition(num_partitions, shuffle),
)
}
/// Returns a new [DataFrame] by skiping the first n rows
pub fn offset(&mut self, num: i32) -> DataFrame {
DataFrame::new(self.spark_session.clone(), self.logical_plan.offset(num))
}
/// Returns the schema of this DataFrame as a [spark::analyze_plan_response::Schema]
/// which contains the schema of a DataFrame
pub async fn schema(&mut self) -> spark::analyze_plan_response::Schema {
let analyze = Some(spark::analyze_plan_request::Analyze::Schema(
spark::analyze_plan_request::Schema {
plan: Some(self.logical_plan.clone().build_plan_root()),
},
));
let schema = self.spark_session.analyze_plan(analyze).await;
match schema {
spark::analyze_plan_response::Result::Schema(schema) => schema,
_ => panic!("Unexpected result"),
}
}
/// Prints the [spark::Plan] to the console
///
/// # Arguments:
/// * `mode`: &str. Defaults to `unspecified`
/// - `simple`
/// - `extended`
/// - `codegen`
/// - `cost`
/// - `formatted`
/// - `unspecified`
///
pub async fn explain(&mut self, mode: &str) {
let explain_mode = match mode {
"simple" => spark::analyze_plan_request::explain::ExplainMode::Simple,
"extended" => spark::analyze_plan_request::explain::ExplainMode::Extended,
"codegen" => spark::analyze_plan_request::explain::ExplainMode::Codegen,
"cost" => spark::analyze_plan_request::explain::ExplainMode::Cost,
"formatted" => spark::analyze_plan_request::explain::ExplainMode::Formatted,
_ => spark::analyze_plan_request::explain::ExplainMode::Unspecified,
};
let analyze = Some(spark::analyze_plan_request::Analyze::Explain(
spark::analyze_plan_request::Explain {
plan: Some(self.logical_plan.clone().build_plan_root()),
explain_mode: explain_mode.into(),
},
));
let explain = match self.spark_session.analyze_plan(analyze).await {
spark::analyze_plan_response::Result::Explain(explain) => explain,
_ => panic!("Unexpected result"),
};
println!("{}", explain.explain_string)
}
#[allow(non_snake_case, dead_code)]
async fn createTempView(&mut self, name: &str) {
self.create_view_cmd(name.to_string(), false, false)
.await
.unwrap()
}
#[allow(non_snake_case, dead_code)]
async fn createGlobalTempView(&mut self, name: &str) {
self.create_view_cmd(name.to_string(), true, false)
.await
.unwrap()
}
#[allow(non_snake_case, dead_code)]
async fn createOrReplaceGlobalTempView(&mut self, name: &str) {
self.create_view_cmd(name.to_string(), true, true)
.await
.unwrap()
}
#[allow(non_snake_case, dead_code)]
async fn createOrReplaceTempView(&mut self, name: &str) {
self.create_view_cmd(name.to_string(), false, true)
.await
.unwrap()
}
async fn create_view_cmd(
&mut self,
name: String,
is_global: bool,
replace: bool,
) -> Result<(), ArrowError> {
let command_type =
spark::command::CommandType::CreateDataframeView(spark::CreateDataFrameViewCommand {
input: Some(self.logical_plan.relation.clone()),
name,
is_global,
replace,
});
let plan = self.logical_plan.clone().build_plan_cmd(command_type);
self.spark_session.consume_plan(Some(plan)).await?;
Ok(())
}
/// Prints the first `n` rows to the console
///
/// # Arguments:
///
/// * `num_row`: (int, optional) number of rows to show (default 10)
/// * `truncate`: (int, optional) If set to 0, it truncates the string. Any other number will not truncate the strings
/// * `vertical`: (bool, optional) If set to true, prints output rows vertically (one line per column value).
///
pub async fn show(
&mut self,
num_rows: Option<i32>,
truncate: Option<i32>,
vertical: Option<bool>,
) -> Result<(), ArrowError> {
let show_expr = RelType::ShowString(Box::new(spark::ShowString {
input: self.logical_plan.clone().relation_input(),
num_rows: num_rows.unwrap_or(10),
truncate: truncate.unwrap_or(0),
vertical: vertical.unwrap_or(false),
}));
let plan = self.logical_plan.from(show_expr).build_plan_root();
let rows = self.spark_session.consume_plan(Some(plan)).await.unwrap();
let _ = pretty::print_batches(rows.as_slice());
Ok(())
}
/// Returns the last `n` rows as vector of [RecordBatch]
///
/// Running tail requires moving the data and results in an action
///
pub async fn tail(&mut self, limit: i32) -> Result<Vec<RecordBatch>, ArrowError> {
let limit_expr = RelType::Tail(Box::new(spark::Tail {
input: self.logical_plan.clone().relation_input(),
limit,
}));
let plan = self.logical_plan.from(limit_expr).build_plan_root();
let rows = self.spark_session.consume_plan(Some(plan)).await.unwrap();
Ok(rows)
}
/// Returns all records as a vector of [RecordBatch]
///
/// # Example:
///
/// ```rust
/// async {
/// df.collect().await?;
/// }
/// ```
pub async fn collect(&mut self) -> Result<Vec<RecordBatch>, ArrowError> {
let rows = self
.spark_session
.consume_plan(Some(self.logical_plan.clone().build_plan_root()))
.await
.unwrap();
Ok(rows)
}
/// Returns a [DataFrameWriter] struct based on the current [DataFrame]
pub fn write(self) -> DataFrameWriter {
DataFrameWriter::new(self)
}
}