@@ -245,10 +245,21 @@ impl ExtensionPlanner for ViewExploitationPlanner {
245245 return Ok ( None ) ;
246246 }
247247
248+ // Compare schemas ignoring nullability differences.
249+ // Different table types (FileScanTable, LiveTable, MV) may expose
250+ // different nullability for the same column. For example, a partition
251+ // column in one table is non-nullable, but the same column is a file
252+ // column in a rollup MV (forced nullable for DF 52 RecordBatch
253+ // validation compatibility). Field names and data types must match.
248254 if logical_inputs
249255 . iter ( )
250256 . map ( |plan| plan. schema ( ) )
251- . any ( |schema| schema != logical_inputs[ 0 ] . schema ( ) )
257+ . any ( |schema| {
258+ !schemas_equal_ignoring_nullability (
259+ schema. as_arrow ( ) ,
260+ logical_inputs[ 0 ] . schema ( ) . as_arrow ( ) ,
261+ )
262+ } )
252263 {
253264 return Err ( DataFusionError :: Plan (
254265 "candidate logical plans should have the same schema" . to_string ( ) ,
@@ -258,7 +269,12 @@ impl ExtensionPlanner for ViewExploitationPlanner {
258269 if physical_inputs
259270 . iter ( )
260271 . map ( |plan| plan. schema ( ) )
261- . any ( |schema| schema != physical_inputs[ 0 ] . schema ( ) )
272+ . any ( |schema| {
273+ !schemas_equal_ignoring_nullability (
274+ & schema,
275+ & physical_inputs[ 0 ] . schema ( ) ,
276+ )
277+ } )
262278 {
263279 return Err ( DataFusionError :: Plan (
264280 "candidate physical plans should have the same schema" . to_string ( ) ,
@@ -522,3 +538,66 @@ impl PhysicalOptimizerRule for PruneCandidates {
522538 true
523539 }
524540}
541+
542+ /// Compare two Arrow schemas ignoring field nullability.
543+ ///
544+ /// Returns true if field count, field names, and data types all match.
545+ /// Nullability differences are ignored because different table types
546+ /// (e.g. FileScanTable with forced-nullable file columns vs a table
547+ /// where the same column is a non-nullable partition column) can
548+ /// legitimately differ in nullability while being semantically equivalent.
549+ fn schemas_equal_ignoring_nullability (
550+ a : & arrow_schema:: Schema ,
551+ b : & arrow_schema:: Schema ,
552+ ) -> bool {
553+ a. fields ( ) . len ( ) == b. fields ( ) . len ( )
554+ && a. fields ( )
555+ . iter ( )
556+ . zip ( b. fields ( ) . iter ( ) )
557+ . all ( |( f1, f2) | f1. name ( ) == f2. name ( ) && f1. data_type ( ) == f2. data_type ( ) )
558+ }
559+
560+ #[ cfg( test) ]
561+ mod tests_nullability {
562+ use super :: * ;
563+ use arrow_schema:: { DataType , Field , Schema } ;
564+
565+ #[ test]
566+ fn schemas_equal_when_only_nullability_differs ( ) {
567+ let a = Schema :: new ( vec ! [
568+ Field :: new( "ticker" , DataType :: Utf8 , false ) ,
569+ Field :: new( "date" , DataType :: Utf8 , false ) ,
570+ Field :: new( "price" , DataType :: Float64 , false ) ,
571+ ] ) ;
572+ let b = Schema :: new ( vec ! [
573+ Field :: new( "ticker" , DataType :: Utf8 , true ) ,
574+ Field :: new( "date" , DataType :: Utf8 , true ) ,
575+ Field :: new( "price" , DataType :: Float64 , true ) ,
576+ ] ) ;
577+ assert ! ( schemas_equal_ignoring_nullability( & a, & b) ) ;
578+ }
579+
580+ #[ test]
581+ fn schemas_not_equal_when_types_differ ( ) {
582+ let a = Schema :: new ( vec ! [ Field :: new( "x" , DataType :: Int32 , false ) ] ) ;
583+ let b = Schema :: new ( vec ! [ Field :: new( "x" , DataType :: Int64 , false ) ] ) ;
584+ assert ! ( !schemas_equal_ignoring_nullability( & a, & b) ) ;
585+ }
586+
587+ #[ test]
588+ fn schemas_not_equal_when_names_differ ( ) {
589+ let a = Schema :: new ( vec ! [ Field :: new( "ticker" , DataType :: Utf8 , true ) ] ) ;
590+ let b = Schema :: new ( vec ! [ Field :: new( "symbol" , DataType :: Utf8 , true ) ] ) ;
591+ assert ! ( !schemas_equal_ignoring_nullability( & a, & b) ) ;
592+ }
593+
594+ #[ test]
595+ fn schemas_not_equal_when_field_count_differs ( ) {
596+ let a = Schema :: new ( vec ! [
597+ Field :: new( "x" , DataType :: Int32 , false ) ,
598+ Field :: new( "y" , DataType :: Int32 , false ) ,
599+ ] ) ;
600+ let b = Schema :: new ( vec ! [ Field :: new( "x" , DataType :: Int32 , false ) ] ) ;
601+ assert ! ( !schemas_equal_ignoring_nullability( & a, & b) ) ;
602+ }
603+ }
0 commit comments