blob: a120ab427e1de1d15436bc07beb2f05e45ee2822 [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::ops::Deref;
use std::sync::Arc;
use crate::expressions::Column;
use crate::utils::collect_columns;
use crate::PhysicalExpr;
use arrow::datatypes::{Field, Schema, SchemaRef};
use datafusion_common::stats::{ColumnStatistics, Precision};
use datafusion_common::tree_node::{Transformed, TransformedResult, TreeNode};
use datafusion_common::{internal_datafusion_err, internal_err, plan_err, Result};
use datafusion_physical_expr_common::sort_expr::{LexOrdering, PhysicalSortExpr};
use indexmap::IndexMap;
use itertools::Itertools;
/// A projection expression as used by projection operations.
///
/// The expression is evaluated and the result is stored in a column
/// with the name specified by `alias`.
///
/// For example, the SQL expression `a + b AS sum_ab` would be represented
/// as a `ProjectionExpr` where `expr` is the expression `a + b`
/// and `alias` is the string `sum_ab`.
#[derive(Debug, Clone)]
pub struct ProjectionExpr {
/// The expression that will be evaluated.
pub expr: Arc<dyn PhysicalExpr>,
/// The name of the output column for use an output schema.
pub alias: String,
}
impl std::fmt::Display for ProjectionExpr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.expr.to_string() == self.alias {
write!(f, "{}", self.alias)
} else {
write!(f, "{} AS {}", self.expr, self.alias)
}
}
}
impl ProjectionExpr {
/// Create a new projection expression
pub fn new(expr: Arc<dyn PhysicalExpr>, alias: String) -> Self {
Self { expr, alias }
}
/// Create a new projection expression from an expression and a schema using the expression's output field name as alias.
pub fn new_from_expression(
expr: Arc<dyn PhysicalExpr>,
schema: &Schema,
) -> Result<Self> {
let field = expr.return_field(schema)?;
Ok(Self {
expr,
alias: field.name().to_string(),
})
}
}
impl From<(Arc<dyn PhysicalExpr>, String)> for ProjectionExpr {
fn from(value: (Arc<dyn PhysicalExpr>, String)) -> Self {
Self::new(value.0, value.1)
}
}
impl From<&(Arc<dyn PhysicalExpr>, String)> for ProjectionExpr {
fn from(value: &(Arc<dyn PhysicalExpr>, String)) -> Self {
Self::new(Arc::clone(&value.0), value.1.clone())
}
}
impl From<ProjectionExpr> for (Arc<dyn PhysicalExpr>, String) {
fn from(value: ProjectionExpr) -> Self {
(value.expr, value.alias)
}
}
/// A collection of projection expressions.
///
/// This struct encapsulates multiple `ProjectionExpr` instances,
/// representing a complete projection operation and provides
/// methods to manipulate and analyze the projection as a whole.
#[derive(Debug, Clone)]
pub struct ProjectionExprs {
exprs: Vec<ProjectionExpr>,
}
impl std::fmt::Display for ProjectionExprs {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let exprs: Vec<String> = self.exprs.iter().map(|e| e.to_string()).collect();
write!(f, "Projection[{}]", exprs.join(", "))
}
}
impl From<Vec<ProjectionExpr>> for ProjectionExprs {
fn from(value: Vec<ProjectionExpr>) -> Self {
Self { exprs: value }
}
}
impl From<&[ProjectionExpr]> for ProjectionExprs {
fn from(value: &[ProjectionExpr]) -> Self {
Self {
exprs: value.to_vec(),
}
}
}
impl FromIterator<ProjectionExpr> for ProjectionExprs {
fn from_iter<T: IntoIterator<Item = ProjectionExpr>>(exprs: T) -> Self {
Self {
exprs: exprs.into_iter().collect::<Vec<_>>(),
}
}
}
impl AsRef<[ProjectionExpr]> for ProjectionExprs {
fn as_ref(&self) -> &[ProjectionExpr] {
&self.exprs
}
}
impl ProjectionExprs {
pub fn new<I>(exprs: I) -> Self
where
I: IntoIterator<Item = ProjectionExpr>,
{
Self {
exprs: exprs.into_iter().collect::<Vec<_>>(),
}
}
/// Creates a [`ProjectionExpr`] from a list of column indices.
///
/// This is a convenience method for creating simple column-only projections, where each projection expression is a reference to a column
/// in the input schema.
///
/// # Behavior
/// - Ordering: the output projection preserves the exact order of indices provided in the input slice
/// For example, `[2, 0, 1]` will produce projections for columns 2, 0, then 1 in that order
/// - Duplicates: Duplicate indices are allowed and will create multiple projection expressions referencing the same source column
/// For example, `[0, 0]` creates 2 separate projections both referencing column 0
///
/// # Panics
/// Panics if any index in `indices` is out of bounds for the provided schema.
///
/// # Example
///
/// ```rust
/// use arrow::datatypes::{DataType, Field, Schema};
/// use datafusion_physical_expr::projection::ProjectionExprs;
/// use std::sync::Arc;
///
/// // Create a schema with three columns
/// let schema = Arc::new(Schema::new(vec![
/// Field::new("a", DataType::Int32, false),
/// Field::new("b", DataType::Utf8, false),
/// Field::new("c", DataType::Float64, false),
/// ]));
///
/// // Project columns at indices 2 and 0 (c and a) - ordering is preserved
/// let projection = ProjectionExprs::from_indices(&[2, 0], &schema);
///
/// // This creates: SELECT c@2 AS c, a@0 AS a
/// assert_eq!(projection.as_ref().len(), 2);
/// assert_eq!(projection.as_ref()[0].alias, "c");
/// assert_eq!(projection.as_ref()[1].alias, "a");
///
/// // Duplicate indices are allowed
/// let projection_with_dups = ProjectionExprs::from_indices(&[0, 0, 1], &schema);
/// assert_eq!(projection_with_dups.as_ref().len(), 3);
/// assert_eq!(projection_with_dups.as_ref()[0].alias, "a");
/// assert_eq!(projection_with_dups.as_ref()[1].alias, "a"); // duplicate
/// assert_eq!(projection_with_dups.as_ref()[2].alias, "b");
/// ```
pub fn from_indices(indices: &[usize], schema: &SchemaRef) -> Self {
let projection_exprs = indices.iter().map(|&i| {
let field = schema.field(i);
ProjectionExpr {
expr: Arc::new(Column::new(field.name(), i)),
alias: field.name().clone(),
}
});
Self::from_iter(projection_exprs)
}
/// Returns an iterator over the projection expressions
pub fn iter(&self) -> impl Iterator<Item = &ProjectionExpr> {
self.exprs.iter()
}
/// Creates a ProjectionMapping from this projection
pub fn projection_mapping(
&self,
input_schema: &SchemaRef,
) -> Result<ProjectionMapping> {
ProjectionMapping::try_new(
self.exprs
.iter()
.map(|p| (Arc::clone(&p.expr), p.alias.clone())),
input_schema,
)
}
/// Iterate over a clone of the projection expressions.
pub fn expr_iter(&self) -> impl Iterator<Item = Arc<dyn PhysicalExpr>> + '_ {
self.exprs.iter().map(|e| Arc::clone(&e.expr))
}
/// Apply another projection on top of this projection, returning the combined projection.
/// For example, if this projection is `SELECT c@2 AS x, b@1 AS y, a@0 as z` and the other projection is `SELECT x@0 + 1 AS c1, y@1 + z@2 as c2`,
/// we return a projection equivalent to `SELECT c@2 + 1 AS c1, b@1 + a@0 as c2`.
///
/// # Example
///
/// ```rust
/// use datafusion_common::{Result, ScalarValue};
/// use datafusion_expr::Operator;
/// use datafusion_physical_expr::expressions::{BinaryExpr, Column, Literal};
/// use datafusion_physical_expr::projection::{ProjectionExpr, ProjectionExprs};
/// use std::sync::Arc;
///
/// fn main() -> Result<()> {
/// // Example from the docstring:
/// // Base projection: SELECT c@2 AS x, b@1 AS y, a@0 AS z
/// let base = ProjectionExprs::new(vec![
/// ProjectionExpr {
/// expr: Arc::new(Column::new("c", 2)),
/// alias: "x".to_string(),
/// },
/// ProjectionExpr {
/// expr: Arc::new(Column::new("b", 1)),
/// alias: "y".to_string(),
/// },
/// ProjectionExpr {
/// expr: Arc::new(Column::new("a", 0)),
/// alias: "z".to_string(),
/// },
/// ]);
///
/// // Top projection: SELECT x@0 + 1 AS c1, y@1 + z@2 AS c2
/// let top = ProjectionExprs::new(vec![
/// ProjectionExpr {
/// expr: Arc::new(BinaryExpr::new(
/// Arc::new(Column::new("x", 0)),
/// Operator::Plus,
/// Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
/// )),
/// alias: "c1".to_string(),
/// },
/// ProjectionExpr {
/// expr: Arc::new(BinaryExpr::new(
/// Arc::new(Column::new("y", 1)),
/// Operator::Plus,
/// Arc::new(Column::new("z", 2)),
/// )),
/// alias: "c2".to_string(),
/// },
/// ]);
///
/// // Expected result: SELECT c@2 + 1 AS c1, b@1 + a@0 AS c2
/// let result = base.try_merge(&top)?;
///
/// assert_eq!(result.as_ref().len(), 2);
/// assert_eq!(result.as_ref()[0].alias, "c1");
/// assert_eq!(result.as_ref()[1].alias, "c2");
///
/// Ok(())
/// }
/// ```
///
/// # Errors
/// This function returns an error if any expression in the `other` projection cannot be
/// applied on top of this projection.
pub fn try_merge(&self, other: &ProjectionExprs) -> Result<ProjectionExprs> {
let mut new_exprs = Vec::with_capacity(other.exprs.len());
for proj_expr in &other.exprs {
let new_expr = update_expr(&proj_expr.expr, &self.exprs, true)?
.ok_or_else(|| {
internal_datafusion_err!(
"Failed to combine projections: expression {} could not be applied on top of existing projections {}",
proj_expr.expr,
self.exprs.iter().map(|e| format!("{e}")).join(", ")
)
})?;
new_exprs.push(ProjectionExpr {
expr: new_expr,
alias: proj_expr.alias.clone(),
});
}
Ok(ProjectionExprs::new(new_exprs))
}
/// Extract the column indices used in this projection.
/// For example, for a projection `SELECT a AS x, b + 1 AS y`, where `a` is at index 0 and `b` is at index 1,
/// this function would return `[0, 1]`.
/// Repeated indices are returned only once, and the order is ascending.
pub fn column_indices(&self) -> Vec<usize> {
self.exprs
.iter()
.flat_map(|e| collect_columns(&e.expr).into_iter().map(|col| col.index()))
.sorted_unstable()
.dedup()
.collect_vec()
}
/// Extract the ordered column indices for a column-only projection.
///
/// This function assumes that all expressions in the projection are simple column references.
/// It returns the column indices in the order they appear in the projection.
///
/// # Panics
///
/// Panics if any expression in the projection is not a simple column reference. This includes:
/// - Computed expressions (e.g., `a + 1`, `CAST(a AS INT)`)
/// - Function calls (e.g., `UPPER(name)`, `SUM(amount)`)
/// - Literals (e.g., `42`, `'hello'`)
/// - Complex nested expressions (e.g., `CASE WHEN ... THEN ... END`)
///
/// # Returns
///
/// A vector of column indices in projection order. Unlike [`column_indices()`](Self::column_indices),
/// this function:
/// - Preserves the projection order (does not sort)
/// - Preserves duplicates (does not deduplicate)
///
/// # Example
///
/// For a projection `SELECT c, a, c` where `a` is at index 0 and `c` is at index 2,
/// this function would return `[2, 0, 2]`.
///
/// Use [`column_indices()`](Self::column_indices) instead if the projection may contain
/// non-column expressions or if you need a deduplicated sorted list.
pub fn ordered_column_indices(&self) -> Vec<usize> {
self.exprs
.iter()
.map(|e| {
e.expr
.as_any()
.downcast_ref::<Column>()
.expect("Expected column reference in projection")
.index()
})
.collect()
}
/// Project a schema according to this projection.
/// For example, for a projection `SELECT a AS x, b + 1 AS y`, where `a` is at index 0 and `b` is at index 1,
/// if the input schema is `[a: Int32, b: Int32, c: Int32]`, the output schema would be `[x: Int32, y: Int32]`.
/// Fields' metadata are preserved from the input schema.
pub fn project_schema(&self, input_schema: &Schema) -> Result<Schema> {
let fields: Result<Vec<Field>> = self
.exprs
.iter()
.map(|proj_expr| {
let metadata = proj_expr
.expr
.return_field(input_schema)?
.metadata()
.clone();
let field = Field::new(
&proj_expr.alias,
proj_expr.expr.data_type(input_schema)?,
proj_expr.expr.nullable(input_schema)?,
)
.with_metadata(metadata);
Ok(field)
})
.collect();
Ok(Schema::new_with_metadata(
fields?,
input_schema.metadata().clone(),
))
}
/// Project statistics according to this projection.
/// For example, for a projection `SELECT a AS x, b + 1 AS y`, where `a` is at index 0 and `b` is at index 1,
/// if the input statistics has column statistics for columns `a`, `b`, and `c`, the output statistics would have column statistics for columns `x` and `y`.
pub fn project_statistics(
&self,
mut stats: datafusion_common::Statistics,
input_schema: &Schema,
) -> Result<datafusion_common::Statistics> {
let mut primitive_row_size = 0;
let mut primitive_row_size_possible = true;
let mut column_statistics = vec![];
for proj_expr in &self.exprs {
let expr = &proj_expr.expr;
let col_stats = if let Some(col) = expr.as_any().downcast_ref::<Column>() {
stats.column_statistics[col.index()].clone()
} else {
// TODO stats: estimate more statistics from expressions
// (expressions should compute their statistics themselves)
ColumnStatistics::new_unknown()
};
column_statistics.push(col_stats);
let data_type = expr.data_type(input_schema)?;
if let Some(value) = data_type.primitive_width() {
primitive_row_size += value;
continue;
}
primitive_row_size_possible = false;
}
if primitive_row_size_possible {
stats.total_byte_size =
Precision::Exact(primitive_row_size).multiply(&stats.num_rows);
}
stats.column_statistics = column_statistics;
Ok(stats)
}
}
impl<'a> IntoIterator for &'a ProjectionExprs {
type Item = &'a ProjectionExpr;
type IntoIter = std::slice::Iter<'a, ProjectionExpr>;
fn into_iter(self) -> Self::IntoIter {
self.exprs.iter()
}
}
impl IntoIterator for ProjectionExprs {
type Item = ProjectionExpr;
type IntoIter = std::vec::IntoIter<ProjectionExpr>;
fn into_iter(self) -> Self::IntoIter {
self.exprs.into_iter()
}
}
/// The function operates in two modes:
///
/// 1) When `sync_with_child` is `true`:
///
/// The function updates the indices of `expr` if the expression resides
/// in the input plan. For instance, given the expressions `a@1 + b@2`
/// and `c@0` with the input schema `c@2, a@0, b@1`, the expressions are
/// updated to `a@0 + b@1` and `c@2`.
///
/// 2) When `sync_with_child` is `false`:
///
/// The function determines how the expression would be updated if a projection
/// was placed before the plan associated with the expression. If the expression
/// cannot be rewritten after the projection, it returns `None`. For example,
/// given the expressions `c@0`, `a@1` and `b@2`, and the projection with
/// an output schema of `a, c_new`, then `c@0` becomes `c_new@1`, `a@1` becomes
/// `a@0`, but `b@2` results in `None` since the projection does not include `b`.
///
/// # Errors
/// This function returns an error if `sync_with_child` is `true` and if any expression references
/// an index that is out of bounds for `projected_exprs`.
/// For example:
///
/// - `expr` is `a@3`
/// - `projected_exprs` is \[`a@0`, `b@1`\]
///
/// In this case, `a@3` references index 3, which is out of bounds for `projected_exprs` (which has length 2).
pub fn update_expr(
expr: &Arc<dyn PhysicalExpr>,
projected_exprs: &[ProjectionExpr],
sync_with_child: bool,
) -> Result<Option<Arc<dyn PhysicalExpr>>> {
#[derive(Debug, PartialEq)]
enum RewriteState {
/// The expression is unchanged.
Unchanged,
/// Some part of the expression has been rewritten
RewrittenValid,
/// Some part of the expression has been rewritten, but some column
/// references could not be.
RewrittenInvalid,
}
let mut state = RewriteState::Unchanged;
let new_expr = Arc::clone(expr)
.transform_up(|expr| {
if state == RewriteState::RewrittenInvalid {
return Ok(Transformed::no(expr));
}
let Some(column) = expr.as_any().downcast_ref::<Column>() else {
return Ok(Transformed::no(expr));
};
if sync_with_child {
state = RewriteState::RewrittenValid;
// Update the index of `column`:
let projected_expr = projected_exprs.get(column.index()).ok_or_else(|| {
internal_datafusion_err!(
"Column index {} out of bounds for projected expressions of length {}",
column.index(),
projected_exprs.len()
)
})?;
Ok(Transformed::yes(Arc::clone(&projected_expr.expr)))
} else {
// default to invalid, in case we can't find the relevant column
state = RewriteState::RewrittenInvalid;
// Determine how to update `column` to accommodate `projected_exprs`
projected_exprs
.iter()
.enumerate()
.find_map(|(index, proj_expr)| {
proj_expr.expr.as_any().downcast_ref::<Column>().and_then(
|projected_column| {
(column.name().eq(projected_column.name())
&& column.index() == projected_column.index())
.then(|| {
state = RewriteState::RewrittenValid;
Arc::new(Column::new(&proj_expr.alias, index)) as _
})
},
)
})
.map_or_else(
|| Ok(Transformed::no(expr)),
|c| Ok(Transformed::yes(c)),
)
}
})
.data()?;
Ok((state == RewriteState::RewrittenValid).then_some(new_expr))
}
/// Stores target expressions, along with their indices, that associate with a
/// source expression in a projection mapping.
#[derive(Clone, Debug, Default)]
pub struct ProjectionTargets {
/// A non-empty vector of pairs of target expressions and their indices.
/// Consider using a special non-empty collection type in the future (e.g.
/// if Rust provides one in the standard library).
exprs_indices: Vec<(Arc<dyn PhysicalExpr>, usize)>,
}
impl ProjectionTargets {
/// Returns the first target expression and its index.
pub fn first(&self) -> &(Arc<dyn PhysicalExpr>, usize) {
// Since the vector is non-empty, we can safely unwrap:
self.exprs_indices.first().unwrap()
}
/// Adds a target expression and its index to the list of targets.
pub fn push(&mut self, target: (Arc<dyn PhysicalExpr>, usize)) {
self.exprs_indices.push(target);
}
}
impl Deref for ProjectionTargets {
type Target = [(Arc<dyn PhysicalExpr>, usize)];
fn deref(&self) -> &Self::Target {
&self.exprs_indices
}
}
impl From<Vec<(Arc<dyn PhysicalExpr>, usize)>> for ProjectionTargets {
fn from(exprs_indices: Vec<(Arc<dyn PhysicalExpr>, usize)>) -> Self {
Self { exprs_indices }
}
}
/// Stores the mapping between source expressions and target expressions for a
/// projection.
#[derive(Clone, Debug)]
pub struct ProjectionMapping {
/// Mapping between source expressions and target expressions.
/// Vector indices correspond to the indices after projection.
map: IndexMap<Arc<dyn PhysicalExpr>, ProjectionTargets>,
}
impl ProjectionMapping {
/// Constructs the mapping between a projection's input and output
/// expressions.
///
/// For example, given the input projection expressions (`a + b`, `c + d`)
/// and an output schema with two columns `"c + d"` and `"a + b"`, the
/// projection mapping would be:
///
/// ```text
/// [0]: (c + d, [(col("c + d"), 0)])
/// [1]: (a + b, [(col("a + b"), 1)])
/// ```
///
/// where `col("c + d")` means the column named `"c + d"`.
pub fn try_new(
expr: impl IntoIterator<Item = (Arc<dyn PhysicalExpr>, String)>,
input_schema: &SchemaRef,
) -> Result<Self> {
// Construct a map from the input expressions to the output expression of the projection:
let mut map = IndexMap::<_, ProjectionTargets>::new();
for (expr_idx, (expr, name)) in expr.into_iter().enumerate() {
let target_expr = Arc::new(Column::new(&name, expr_idx)) as _;
let source_expr = expr.transform_down(|e| match e.as_any().downcast_ref::<Column>() {
Some(col) => {
// Sometimes, an expression and its name in the input_schema
// doesn't match. This can cause problems, so we make sure
// that the expression name matches with the name in `input_schema`.
// Conceptually, `source_expr` and `expression` should be the same.
let idx = col.index();
let matching_field = input_schema.field(idx);
let matching_name = matching_field.name();
if col.name() != matching_name {
return internal_err!(
"Input field name {} does not match with the projection expression {}",
matching_name,
col.name()
);
}
let matching_column = Column::new(matching_name, idx);
Ok(Transformed::yes(Arc::new(matching_column)))
}
None => Ok(Transformed::no(e)),
})
.data()?;
map.entry(source_expr)
.or_default()
.push((target_expr, expr_idx));
}
Ok(Self { map })
}
/// Constructs a subset mapping using the provided indices.
///
/// This is used when the output is a subset of the input without any
/// other transformations. The indices are for columns in the schema.
pub fn from_indices(indices: &[usize], schema: &SchemaRef) -> Result<Self> {
let projection_exprs = indices.iter().map(|index| {
let field = schema.field(*index);
let column = Arc::new(Column::new(field.name(), *index));
(column as _, field.name().clone())
});
ProjectionMapping::try_new(projection_exprs, schema)
}
}
impl Deref for ProjectionMapping {
type Target = IndexMap<Arc<dyn PhysicalExpr>, ProjectionTargets>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl FromIterator<(Arc<dyn PhysicalExpr>, ProjectionTargets)> for ProjectionMapping {
fn from_iter<T: IntoIterator<Item = (Arc<dyn PhysicalExpr>, ProjectionTargets)>>(
iter: T,
) -> Self {
Self {
map: IndexMap::from_iter(iter),
}
}
}
/// Projects a slice of [LexOrdering]s onto the given schema.
///
/// This is a convenience wrapper that applies [project_ordering] to each
/// input ordering and collects the successful projections:
/// - For each input ordering, the result of [project_ordering] is appended to
/// the output if it is `Some(...)`.
/// - Order is preserved and no deduplication is attempted.
/// - If none of the input orderings can be projected, an empty `Vec` is
/// returned.
///
/// See [project_ordering] for the semantics of projecting a single
/// [LexOrdering].
pub fn project_orderings(
orderings: &[LexOrdering],
schema: &SchemaRef,
) -> Vec<LexOrdering> {
let mut projected_orderings = vec![];
for ordering in orderings {
projected_orderings.extend(project_ordering(ordering, schema));
}
projected_orderings
}
/// Projects a single [LexOrdering] onto the given schema.
///
/// This function attempts to rewrite every [PhysicalSortExpr] in the provided
/// [LexOrdering] so that any [Column] expressions point at the correct field
/// indices in `schema`.
///
/// Key details:
/// - Columns are matched by name, not by index. The index of each matched
/// column is looked up with [Schema::column_with_name](arrow::datatypes::Schema::column_with_name) and a new
/// [Column] with the correct [index](Column::index) is substituted.
/// - If an expression references a column name that does not exist in
/// `schema`, projection of the current ordering stops and only the already
/// rewritten prefix is kept. This models the fact that a lexicographical
/// ordering remains valid for any leading prefix whose expressions are
/// present in the projected schema.
/// - If no expressions can be projected (i.e. the first one is missing), the
/// function returns `None`.
///
/// Return value:
/// - `Some(LexOrdering)` if at least one sort expression could be projected.
/// The returned ordering may be a strict prefix of the input ordering.
/// - `None` if no part of the ordering can be projected onto `schema`.
///
/// Example
///
/// Suppose we have an input ordering `[col("a@0"), col("b@1")]` but the projected
/// schema only contains b and not a. The result will be `Some([col("a@0")])`. In other
/// words, the column reference is reindexed to match the projected schema.
/// If neither a nor b is present, the result will be None.
pub fn project_ordering(
ordering: &LexOrdering,
schema: &SchemaRef,
) -> Option<LexOrdering> {
let mut projected_exprs = vec![];
for PhysicalSortExpr { expr, options } in ordering.iter() {
let transformed = Arc::clone(expr).transform_up(|expr| {
let Some(col) = expr.as_any().downcast_ref::<Column>() else {
return Ok(Transformed::no(expr));
};
let name = col.name();
if let Some((idx, _)) = schema.column_with_name(name) {
// Compute the new column expression (with correct index) after projection:
Ok(Transformed::yes(Arc::new(Column::new(name, idx))))
} else {
// Cannot find expression in the projected_schema,
// signal this using an Err result
plan_err!("")
}
});
match transformed {
Ok(transformed) => {
projected_exprs.push(PhysicalSortExpr::new(transformed.data, *options));
}
Err(_) => {
// Err result indicates an expression could not be found in the
// projected_schema, stop iterating since rest of the orderings are violated
break;
}
}
}
LexOrdering::new(projected_exprs)
}
#[cfg(test)]
pub(crate) mod tests {
use std::collections::HashMap;
use super::*;
use crate::equivalence::{convert_to_orderings, EquivalenceProperties};
use crate::expressions::{col, BinaryExpr, Literal};
use crate::utils::tests::TestScalarUDF;
use crate::{PhysicalExprRef, ScalarFunctionExpr};
use arrow::compute::SortOptions;
use arrow::datatypes::{DataType, Field, Schema, TimeUnit};
use datafusion_common::config::ConfigOptions;
use datafusion_common::{ScalarValue, Statistics};
use datafusion_expr::{Operator, ScalarUDF};
use insta::assert_snapshot;
pub(crate) fn output_schema(
mapping: &ProjectionMapping,
input_schema: &Arc<Schema>,
) -> Result<SchemaRef> {
// Calculate output schema:
let mut fields = vec![];
for (source, targets) in mapping.iter() {
let data_type = source.data_type(input_schema)?;
let nullable = source.nullable(input_schema)?;
for (target, _) in targets.iter() {
let Some(column) = target.as_any().downcast_ref::<Column>() else {
return plan_err!("Expects to have column");
};
fields.push(Field::new(column.name(), data_type.clone(), nullable));
}
}
let output_schema = Arc::new(Schema::new_with_metadata(
fields,
input_schema.metadata().clone(),
));
Ok(output_schema)
}
#[test]
fn project_orderings() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("b", DataType::Int32, true),
Field::new("c", DataType::Int32, true),
Field::new("d", DataType::Int32, true),
Field::new("e", DataType::Int32, true),
Field::new("ts", DataType::Timestamp(TimeUnit::Nanosecond, None), true),
]));
let col_a = &col("a", &schema)?;
let col_b = &col("b", &schema)?;
let col_c = &col("c", &schema)?;
let col_d = &col("d", &schema)?;
let col_e = &col("e", &schema)?;
let col_ts = &col("ts", &schema)?;
let a_plus_b = Arc::new(BinaryExpr::new(
Arc::clone(col_a),
Operator::Plus,
Arc::clone(col_b),
)) as Arc<dyn PhysicalExpr>;
let b_plus_d = Arc::new(BinaryExpr::new(
Arc::clone(col_b),
Operator::Plus,
Arc::clone(col_d),
)) as Arc<dyn PhysicalExpr>;
let b_plus_e = Arc::new(BinaryExpr::new(
Arc::clone(col_b),
Operator::Plus,
Arc::clone(col_e),
)) as Arc<dyn PhysicalExpr>;
let c_plus_d = Arc::new(BinaryExpr::new(
Arc::clone(col_c),
Operator::Plus,
Arc::clone(col_d),
)) as Arc<dyn PhysicalExpr>;
let option_asc = SortOptions {
descending: false,
nulls_first: false,
};
let option_desc = SortOptions {
descending: true,
nulls_first: true,
};
let test_cases = vec![
// ---------- TEST CASE 1 ------------
(
// orderings
vec![
// [b ASC]
vec![(col_b, option_asc)],
],
// projection exprs
vec![(col_b, "b_new".to_string()), (col_a, "a_new".to_string())],
// expected
vec![
// [b_new ASC]
vec![("b_new", option_asc)],
],
),
// ---------- TEST CASE 2 ------------
(
// orderings
vec![
// empty ordering
],
// projection exprs
vec![(col_c, "c_new".to_string()), (col_b, "b_new".to_string())],
// expected
vec![
// no ordering at the output
],
),
// ---------- TEST CASE 3 ------------
(
// orderings
vec![
// [ts ASC]
vec![(col_ts, option_asc)],
],
// projection exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_ts, "ts_new".to_string()),
],
// expected
vec![
// [ts_new ASC]
vec![("ts_new", option_asc)],
],
),
// ---------- TEST CASE 4 ------------
(
// orderings
vec![
// [a ASC, ts ASC]
vec![(col_a, option_asc), (col_ts, option_asc)],
// [b ASC, ts ASC]
vec![(col_b, option_asc), (col_ts, option_asc)],
],
// projection exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_ts, "ts_new".to_string()),
],
// expected
vec![
// [a_new ASC, ts_new ASC]
vec![("a_new", option_asc), ("ts_new", option_asc)],
// [b_new ASC, ts_new ASC]
vec![("b_new", option_asc), ("ts_new", option_asc)],
],
),
// ---------- TEST CASE 5 ------------
(
// orderings
vec![
// [a + b ASC]
vec![(&a_plus_b, option_asc)],
],
// projection exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(&a_plus_b, "a+b".to_string()),
],
// expected
vec![
// [a + b ASC]
vec![("a+b", option_asc)],
],
),
// ---------- TEST CASE 6 ------------
(
// orderings
vec![
// [a + b ASC, c ASC]
vec![(&a_plus_b, option_asc), (col_c, option_asc)],
],
// projection exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_c, "c_new".to_string()),
(&a_plus_b, "a+b".to_string()),
],
// expected
vec![
// [a + b ASC, c_new ASC]
vec![("a+b", option_asc), ("c_new", option_asc)],
],
),
// ------- TEST CASE 7 ----------
(
vec![
// [a ASC, b ASC, c ASC]
vec![(col_a, option_asc), (col_b, option_asc)],
// [a ASC, d ASC]
vec![(col_a, option_asc), (col_d, option_asc)],
],
// b as b_new, a as a_new, d as d_new b+d
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_d, "d_new".to_string()),
(&b_plus_d, "b+d".to_string()),
],
// expected
vec![
// [a_new ASC, b_new ASC]
vec![("a_new", option_asc), ("b_new", option_asc)],
// [a_new ASC, d_new ASC]
vec![("a_new", option_asc), ("d_new", option_asc)],
// [a_new ASC, b+d ASC]
vec![("a_new", option_asc), ("b+d", option_asc)],
],
),
// ------- TEST CASE 8 ----------
(
// orderings
vec![
// [b+d ASC]
vec![(&b_plus_d, option_asc)],
],
// proj exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_d, "d_new".to_string()),
(&b_plus_d, "b+d".to_string()),
],
// expected
vec![
// [b+d ASC]
vec![("b+d", option_asc)],
],
),
// ------- TEST CASE 9 ----------
(
// orderings
vec![
// [a ASC, d ASC, b ASC]
vec![
(col_a, option_asc),
(col_d, option_asc),
(col_b, option_asc),
],
// [c ASC]
vec![(col_c, option_asc)],
],
// proj exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_d, "d_new".to_string()),
(col_c, "c_new".to_string()),
],
// expected
vec![
// [a_new ASC, d_new ASC, b_new ASC]
vec![
("a_new", option_asc),
("d_new", option_asc),
("b_new", option_asc),
],
// [c_new ASC],
vec![("c_new", option_asc)],
],
),
// ------- TEST CASE 10 ----------
(
vec![
// [a ASC, b ASC, c ASC]
vec![
(col_a, option_asc),
(col_b, option_asc),
(col_c, option_asc),
],
// [a ASC, d ASC]
vec![(col_a, option_asc), (col_d, option_asc)],
],
// proj exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_c, "c_new".to_string()),
(&c_plus_d, "c+d".to_string()),
],
// expected
vec![
// [a_new ASC, b_new ASC, c_new ASC]
vec![
("a_new", option_asc),
("b_new", option_asc),
("c_new", option_asc),
],
// [a_new ASC, b_new ASC, c+d ASC]
vec![
("a_new", option_asc),
("b_new", option_asc),
("c+d", option_asc),
],
],
),
// ------- TEST CASE 11 ----------
(
// orderings
vec![
// [a ASC, b ASC]
vec![(col_a, option_asc), (col_b, option_asc)],
// [a ASC, d ASC]
vec![(col_a, option_asc), (col_d, option_asc)],
],
// proj exprs
vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(&b_plus_d, "b+d".to_string()),
],
// expected
vec![
// [a_new ASC, b_new ASC]
vec![("a_new", option_asc), ("b_new", option_asc)],
// [a_new ASC, b + d ASC]
vec![("a_new", option_asc), ("b+d", option_asc)],
],
),
// ------- TEST CASE 12 ----------
(
// orderings
vec![
// [a ASC, b ASC, c ASC]
vec![
(col_a, option_asc),
(col_b, option_asc),
(col_c, option_asc),
],
],
// proj exprs
vec![(col_c, "c_new".to_string()), (col_a, "a_new".to_string())],
// expected
vec![
// [a_new ASC]
vec![("a_new", option_asc)],
],
),
// ------- TEST CASE 13 ----------
(
// orderings
vec![
// [a ASC, b ASC, c ASC]
vec![
(col_a, option_asc),
(col_b, option_asc),
(col_c, option_asc),
],
// [a ASC, a + b ASC, c ASC]
vec![
(col_a, option_asc),
(&a_plus_b, option_asc),
(col_c, option_asc),
],
],
// proj exprs
vec![
(col_c, "c_new".to_string()),
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(&a_plus_b, "a+b".to_string()),
],
// expected
vec![
// [a_new ASC, b_new ASC, c_new ASC]
vec![
("a_new", option_asc),
("b_new", option_asc),
("c_new", option_asc),
],
// [a_new ASC, a+b ASC, c_new ASC]
vec![
("a_new", option_asc),
("a+b", option_asc),
("c_new", option_asc),
],
],
),
// ------- TEST CASE 14 ----------
(
// orderings
vec![
// [a ASC, b ASC]
vec![(col_a, option_asc), (col_b, option_asc)],
// [c ASC, b ASC]
vec![(col_c, option_asc), (col_b, option_asc)],
// [d ASC, e ASC]
vec![(col_d, option_asc), (col_e, option_asc)],
],
// proj exprs
vec![
(col_c, "c_new".to_string()),
(col_d, "d_new".to_string()),
(col_a, "a_new".to_string()),
(&b_plus_e, "b+e".to_string()),
],
// expected
vec![
// [a_new ASC, d_new ASC, b+e ASC]
vec![
("a_new", option_asc),
("d_new", option_asc),
("b+e", option_asc),
],
// [d_new ASC, a_new ASC, b+e ASC]
vec![
("d_new", option_asc),
("a_new", option_asc),
("b+e", option_asc),
],
// [c_new ASC, d_new ASC, b+e ASC]
vec![
("c_new", option_asc),
("d_new", option_asc),
("b+e", option_asc),
],
// [d_new ASC, c_new ASC, b+e ASC]
vec![
("d_new", option_asc),
("c_new", option_asc),
("b+e", option_asc),
],
],
),
// ------- TEST CASE 15 ----------
(
// orderings
vec![
// [a ASC, c ASC, b ASC]
vec![
(col_a, option_asc),
(col_c, option_asc),
(col_b, option_asc),
],
],
// proj exprs
vec![
(col_c, "c_new".to_string()),
(col_a, "a_new".to_string()),
(&a_plus_b, "a+b".to_string()),
],
// expected
vec![
// [a_new ASC, d_new ASC, b+e ASC]
vec![
("a_new", option_asc),
("c_new", option_asc),
("a+b", option_asc),
],
],
),
// ------- TEST CASE 16 ----------
(
// orderings
vec![
// [a ASC, b ASC]
vec![(col_a, option_asc), (col_b, option_asc)],
// [c ASC, b DESC]
vec![(col_c, option_asc), (col_b, option_desc)],
// [e ASC]
vec![(col_e, option_asc)],
],
// proj exprs
vec![
(col_c, "c_new".to_string()),
(col_a, "a_new".to_string()),
(col_b, "b_new".to_string()),
(&b_plus_e, "b+e".to_string()),
],
// expected
vec![
// [a_new ASC, b_new ASC]
vec![("a_new", option_asc), ("b_new", option_asc)],
// [a_new ASC, b_new ASC]
vec![("a_new", option_asc), ("b+e", option_asc)],
// [c_new ASC, b_new DESC]
vec![("c_new", option_asc), ("b_new", option_desc)],
],
),
];
for (idx, (orderings, proj_exprs, expected)) in test_cases.into_iter().enumerate()
{
let mut eq_properties = EquivalenceProperties::new(Arc::clone(&schema));
let orderings = convert_to_orderings(&orderings);
eq_properties.add_orderings(orderings);
let proj_exprs = proj_exprs
.into_iter()
.map(|(expr, name)| (Arc::clone(expr), name));
let projection_mapping = ProjectionMapping::try_new(proj_exprs, &schema)?;
let output_schema = output_schema(&projection_mapping, &schema)?;
let expected = expected
.into_iter()
.map(|ordering| {
ordering
.into_iter()
.map(|(name, options)| {
(col(name, &output_schema).unwrap(), options)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>();
let expected = convert_to_orderings(&expected);
let projected_eq = eq_properties.project(&projection_mapping, output_schema);
let orderings = projected_eq.oeq_class();
let err_msg = format!(
"test_idx: {idx:?}, actual: {orderings:?}, expected: {expected:?}, projection_mapping: {projection_mapping:?}"
);
assert_eq!(orderings.len(), expected.len(), "{err_msg}");
for expected_ordering in &expected {
assert!(orderings.contains(expected_ordering), "{}", err_msg)
}
}
Ok(())
}
#[test]
fn project_orderings2() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("b", DataType::Int32, true),
Field::new("c", DataType::Int32, true),
Field::new("d", DataType::Int32, true),
Field::new("ts", DataType::Timestamp(TimeUnit::Nanosecond, None), true),
]));
let col_a = &col("a", &schema)?;
let col_b = &col("b", &schema)?;
let col_c = &col("c", &schema)?;
let col_ts = &col("ts", &schema)?;
let a_plus_b = Arc::new(BinaryExpr::new(
Arc::clone(col_a),
Operator::Plus,
Arc::clone(col_b),
)) as Arc<dyn PhysicalExpr>;
let test_fun = Arc::new(ScalarUDF::new_from_impl(TestScalarUDF::new()));
let round_c = Arc::new(ScalarFunctionExpr::try_new(
test_fun,
vec![Arc::clone(col_c)],
&schema,
Arc::new(ConfigOptions::default()),
)?) as PhysicalExprRef;
let option_asc = SortOptions {
descending: false,
nulls_first: false,
};
let proj_exprs = vec![
(col_b, "b_new".to_string()),
(col_a, "a_new".to_string()),
(col_c, "c_new".to_string()),
(&round_c, "round_c_res".to_string()),
];
let proj_exprs = proj_exprs
.into_iter()
.map(|(expr, name)| (Arc::clone(expr), name));
let projection_mapping = ProjectionMapping::try_new(proj_exprs, &schema)?;
let output_schema = output_schema(&projection_mapping, &schema)?;
let col_a_new = &col("a_new", &output_schema)?;
let col_b_new = &col("b_new", &output_schema)?;
let col_c_new = &col("c_new", &output_schema)?;
let col_round_c_res = &col("round_c_res", &output_schema)?;
let a_new_plus_b_new = Arc::new(BinaryExpr::new(
Arc::clone(col_a_new),
Operator::Plus,
Arc::clone(col_b_new),
)) as Arc<dyn PhysicalExpr>;
let test_cases = [
// ---------- TEST CASE 1 ------------
(
// orderings
vec![
// [a ASC]
vec![(col_a, option_asc)],
],
// expected
vec![
// [b_new ASC]
vec![(col_a_new, option_asc)],
],
),
// ---------- TEST CASE 2 ------------
(
// orderings
vec![
// [a+b ASC]
vec![(&a_plus_b, option_asc)],
],
// expected
vec![
// [b_new ASC]
vec![(&a_new_plus_b_new, option_asc)],
],
),
// ---------- TEST CASE 3 ------------
(
// orderings
vec![
// [a ASC, ts ASC]
vec![(col_a, option_asc), (col_ts, option_asc)],
],
// expected
vec![
// [a_new ASC, date_bin_res ASC]
vec![(col_a_new, option_asc)],
],
),
// ---------- TEST CASE 4 ------------
(
// orderings
vec![
// [a ASC, ts ASC, b ASC]
vec![
(col_a, option_asc),
(col_ts, option_asc),
(col_b, option_asc),
],
],
// expected
vec![
// [a_new ASC, date_bin_res ASC]
vec![(col_a_new, option_asc)],
],
),
// ---------- TEST CASE 5 ------------
(
// orderings
vec![
// [a ASC, c ASC]
vec![(col_a, option_asc), (col_c, option_asc)],
],
// expected
vec![
// [a_new ASC, round_c_res ASC, c_new ASC]
vec![(col_a_new, option_asc), (col_round_c_res, option_asc)],
// [a_new ASC, c_new ASC]
vec![(col_a_new, option_asc), (col_c_new, option_asc)],
],
),
// ---------- TEST CASE 6 ------------
(
// orderings
vec![
// [c ASC, b ASC]
vec![(col_c, option_asc), (col_b, option_asc)],
],
// expected
vec![
// [round_c_res ASC]
vec![(col_round_c_res, option_asc)],
// [c_new ASC, b_new ASC]
vec![(col_c_new, option_asc), (col_b_new, option_asc)],
],
),
// ---------- TEST CASE 7 ------------
(
// orderings
vec![
// [a+b ASC, c ASC]
vec![(&a_plus_b, option_asc), (col_c, option_asc)],
],
// expected
vec![
// [a+b ASC, round(c) ASC, c_new ASC]
vec![
(&a_new_plus_b_new, option_asc),
(col_round_c_res, option_asc),
],
// [a+b ASC, c_new ASC]
vec![(&a_new_plus_b_new, option_asc), (col_c_new, option_asc)],
],
),
];
for (idx, (orderings, expected)) in test_cases.iter().enumerate() {
let mut eq_properties = EquivalenceProperties::new(Arc::clone(&schema));
let orderings = convert_to_orderings(orderings);
eq_properties.add_orderings(orderings);
let expected = convert_to_orderings(expected);
let projected_eq =
eq_properties.project(&projection_mapping, Arc::clone(&output_schema));
let orderings = projected_eq.oeq_class();
let err_msg = format!(
"test idx: {idx:?}, actual: {orderings:?}, expected: {expected:?}, projection_mapping: {projection_mapping:?}"
);
assert_eq!(orderings.len(), expected.len(), "{err_msg}");
for expected_ordering in &expected {
assert!(orderings.contains(expected_ordering), "{}", err_msg)
}
}
Ok(())
}
#[test]
fn project_orderings3() -> Result<()> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::Int32, true),
Field::new("b", DataType::Int32, true),
Field::new("c", DataType::Int32, true),
Field::new("d", DataType::Int32, true),
Field::new("e", DataType::Int32, true),
Field::new("f", DataType::Int32, true),
]));
let col_a = &col("a", &schema)?;
let col_b = &col("b", &schema)?;
let col_c = &col("c", &schema)?;
let col_d = &col("d", &schema)?;
let col_e = &col("e", &schema)?;
let col_f = &col("f", &schema)?;
let a_plus_b = Arc::new(BinaryExpr::new(
Arc::clone(col_a),
Operator::Plus,
Arc::clone(col_b),
)) as Arc<dyn PhysicalExpr>;
let option_asc = SortOptions {
descending: false,
nulls_first: false,
};
let proj_exprs = vec![
(col_c, "c_new".to_string()),
(col_d, "d_new".to_string()),
(&a_plus_b, "a+b".to_string()),
];
let proj_exprs = proj_exprs
.into_iter()
.map(|(expr, name)| (Arc::clone(expr), name));
let projection_mapping = ProjectionMapping::try_new(proj_exprs, &schema)?;
let output_schema = output_schema(&projection_mapping, &schema)?;
let col_a_plus_b_new = &col("a+b", &output_schema)?;
let col_c_new = &col("c_new", &output_schema)?;
let col_d_new = &col("d_new", &output_schema)?;
let test_cases = vec![
// ---------- TEST CASE 1 ------------
(
// orderings
vec![
// [d ASC, b ASC]
vec![(col_d, option_asc), (col_b, option_asc)],
// [c ASC, a ASC]
vec![(col_c, option_asc), (col_a, option_asc)],
],
// equal conditions
vec![],
// expected
vec![
// [d_new ASC, c_new ASC, a+b ASC]
vec![
(col_d_new, option_asc),
(col_c_new, option_asc),
(col_a_plus_b_new, option_asc),
],
// [c_new ASC, d_new ASC, a+b ASC]
vec![
(col_c_new, option_asc),
(col_d_new, option_asc),
(col_a_plus_b_new, option_asc),
],
],
),
// ---------- TEST CASE 2 ------------
(
// orderings
vec![
// [d ASC, b ASC]
vec![(col_d, option_asc), (col_b, option_asc)],
// [c ASC, e ASC], Please note that a=e
vec![(col_c, option_asc), (col_e, option_asc)],
],
// equal conditions
vec![(col_e, col_a)],
// expected
vec![
// [d_new ASC, c_new ASC, a+b ASC]
vec![
(col_d_new, option_asc),
(col_c_new, option_asc),
(col_a_plus_b_new, option_asc),
],
// [c_new ASC, d_new ASC, a+b ASC]
vec![
(col_c_new, option_asc),
(col_d_new, option_asc),
(col_a_plus_b_new, option_asc),
],
],
),
// ---------- TEST CASE 3 ------------
(
// orderings
vec![
// [d ASC, b ASC]
vec![(col_d, option_asc), (col_b, option_asc)],
// [c ASC, e ASC], Please note that a=f
vec![(col_c, option_asc), (col_e, option_asc)],
],
// equal conditions
vec![(col_a, col_f)],
// expected
vec![
// [d_new ASC]
vec![(col_d_new, option_asc)],
// [c_new ASC]
vec![(col_c_new, option_asc)],
],
),
];
for (orderings, equal_columns, expected) in test_cases {
let mut eq_properties = EquivalenceProperties::new(Arc::clone(&schema));
for (lhs, rhs) in equal_columns {
eq_properties.add_equal_conditions(Arc::clone(lhs), Arc::clone(rhs))?;
}
let orderings = convert_to_orderings(&orderings);
eq_properties.add_orderings(orderings);
let expected = convert_to_orderings(&expected);
let projected_eq =
eq_properties.project(&projection_mapping, Arc::clone(&output_schema));
let orderings = projected_eq.oeq_class();
let err_msg = format!(
"actual: {orderings:?}, expected: {expected:?}, projection_mapping: {projection_mapping:?}"
);
assert_eq!(orderings.len(), expected.len(), "{err_msg}");
for expected_ordering in &expected {
assert!(orderings.contains(expected_ordering), "{}", err_msg)
}
}
Ok(())
}
fn get_stats() -> Statistics {
Statistics {
num_rows: Precision::Exact(5),
total_byte_size: Precision::Exact(23),
column_statistics: vec![
ColumnStatistics {
distinct_count: Precision::Exact(5),
max_value: Precision::Exact(ScalarValue::Int64(Some(21))),
min_value: Precision::Exact(ScalarValue::Int64(Some(-4))),
sum_value: Precision::Exact(ScalarValue::Int64(Some(42))),
null_count: Precision::Exact(0),
},
ColumnStatistics {
distinct_count: Precision::Exact(1),
max_value: Precision::Exact(ScalarValue::from("x")),
min_value: Precision::Exact(ScalarValue::from("a")),
sum_value: Precision::Absent,
null_count: Precision::Exact(3),
},
ColumnStatistics {
distinct_count: Precision::Absent,
max_value: Precision::Exact(ScalarValue::Float32(Some(1.1))),
min_value: Precision::Exact(ScalarValue::Float32(Some(0.1))),
sum_value: Precision::Exact(ScalarValue::Float32(Some(5.5))),
null_count: Precision::Absent,
},
],
}
}
fn get_schema() -> Schema {
let field_0 = Field::new("col0", DataType::Int64, false);
let field_1 = Field::new("col1", DataType::Utf8, false);
let field_2 = Field::new("col2", DataType::Float32, false);
Schema::new(vec![field_0, field_1, field_2])
}
#[test]
fn test_stats_projection_columns_only() {
let source = get_stats();
let schema = get_schema();
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("col1", 1)),
alias: "col1".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("col0", 0)),
alias: "col0".to_string(),
},
]);
let result = projection.project_statistics(source, &schema).unwrap();
let expected = Statistics {
num_rows: Precision::Exact(5),
total_byte_size: Precision::Exact(23),
column_statistics: vec![
ColumnStatistics {
distinct_count: Precision::Exact(1),
max_value: Precision::Exact(ScalarValue::from("x")),
min_value: Precision::Exact(ScalarValue::from("a")),
sum_value: Precision::Absent,
null_count: Precision::Exact(3),
},
ColumnStatistics {
distinct_count: Precision::Exact(5),
max_value: Precision::Exact(ScalarValue::Int64(Some(21))),
min_value: Precision::Exact(ScalarValue::Int64(Some(-4))),
sum_value: Precision::Exact(ScalarValue::Int64(Some(42))),
null_count: Precision::Exact(0),
},
],
};
assert_eq!(result, expected);
}
#[test]
fn test_stats_projection_column_with_primitive_width_only() {
let source = get_stats();
let schema = get_schema();
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("col2", 2)),
alias: "col2".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("col0", 0)),
alias: "col0".to_string(),
},
]);
let result = projection.project_statistics(source, &schema).unwrap();
let expected = Statistics {
num_rows: Precision::Exact(5),
total_byte_size: Precision::Exact(60),
column_statistics: vec![
ColumnStatistics {
distinct_count: Precision::Absent,
max_value: Precision::Exact(ScalarValue::Float32(Some(1.1))),
min_value: Precision::Exact(ScalarValue::Float32(Some(0.1))),
sum_value: Precision::Exact(ScalarValue::Float32(Some(5.5))),
null_count: Precision::Absent,
},
ColumnStatistics {
distinct_count: Precision::Exact(5),
max_value: Precision::Exact(ScalarValue::Int64(Some(21))),
min_value: Precision::Exact(ScalarValue::Int64(Some(-4))),
sum_value: Precision::Exact(ScalarValue::Int64(Some(42))),
null_count: Precision::Exact(0),
},
],
};
assert_eq!(result, expected);
}
// Tests for Projection struct
#[test]
fn test_projection_new() -> Result<()> {
let exprs = vec![
ProjectionExpr {
expr: Arc::new(Column::new("a", 0)),
alias: "a".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("b", 1)),
alias: "b".to_string(),
},
];
let projection = ProjectionExprs::new(exprs.clone());
assert_eq!(projection.as_ref().len(), 2);
Ok(())
}
#[test]
fn test_projection_from_vec() -> Result<()> {
let exprs = vec![ProjectionExpr {
expr: Arc::new(Column::new("x", 0)),
alias: "x".to_string(),
}];
let projection: ProjectionExprs = exprs.clone().into();
assert_eq!(projection.as_ref().len(), 1);
Ok(())
}
#[test]
fn test_projection_as_ref() -> Result<()> {
let exprs = vec![
ProjectionExpr {
expr: Arc::new(Column::new("col1", 0)),
alias: "col1".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("col2", 1)),
alias: "col2".to_string(),
},
];
let projection = ProjectionExprs::new(exprs);
let as_ref: &[ProjectionExpr] = projection.as_ref();
assert_eq!(as_ref.len(), 2);
Ok(())
}
#[test]
fn test_column_indices_multiple_columns() -> Result<()> {
// Test with reversed column order to ensure proper reordering
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("c", 5)),
alias: "c".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("b", 2)),
alias: "b".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("a", 0)),
alias: "a".to_string(),
},
]);
// Should return sorted indices regardless of projection order
assert_eq!(projection.column_indices(), vec![0, 2, 5]);
Ok(())
}
#[test]
fn test_column_indices_duplicates() -> Result<()> {
// Test that duplicate column indices appear only once
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("a", 1)),
alias: "a".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("b", 3)),
alias: "b".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("a2", 1)), // duplicate index
alias: "a2".to_string(),
},
]);
assert_eq!(projection.column_indices(), vec![1, 3]);
Ok(())
}
#[test]
fn test_column_indices_unsorted() -> Result<()> {
// Test that column indices are sorted in the output
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("c", 5)),
alias: "c".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("a", 1)),
alias: "a".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("b", 3)),
alias: "b".to_string(),
},
]);
assert_eq!(projection.column_indices(), vec![1, 3, 5]);
Ok(())
}
#[test]
fn test_column_indices_complex_expr() -> Result<()> {
// Test with complex expressions containing multiple columns
let expr = Arc::new(BinaryExpr::new(
Arc::new(Column::new("a", 1)),
Operator::Plus,
Arc::new(Column::new("b", 4)),
));
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr,
alias: "sum".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("c", 2)),
alias: "c".to_string(),
},
]);
// Should return [1, 2, 4] - all columns used, sorted and deduplicated
assert_eq!(projection.column_indices(), vec![1, 2, 4]);
Ok(())
}
#[test]
fn test_column_indices_empty() -> Result<()> {
let projection = ProjectionExprs::new(vec![]);
assert_eq!(projection.column_indices(), Vec::<usize>::new());
Ok(())
}
#[test]
fn test_merge_simple_columns() -> Result<()> {
// First projection: SELECT c@2 AS x, b@1 AS y, a@0 AS z
let base_projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("c", 2)),
alias: "x".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("b", 1)),
alias: "y".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("a", 0)),
alias: "z".to_string(),
},
]);
// Second projection: SELECT y@1 AS col2, x@0 AS col1
let top_projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("y", 1)),
alias: "col2".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("x", 0)),
alias: "col1".to_string(),
},
]);
// Merge should produce: SELECT b@1 AS col2, c@2 AS col1
let merged = base_projection.try_merge(&top_projection)?;
assert_snapshot!(format!("{merged}"), @"Projection[b@1 AS col2, c@2 AS col1]");
Ok(())
}
#[test]
fn test_merge_with_expressions() -> Result<()> {
// First projection: SELECT c@2 AS x, b@1 AS y, a@0 AS z
let base_projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("c", 2)),
alias: "x".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("b", 1)),
alias: "y".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("a", 0)),
alias: "z".to_string(),
},
]);
// Second projection: SELECT y@1 + z@2 AS c2, x@0 + 1 AS c1
let top_projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(BinaryExpr::new(
Arc::new(Column::new("y", 1)),
Operator::Plus,
Arc::new(Column::new("z", 2)),
)),
alias: "c2".to_string(),
},
ProjectionExpr {
expr: Arc::new(BinaryExpr::new(
Arc::new(Column::new("x", 0)),
Operator::Plus,
Arc::new(Literal::new(ScalarValue::Int32(Some(1)))),
)),
alias: "c1".to_string(),
},
]);
// Merge should produce: SELECT b@1 + a@0 AS c2, c@2 + 1 AS c1
let merged = base_projection.try_merge(&top_projection)?;
assert_snapshot!(format!("{merged}"), @"Projection[b@1 + a@0 AS c2, c@2 + 1 AS c1]");
Ok(())
}
#[test]
fn try_merge_error() {
// Create a base projection
let base = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("a", 0)),
alias: "x".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("b", 1)),
alias: "y".to_string(),
},
]);
// Create a top projection that references a non-existent column index
let top = ProjectionExprs::new(vec![ProjectionExpr {
expr: Arc::new(Column::new("z", 5)), // Invalid index
alias: "result".to_string(),
}]);
// Attempt to merge and expect an error
let err_msg = base.try_merge(&top).unwrap_err().to_string();
assert!(
err_msg.contains("Internal error: Column index 5 out of bounds for projected expressions of length 2"),
"Unexpected error message: {err_msg}",
);
}
#[test]
fn test_project_schema_simple_columns() -> Result<()> {
// Input schema: [col0: Int64, col1: Utf8, col2: Float32]
let input_schema = get_schema();
// Projection: SELECT col2 AS c, col0 AS a
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("col2", 2)),
alias: "c".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("col0", 0)),
alias: "a".to_string(),
},
]);
let output_schema = projection.project_schema(&input_schema)?;
// Should have 2 fields
assert_eq!(output_schema.fields().len(), 2);
// First field should be "c" with Float32 type
assert_eq!(output_schema.field(0).name(), "c");
assert_eq!(output_schema.field(0).data_type(), &DataType::Float32);
// Second field should be "a" with Int64 type
assert_eq!(output_schema.field(1).name(), "a");
assert_eq!(output_schema.field(1).data_type(), &DataType::Int64);
Ok(())
}
#[test]
fn test_project_schema_with_expressions() -> Result<()> {
// Input schema: [col0: Int64, col1: Utf8, col2: Float32]
let input_schema = get_schema();
// Projection: SELECT col0 + 1 AS incremented
let projection = ProjectionExprs::new(vec![ProjectionExpr {
expr: Arc::new(BinaryExpr::new(
Arc::new(Column::new("col0", 0)),
Operator::Plus,
Arc::new(Literal::new(ScalarValue::Int64(Some(1)))),
)),
alias: "incremented".to_string(),
}]);
let output_schema = projection.project_schema(&input_schema)?;
// Should have 1 field
assert_eq!(output_schema.fields().len(), 1);
// Field should be "incremented" with Int64 type
assert_eq!(output_schema.field(0).name(), "incremented");
assert_eq!(output_schema.field(0).data_type(), &DataType::Int64);
Ok(())
}
#[test]
fn test_project_schema_preserves_metadata() -> Result<()> {
// Create schema with metadata
let mut metadata = HashMap::new();
metadata.insert("key".to_string(), "value".to_string());
let field_with_metadata =
Field::new("col0", DataType::Int64, false).with_metadata(metadata.clone());
let input_schema = Schema::new(vec![
field_with_metadata,
Field::new("col1", DataType::Utf8, false),
]);
// Projection: SELECT col0 AS renamed
let projection = ProjectionExprs::new(vec![ProjectionExpr {
expr: Arc::new(Column::new("col0", 0)),
alias: "renamed".to_string(),
}]);
let output_schema = projection.project_schema(&input_schema)?;
// Should have 1 field
assert_eq!(output_schema.fields().len(), 1);
// Field should be "renamed" with metadata preserved
assert_eq!(output_schema.field(0).name(), "renamed");
assert_eq!(output_schema.field(0).metadata(), &metadata);
Ok(())
}
#[test]
fn test_project_schema_empty() -> Result<()> {
let input_schema = get_schema();
let projection = ProjectionExprs::new(vec![]);
let output_schema = projection.project_schema(&input_schema)?;
assert_eq!(output_schema.fields().len(), 0);
Ok(())
}
#[test]
fn test_project_statistics_columns_only() -> Result<()> {
let input_stats = get_stats();
let input_schema = get_schema();
// Projection: SELECT col1 AS text, col0 AS num
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("col1", 1)),
alias: "text".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("col0", 0)),
alias: "num".to_string(),
},
]);
let output_stats = projection.project_statistics(input_stats, &input_schema)?;
// Row count should be preserved
assert_eq!(output_stats.num_rows, Precision::Exact(5));
// Should have 2 column statistics (reordered from input)
assert_eq!(output_stats.column_statistics.len(), 2);
// First column (col1 from input)
assert_eq!(
output_stats.column_statistics[0].distinct_count,
Precision::Exact(1)
);
assert_eq!(
output_stats.column_statistics[0].max_value,
Precision::Exact(ScalarValue::from("x"))
);
// Second column (col0 from input)
assert_eq!(
output_stats.column_statistics[1].distinct_count,
Precision::Exact(5)
);
assert_eq!(
output_stats.column_statistics[1].max_value,
Precision::Exact(ScalarValue::Int64(Some(21)))
);
Ok(())
}
#[test]
fn test_project_statistics_with_expressions() -> Result<()> {
let input_stats = get_stats();
let input_schema = get_schema();
// Projection with expression: SELECT col0 + 1 AS incremented, col1 AS text
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(BinaryExpr::new(
Arc::new(Column::new("col0", 0)),
Operator::Plus,
Arc::new(Literal::new(ScalarValue::Int64(Some(1)))),
)),
alias: "incremented".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("col1", 1)),
alias: "text".to_string(),
},
]);
let output_stats = projection.project_statistics(input_stats, &input_schema)?;
// Row count should be preserved
assert_eq!(output_stats.num_rows, Precision::Exact(5));
// Should have 2 column statistics
assert_eq!(output_stats.column_statistics.len(), 2);
// First column (expression) should have unknown statistics
assert_eq!(
output_stats.column_statistics[0].distinct_count,
Precision::Absent
);
assert_eq!(
output_stats.column_statistics[0].max_value,
Precision::Absent
);
// Second column (col1) should preserve statistics
assert_eq!(
output_stats.column_statistics[1].distinct_count,
Precision::Exact(1)
);
Ok(())
}
#[test]
fn test_project_statistics_primitive_width_only() -> Result<()> {
let input_stats = get_stats();
let input_schema = get_schema();
// Projection with only primitive width columns: SELECT col2 AS f, col0 AS i
let projection = ProjectionExprs::new(vec![
ProjectionExpr {
expr: Arc::new(Column::new("col2", 2)),
alias: "f".to_string(),
},
ProjectionExpr {
expr: Arc::new(Column::new("col0", 0)),
alias: "i".to_string(),
},
]);
let output_stats = projection.project_statistics(input_stats, &input_schema)?;
// Row count should be preserved
assert_eq!(output_stats.num_rows, Precision::Exact(5));
// Total byte size should be recalculated for primitive types
// Float32 (4 bytes) + Int64 (8 bytes) = 12 bytes per row, 5 rows = 60 bytes
assert_eq!(output_stats.total_byte_size, Precision::Exact(60));
// Should have 2 column statistics
assert_eq!(output_stats.column_statistics.len(), 2);
Ok(())
}
#[test]
fn test_project_statistics_empty() -> Result<()> {
let input_stats = get_stats();
let input_schema = get_schema();
let projection = ProjectionExprs::new(vec![]);
let output_stats = projection.project_statistics(input_stats, &input_schema)?;
// Row count should be preserved
assert_eq!(output_stats.num_rows, Precision::Exact(5));
// Should have no column statistics
assert_eq!(output_stats.column_statistics.len(), 0);
// Total byte size should be 0 for empty projection
assert_eq!(output_stats.total_byte_size, Precision::Exact(0));
Ok(())
}
}