blob: b128b165093af5a401f4cf8aed229dff76f51987 [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 crate::{Cast, EvalMode, IfExpr, NormalizeNaNAndZero, SparkCastOptions};
use arrow::array::{
downcast_primitive, Array, ArrayRef, AsArray, BooleanArray, GenericByteArray, PrimitiveArray,
};
use arrow::buffer::{BooleanBuffer, NullBuffer, OffsetBuffer, ScalarBuffer};
use arrow::compute::nullif;
use arrow::datatypes::{
ArrowNativeType, ArrowPrimitiveType, BinaryType, ByteArrayType, DataType, LargeBinaryType,
LargeUtf8Type, Schema, Utf8Type,
};
use arrow::record_batch::RecordBatch;
use datafusion::common::cast::as_boolean_array;
use datafusion::common::{
exec_err, internal_datafusion_err, internal_err, DataFusionError, Result, ScalarValue,
};
use datafusion::logical_expr::type_coercion::binary::type_union_coercion;
use datafusion::logical_expr::type_coercion::other::get_coerce_type_for_case_expression;
use datafusion::logical_expr::{ColumnarValue, Operator};
use datafusion::physical_expr::expressions::{
BinaryExpr, CaseExpr, Column, IsNotNullExpr, IsNullExpr, Literal, NotExpr,
};
use datafusion::physical_expr::PhysicalExpr;
use std::fmt::{Display, Formatter};
use std::hash::Hash;
use std::sync::{Arc, OnceLock};
type WhenThen = (Arc<dyn PhysicalExpr>, Arc<dyn PhysicalExpr>);
/// Creates a `CASE WHEN`, casting any branch value whose type differs from the others to their
/// common type.
pub fn create_case_when(
when_then: Vec<WhenThen>,
else_expr: Option<Arc<dyn PhysicalExpr>>,
input_schema: &Schema,
) -> Result<Arc<dyn PhysicalExpr>> {
let then_types = when_then
.iter()
.map(|(_, then)| then.data_type(input_schema))
.collect::<Result<Vec<_>>>()?;
let else_type = else_expr
.as_ref()
.map(|e| e.data_type(input_schema))
.transpose()?
.unwrap_or(DataType::Null);
let Some(coerce_type) = get_coerce_type_for_case_expression(&then_types, Some(&else_type))
else {
return Ok(Arc::new(CaseWhenExpr::try_new(when_then, else_expr)?));
};
let when_then = when_then
.into_iter()
.zip(&then_types)
.map(|((when, then), then_type)| (when, coerce_branch(then, then_type, &coerce_type)))
.collect();
let else_expr = else_expr.map(|e| coerce_branch(e, &else_type, &coerce_type));
Ok(Arc::new(CaseWhenExpr::try_new(when_then, else_expr)?))
}
/// Creates an `IF`, casting a branch whose type differs from the other's to their common type.
///
/// Spark adds no cast when the branches differ only in whether a nested field can be NULL, or in
/// the case of a struct field name. But [`IfExpr`] reports the THEN branch's type, and returns the
/// ELSE branch's array unchanged when no row of a batch takes the THEN branch.
pub fn create_if_expr(
if_expr: Arc<dyn PhysicalExpr>,
true_expr: Arc<dyn PhysicalExpr>,
false_expr: Arc<dyn PhysicalExpr>,
input_schema: &Schema,
) -> Result<Arc<dyn PhysicalExpr>> {
let true_type = true_expr.data_type(input_schema)?;
let false_type = false_expr.data_type(input_schema)?;
let Some(common_type) = if_common_type(&true_type, &false_type) else {
return Ok(Arc::new(IfExpr::new(if_expr, true_expr, false_expr)));
};
Ok(Arc::new(IfExpr::new(
if_expr,
coerce_branch(true_expr, &true_type, &common_type),
coerce_branch(false_expr, &false_type, &common_type),
)))
}
/// Reconciles Spark IF branches positionally, retaining THEN names and merging nullability.
/// Spark has already coerced the branches to the same SQL type. DataFusion's struct union may
/// instead match by name, pairing different positions when names differ only in case.
fn if_common_type(then_type: &DataType, else_type: &DataType) -> Option<DataType> {
use arrow::datatypes::FieldRef;
fn field(then_field: &FieldRef, else_field: &FieldRef) -> Option<FieldRef> {
Some(Arc::new(
then_field
.as_ref()
.clone()
.with_data_type(if_common_type(
then_field.data_type(),
else_field.data_type(),
)?)
.with_nullable(then_field.is_nullable() || else_field.is_nullable()),
))
}
match (then_type, else_type) {
(DataType::Struct(then_fields), DataType::Struct(else_fields)) => {
if then_fields.len() != else_fields.len() {
return None;
}
Some(DataType::Struct(
then_fields
.iter()
.zip(else_fields)
.map(|(t, e)| field(t, e))
.collect::<Option<Vec<_>>>()?
.into(),
))
}
(DataType::List(t), DataType::List(e)) => Some(DataType::List(field(t, e)?)),
(DataType::Map(t, t_sorted), DataType::Map(e, e_sorted)) => {
Some(DataType::Map(field(t, e)?, *t_sorted && *e_sorted))
}
_ => type_union_coercion(then_type, else_type),
}
}
/// Casts a CASE WHEN or IF branch whose type is `data_type` to the branches' `common_type`.
///
/// The branches share a Spark type, so any difference is in the Arrow representation. For a
/// timestamp that is the timezone label, and the cast only relabels it, but Comet's cast still
/// needs a timezone. Every `TimestampType` value in a native plan is labelled UTC.
fn coerce_branch(
expr: Arc<dyn PhysicalExpr>,
data_type: &DataType,
common_type: &DataType,
) -> Arc<dyn PhysicalExpr> {
// A branch that already has the common type is not wrapped in a cast, which would do nothing
// but hide what the branch is from the evaluation.
if data_type == common_type {
return expr;
}
let cast_options = SparkCastOptions::new(EvalMode::Legacy, "UTC", false);
Arc::new(Cast::new(
expr,
common_type.clone(),
cast_options,
None,
None,
))
}
/// Spark's `CASE WHEN`, which Comet also uses for `IF` and `COALESCE`.
///
/// Spark evaluates a WHEN only for the rows that no earlier WHEN matched, and a branch's value
/// only for the rows that choose that branch. DataFusion's [`CaseExpr`] does the same, by filtering
/// the batch down to those rows for every branch and merging the partial results. That is needed
/// when an expression could fail for a row Spark would not evaluate it for, such as a division by
/// a zero that the WHEN rules out.
///
/// When none of them can fail, evaluating them over the whole batch returns the same values
/// without the filters and the merge. This does that and then takes each row's value from the
/// branch the row chose. It uses [`CaseExpr`] otherwise.
#[derive(Debug)]
pub struct CaseWhenExpr {
/// Holds the branches, and evaluates each one for just the rows that choose it
lazy: CaseExpr,
/// The branches with a CASE or IF in the ELSE spliced in, as they are evaluated eagerly.
/// `IF(a, x, IF(b, y, z))` is `CASE WHEN a THEN x WHEN b THEN y ELSE z END`, and merging
/// the branches once is cheaper than merging each level into the next.
flat_when_then: Vec<WhenThen>,
flat_else_expr: Option<Arc<dyn PhysicalExpr>>,
/// The result type if every branch is evaluated over the whole batch, or `None` if they are
/// evaluated lazily, decided on the first batch
eager: OnceLock<Option<DataType>>,
}
impl CaseWhenExpr {
pub fn try_new(
when_then: Vec<WhenThen>,
else_expr: Option<Arc<dyn PhysicalExpr>>,
) -> Result<Self> {
let lazy = CaseExpr::try_new(None, when_then, else_expr)?;
let nested = lazy.else_expr().and_then(|e| {
e.downcast_ref::<CaseWhenExpr>()
.or_else(|| e.downcast_ref::<IfExpr>().map(IfExpr::case_when))
});
let (flat_when_then, flat_else_expr) = match nested {
Some(nested) => (
lazy.when_then_expr()
.iter()
.chain(&nested.flat_when_then)
.cloned()
.collect(),
nested.flat_else_expr.clone(),
),
None => (lazy.when_then_expr().to_vec(), lazy.else_expr().cloned()),
};
Ok(Self {
lazy,
flat_when_then,
flat_else_expr,
eager: OnceLock::new(),
})
}
/// The branch values, THEN and ELSE, in the order they are evaluated eagerly.
fn values(&self) -> impl Iterator<Item = &Arc<dyn PhysicalExpr>> {
self.flat_when_then
.iter()
.map(|(_, then)| then)
.chain(&self.flat_else_expr)
}
/// The result type, if every expression that Spark evaluates for only some of the rows can
/// be evaluated for all of them and the result is a type that [`merge`] handles.
fn eager_result_type(&self, input_schema: &Schema) -> Option<DataType> {
let data_type = self.data_type(input_schema).ok()?;
// Spark evaluates the first WHEN for every row as well
let later_whens = self.flat_when_then.iter().skip(1).map(|(when, _)| when);
let eager = can_merge(&data_type)
&& self
.values()
.all(|v| v.data_type(input_schema).is_ok_and(|t| t == data_type))
&& self
.values()
.chain(later_whens)
.all(|e| is_infallible(e, input_schema));
eager.then_some(data_type)
}
fn evaluate_eagerly(&self, batch: &RecordBatch, data_type: &DataType) -> Result<ColumnarValue> {
let num_rows = batch.num_rows();
// The rows that no WHEN has matched yet
let mut remaining = BooleanBuffer::new_set(num_rows);
let mut remaining_count = num_rows;
// The rows that choose each branch, and how many, for the branches that some row chooses
let mut chosen: Vec<(BooleanBuffer, usize, &Arc<dyn PhysicalExpr>)> = vec![];
for (when, then) in &self.flat_when_then {
if remaining_count == 0 {
break;
}
let matched = match when.evaluate(batch)? {
ColumnarValue::Array(array) => {
if array.len() != num_rows {
return internal_err!(
"WHEN returned {} rows for a batch of {num_rows}",
array.len()
);
}
let array = as_boolean_array(&array)?;
// A NULL predicate does not match
match array.nulls() {
Some(nulls) => array.values() & nulls.inner(),
None => array.values().clone(),
}
}
ColumnarValue::Scalar(ScalarValue::Boolean(Some(true))) => remaining.clone(),
ColumnarValue::Scalar(ScalarValue::Boolean(_) | ScalarValue::Null) => continue,
ColumnarValue::Scalar(other) => {
return exec_err!("WHEN returned {other:?} rather than a boolean")
}
};
let rows = &remaining & &matched;
let count = rows.count_set_bits();
if count == 0 {
continue;
}
remaining ^= &rows;
remaining_count -= count;
chosen.push((rows, count, then));
}
if remaining_count > 0 {
if let Some(else_expr) = &self.flat_else_expr {
chosen.push((remaining, remaining_count, else_expr));
remaining_count = 0;
}
}
// Every row chooses the same branch, so its value is the result as it is
if let [(_, _, expr)] = chosen.as_slice() {
if remaining_count == 0 {
return expr.evaluate(batch);
}
}
let mut branches = chosen
.into_iter()
.map(|(rows, row_count, expr)| {
Branch::try_new(rows, row_count, expr.evaluate(batch)?, num_rows)
})
.collect::<Result<Vec<_>>>()?;
// The rows of a NULL literal are NULL, as are the rows that choose no branch
branches.retain(|b| !b.is_scalar || b.values.is_valid(0));
if branches.is_empty() {
return Ok(ColumnarValue::Scalar(ScalarValue::try_new_null(data_type)?));
}
merge(data_type, num_rows, &branches).map(ColumnarValue::Array)
}
}
impl Hash for CaseWhenExpr {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.lazy.hash(state);
}
}
impl PartialEq for CaseWhenExpr {
fn eq(&self, other: &Self) -> bool {
self.lazy == other.lazy
}
}
impl Eq for CaseWhenExpr {}
impl Display for CaseWhenExpr {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.lazy, f)
}
}
impl PhysicalExpr for CaseWhenExpr {
fn data_type(&self, input_schema: &Schema) -> Result<DataType> {
self.lazy.data_type(input_schema)
}
/// Spark's rule: the result can be NULL when a branch's value can, or when there is no ELSE.
fn nullable(&self, input_schema: &Schema) -> Result<bool> {
for (_, then) in self.lazy.when_then_expr() {
if then.nullable(input_schema)? {
return Ok(true);
}
}
match self.lazy.else_expr() {
Some(else_expr) => else_expr.nullable(input_schema),
None => Ok(true),
}
}
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
match self
.eager
.get_or_init(|| self.eager_result_type(&batch.schema()))
{
Some(data_type) => self.evaluate_eagerly(batch, data_type),
None => self.lazy.evaluate(batch),
}
}
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
self.lazy.children()
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> Result<Arc<dyn PhysicalExpr>> {
let expected = self.children().len();
if children.len() != expected {
return internal_err!(
"CaseWhenExpr expects {expected} children, got {}",
children.len()
);
}
let mut children = children.into_iter();
let when_then = (0..self.lazy.when_then_expr().len())
.map(|_| (children.next().unwrap(), children.next().unwrap()))
.collect();
Ok(Arc::new(CaseWhenExpr::try_new(when_then, children.next())?))
}
fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.lazy.fmt_sql(f)
}
}
/// Whether `expr` can be evaluated for rows that Spark would not evaluate it for.
///
/// That holds when it can neither fail nor return something different for seeing more rows: a
/// column, a literal, and comparisons, boolean logic, null checks, widening casts and wrapping
/// arithmetic over them. Anything else, including every function, is assumed to be able to fail.
fn is_infallible(expr: &Arc<dyn PhysicalExpr>, input_schema: &Schema) -> bool {
if expr.is::<Column>() || expr.is::<Literal>() {
return true;
}
let node_is_infallible = if let Some(binary) = expr.downcast_ref::<BinaryExpr>() {
binary_is_infallible(binary, input_schema)
} else if let Some(cast) = expr.downcast_ref::<Cast>() {
cast.is_infallible(input_schema)
} else if let Some(normalize) = expr.downcast_ref::<NormalizeNaNAndZero>() {
normalize.is_infallible()
} else if let Some(not) = expr.downcast_ref::<NotExpr>() {
not.arg()
.data_type(input_schema)
.is_ok_and(|t| t == DataType::Boolean)
} else {
// A nested CASE or IF cannot fail when none of its parts can, however it evaluates them
expr.is::<IsNullExpr>()
|| expr.is::<IsNotNullExpr>()
|| expr.is::<CaseWhenExpr>()
|| expr.is::<IfExpr>()
};
node_is_infallible
&& expr
.children()
.into_iter()
.all(|child| is_infallible(child, input_schema))
}
fn binary_is_infallible(binary: &BinaryExpr, input_schema: &Schema) -> bool {
let (Ok(left), Ok(right)) = (
binary.left().data_type(input_schema),
binary.right().data_type(input_schema),
) else {
return false;
};
match binary.op() {
Operator::Eq
| Operator::NotEq
| Operator::Lt
| Operator::LtEq
| Operator::Gt
| Operator::GtEq
| Operator::IsDistinctFrom
| Operator::IsNotDistinctFrom => left == right && is_comparable(&left),
Operator::And | Operator::Or => left == DataType::Boolean && right == DataType::Boolean,
// Integers wrap on overflow, unless the expression was built to fail instead
Operator::Plus | Operator::Minus | Operator::Multiply => {
left == right && (left.is_integer() || left.is_floating()) && !fails_on_overflow(binary)
}
// Floating point division and remainder return infinity or NaN rather than failing
Operator::Divide | Operator::Modulo => left == right && left.is_floating(),
_ => false,
}
}
/// The types of Spark's scalar values, which DataFusion compares without failing.
fn is_comparable(data_type: &DataType) -> bool {
use DataType::*;
data_type.is_numeric()
|| matches!(
data_type,
Boolean | Utf8 | LargeUtf8 | Binary | LargeBinary | Date32 | Timestamp(_, _)
)
}
/// `BinaryExpr` does not expose whether it fails on overflow, but compares it.
fn fails_on_overflow(binary: &BinaryExpr) -> bool {
let wrapping = BinaryExpr::new(
Arc::clone(binary.left()),
*binary.op(),
Arc::clone(binary.right()),
);
*binary != wrapping
}
macro_rules! supported {
($t:ty) => {
true
};
}
/// Whether [`merge`] handles `data_type`.
fn can_merge(data_type: &DataType) -> bool {
downcast_primitive! {
data_type => (supported),
DataType::Boolean
| DataType::Utf8
| DataType::LargeUtf8
| DataType::Binary
| DataType::LargeBinary => true,
_ => false,
}
}
/// A branch's value, with the rows that choose the branch.
struct Branch {
rows: BooleanBuffer,
row_count: usize,
/// One value per row of the batch, or a single value for a scalar
values: ArrayRef,
is_scalar: bool,
}
impl Branch {
fn try_new(
rows: BooleanBuffer,
row_count: usize,
value: ColumnarValue,
num_rows: usize,
) -> Result<Self> {
let (values, is_scalar) = match value {
ColumnarValue::Array(array) if array.len() == num_rows => (array, false),
ColumnarValue::Array(array) => {
return internal_err!(
"CASE branch returned {} rows for a batch of {num_rows}",
array.len()
)
}
ColumnarValue::Scalar(scalar) => (scalar.to_array_of_size(1)?, true),
};
Ok(Self {
rows,
row_count,
values,
is_scalar,
})
}
fn is_valid_scalar(&self) -> bool {
self.is_scalar && self.values.is_valid(0)
}
/// The error for values that are not the array type the result needs, which a downcast would
/// otherwise panic on.
fn type_mismatch(&self, data_type: &DataType) -> DataFusionError {
internal_datafusion_err!(
"CASE branch returned {} for a {data_type} result",
self.values.data_type()
)
}
}
/// Takes each row's value from the branch that the row chose. No row chooses two branches, and a
/// row that chooses none is NULL.
fn merge(data_type: &DataType, num_rows: usize, branches: &[Branch]) -> Result<ArrayRef> {
// Only the rows of this array are valid, so it is the result once the others are NULL, and
// its buffers can be kept
if let [branch] = branches {
if !branch.is_scalar {
let others = BooleanArray::new(!&branch.rows, None);
return Ok(nullif(&branch.values, &others)?);
}
}
macro_rules! primitive {
($t:ty) => {
merge_primitive::<$t>(data_type, num_rows, branches)
};
}
downcast_primitive! {
data_type => (primitive),
DataType::Boolean => merge_boolean(data_type, num_rows, branches),
DataType::Utf8 => merge_bytes::<Utf8Type>(data_type, num_rows, branches),
DataType::LargeUtf8 => merge_bytes::<LargeUtf8Type>(data_type, num_rows, branches),
DataType::Binary => merge_bytes::<BinaryType>(data_type, num_rows, branches),
DataType::LargeBinary => merge_bytes::<LargeBinaryType>(data_type, num_rows, branches),
_ => internal_err!("CASE WHEN cannot merge {data_type}"),
}
}
fn merge_primitive<T: ArrowPrimitiveType>(
data_type: &DataType,
num_rows: usize,
branches: &[Branch],
) -> Result<ArrayRef> {
let arrays = branches
.iter()
.map(|b| {
b.values
.as_primitive_opt::<T>()
.ok_or_else(|| b.type_mismatch(data_type))
})
.collect::<Result<Vec<_>>>()?;
// Start from the branch that most rows choose, whole, then write the others' rows over it.
// The rows that choose no branch are NULL, so what they hold does not matter.
let base = (0..branches.len())
.max_by_key(|&i| branches[i].row_count)
.unwrap();
let mut values = if branches[base].is_scalar {
vec![arrays[base].value(0); num_rows]
} else {
arrays[base].values().to_vec()
};
for (i, (branch, array)) in branches.iter().zip(&arrays).enumerate() {
if i == base {
continue;
} else if !branch.is_scalar {
copy_rows(&mut values, &branch.rows, array.values());
} else {
fill_rows(&mut values, &branch.rows, array.value(0));
}
}
let nulls = merge_nulls(num_rows, branches);
let array = PrimitiveArray::<T>::new(ScalarBuffer::from(values), nulls);
Ok(Arc::new(array.with_data_type(data_type.clone())))
}
/// Calls `f(start, len, bits)` for each word of `rows`, where bit `i` of `bits` is row
/// `start + i`.
#[inline(always)]
fn for_each_word(rows: &BooleanBuffer, mut f: impl FnMut(usize, usize, u64)) {
let chunks = rows.bit_chunks();
let mut start = 0;
for bits in chunks.iter() {
f(start, 64, bits);
start += 64;
}
if chunks.remainder_len() > 0 {
f(start, chunks.remainder_len(), chunks.remainder_bits());
}
}
/// The bits of a word of `len` rows that are all set.
#[inline(always)]
fn all_set(len: usize) -> u64 {
u64::MAX >> (64 - len)
}
/// Below this many rows in a word, writing just those rows beats blending the whole word.
const SPARSE_WORD: u32 = 12;
/// Copies `source[i]` to `out[i]` for each row `i` in `rows`.
fn copy_rows<T: Copy>(out: &mut [T], rows: &BooleanBuffer, source: &[T]) {
for_each_word(rows, |start, len, mut bits| {
let out = &mut out[start..start + len];
let source = &source[start..start + len];
if bits == all_set(len) {
out.copy_from_slice(source);
} else if bits.count_ones() < SPARSE_WORD {
while bits != 0 {
let i = bits.trailing_zeros() as usize;
out[i] = source[i];
bits &= bits - 1;
}
} else {
for (i, (out, source)) in out.iter_mut().zip(source).enumerate() {
*out = if bits & (1 << i) != 0 { *source } else { *out };
}
}
});
}
/// Sets `out[i]` to `value` for each row `i` in `rows`.
fn fill_rows<T: Copy>(out: &mut [T], rows: &BooleanBuffer, value: T) {
for_each_word(rows, |start, len, mut bits| {
let out = &mut out[start..start + len];
if bits == all_set(len) {
out.fill(value);
} else if bits.count_ones() < SPARSE_WORD {
while bits != 0 {
out[bits.trailing_zeros() as usize] = value;
bits &= bits - 1;
}
} else {
for (i, out) in out.iter_mut().enumerate() {
*out = if bits & (1 << i) != 0 { value } else { *out };
}
}
});
}
fn merge_boolean(data_type: &DataType, num_rows: usize, branches: &[Branch]) -> Result<ArrayRef> {
let mut values = BooleanBuffer::new_unset(num_rows);
for branch in branches {
let array = branch
.values
.as_boolean_opt()
.ok_or_else(|| branch.type_mismatch(data_type))?;
if !branch.is_scalar {
values |= &(&branch.rows & array.values());
} else if branch.is_valid_scalar() && array.value(0) {
values |= &branch.rows;
}
}
Ok(Arc::new(BooleanArray::new(
values,
merge_nulls(num_rows, branches),
)))
}
/// How far past a value [`merge_bytes`] may copy: copying a fixed number of bytes is much cheaper
/// than copying a value's exact length, and the next value overwrites the excess.
const SLACK: usize = 16;
/// Where one branch keeps its values, for [`merge_bytes`].
struct ByteSource<'a, T: ByteArrayType> {
offsets: &'a [T::Offset],
data: &'a [u8],
/// Turns a row into its position in `offsets`: every bit set for an array, none for a scalar
position_mask: usize,
/// A scalar's value
scalar: Option<&'a [u8]>,
/// A scalar's value padded to `SLACK` bytes, when it fits in them
padded_scalar: Option<[u8; SLACK]>,
}
impl<'a, T: ByteArrayType> ByteSource<'a, T> {
fn try_new(branch: &'a Branch, data_type: &DataType) -> Result<Self> {
let array = branch
.values
.as_bytes_opt::<T>()
.ok_or_else(|| branch.type_mismatch(data_type))?;
let scalar: Option<&[u8]> = branch.is_scalar.then(|| array.value(0).as_ref());
let padded_scalar = scalar.filter(|v| v.len() <= SLACK).map(|v| {
let mut padded = [0; SLACK];
padded[..v.len()].copy_from_slice(v);
padded
});
Ok(Self {
offsets: array.value_offsets(),
data: array.value_data(),
position_mask: if branch.is_scalar { 0 } else { usize::MAX },
scalar,
padded_scalar,
})
}
fn value_len(&self, row: usize) -> usize {
let at = row & self.position_mask;
(self.offsets[at + 1] - self.offsets[at]).as_usize()
}
}
fn merge_bytes<T: ByteArrayType>(
data_type: &DataType,
num_rows: usize,
branches: &[Branch],
) -> Result<ArrayRef> {
// The branch that each row takes its value from, or `NONE` when the row is NULL
const NONE: u32 = u32::MAX;
let mut chosen = vec![NONE; num_rows];
for (i, branch) in branches.iter().enumerate() {
fill_rows(&mut chosen, &branch.rows, i as u32);
}
let sources = branches
.iter()
.map(|b| ByteSource::<T>::try_new(b, data_type))
.collect::<Result<Vec<_>>>()?;
let mut offsets = Vec::with_capacity(num_rows + 1);
offsets.push(T::Offset::usize_as(0));
let mut len = 0;
for (row, &i) in chosen.iter().enumerate() {
// `NONE` is out of range
if let Some(source) = sources.get(i as usize) {
len += source.value_len(row);
}
offsets.push(T::Offset::usize_as(len));
}
if T::Offset::from_usize(len).is_none() {
return exec_err!("CASE WHEN result is too large for {}", T::DATA_TYPE);
}
// Copy the values a run of rows that chose the same branch at a time. An array's values for
// a run are contiguous, and a scalar repeats.
let mut values = vec![0; len + SLACK];
let mut pos = 0;
let mut start = 0;
while start < num_rows {
let i = chosen[start];
let end = chosen[start..]
.iter()
.position(|&c| c != i)
.map_or(num_rows, |n| start + n);
match sources.get(i as usize) {
None => {}
Some(ByteSource {
scalar: Some(value),
padded_scalar,
..
}) => match padded_scalar {
Some(padded) => {
for _ in start..end {
values[pos..pos + SLACK].copy_from_slice(padded);
pos += value.len();
}
}
None => {
for _ in start..end {
values[pos..pos + value.len()].copy_from_slice(value);
pos += value.len();
}
}
},
Some(source) => {
let from = source.offsets[start].as_usize();
let run_len = source.offsets[end].as_usize() - from;
if run_len <= SLACK && from + SLACK <= source.data.len() {
values[pos..pos + SLACK].copy_from_slice(&source.data[from..from + SLACK]);
} else {
values[pos..pos + run_len].copy_from_slice(&source.data[from..from + run_len]);
}
pos += run_len;
}
}
start = end;
}
values.truncate(len);
// SAFETY: every value is a whole value copied from an array of the same type, and the offsets
// were accumulated from the lengths of those values, so they are monotonic, end at the length
// of `values`, and fall on the boundaries between whole values.
let array = unsafe {
GenericByteArray::<T>::new_unchecked(
OffsetBuffer::new_unchecked(offsets.into()),
values.into(),
merge_nulls(num_rows, branches),
)
};
Ok(Arc::new(array))
}
/// A row is valid when the branch that it chose has a valid value for it.
fn merge_nulls(num_rows: usize, branches: &[Branch]) -> Option<NullBuffer> {
let chosen: usize = branches.iter().map(|b| b.row_count).sum();
if chosen == num_rows && branches.iter().all(|b| b.values.null_count() == 0) {
return None;
}
let mut valid = BooleanBuffer::new_unset(num_rows);
for branch in branches {
if branch.is_scalar {
if branch.is_valid_scalar() {
valid |= &branch.rows;
}
} else {
match branch.values.nulls() {
Some(nulls) => valid |= &(&branch.rows & nulls.inner()),
None => valid |= &branch.rows,
}
}
}
Some(NullBuffer::new(valid)).filter(|nulls| nulls.null_count() > 0)
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::{
BinaryArray, Int32Array, Int64Array, LargeStringArray, StringArray, StructArray,
TimestampMicrosecondArray,
};
use arrow::compute::cast;
use arrow::datatypes::{Field, FieldRef, TimeUnit, TimestampMicrosecondType};
use datafusion::physical_expr::expressions::{col, lit};
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
fn value_types() -> Vec<DataType> {
vec![
DataType::Int32,
DataType::Int64,
DataType::Float64,
DataType::Decimal128(12, 3),
DataType::Date32,
DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
DataType::Boolean,
DataType::Utf8,
DataType::LargeUtf8,
DataType::Binary,
]
}
fn random_values(data_type: &DataType, len: usize, nulls: f64, rng: &mut StdRng) -> ArrayRef {
let ints: Int64Array = (0..len)
.map(|_| (!rng.random_bool(nulls)).then(|| rng.random_range(-9i64..9)))
.collect();
// Lengths either side of `SLACK`, some multi-byte
let text = |v: i64| {
let pad = if v % 2 == 0 { "é" } else { "x" };
format!("{}{v}", pad.repeat(3 * v.unsigned_abs() as usize))
};
match data_type {
DataType::Boolean => cast(&ints, data_type).unwrap(),
DataType::Date32 => cast(&cast(&ints, &DataType::Int32).unwrap(), data_type).unwrap(),
DataType::Utf8 => Arc::new(ints.iter().map(|v| v.map(text)).collect::<StringArray>()),
DataType::LargeUtf8 => Arc::new(
ints.iter()
.map(|v| v.map(text))
.collect::<LargeStringArray>(),
),
DataType::Binary => Arc::new(
ints.iter()
.map(|v| v.map(|v| text(v).into_bytes()))
.collect::<BinaryArray>(),
),
_ => cast(&ints, data_type).unwrap(),
}
}
/// The branches and ELSE of a CASE.
type CaseShape = (Vec<WhenThen>, Option<Arc<dyn PhysicalExpr>>);
/// How the predicate columns are laid out.
#[derive(Clone, Copy, Debug)]
enum Predicates {
Random,
WithNulls,
AllTrue,
AllFalse,
/// Each predicate is true for one contiguous run of rows
Runs,
}
fn random_predicate(len: usize, layout: Predicates, rng: &mut StdRng) -> ArrayRef {
let (start, end) = (rng.random_range(0..len), rng.random_range(0..len));
let values: BooleanBuffer = (0..len)
.map(|row| match layout {
Predicates::Random | Predicates::WithNulls => rng.random_bool(0.4),
Predicates::AllTrue => true,
Predicates::AllFalse => false,
Predicates::Runs => start.min(end) <= row && row < start.max(end),
})
.collect();
// A comparison kernel leaves whatever it computed under a NULL, so the value bits under
// these are random too, and a NULL must not match even where its bit is set
let nulls = matches!(layout, Predicates::WithNulls)
.then(|| NullBuffer::from_iter((0..len).map(|_| !rng.random_bool(0.3))));
Arc::new(BooleanArray::new(values, nulls))
}
/// Predicate columns `p0..p2` and value columns `v0..v2` of `data_type`.
fn test_batch(
data_type: &DataType,
len: usize,
nulls: f64,
layout: Predicates,
rng: &mut StdRng,
) -> RecordBatch {
let mut fields = vec![];
let mut columns = vec![];
for i in 0..3 {
fields.push(Field::new(format!("p{i}"), DataType::Boolean, true));
columns.push(random_predicate(len, layout, rng));
}
for i in 0..3 {
fields.push(Field::new(format!("v{i}"), data_type.clone(), true));
columns.push(random_values(data_type, len, nulls, rng));
}
RecordBatch::try_new(Arc::new(Schema::new(fields)), columns).unwrap()
}
/// The first valid value in `values`, as a literal.
fn first_valid(values: &ArrayRef) -> Option<ScalarValue> {
(0..values.len())
.find(|&i| values.is_valid(i))
.map(|i| ScalarValue::try_from_array(values, i).unwrap())
}
/// Evaluates the CASE with both `CaseWhenExpr` and DataFusion's `CaseExpr`, checks they agree,
/// and returns whether `CaseWhenExpr` evaluated it eagerly.
fn check_against_case_expr(
batch: &RecordBatch,
when_then: Vec<WhenThen>,
else_expr: Option<Arc<dyn PhysicalExpr>>,
) -> bool {
let expected = CaseExpr::try_new(None, when_then.clone(), else_expr.clone())
.unwrap()
.evaluate(batch)
.unwrap()
.into_array(batch.num_rows())
.unwrap();
let expr = CaseWhenExpr::try_new(when_then, else_expr).unwrap();
let actual = expr
.evaluate(batch)
.unwrap()
.into_array(batch.num_rows())
.unwrap();
assert_eq!(actual.data_type(), expected.data_type(), "{expr}");
assert_eq!(actual.as_ref(), expected.as_ref(), "{expr}");
expr.eager.get().unwrap().is_some()
}
#[test]
fn eager_evaluation_matches_case_expr() {
let mut rng = StdRng::seed_from_u64(42);
let layouts = [
Predicates::Random,
Predicates::WithNulls,
Predicates::AllTrue,
Predicates::AllFalse,
Predicates::Runs,
];
for data_type in value_types() {
for nulls in [0.0, 0.2, 0.9] {
for layout in layouts {
// Longer than a word of the bitmap, and not a multiple of it
let batch = test_batch(&data_type, 300, nulls, layout, &mut rng);
for batch in [batch.clone(), batch.slice(7, 250)] {
let schema = batch.schema();
let c = |name: &str| col(name, &schema).unwrap();
let null = ScalarValue::try_new_null(&data_type).unwrap();
let literal = first_valid(batch.column(3)).unwrap_or(null.clone());
let shapes: Vec<CaseShape> = vec![
(vec![(c("p0"), c("v0"))], Some(c("v1"))),
(vec![(c("p0"), c("v0"))], None),
(
vec![
(c("p0"), c("v0")),
(c("p1"), c("v1")),
(c("p2"), lit(literal.clone())),
],
Some(c("v2")),
),
(vec![(c("p0"), c("v0")), (c("p1"), lit(null.clone()))], None),
(
vec![(c("p0"), lit(literal.clone())), (c("p1"), lit(null))],
Some(lit(literal.clone())),
),
];
for (when_then, else_expr) in shapes {
let eager = check_against_case_expr(&batch, when_then, else_expr);
assert!(eager, "{data_type} should be evaluated eagerly");
}
}
}
}
}
}
#[test]
fn nested_else_is_flattened() {
let mut rng = StdRng::seed_from_u64(7);
for data_type in [DataType::Int64, DataType::Utf8, DataType::Boolean] {
let batch = test_batch(&data_type, 300, 0.2, Predicates::WithNulls, &mut rng);
let schema = batch.schema();
let c = |name: &str| col(name, &schema).unwrap();
let literal = first_valid(batch.column(3)).unwrap();
// IF(p0, v0, IF(p1, v1, CASE WHEN p2 THEN literal ELSE v2 END))
let inner: Arc<dyn PhysicalExpr> = Arc::new(
CaseWhenExpr::try_new(vec![(c("p2"), lit(literal.clone()))], Some(c("v2")))
.unwrap(),
);
let middle: Arc<dyn PhysicalExpr> = Arc::new(IfExpr::new(c("p1"), c("v1"), inner));
let expr = CaseWhenExpr::try_new(vec![(c("p0"), c("v0"))], Some(middle)).unwrap();
assert_eq!(expr.flat_when_then.len(), 3);
let reference = CaseExpr::try_new(
None,
vec![(c("p0"), c("v0"))],
Some(Arc::new(
CaseExpr::try_new(
None,
vec![(c("p1"), c("v1"))],
Some(Arc::new(
CaseExpr::try_new(None, vec![(c("p2"), lit(literal))], Some(c("v2")))
.unwrap(),
)),
)
.unwrap(),
)),
)
.unwrap();
let expected = reference.evaluate(&batch).unwrap().into_array(300).unwrap();
let actual = expr.evaluate(&batch).unwrap().into_array(300).unwrap();
assert_eq!(actual.as_ref(), expected.as_ref(), "{data_type}");
assert!(expr.eager.get().unwrap().is_some());
}
}
fn int_batch(a: Vec<Option<i64>>, b: Vec<Option<i64>>) -> RecordBatch {
let schema = Schema::new(vec![
Field::new("a", DataType::Int64, true),
Field::new("b", DataType::Int64, true),
]);
RecordBatch::try_new(
Arc::new(schema),
vec![Arc::new(Int64Array::from(a)), Arc::new(Int64Array::from(b))],
)
.unwrap()
}
fn binary(
left: Arc<dyn PhysicalExpr>,
op: Operator,
right: Arc<dyn PhysicalExpr>,
) -> Arc<dyn PhysicalExpr> {
Arc::new(BinaryExpr::new(left, op, right))
}
#[test]
fn branch_that_can_fail_is_evaluated_lazily() {
// CASE WHEN b != 0 THEN a / b ELSE 0 END: integer division fails for b = 0, which the
// WHEN excludes
let batch = int_batch(
vec![Some(10), Some(20), Some(30), None],
vec![Some(2), Some(0), Some(3), Some(0)],
);
let schema = batch.schema();
let (a, b) = (col("a", &schema).unwrap(), col("b", &schema).unwrap());
let when_then = vec![(
binary(Arc::clone(&b), Operator::NotEq, lit(0i64)),
binary(a, Operator::Divide, b),
)];
let eager = check_against_case_expr(&batch, when_then, Some(lit(0i64)));
assert!(!eager);
}
#[test]
fn later_when_that_can_fail_is_evaluated_lazily() {
// CASE WHEN b = 0 THEN -1 WHEN a / b > 5 THEN 1 ELSE 0 END: the second WHEN only sees the
// rows where b is not 0
let batch = int_batch(
vec![Some(10), Some(20), Some(30), Some(1)],
vec![Some(2), Some(0), Some(3), Some(0)],
);
let schema = batch.schema();
let (a, b) = (col("a", &schema).unwrap(), col("b", &schema).unwrap());
let when_then = vec![
(binary(Arc::clone(&b), Operator::Eq, lit(0i64)), lit(-1i64)),
(
binary(binary(a, Operator::Divide, b), Operator::Gt, lit(5i64)),
lit(1i64),
),
];
let eager = check_against_case_expr(&batch, when_then, Some(lit(0i64)));
assert!(!eager);
}
#[test]
fn first_when_can_fail_and_still_be_eager() {
// Spark evaluates the first WHEN for every row, so it may be anything
let batch = int_batch(vec![Some(10), Some(-20)], vec![Some(2), Some(5)]);
let schema = batch.schema();
let (a, b) = (col("a", &schema).unwrap(), col("b", &schema).unwrap());
let when_then = vec![(
binary(
binary(Arc::clone(&a), Operator::Divide, b),
Operator::Gt,
lit(0i64),
),
Arc::clone(&a),
)];
let eager = check_against_case_expr(&batch, when_then, Some(lit(0i64)));
assert!(eager);
}
#[test]
fn infallible_expressions() {
let schema = Schema::new(vec![
Field::new("i", DataType::Int64, true),
Field::new("j", DataType::Int32, true),
Field::new("d", DataType::Float64, true),
Field::new("s", DataType::Utf8, true),
Field::new("dec", DataType::Decimal128(10, 2), true),
]);
let c = |name: &str| col(name, &schema).unwrap();
let cast = |e: Arc<dyn PhysicalExpr>, to: DataType| -> Arc<dyn PhysicalExpr> {
Arc::new(Cast::new(
e,
to,
SparkCastOptions::new_without_timezone(EvalMode::Ansi, false),
None,
None,
))
};
let infallible = |e: Arc<dyn PhysicalExpr>| is_infallible(&e, &schema);
assert!(infallible(c("i")));
assert!(infallible(lit("x")));
assert!(infallible(binary(c("i"), Operator::Lt, lit(0i64))));
assert!(infallible(binary(c("s"), Operator::Eq, lit("x"))));
assert!(infallible(binary(c("i"), Operator::Plus, c("i"))));
assert!(infallible(binary(c("d"), Operator::Divide, c("d"))));
assert!(infallible(binary(c("d"), Operator::Modulo, c("d"))));
assert!(infallible(Arc::new(NotExpr::new(binary(
c("i"),
Operator::Lt,
lit(0i64)
)))));
assert!(infallible(Arc::new(IsNullExpr::new(c("s")))));
assert!(infallible(cast(c("j"), DataType::Int64)));
assert!(infallible(cast(c("i"), DataType::Float64)));
assert!(infallible(Arc::new(IfExpr::new(
binary(c("i"), Operator::Lt, lit(0i64)),
c("i"),
lit(0i64)
))));
assert!(infallible(Arc::new(NormalizeNaNAndZero::new(
DataType::Float64,
c("d")
))));
// Only a boolean can be negated, and only a float normalized
assert!(!infallible(Arc::new(NotExpr::new(c("i")))));
assert!(!infallible(Arc::new(NormalizeNaNAndZero::new(
DataType::Int64,
c("i")
))));
// Integer division and remainder fail on a zero divisor
assert!(!infallible(binary(c("i"), Operator::Divide, c("i"))));
assert!(!infallible(binary(c("i"), Operator::Modulo, c("i"))));
// Checked arithmetic fails on overflow
let checked: Arc<dyn PhysicalExpr> =
Arc::new(BinaryExpr::new(c("i"), Operator::Plus, c("i")).with_fail_on_overflow(true));
assert!(!infallible(checked));
// Decimal arithmetic can overflow
assert!(!infallible(binary(c("dec"), Operator::Plus, c("dec"))));
// Narrowing and parsing casts can fail
assert!(!infallible(cast(c("i"), DataType::Int32)));
assert!(!infallible(cast(c("s"), DataType::Int32)));
// So can anything built from a part that can fail
assert!(!infallible(binary(
binary(c("i"), Operator::Divide, c("i")),
Operator::Lt,
lit(0i64)
)));
assert!(!infallible(Arc::new(IfExpr::new(
binary(c("i"), Operator::Lt, lit(0i64)),
binary(c("i"), Operator::Divide, c("i")),
lit(0i64)
))));
}
#[test]
fn result_of_one_branch_is_returned_as_is() {
let batch = int_batch(vec![Some(1), Some(2)], vec![Some(1), Some(2)]);
let schema = batch.schema();
let a = col("a", &schema).unwrap();
let scalar = |expr: CaseWhenExpr| match expr.evaluate(&batch).unwrap() {
ColumnarValue::Scalar(v) => v,
other => panic!("expected a scalar, got {other:?}"),
};
// Every row chooses THEN, a literal
let expr = CaseWhenExpr::try_new(
vec![(binary(Arc::clone(&a), Operator::Gt, lit(0i64)), lit(7i64))],
Some(Arc::clone(&a)),
)
.unwrap();
assert_eq!(scalar(expr), ScalarValue::Int64(Some(7)));
// No row chooses a branch and there is no ELSE
let expr = CaseWhenExpr::try_new(
vec![(binary(Arc::clone(&a), Operator::Lt, lit(0i64)), a)],
None,
)
.unwrap();
assert_eq!(scalar(expr), ScalarValue::Int64(None));
}
#[test]
fn empty_batch() {
let batch = int_batch(vec![], vec![]);
let schema = batch.schema();
let a = col("a", &schema).unwrap();
let b = col("b", &schema).unwrap();
let when_then = vec![(binary(Arc::clone(&a), Operator::Lt, lit(0i64)), a)];
check_against_case_expr(&batch, when_then, Some(b));
}
#[test]
fn nullable_follows_spark() {
let schema = Schema::new(vec![
Field::new("p", DataType::Boolean, false),
Field::new("a", DataType::Int64, false),
Field::new("n", DataType::Int64, true),
]);
let c = |name: &str| col(name, &schema).unwrap();
let nullable = |when_then: Vec<WhenThen>, else_expr: Option<Arc<dyn PhysicalExpr>>| {
CaseWhenExpr::try_new(when_then, else_expr)
.unwrap()
.nullable(&schema)
.unwrap()
};
assert!(!nullable(vec![(c("p"), c("a"))], Some(lit(0i64))));
assert!(nullable(vec![(c("p"), c("a"))], None));
assert!(nullable(vec![(c("p"), c("n"))], Some(lit(0i64))));
assert!(nullable(vec![(c("p"), c("a"))], Some(c("n"))));
assert!(nullable(
vec![(c("p"), c("a"))],
Some(lit(ScalarValue::Int64(None)))
));
}
#[test]
fn create_case_when_casts_only_differing_branches() {
let schema = Schema::new(vec![
Field::new("p", DataType::Boolean, true),
Field::new("i", DataType::Int32, true),
Field::new("l", DataType::Int64, true),
]);
let c = |name: &str| col(name, &schema).unwrap();
let expr = create_case_when(
vec![(c("p"), c("l")), (c("p"), c("i"))],
Some(c("l")),
&schema,
)
.unwrap();
let children = expr.children();
assert!(children[1].is::<Column>());
let cast = children[3].downcast_ref::<Cast>().unwrap();
assert_eq!(cast.data_type, DataType::Int64);
assert!(children[4].is::<Column>());
let batch = RecordBatch::try_new(
Arc::new(schema.clone()),
vec![
Arc::new(BooleanArray::from(vec![Some(false), Some(true), None])),
Arc::new(Int32Array::from(vec![Some(1), Some(2), Some(3)])),
Arc::new(Int64Array::from(vec![Some(10), None, Some(30)])),
],
)
.unwrap();
let result = expr.evaluate(&batch).unwrap().into_array(3).unwrap();
assert_eq!(
result.as_ref(),
&Int64Array::from(vec![Some(10), None, Some(30)]) as &dyn Array
);
}
/// CASE branches that share a Spark timestamp type but carry different Arrow timezone labels
/// are reconciled by relabelling them, which used to panic because the cast had no timezone.
#[test]
fn case_reconciles_timestamp_timezone_labels() {
let utc = DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into()));
for then_label in [Some("Etc/UTC"), None] {
let then_type = DataType::Timestamp(TimeUnit::Microsecond, then_label.map(Into::into));
let schema = Schema::new(vec![
Field::new("b", DataType::Boolean, true),
Field::new("t", then_type, true),
Field::new("e", utc.clone(), true),
]);
let batch = RecordBatch::try_new(
Arc::new(schema.clone()),
vec![
Arc::new(BooleanArray::from(vec![true, false])),
Arc::new(
TimestampMicrosecondArray::from(vec![1, 2]).with_timezone_opt(then_label),
),
Arc::new(TimestampMicrosecondArray::from(vec![10, 20]).with_timezone("UTC")),
],
)
.unwrap();
let c = |name: &str| col(name, &schema).unwrap();
let case = create_case_when(vec![(c("b"), c("t"))], Some(c("e")), &schema).unwrap();
let result = case.evaluate(&batch).unwrap().into_array(2).unwrap();
assert_eq!(result.data_type(), &utc, "{then_label:?}");
let values = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(values.values().to_vec(), vec![1, 20], "{then_label:?}");
}
}
/// Evaluates an IF over `batch`, whose first row takes the THEN branch and second row the ELSE
/// branch, and over each row alone, and checks that every result has the type the IF reports.
/// Returns the result for the whole batch.
fn evaluate_if(expr: &Arc<dyn PhysicalExpr>, batch: &RecordBatch) -> ArrayRef {
let data_type = expr.data_type(&batch.schema()).unwrap();
// Both branches, only THEN, and only ELSE, whose array `IfExpr` returns unchanged
for (offset, len) in [(0, 2), (0, 1), (1, 1)] {
let result = expr.evaluate(&batch.slice(offset, len)).unwrap();
let result = result.into_array(len).unwrap();
assert_eq!(
result.data_type(),
&data_type,
"{expr} over rows {offset}..+{len}"
);
}
expr.evaluate(batch).unwrap().into_array(2).unwrap()
}
/// Spark matches struct fields by position even when their case-distinct names are reordered.
/// Name-based union pairs the first INT with the second DOUBLE and silently widens it.
#[test]
fn if_reconciles_case_variant_fields_positionally() {
use arrow::array::Float64Array;
let then_fields: arrow::datatypes::Fields = vec![
Field::new("x", DataType::Int32, true),
Field::new("X", DataType::Float64, false),
]
.into();
let else_fields: arrow::datatypes::Fields = vec![
Field::new("X", DataType::Int32, false),
Field::new("x", DataType::Float64, true),
]
.into();
let expected_fields: arrow::datatypes::Fields = vec![
Field::new("x", DataType::Int32, true),
Field::new("X", DataType::Float64, true),
]
.into();
let schema = Schema::new(vec![
Field::new("b", DataType::Boolean, false),
Field::new("t", DataType::Struct(then_fields.clone()), false),
Field::new("e", DataType::Struct(else_fields.clone()), false),
]);
let batch = RecordBatch::try_new(
Arc::new(schema.clone()),
vec![
Arc::new(BooleanArray::from(vec![true, false])),
Arc::new(StructArray::new(
then_fields,
vec![
Arc::new(Int32Array::from(vec![Some(7), None])),
Arc::new(Float64Array::from(vec![5.5, 5.5])),
],
None,
)),
Arc::new(StructArray::new(
else_fields,
vec![
Arc::new(Int32Array::from(vec![0, 0])),
Arc::new(Float64Array::from(vec![Some(9.5), None])),
],
None,
)),
],
)
.unwrap();
let c = |name: &str| col(name, &schema).unwrap();
let expr = create_if_expr(c("b"), c("t"), c("e"), &schema).unwrap();
assert_eq!(
expr.data_type(&schema).unwrap(),
DataType::Struct(expected_fields.clone())
);
let result = evaluate_if(&expr, &batch);
let expected = StructArray::new(
expected_fields,
vec![
Arc::new(Int32Array::from(vec![7, 0])),
Arc::new(Float64Array::from(vec![Some(5.5), None])),
],
None,
);
assert_eq!(result.as_ref(), &expected);
}
/// IF branches that share a Spark timestamp type but carry different Arrow timezone labels are
/// reconciled by relabelling one of them, as CASE branches are. The result has the THEN
/// branch's label, which `IfExpr` reported before the ELSE branch was cast, unless it has none.
/// Every case ends up labelled UTC, like every `TimestampType` value in a native plan.
#[test]
fn if_reconciles_timestamp_timezone_labels() {
let timestamp =
|label: Option<&str>| DataType::Timestamp(TimeUnit::Microsecond, label.map(Into::into));
// The THEN branch's label, the ELSE branch's label, and the result's
for (then_label, else_label, label) in [
(None, Some("UTC"), Some("UTC")),
(Some("UTC"), Some("Etc/UTC"), Some("UTC")),
(Some("UTC"), None, Some("UTC")),
] {
let schema = Schema::new(vec![
Field::new("b", DataType::Boolean, true),
Field::new("t", timestamp(then_label), true),
Field::new("e", timestamp(else_label), true),
]);
let batch = RecordBatch::try_new(
Arc::new(schema.clone()),
vec![
Arc::new(BooleanArray::from(vec![true, false])),
Arc::new(
TimestampMicrosecondArray::from(vec![1, 2]).with_timezone_opt(then_label),
),
Arc::new(
TimestampMicrosecondArray::from(vec![10, 20]).with_timezone_opt(else_label),
),
],
)
.unwrap();
let c = |name: &str| col(name, &schema).unwrap();
let expr = create_if_expr(c("b"), c("t"), c("e"), &schema).unwrap();
let labels = format!("{then_label:?} {else_label:?}");
assert_eq!(
expr.data_type(&schema).unwrap(),
timestamp(label),
"{labels}"
);
let result = evaluate_if(&expr, &batch);
let values = result.as_primitive::<TimestampMicrosecondType>();
assert_eq!(values.values().to_vec(), vec![1, 20], "{labels}");
}
}
/// IF branches whose struct field differs only in whether it can be NULL, or in the case of its
/// name, which Spark adds no cast for. The result's field can be NULL if either branch's can,
/// and has the THEN branch's name, as in Spark's `If.dataType`.
#[test]
fn if_reconciles_struct_field_nullability_and_names() {
let field =
|name: &str, nullable: bool| Arc::new(Field::new(name, DataType::Int32, nullable));
let struct_type = |field: &FieldRef| DataType::Struct(vec![Arc::clone(field)].into());
let column = |field: &FieldRef, values: Vec<Option<i32>>| -> ArrayRef {
Arc::new(StructArray::new(
vec![Arc::clone(field)].into(),
vec![Arc::new(Int32Array::from(values)) as ArrayRef],
None,
))
};
// The THEN branch's field, the ELSE branch's field, and the result's
for (then_field, else_field, result_field) in [
(field("x", false), field("x", true), field("x", true)),
(field("x", true), field("x", false), field("x", true)),
(field("x", true), field("X", true), field("x", true)),
(field("X", false), field("x", true), field("X", true)),
] {
let schema = Schema::new(vec![
Field::new("b", DataType::Boolean, true),
Field::new("t", struct_type(&then_field), true),
Field::new("e", struct_type(&else_field), true),
]);
// A NULL field in the row that takes the ELSE branch, where it can be NULL
let else_values = if else_field.is_nullable() {
vec![Some(10), None]
} else {
vec![Some(10), Some(20)]
};
let batch = RecordBatch::try_new(
Arc::new(schema.clone()),
vec![
Arc::new(BooleanArray::from(vec![true, false])),
column(&then_field, vec![Some(1), Some(2)]),
column(&else_field, else_values.clone()),
],
)
.unwrap();
let c = |name: &str| col(name, &schema).unwrap();
let expr = create_if_expr(c("b"), c("t"), c("e"), &schema).unwrap();
let fields = format!("{then_field:?} {else_field:?}");
assert_eq!(
expr.data_type(&schema).unwrap(),
struct_type(&result_field),
"{fields}"
);
let result = evaluate_if(&expr, &batch);
let expected = column(&result_field, vec![Some(1), else_values[1]]);
assert_eq!(result.as_ref(), expected.as_ref(), "{fields}");
}
}
#[test]
fn many_branches() {
let len = 1000;
let values: Int64Array = (0..len as i64).map(Some).collect();
let schema = Schema::new(vec![Field::new("v", DataType::Int64, true)]);
let batch = RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(values)]).unwrap();
let v = col("v", &schema).unwrap();
// 300 branches, each matching the rows below its bound
let when_then = (0..300)
.map(|i| {
let when = binary(Arc::clone(&v), Operator::Lt, lit(3 * i as i64));
(when, lit(format!("b{i}")))
})
.collect();
assert!(check_against_case_expr(
&batch,
when_then,
Some(lit("rest"))
));
}
#[test]
fn timestamp_keeps_its_time_zone() {
let data_type = DataType::Timestamp(TimeUnit::Microsecond, Some("+05:00".into()));
let values =
TimestampMicrosecondArray::from(vec![Some(1), Some(2), None]).with_timezone("+05:00");
let schema = Schema::new(vec![
Field::new("p", DataType::Boolean, true),
Field::new("t", data_type.clone(), true),
]);
let batch = RecordBatch::try_new(
Arc::new(schema.clone()),
vec![
Arc::new(BooleanArray::from(vec![
Some(true),
Some(false),
Some(true),
])),
Arc::new(values),
],
)
.unwrap();
let c = |name: &str| col(name, &schema).unwrap();
let null = lit(ScalarValue::try_new_null(&data_type).unwrap());
assert!(check_against_case_expr(
&batch,
vec![(c("p"), c("t"))],
Some(null)
));
}
}