blob: 8b3791017a8af56650ea945e40336d32bb57edb3 [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.
use std::{cmp::Ordering, sync::Arc, vec};
use super::{
dialect::CharacterLengthStyle, dialect::DateFieldExtractStyle,
rewrite::TableAliasRewriter, Unparser,
};
use datafusion_common::{
internal_err,
tree_node::{Transformed, TransformedResult, TreeNode},
Column, DataFusionError, Result, ScalarValue,
};
use datafusion_expr::{
expr, utils::grouping_set_to_exprlist, Aggregate, Expr, LogicalPlan,
LogicalPlanBuilder, Projection, SortExpr, Unnest, Window,
};
use indexmap::IndexSet;
use sqlparser::ast;
use sqlparser::tokenizer::Span;
/// Recursively searches children of [LogicalPlan] to find an Aggregate node if exists
/// prior to encountering a Join, TableScan, or a nested subquery (derived table factor).
/// If an Aggregate or node is not found prior to this or at all before reaching the end
/// of the tree, None is returned.
pub(crate) fn find_agg_node_within_select(
plan: &LogicalPlan,
already_projected: bool,
) -> Option<&Aggregate> {
// Note that none of the nodes that have a corresponding node can have more
// than 1 input node. E.g. Projection / Filter always have 1 input node.
let input = plan.inputs();
let input = if input.len() > 1 {
return None;
} else {
input.first()?
};
// Agg nodes explicitly return immediately with a single node
if let LogicalPlan::Aggregate(agg) = input {
Some(agg)
} else if let LogicalPlan::TableScan(_) = input {
None
} else if let LogicalPlan::Projection(_) = input {
if already_projected {
None
} else {
find_agg_node_within_select(input, true)
}
} else {
find_agg_node_within_select(input, already_projected)
}
}
/// Recursively searches children of [LogicalPlan] to find Unnest node if exist
pub(crate) fn find_unnest_node_within_select(plan: &LogicalPlan) -> Option<&Unnest> {
// Note that none of the nodes that have a corresponding node can have more
// than 1 input node. E.g. Projection / Filter always have 1 input node.
let input = plan.inputs();
let input = if input.len() > 1 {
return None;
} else {
input.first()?
};
if let LogicalPlan::Unnest(unnest) = input {
Some(unnest)
} else if let LogicalPlan::TableScan(_) = input {
None
} else if let LogicalPlan::Projection(_) = input {
None
} else {
find_unnest_node_within_select(input)
}
}
/// Recursively searches children of [LogicalPlan] to find Unnest node if exist
/// until encountering a Relation node with single input
pub(crate) fn find_unnest_node_until_relation(plan: &LogicalPlan) -> Option<&Unnest> {
// Note that none of the nodes that have a corresponding node can have more
// than 1 input node. E.g. Projection / Filter always have 1 input node.
let input = plan.inputs();
let input = if input.len() > 1 {
return None;
} else {
input.first()?
};
if let LogicalPlan::Unnest(unnest) = input {
Some(unnest)
} else if let LogicalPlan::TableScan(_) = input {
None
} else if let LogicalPlan::Subquery(_) = input {
None
} else if let LogicalPlan::SubqueryAlias(_) = input {
None
} else {
find_unnest_node_within_select(input)
}
}
/// Recursively searches children of [LogicalPlan] to find Window nodes if exist
/// prior to encountering a Join, TableScan, or a nested subquery (derived table factor).
/// If Window node is not found prior to this or at all before reaching the end
/// of the tree, None is returned.
pub(crate) fn find_window_nodes_within_select<'a>(
plan: &'a LogicalPlan,
mut prev_windows: Option<Vec<&'a Window>>,
already_projected: bool,
) -> Option<Vec<&'a Window>> {
// Note that none of the nodes that have a corresponding node can have more
// than 1 input node. E.g. Projection / Filter always have 1 input node.
let input = plan.inputs();
let input = if input.len() > 1 {
return prev_windows;
} else {
input.first()?
};
// Window nodes accumulate in a vec until encountering a TableScan or 2nd projection
match input {
LogicalPlan::Window(window) => {
prev_windows = match &mut prev_windows {
Some(windows) => {
windows.push(window);
prev_windows
}
_ => Some(vec![window]),
};
find_window_nodes_within_select(input, prev_windows, already_projected)
}
LogicalPlan::Projection(_) => {
if already_projected {
prev_windows
} else {
find_window_nodes_within_select(input, prev_windows, true)
}
}
LogicalPlan::TableScan(_) => prev_windows,
_ => find_window_nodes_within_select(input, prev_windows, already_projected),
}
}
/// Recursively identify Column expressions and transform them into the appropriate unnest expression
///
/// For example, if expr contains the column expr "__unnest_placeholder(make_array(Int64(1),Int64(2),Int64(2),Int64(5),NULL),depth=1)"
/// it will be transformed into an actual unnest expression UNNEST([1, 2, 2, 5, NULL])
pub(crate) fn unproject_unnest_expr(expr: Expr, unnest: &Unnest) -> Result<Expr> {
expr.transform(|sub_expr| {
if let Expr::Column(col_ref) = &sub_expr {
// Check if the column is among the columns to run unnest on.
// Currently, only List/Array columns (defined in `list_type_columns`) are supported for unnesting.
if unnest.list_type_columns.iter().any(|e| e.1.output_column.name == col_ref.name) {
if let Ok(idx) = unnest.schema.index_of_column(col_ref) {
if let LogicalPlan::Projection(Projection { expr, .. }) = unnest.input.as_ref() {
if let Some(unprojected_expr) = expr.get(idx) {
let unnest_expr = Expr::Unnest(expr::Unnest::new(unprojected_expr.clone()));
return Ok(Transformed::yes(unnest_expr));
}
}
}
return internal_err!(
"Tried to unproject unnest expr for column '{}' that was not found in the provided Unnest!", &col_ref.name
);
}
}
Ok(Transformed::no(sub_expr))
}).map(|e| e.data)
}
/// Recursively identify all Column expressions and transform them into the appropriate
/// aggregate expression contained in agg.
///
/// For example, if expr contains the column expr "COUNT(*)" it will be transformed
/// into an actual aggregate expression COUNT(*) as identified in the aggregate node.
pub(crate) fn unproject_agg_exprs(
expr: Expr,
agg: &Aggregate,
windows: Option<&[&Window]>,
) -> Result<Expr> {
expr.transform(|sub_expr| {
if let Expr::Column(c) = sub_expr {
if let Some(unprojected_expr) = find_agg_expr(agg, &c)? {
Ok(Transformed::yes(unprojected_expr.clone()))
} else if let Some(unprojected_expr) =
windows.and_then(|w| find_window_expr(w, &c.name).cloned())
{
// Window function can contain an aggregation columns, e.g., 'avg(sum(ss_sales_price)) over ...' that needs to be unprojected
Ok(Transformed::yes(unproject_agg_exprs(unprojected_expr, agg, None)?))
} else {
internal_err!(
"Tried to unproject agg expr for column '{}' that was not found in the provided Aggregate!", &c.name
)
}
} else {
Ok(Transformed::no(sub_expr))
}
})
.map(|e| e.data)
}
/// Recursively identify all Column expressions and transform them into the appropriate
/// window expression contained in window.
///
/// For example, if expr contains the column expr "COUNT(*) PARTITION BY id" it will be transformed
/// into an actual window expression as identified in the window node.
pub(crate) fn unproject_window_exprs(expr: Expr, windows: &[&Window]) -> Result<Expr> {
expr.transform(|sub_expr| {
if let Expr::Column(c) = sub_expr {
if let Some(unproj) = find_window_expr(windows, &c.name) {
Ok(Transformed::yes(unproj.clone()))
} else {
Ok(Transformed::no(Expr::Column(c)))
}
} else {
Ok(Transformed::no(sub_expr))
}
})
.map(|e| e.data)
}
fn find_agg_expr<'a>(agg: &'a Aggregate, column: &Column) -> Result<Option<&'a Expr>> {
if let Ok(index) = agg.schema.index_of_column(column) {
if matches!(agg.group_expr.as_slice(), [Expr::GroupingSet(_)]) {
// For grouping set expr, we must operate by expression list from the grouping set
let grouping_expr = grouping_set_to_exprlist(agg.group_expr.as_slice())?;
match index.cmp(&grouping_expr.len()) {
Ordering::Less => Ok(grouping_expr.into_iter().nth(index)),
Ordering::Equal => {
internal_err!(
"Tried to unproject column referring to internal grouping id"
)
}
Ordering::Greater => {
Ok(agg.aggr_expr.get(index - grouping_expr.len() - 1))
}
}
} else {
Ok(agg.group_expr.iter().chain(agg.aggr_expr.iter()).nth(index))
}
} else {
Ok(None)
}
}
fn find_window_expr<'a>(
windows: &'a [&'a Window],
column_name: &'a str,
) -> Option<&'a Expr> {
windows
.iter()
.flat_map(|w| w.window_expr.iter())
.find(|expr| expr.schema_name().to_string() == column_name)
}
/// Transforms all Column expressions in a sort expression into the actual expression from aggregation or projection if found.
/// This is required because if an ORDER BY expression is present in an Aggregate or Select, it is replaced
/// with a Column expression (e.g., "sum(catalog_returns.cr_net_loss)"). We need to transform it back to
/// the actual expression, such as sum("catalog_returns"."cr_net_loss").
pub(crate) fn unproject_sort_expr(
mut sort_expr: SortExpr,
agg: Option<&Aggregate>,
input: &LogicalPlan,
) -> Result<SortExpr> {
sort_expr.expr = sort_expr
.expr
.transform(|sub_expr| {
match sub_expr {
// Remove alias if present, because ORDER BY cannot use aliases
Expr::Alias(alias) => Ok(Transformed::yes(*alias.expr)),
Expr::Column(col) => {
if col.relation.is_some() {
return Ok(Transformed::no(Expr::Column(col)));
}
// In case of aggregation there could be columns containing aggregation functions we need to unproject
if let Some(agg) = agg {
if agg.schema.is_column_from_schema(&col) {
return Ok(Transformed::yes(unproject_agg_exprs(
Expr::Column(col),
agg,
None,
)?));
}
}
// If SELECT and ORDER BY contain the same expression with a scalar function, the ORDER BY expression will
// be replaced by a Column expression (e.g., "substr(customer.c_last_name, Int64(0), Int64(5))"), and we need
// to transform it back to the actual expression.
if let LogicalPlan::Projection(Projection { expr, schema, .. }) =
input
{
if let Ok(idx) = schema.index_of_column(&col) {
if let Some(Expr::ScalarFunction(scalar_fn)) = expr.get(idx) {
return Ok(Transformed::yes(Expr::ScalarFunction(
scalar_fn.clone(),
)));
}
}
}
Ok(Transformed::no(Expr::Column(col)))
}
_ => Ok(Transformed::no(sub_expr)),
}
})
.map(|e| e.data)?;
Ok(sort_expr)
}
/// Iterates through the children of a [LogicalPlan] to find a TableScan node before encountering
/// a Projection or any unexpected node that indicates the presence of a Projection (SELECT) in the plan.
/// If a TableScan node is found, returns the TableScan node without filters, along with the collected filters separately.
/// If the plan contains a Projection, returns None.
///
/// Note: If a table alias is present, TableScan filters are rewritten to reference the alias.
///
/// LogicalPlan example:
/// Filter: ta.j1_id < 5
/// Alias: ta
/// TableScan: j1, j1_id > 10
///
/// Will return LogicalPlan below:
/// Alias: ta
/// TableScan: j1
/// And filters: [ta.j1_id < 5, ta.j1_id > 10]
pub(crate) fn try_transform_to_simple_table_scan_with_filters(
plan: &LogicalPlan,
) -> Result<Option<(LogicalPlan, Vec<Expr>)>> {
let mut filters: IndexSet<Expr> = IndexSet::new();
let mut plan_stack = vec![plan];
let mut table_alias = None;
while let Some(current_plan) = plan_stack.pop() {
match current_plan {
LogicalPlan::SubqueryAlias(alias) => {
table_alias = Some(alias.alias.clone());
plan_stack.push(alias.input.as_ref());
}
LogicalPlan::Filter(filter) => {
if !filters.contains(&filter.predicate) {
filters.insert(filter.predicate.clone());
}
plan_stack.push(filter.input.as_ref());
}
LogicalPlan::TableScan(table_scan) => {
let table_schema = table_scan.source.schema();
// optional rewriter if table has an alias
let mut filter_alias_rewriter =
table_alias.as_ref().map(|alias_name| TableAliasRewriter {
table_schema: &table_schema,
alias_name: alias_name.clone(),
});
// rewrite filters to use table alias if present
let table_scan_filters = table_scan
.filters
.iter()
.cloned()
.map(|expr| {
if let Some(ref mut rewriter) = filter_alias_rewriter {
expr.rewrite(rewriter).data()
} else {
Ok(expr)
}
})
.collect::<Result<Vec<_>, DataFusionError>>()?;
for table_scan_filter in table_scan_filters {
if !filters.contains(&table_scan_filter) {
filters.insert(table_scan_filter);
}
}
let mut builder = LogicalPlanBuilder::scan(
table_scan.table_name.clone(),
Arc::clone(&table_scan.source),
table_scan.projection.clone(),
)?;
if let Some(alias) = table_alias.take() {
builder = builder.alias(alias)?;
}
let plan = builder.build()?;
let filters = filters.into_iter().collect();
return Ok(Some((plan, filters)));
}
_ => {
return Ok(None);
}
}
}
Ok(None)
}
/// Converts a date_part function to SQL, tailoring it to the supported date field extraction style.
pub(crate) fn date_part_to_sql(
unparser: &Unparser,
style: DateFieldExtractStyle,
date_part_args: &[Expr],
) -> Result<Option<ast::Expr>> {
match (style, date_part_args.len()) {
(DateFieldExtractStyle::Extract, 2) => {
let date_expr = unparser.expr_to_sql(&date_part_args[1])?;
if let Expr::Literal(ScalarValue::Utf8(Some(field)), _) = &date_part_args[0] {
let field = match field.to_lowercase().as_str() {
"year" => ast::DateTimeField::Year,
"month" => ast::DateTimeField::Month,
"day" => ast::DateTimeField::Day,
"hour" => ast::DateTimeField::Hour,
"minute" => ast::DateTimeField::Minute,
"second" => ast::DateTimeField::Second,
_ => return Ok(None),
};
return Ok(Some(ast::Expr::Extract {
field,
expr: Box::new(date_expr),
syntax: ast::ExtractSyntax::From,
}));
}
}
(DateFieldExtractStyle::Strftime, 2) => {
let column = unparser.expr_to_sql(&date_part_args[1])?;
if let Expr::Literal(ScalarValue::Utf8(Some(field)), _) = &date_part_args[0] {
let field = match field.to_lowercase().as_str() {
"year" => "%Y",
"month" => "%m",
"day" => "%d",
"hour" => "%H",
"minute" => "%M",
"second" => "%S",
_ => return Ok(None),
};
return Ok(Some(ast::Expr::Function(ast::Function {
name: ast::ObjectName::from(vec![ast::Ident {
value: "strftime".to_string(),
quote_style: None,
span: Span::empty(),
}]),
args: ast::FunctionArguments::List(ast::FunctionArgumentList {
duplicate_treatment: None,
args: vec![
ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(
ast::Expr::value(ast::Value::SingleQuotedString(
field.to_string(),
)),
)),
ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(column)),
],
clauses: vec![],
}),
filter: None,
null_treatment: None,
over: None,
within_group: vec![],
parameters: ast::FunctionArguments::None,
uses_odbc_syntax: false,
})));
}
}
(DateFieldExtractStyle::DatePart, _) => {
return Ok(Some(
unparser.scalar_function_to_sql("date_part", date_part_args)?,
));
}
_ => {}
};
Ok(None)
}
pub(crate) fn character_length_to_sql(
unparser: &Unparser,
style: CharacterLengthStyle,
character_length_args: &[Expr],
) -> Result<Option<ast::Expr>> {
let func_name = match style {
CharacterLengthStyle::CharacterLength => "character_length",
CharacterLengthStyle::Length => "length",
};
Ok(Some(unparser.scalar_function_to_sql(
func_name,
character_length_args,
)?))
}
/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc.
/// This remaps `from_unixtime` to `datetime(expr, 'unixepoch')`, expecting the input to be in seconds.
/// It supports no other arguments, so if any are supplied it will return an error.
///
/// # Errors
///
/// - If the number of arguments is not 1 - the column or expression to convert.
/// - If the scalar function cannot be converted to SQL.
pub(crate) fn sqlite_from_unixtime_to_sql(
unparser: &Unparser,
from_unixtime_args: &[Expr],
) -> Result<Option<ast::Expr>> {
if from_unixtime_args.len() != 1 {
return internal_err!(
"from_unixtime for SQLite expects 1 argument, found {}",
from_unixtime_args.len()
);
}
Ok(Some(unparser.scalar_function_to_sql(
"datetime",
&[
from_unixtime_args[0].clone(),
Expr::Literal(ScalarValue::Utf8(Some("unixepoch".to_string())), None),
],
)?))
}
/// SQLite does not support timestamp/date scalars like `to_timestamp`, `from_unixtime`, `date_trunc`, etc.
/// This uses the `strftime` function to format the timestamp as a string depending on the truncation unit.
///
/// # Errors
///
/// - If the number of arguments is not 2 - truncation unit and the column or expression to convert.
/// - If the scalar function cannot be converted to SQL.
pub(crate) fn sqlite_date_trunc_to_sql(
unparser: &Unparser,
date_trunc_args: &[Expr],
) -> Result<Option<ast::Expr>> {
if date_trunc_args.len() != 2 {
return internal_err!(
"date_trunc for SQLite expects 2 arguments, found {}",
date_trunc_args.len()
);
}
if let Expr::Literal(ScalarValue::Utf8(Some(unit)), _) = &date_trunc_args[0] {
let format = match unit.to_lowercase().as_str() {
"year" => "%Y",
"month" => "%Y-%m",
"day" => "%Y-%m-%d",
"hour" => "%Y-%m-%d %H",
"minute" => "%Y-%m-%d %H:%M",
"second" => "%Y-%m-%d %H:%M:%S",
_ => return Ok(None),
};
return Ok(Some(unparser.scalar_function_to_sql(
"strftime",
&[
Expr::Literal(ScalarValue::Utf8(Some(format.to_string())), None),
date_trunc_args[1].clone(),
],
)?));
}
Ok(None)
}