blob: 62447624f5bab801504cdea097e19ba1d9ddf745 [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 arrow::array::{make_array, Array, ArrayRef, StructArray};
use arrow::buffer::NullBuffer;
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use datafusion::common::{DataFusionError, Result as DataFusionResult, ScalarValue};
use datafusion::logical_expr::ColumnarValue;
use datafusion::physical_expr::PhysicalExpr;
use std::{
fmt::{Display, Formatter},
hash::Hash,
sync::Arc,
};
#[derive(Debug, Eq)]
pub struct GetStructField {
child: Arc<dyn PhysicalExpr>,
ordinal: usize,
}
impl Hash for GetStructField {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.child.hash(state);
self.ordinal.hash(state);
}
}
impl PartialEq for GetStructField {
fn eq(&self, other: &Self) -> bool {
self.child.eq(&other.child) && self.ordinal.eq(&other.ordinal)
}
}
impl GetStructField {
pub fn new(child: Arc<dyn PhysicalExpr>, ordinal: usize) -> Self {
Self { child, ordinal }
}
fn child_field(&self, input_schema: &Schema) -> DataFusionResult<Arc<Field>> {
match self.child.data_type(input_schema)? {
DataType::Struct(fields) => Ok(Arc::clone(&fields[self.ordinal])),
data_type => Err(DataFusionError::Plan(format!(
"Expect struct field, got {data_type:?}"
))),
}
}
/// Extract field `ordinal` from a struct array, propagating the parent struct's null mask.
///
/// Spark semantics: a field of a NULL struct is NULL. Arrow stores a StructArray's child
/// arrays with their own validity, INDEPENDENT of the parent struct's null buffer -- so the
/// raw child value at a row where the struct itself is null can be non-null (e.g. parquet
/// files where a logically-null struct column still has a populated child buffer). Returning
/// the child column verbatim then makes `isnotnull(struct.field)` wrongly true for a null
/// struct. Union the struct's null mask into the child's (null where the struct is null OR
/// the child is null).
fn project_field(struct_array: &StructArray, ordinal: usize) -> DataFusionResult<ArrayRef> {
let child = struct_array.column(ordinal);
match struct_array.nulls() {
Some(_) => {
let combined = NullBuffer::union(struct_array.nulls(), child.nulls());
let data = child.to_data().into_builder().nulls(combined).build()?;
Ok(make_array(data))
}
None => Ok(Arc::clone(child)),
}
}
}
impl PhysicalExpr for GetStructField {
fn fmt_sql(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(self, f)
}
fn data_type(&self, input_schema: &Schema) -> DataFusionResult<DataType> {
Ok(self.child_field(input_schema)?.data_type().clone())
}
fn nullable(&self, input_schema: &Schema) -> DataFusionResult<bool> {
// A field extracted from a struct is nullable if EITHER the field itself is declared
// nullable OR the parent struct can be null -- a field of a null struct is null (Spark
// semantics, enforced by `project_field` unioning the parent null mask). Reporting only
// the field's own nullability under-declares: a non-nullable field of a nullable struct
// then carries the parent's nulls while claiming non-nullable, which fails Arrow's
// RecordBatch validation downstream with "declared as non-nullable but contains null
// values" (e.g. once the projected column reaches a shuffle/sort). Mirrors Spark's
// `GetStructField.nullable = child.nullable || field.nullable`.
Ok(self.child.nullable(input_schema)? || self.child_field(input_schema)?.is_nullable())
}
fn evaluate(&self, batch: &RecordBatch) -> DataFusionResult<ColumnarValue> {
let child_value = self.child.evaluate(batch)?;
match child_value {
ColumnarValue::Array(array) => {
let struct_array = array
.as_any()
.downcast_ref::<StructArray>()
.expect("A struct is expected");
Ok(ColumnarValue::Array(Self::project_field(
struct_array,
self.ordinal,
)?))
}
ColumnarValue::Scalar(ScalarValue::Struct(struct_array)) => Ok(ColumnarValue::Array(
Self::project_field(&struct_array, self.ordinal)?,
)),
value => Err(DataFusionError::Execution(format!(
"Expected a struct array, got {value:?}"
))),
}
}
fn children(&self) -> Vec<&Arc<dyn PhysicalExpr>> {
vec![&self.child]
}
fn with_new_children(
self: Arc<Self>,
children: Vec<Arc<dyn PhysicalExpr>>,
) -> datafusion::common::Result<Arc<dyn PhysicalExpr>> {
Ok(Arc::new(GetStructField::new(
Arc::clone(&children[0]),
self.ordinal,
)))
}
}
impl Display for GetStructField {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"GetStructField [child: {:?}, ordinal: {:?}]",
self.child, self.ordinal
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use arrow::array::Int64Array;
use arrow::datatypes::Fields;
use datafusion::physical_expr::expressions::Column;
// A field of a NULL struct must be NULL (Spark semantics) even when the child buffer holds a
// non-null value at that row -- Arrow stores child validity independently of the parent
// struct's null mask, so a logically-null struct column read from parquet can still carry a
// populated child buffer. Without propagating the parent null mask, `isnotnull(struct.field)`
// wrongly evaluates TRUE for a null struct.
#[test]
fn field_of_null_struct_is_null() {
// Child is non-null at every row; the struct itself is null at rows 1 and 3.
let child = Arc::new(Int64Array::from(vec![10_i64, 20, 30, 40])) as ArrayRef;
let fields: Fields = Fields::from(vec![Field::new("version", DataType::Int64, true)]);
let nulls = NullBuffer::from(vec![true, false, true, false]);
let struct_array = StructArray::new(fields.clone(), vec![child], Some(nulls));
let schema = Schema::new(vec![Field::new("cm", DataType::Struct(fields), true)]);
let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(struct_array)]).unwrap();
let expr = GetStructField::new(Arc::new(Column::new("cm", 0)), 0);
let out = expr
.evaluate(&batch)
.unwrap()
.into_array(batch.num_rows())
.unwrap();
let out = out.as_any().downcast_ref::<Int64Array>().unwrap();
assert!(!out.is_null(0) && out.value(0) == 10);
assert!(out.is_null(1), "field of a null struct must be null");
assert!(!out.is_null(2) && out.value(2) == 30);
assert!(out.is_null(3), "field of a null struct must be null");
}
// A NON-nullable field of a NULLABLE struct must report `nullable() == true`: `project_field`
// unions the parent struct's null mask, so the projected column carries nulls wherever the
// struct is null. Reporting the field's own (non-nullable) flag would make the output schema
// lie, failing Arrow RecordBatch validation downstream with "declared as non-nullable but
// contains null values" once the column reaches a shuffle/sort.
#[test]
fn non_nullable_field_of_nullable_struct_is_nullable() {
// `size` is declared non-nullable, but the enclosing struct is nullable.
let inner: Fields = Fields::from(vec![Field::new("size", DataType::Int64, false)]);
let schema = Schema::new(vec![Field::new(
"add",
DataType::Struct(inner),
/* struct nullable */ true,
)]);
let expr = GetStructField::new(Arc::new(Column::new("add", 0)), 0);
assert!(
expr.nullable(&schema).unwrap(),
"a field of a nullable struct must be nullable even if the field itself is non-nullable"
);
}
// A non-nullable field of a NON-nullable struct stays non-nullable (no over-declaring).
#[test]
fn non_nullable_field_of_non_nullable_struct_stays_non_nullable() {
let inner: Fields = Fields::from(vec![Field::new("size", DataType::Int64, false)]);
let schema = Schema::new(vec![Field::new(
"add",
DataType::Struct(inner),
/* struct nullable */ false,
)]);
let expr = GetStructField::new(Arc::new(Column::new("add", 0)), 0);
assert!(
!expr.nullable(&schema).unwrap(),
"a non-nullable field of a non-nullable struct must remain non-nullable"
);
}
}