Skip to content

Commit c549a29

Browse files
committed
Elide same-type field-aware casts in CastExpr
Restore no-op cast canonicalization in cast_with_target_field_and_options(...). Update CastExpr::nullable() to align with runtime nullability, while maintaining logical target field semantics via return_field(). Adjust tests to ensure type-changing casts preserve logical target field semantics, and validate cast nullability against runtime semantics.
1 parent 9aeb678 commit c549a29

2 files changed

Lines changed: 15 additions & 27 deletions

File tree

datafusion/physical-expr/src/expressions/cast.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -243,14 +243,7 @@ impl PhysicalExpr for CastExpr {
243243
}
244244

245245
fn nullable(&self, input_schema: &Schema) -> Result<bool> {
246-
// A cast is nullable if **either** the child is nullable or the
247-
// target field allows nulls. This conservative rule prevents
248-
// optimizers from assuming a non-null result when a null input could
249-
// still propagate. `return_field()` continues to expose the exact
250-
// target metadata separately.
251-
let child_nullable = self.expr.nullable(input_schema)?;
252-
let target_nullable = self.resolved_target_field(input_schema)?.is_nullable();
253-
Ok(child_nullable || target_nullable)
246+
self.expr.nullable(input_schema)
254247
}
255248

256249
fn evaluate(&self, batch: &RecordBatch) -> Result<ColumnarValue> {
@@ -338,8 +331,11 @@ pub(crate) fn cast_with_target_field_and_options(
338331
) -> Result<Arc<dyn PhysicalExpr>> {
339332
let expr_type = expr.data_type(input_schema)?;
340333
let cast_type = target_field.data_type();
341-
let is_valid_cast = expr_type == *cast_type
342-
|| match (&expr_type, cast_type) {
334+
if expr_type == *cast_type {
335+
return Ok(Arc::clone(&expr));
336+
}
337+
338+
let is_valid_cast = match (&expr_type, cast_type) {
343339
// Allow struct-to-struct casts that pass name-based compatibility validation.
344340
// This validation is applied at planning time (now) to fail fast, rather than
345341
// deferring errors to execution time. The name-based casting logic will be
@@ -954,16 +950,16 @@ mod tests {
954950

955951
#[test]
956952
fn field_aware_cast_nullable_child_nonnullable_targets_nullable() -> Result<()> {
957-
// child is non-nullable but the target field is marked nullable; the
958-
// nullable() result should still be true because the field allows nulls.
953+
// Runtime cast nullability follows the child expression, while
954+
// return_field() preserves the logical target field nullability.
959955
let schema = Schema::new(vec![Field::new("a", Int32, false)]);
960956
let expr = CastExpr::new_with_target_field(
961957
col("a", &schema)?,
962958
Arc::new(Field::new("cast_target", Int64, true)),
963959
None,
964960
);
965961

966-
assert!(expr.nullable(&schema)?);
962+
assert!(!expr.nullable(&schema)?);
967963
assert!(expr.return_field(&schema)?.is_nullable());
968964

969965
Ok(())

datafusion/physical-expr/src/planner.rs

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ mod tests {
491491
let field = physical_expr.return_field(&schema)?;
492492
assert_eq!(field.data_type(), extension_field_type.data_type());
493493
assert_eq!(field.metadata(), extension_field_type.metadata());
494-
assert!(physical_expr.nullable(&schema)?);
494+
assert!(!physical_expr.nullable(&schema)?);
495495

496496
let try_cast_expr = Expr::TryCast(TryCast::new_from_field(
497497
Box::new(expr.clone()),
@@ -536,13 +536,13 @@ mod tests {
536536
field.metadata().get("semantic").map(String::as_str),
537537
Some("target")
538538
);
539-
assert!(physical_expr.nullable(&schema)?);
539+
assert!(!physical_expr.nullable(&schema)?);
540540

541541
Ok(())
542542
}
543543

544544
#[test]
545-
fn test_create_physical_expr_same_type_cast_keeps_target_field() -> Result<()> {
545+
fn test_create_physical_expr_same_type_cast_is_elided() -> Result<()> {
546546
let schema = Schema::new(vec![Field::new("value", DataType::Int32, false)]);
547547
let df_schema = DFSchema::try_from(schema.clone())?;
548548
let target_field = Arc::new(
@@ -558,17 +558,9 @@ mod tests {
558558
let physical_expr =
559559
create_physical_expr(&expr, &df_schema, &ExecutionProps::new())?;
560560

561-
assert!(physical_expr.as_any().downcast_ref::<Column>().is_none());
562-
assert!(physical_expr.as_any().downcast_ref::<CastExpr>().is_some());
563-
564-
let field = physical_expr.return_field(&schema)?;
565-
assert_eq!(field.name(), "same_type_cast");
566-
assert_eq!(field.data_type(), &DataType::Int32);
567-
assert!(field.is_nullable());
568-
assert_eq!(
569-
field.metadata().get("semantic").map(String::as_str),
570-
Some("same_type")
571-
);
561+
assert!(physical_expr.as_any().downcast_ref::<Column>().is_some());
562+
assert!(physical_expr.as_any().downcast_ref::<CastExpr>().is_none());
563+
assert_eq!(physical_expr.return_field(&schema)?.name(), "value");
572564

573565
Ok(())
574566
}

0 commit comments

Comments
 (0)