@@ -130,12 +130,23 @@ impl VTable for MaskedVTable {
130130 }
131131
132132 fn execute ( array : & Self :: Array , ctx : & mut ExecutionCtx ) -> VortexResult < Canonical > {
133- if let Some ( canonical) = execute_fast_path ( array, ctx) ? {
134- return Ok ( canonical) ;
133+ let validity_mask = array. validity_mask ( ) ;
134+
135+ // Fast path: all masked means result is all nulls.
136+ if validity_mask. all_false ( ) {
137+ return ConstantArray :: new ( Scalar :: null ( array. dtype ( ) . as_nullable ( ) ) , array. len ( ) )
138+ . into_array ( )
139+ . execute :: < Canonical > ( ctx) ;
135140 }
136141
142+ // NB: We intentionally do NOT have a fast path for `validity_mask.all_true()`.
143+ // `MaskedArray`'s dtype is always `Nullable`, but the child has `NonNullable` `DType` (by
144+ // invariant). Simply returning the child's canonical would cause a dtype mismatch.
145+ // While we could manually convert the dtype, `mask_validity_canonical` is already O(1) for
146+ // `AllTrue` masks (no data copying), so there's no benefit.
147+
137148 let child = array. child ( ) . clone ( ) . execute :: < Canonical > ( ctx) ?;
138- let canonical = mask_validity_canonical ( child, & array . validity_mask ( ) ) ?;
149+ let canonical = mask_validity_canonical ( child, & validity_mask) ?;
139150
140151 vortex_ensure ! (
141152 canonical. as_ref( ) . dtype( ) == array. dtype( ) ,
@@ -176,42 +187,18 @@ impl VTable for MaskedVTable {
176187 }
177188}
178189
179- /// Check for fast-path execution conditions.
180- pub ( super ) fn execute_fast_path (
181- array : & MaskedArray ,
182- ctx : & mut ExecutionCtx ,
183- ) -> VortexResult < Option < Canonical > > {
184- let validity_mask = array. validity_mask ( ) ;
185-
186- // All valid - no masking needed
187- if validity_mask. all_true ( ) {
188- return Ok ( Some ( array. child . clone ( ) . execute ( ctx) ?) ) ;
189- }
190-
191- // All masked - result is all nulls
192- if validity_mask. all_false ( ) {
193- return Ok ( Some (
194- ConstantArray :: new ( Scalar :: null ( array. dtype ( ) . as_nullable ( ) ) , array. len ( ) )
195- . into_array ( )
196- . execute :: < Canonical > ( ctx) ?,
197- ) ) ;
198- }
199-
200- // Child is already all nulls - masking has no effect
201- if array. child . all_invalid ( ) {
202- return Ok ( Some ( array. child . clone ( ) . execute ( ctx) ?) ) ;
203- }
204-
205- Ok ( None )
206- }
207-
208190#[ cfg( test) ]
209191mod tests {
210192 use rstest:: rstest;
211193 use vortex_buffer:: ByteBufferMut ;
194+ use vortex_dtype:: Nullability ;
195+ use vortex_error:: VortexError ;
212196
213197 use crate :: ArrayContext ;
198+ use crate :: Canonical ;
214199 use crate :: IntoArray ;
200+ use crate :: LEGACY_SESSION ;
201+ use crate :: VortexSessionExecute ;
215202 use crate :: arrays:: MaskedArray ;
216203 use crate :: arrays:: MaskedVTable ;
217204 use crate :: arrays:: PrimitiveArray ;
@@ -265,4 +252,33 @@ mod tests {
265252 decoded. display_values( ) . to_string( )
266253 ) ;
267254 }
255+
256+ /// Regression test for issue #5989: execute_fast_path returns child with wrong dtype.
257+ ///
258+ /// When MaskedArray's validity mask is all true, returning the child's canonical form
259+ /// directly would cause a dtype mismatch because the child has NonNullable dtype while
260+ /// MaskedArray always has Nullable dtype.
261+ #[ test]
262+ fn test_execute_with_all_valid_preserves_nullable_dtype ( ) -> Result < ( ) , VortexError > {
263+ // Create a MaskedArray with AllValid validity.
264+
265+ // Child has NonNullable dtype, but MaskedArray's dtype is Nullable.
266+ let child = PrimitiveArray :: from_iter ( [ 1i32 , 2 , 3 ] ) . into_array ( ) ;
267+ assert_eq ! ( child. dtype( ) . nullability( ) , Nullability :: NonNullable ) ;
268+
269+ let array = MaskedArray :: try_new ( child, Validity :: AllValid ) ?;
270+ assert_eq ! ( array. dtype( ) . nullability( ) , Nullability :: Nullable ) ;
271+
272+ // Execute the array. This should produce a Canonical with Nullable dtype.
273+ let mut ctx = LEGACY_SESSION . create_execution_ctx ( ) ;
274+ let result: Canonical = array. into_array ( ) . execute ( & mut ctx) ?;
275+
276+ assert_eq ! (
277+ result. as_ref( ) . dtype( ) . nullability( ) ,
278+ Nullability :: Nullable ,
279+ "MaskedArray execute should produce Nullable dtype"
280+ ) ;
281+
282+ Ok ( ( ) )
283+ }
268284}
0 commit comments