@@ -243,6 +243,12 @@ impl PhysicalExpr for CastExpr {
243243 }
244244
245245 fn nullable ( & self , input_schema : & Schema ) -> Result < bool > {
246+ // Runtime nullability is determined solely by the child expression.
247+ // The target field's nullability is metadata used during planning and
248+ // is surfaced via `return_field()`; it should not influence the
249+ // physical runtime check. Keeping this method child-driven avoids
250+ // misinforming optimizers about possible null outputs while still
251+ // preserving logical nullability separately.
246252 self . expr . nullable ( input_schema)
247253 }
248254
@@ -336,13 +342,13 @@ pub(crate) fn cast_with_target_field_and_options(
336342 }
337343
338344 let is_valid_cast = match ( & expr_type, cast_type) {
339- // Allow struct-to-struct casts that pass name-based compatibility validation.
340- // This validation is applied at planning time (now) to fail fast, rather than
341- // deferring errors to execution time. The name-based casting logic will be
342- // executed at runtime via ColumnarValue::cast_to.
343- ( Struct ( _) , Struct ( _) ) => can_cast_struct_types ( & expr_type, cast_type) ,
344- _ => can_cast_types ( & expr_type, cast_type) ,
345- } ;
345+ // Allow struct-to-struct casts that pass name-based compatibility validation.
346+ // This validation is applied at planning time (now) to fail fast, rather than
347+ // deferring errors to execution time. The name-based casting logic will be
348+ // executed at runtime via ColumnarValue::cast_to.
349+ ( Struct ( _) , Struct ( _) ) => can_cast_struct_types ( & expr_type, cast_type) ,
350+ _ => can_cast_types ( & expr_type, cast_type) ,
351+ } ;
346352
347353 if !is_valid_cast {
348354 not_impl_err ! ( "Unsupported CAST from {expr_type} to {cast_type}" )
0 commit comments