@@ -5,6 +5,7 @@ use std::fmt::Debug;
55use std:: hash:: Hash ;
66use std:: hash:: Hasher ;
77
8+ use itertools:: Itertools ;
89use vortex_buffer:: ByteBufferMut ;
910use vortex_error:: VortexExpect ;
1011use vortex_error:: VortexResult ;
@@ -26,26 +27,29 @@ use crate::array::ArrayId;
2627use crate :: array:: ArrayView ;
2728use crate :: array:: VTable ;
2829use crate :: array:: unsupported_buffer_replacement;
30+ use crate :: arrays:: ExtensionArray ;
2931use crate :: arrays:: constant:: ConstantData ;
3032use crate :: arrays:: constant:: compute:: rules:: PARENT_RULES ;
3133use crate :: arrays:: constant:: vtable:: canonical:: constant_canonicalize;
3234use crate :: buffer:: BufferHandle ;
3335use crate :: builders:: ArrayBuilder ;
3436use crate :: builders:: BoolBuilder ;
3537use crate :: builders:: DecimalBuilder ;
36- use crate :: builders:: ExtensionBuilder ;
3738use crate :: builders:: FixedSizeListBuilder ;
39+ use crate :: builders:: ListViewBuilder ;
3840use crate :: builders:: NullBuilder ;
3941use crate :: builders:: PrimitiveBuilder ;
40- use crate :: builders:: StructBuilder ;
4142use crate :: builders:: VarBinViewBuilder ;
43+ use crate :: builders:: builder_with_capacity;
4244use crate :: canonical:: Canonical ;
4345use crate :: dtype:: DType ;
46+ use crate :: dtype:: OffsetBuilderPType ;
4447use crate :: match_each_decimal_value;
45- use crate :: match_each_list_builder ;
48+ use crate :: match_each_listview_builder ;
4649use crate :: match_each_native_ptype;
4750use crate :: match_each_varbin_builder;
4851use crate :: scalar:: DecimalValue ;
52+ use crate :: scalar:: ListScalar ;
4953use crate :: scalar:: Scalar ;
5054use crate :: scalar:: ScalarValue ;
5155use crate :: serde:: ArrayChildren ;
@@ -257,20 +261,22 @@ impl VTable for Constant {
257261 }
258262 }
259263 DType :: List ( ..) => append_constant_list_run ( array, n, builder, ctx) ?,
260- DType :: Struct ( ..) => match builder. as_any_mut ( ) . downcast_mut :: < StructBuilder > ( ) {
261- Some ( b) => b. append_constant ( scalar. as_struct ( ) , n, ctx) ?,
262- None => append_via_canonical ( array, builder, ctx) ?,
263- } ,
264- DType :: Extension ( ..) => match builder. as_any_mut ( ) . downcast_mut :: < ExtensionBuilder > ( ) {
265- Some ( b) => b. append_constant ( scalar. as_extension ( ) , n, ctx) ?,
266- None => append_via_canonical ( array, builder, ctx) ?,
267- } ,
264+ DType :: Extension ( ext_dtype) => {
265+ // An extension array is its storage wearing a dtype, so a run of identical values
266+ // is a constant storage array, which stays constant-encoded in the builder.
267+ // Canonicalizing instead would materialize the storage: see the note in
268+ // `constant_canonicalize` about `ExtensionConstantRule`.
269+ let storage = ConstantArray :: new ( scalar. as_extension ( ) . to_storage_scalar ( ) , n) ;
270+ ExtensionArray :: new ( ext_dtype. clone ( ) , storage. into_array ( ) )
271+ . into_array ( )
272+ . append_to_builder ( builder, ctx) ?
273+ }
268274 DType :: FixedSizeList ( ..) => {
269- match builder. as_any_mut ( ) . downcast_mut :: < FixedSizeListBuilder > ( ) {
270- Some ( b) => b. append_constant ( scalar. as_list ( ) , n, ctx) ?,
271- None => append_via_canonical ( array, builder, ctx) ?,
272- }
275+ append_constant_fixed_size_list_run ( array, n, builder, ctx) ?
273276 }
277+ // The remaining dtypes canonicalize cheaply: a constant struct canonicalizes to
278+ // constant fields, and a constant map to views sharing one copy of the entries, so
279+ // appending the canonical array preserves the run's economy.
274280 // TODO: add a fast path for DType::Union once it has a builder.
275281 _ => append_via_canonical ( array, builder, ctx) ?,
276282 }
@@ -279,20 +285,22 @@ impl VTable for Constant {
279285 }
280286}
281287
282- /// Appends the constant list `array` as one repeated run per list builder .
288+ /// Appends the constant list `array` as one run sharing a single copy of its elements .
283289///
284- /// Only the concrete list builders can record a run from the scalar; any other builder for a list
285- /// dtype has to go the long way around.
286- // The complexity comes from the expansion of `match_each_list_builder!`.
287- #[ expect( clippy:: cognitive_complexity) ]
290+ /// The list's elements materialize once, and
291+ /// [`ListViewBuilder::append_array_as_repeated_list`] points the run's `n` views at that one
292+ /// copy. Only a list-view builder has a layout that can share elements; any other builder for a
293+ /// list dtype - a [`ListBuilder`](crate::builders::ListBuilder), whose offsets can only describe
294+ /// contiguous lists - appends the canonical run instead.
288295fn append_constant_list_run (
289296 array : ArrayView < ' _ , Constant > ,
290297 n : usize ,
291298 builder : & mut dyn ArrayBuilder ,
292299 ctx : & mut ExecutionCtx ,
293300) -> VortexResult < ( ) > {
294301 let scalar = array. scalar ( ) ;
295- match match_each_list_builder ! ( builder, |b| b. append_constant_list(
302+ match match_each_listview_builder ! ( builder, |b| append_repeated_list_run(
303+ b,
296304 scalar. as_list( ) ,
297305 n,
298306 ctx
@@ -302,6 +310,71 @@ fn append_constant_list_run(
302310 }
303311}
304312
313+ /// Appends the list `scalar` to a [`ListViewBuilder`] `n` times, storing its elements once.
314+ fn append_repeated_list_run < O : OffsetBuilderPType , S : OffsetBuilderPType > (
315+ builder : & mut ListViewBuilder < O , S > ,
316+ scalar : ListScalar ,
317+ n : usize ,
318+ ctx : & mut ExecutionCtx ,
319+ ) -> VortexResult < ( ) > {
320+ if n == 0 {
321+ return Ok ( ( ) ) ;
322+ }
323+
324+ let Some ( elements) = scalar. elements ( ) else {
325+ // A null run stores no elements at all.
326+ builder. append_nulls ( n) ;
327+ return Ok ( ( ) ) ;
328+ } ;
329+
330+ let mut elements_builder = builder_with_capacity ( scalar. element_dtype ( ) , elements. len ( ) ) ;
331+ for element in & elements {
332+ elements_builder. append_scalar ( element) ?;
333+ }
334+
335+ builder. append_array_as_repeated_list ( & elements_builder. finish ( ) , n, ctx)
336+ }
337+
338+ /// Appends the constant fixed-size-list `array` as its list's elements tiled `n` times.
339+ ///
340+ /// The list's elements materialize into a tile once - a single [`ConstantArray`] when they are
341+ /// all the same scalar, so that the tiling costs nothing - and
342+ /// [`FixedSizeListBuilder::append_array_as_repeated_list`] shares that one tile across the run.
343+ fn append_constant_fixed_size_list_run (
344+ array : ArrayView < ' _ , Constant > ,
345+ n : usize ,
346+ builder : & mut dyn ArrayBuilder ,
347+ ctx : & mut ExecutionCtx ,
348+ ) -> VortexResult < ( ) > {
349+ let Some ( builder) = builder. as_any_mut ( ) . downcast_mut :: < FixedSizeListBuilder > ( ) else {
350+ return append_via_canonical ( array, builder, ctx) ;
351+ } ;
352+
353+ if n == 0 {
354+ return Ok ( ( ) ) ;
355+ }
356+
357+ let scalar = array. scalar ( ) . as_list ( ) ;
358+ let Some ( elements) = scalar. elements ( ) else {
359+ // A null run stores no elements of its own, only the placeholders the builder writes.
360+ builder. append_nulls ( n) ;
361+ return Ok ( ( ) ) ;
362+ } ;
363+
364+ let tile = match elements. iter ( ) . all_equal_value ( ) {
365+ Ok ( uniform) => ConstantArray :: new ( uniform. clone ( ) , elements. len ( ) ) . into_array ( ) ,
366+ Err ( _) => {
367+ let mut tile_builder = builder_with_capacity ( builder. element_dtype ( ) , elements. len ( ) ) ;
368+ for element in & elements {
369+ tile_builder. append_scalar ( element) ?;
370+ }
371+ tile_builder. finish ( )
372+ }
373+ } ;
374+
375+ builder. append_array_as_repeated_list ( & tile, n, ctx)
376+ }
377+
305378/// Appends `array` by canonicalizing it first, for the dtypes with no fast path of their own.
306379fn append_via_canonical (
307380 array : ArrayView < ' _ , Constant > ,
@@ -347,16 +420,22 @@ mod tests {
347420
348421 use crate :: IntoArray ;
349422 use crate :: VortexSessionExecute ;
423+ use crate :: arrays:: Chunked ;
350424 use crate :: arrays:: Constant ;
351425 use crate :: arrays:: ConstantArray ;
352426 use crate :: arrays:: Extension ;
353427 use crate :: arrays:: FixedSizeList ;
428+ use crate :: arrays:: ListView ;
354429 use crate :: arrays:: Struct ;
430+ use crate :: arrays:: chunked:: ChunkedArrayExt ;
355431 use crate :: arrays:: constant:: vtable:: canonical:: constant_canonicalize;
356432 use crate :: arrays:: extension:: ExtensionArrayExt ;
357433 use crate :: arrays:: fixed_size_list:: FixedSizeListArraySlotsExt ;
434+ use crate :: arrays:: listview:: ListViewArraySlotsExt ;
358435 use crate :: arrays:: struct_:: StructArrayExt ;
359436 use crate :: assert_arrays_eq;
437+ use crate :: builders:: ArrayBuilder ;
438+ use crate :: builders:: ListBuilder ;
360439 use crate :: builders:: builder_with_capacity;
361440 use crate :: dtype:: DType ;
362441 use crate :: dtype:: Nullability ;
@@ -470,6 +549,79 @@ mod tests {
470549 ) )
471550 }
472551
552+ #[ rstest]
553+ #[ case:: non_empty( vec![ Scalar :: from( 1i32 ) , Scalar :: from( 2i32 ) ] , 4 ) ]
554+ #[ case:: empty( vec![ ] , 3 ) ]
555+ #[ case:: n_zero( vec![ Scalar :: from( 1i32 ) ] , 0 ) ]
556+ fn test_list_constant_append (
557+ #[ case] elements : Vec < Scalar > ,
558+ #[ case] n : usize ,
559+ ) -> VortexResult < ( ) > {
560+ let scalar = Scalar :: list (
561+ Arc :: new ( DType :: Primitive ( PType :: I32 , Nullability :: NonNullable ) ) ,
562+ elements,
563+ Nullability :: NonNullable ,
564+ ) ;
565+ assert_append_matches_canonical ( ConstantArray :: new ( scalar, n) )
566+ }
567+
568+ #[ test]
569+ fn test_null_list_constant_append ( ) -> VortexResult < ( ) > {
570+ let dtype = DType :: List (
571+ Arc :: new ( DType :: Primitive ( PType :: I32 , Nullability :: NonNullable ) ) ,
572+ Nullability :: Nullable ,
573+ ) ;
574+ assert_append_matches_canonical ( ConstantArray :: new ( Scalar :: null ( dtype) , 3 ) )
575+ }
576+
577+ /// A run of identical lists appended into a list-view builder shares one copy of its elements
578+ /// across the whole run.
579+ #[ test]
580+ fn test_list_constant_append_keeps_one_copy_of_the_elements ( ) -> VortexResult < ( ) > {
581+ let mut ctx = crate :: array_session ( ) . create_execution_ctx ( ) ;
582+ let scalar = Scalar :: list (
583+ Arc :: new ( DType :: Primitive ( PType :: I32 , Nullability :: NonNullable ) ) ,
584+ vec ! [ Scalar :: from( 1i32 ) , Scalar :: from( 2i32 ) , Scalar :: from( 3i32 ) ] ,
585+ Nullability :: NonNullable ,
586+ ) ;
587+ let array = ConstantArray :: new ( scalar, 1_000 ) ;
588+
589+ let mut builder = builder_with_capacity ( array. dtype ( ) , array. len ( ) ) ;
590+ array
591+ . into_array ( )
592+ . append_to_builder ( builder. as_mut ( ) , & mut ctx) ?;
593+ let result = builder. finish ( ) ;
594+
595+ assert_eq ! (
596+ result. as_:: <ListView >( ) . elements( ) . len( ) ,
597+ 3 ,
598+ "the run's elements should be stored once, not once per row" ,
599+ ) ;
600+ Ok ( ( ) )
601+ }
602+
603+ /// A `ListBuilder`'s offsets can only describe contiguous lists, so a constant run cannot
604+ /// share its elements there and takes the canonical path instead.
605+ #[ test]
606+ fn test_list_constant_append_into_list_builder ( ) -> VortexResult < ( ) > {
607+ let mut ctx = crate :: array_session ( ) . create_execution_ctx ( ) ;
608+ let element_dtype: Arc < DType > =
609+ Arc :: new ( DType :: Primitive ( PType :: I32 , Nullability :: NonNullable ) ) ;
610+ let scalar = Scalar :: list (
611+ Arc :: clone ( & element_dtype) ,
612+ vec ! [ Scalar :: from( 1i32 ) , Scalar :: from( 2i32 ) ] ,
613+ Nullability :: NonNullable ,
614+ ) ;
615+ let array = ConstantArray :: new ( scalar, 4 ) . into_array ( ) ;
616+
617+ let mut builder =
618+ ListBuilder :: < u32 > :: with_capacity ( element_dtype, Nullability :: NonNullable , 0 , 0 ) ;
619+ array. append_to_builder ( & mut builder, & mut ctx) ?;
620+
621+ assert_arrays_eq ! ( & builder. finish( ) , & array, & mut ctx) ;
622+ Ok ( ( ) )
623+ }
624+
473625 #[ test]
474626 fn test_struct_constant_append ( ) -> VortexResult < ( ) > {
475627 let fields = StructFields :: new (
@@ -558,8 +710,8 @@ mod tests {
558710 assert_append_matches_canonical ( ConstantArray :: new ( Scalar :: null ( dtype) , 3 ) )
559711 }
560712
561- /// A fixed-size list whose elements are all the same scalar tiles to a constant array, so the
562- /// elements child should not be materialized at all .
713+ /// A fixed-size list whose elements are all the same scalar tiles a constant array, so the
714+ /// tile's chunks stay constant-encoded rather than materializing a value per row .
563715 #[ test]
564716 fn test_uniform_fixed_size_list_constant_append_keeps_elements_constant ( ) -> VortexResult < ( ) > {
565717 let mut ctx = crate :: array_session ( ) . create_execution_ctx ( ) ;
@@ -576,8 +728,12 @@ mod tests {
576728 . append_to_builder ( builder. as_mut ( ) , & mut ctx) ?;
577729 let result = builder. finish ( ) ;
578730
731+ let elements = result. as_ :: < FixedSizeList > ( ) . elements ( ) . clone ( ) ;
579732 assert ! (
580- result. as_:: <FixedSizeList >( ) . elements( ) . is:: <Constant >( ) ,
733+ elements
734+ . as_:: <Chunked >( )
735+ . iter_chunks( )
736+ . all( |chunk| chunk. is:: <Constant >( ) ) ,
581737 "a uniform tile should have stayed constant-encoded" ,
582738 ) ;
583739 Ok ( ( ) )
0 commit comments