11// SPDX-License-Identifier: Apache-2.0
22// SPDX-FileCopyrightText: Copyright the Vortex contributors
33
4+ use std:: hash:: Hash ;
45use std:: sync:: Arc ;
56
67use arrow_array:: ArrayRef as ArrowArrayRef ;
@@ -9,21 +10,37 @@ use arrow_array::PrimitiveArray;
910use arrow_array:: cast:: AsArray ;
1011use arrow_array:: new_null_array;
1112use arrow_array:: types:: * ;
13+ use arrow_schema:: ArrowError ;
1214use arrow_schema:: DataType ;
15+ use rustc_hash:: FxBuildHasher ;
1316use vortex_array:: ArrayRef ;
1417use vortex_array:: ExecutionCtx ;
1518use vortex_array:: IntoArray ;
1619use vortex_array:: arrays:: Constant ;
1720use vortex_array:: arrays:: ConstantArray ;
1821use vortex_array:: arrays:: Dict ;
1922use vortex_array:: arrays:: DictArray ;
23+ use vortex_array:: arrays:: PrimitiveArray as VortexPrimitiveArray ;
2024use vortex_array:: arrays:: dict:: DictArraySlotsExt ;
25+ use vortex_array:: arrays:: primitive:: NativeValue ;
26+ use vortex_array:: dtype:: DType ;
27+ use vortex_array:: dtype:: NativePType ;
28+ use vortex_array:: dtype:: UnsignedPType ;
29+ use vortex_array:: match_each_native_ptype;
30+ use vortex_array:: match_each_unsigned_integer_ptype;
2131use vortex_array:: matcher:: Matcher ;
32+ use vortex_array:: validity:: Validity ;
33+ use vortex_buffer:: BufferMut ;
2234use vortex_error:: VortexError ;
35+ use vortex_error:: VortexExpect ;
2336use vortex_error:: VortexResult ;
2437use vortex_error:: vortex_bail;
38+ use vortex_mask:: Mask ;
39+ use vortex_utils:: aliases:: hash_map:: Entry ;
40+ use vortex_utils:: aliases:: hash_map:: HashMap ;
2541
2642use crate :: ArrowExporter ;
43+ use crate :: dtype:: ptype_from_arrow;
2744use crate :: executor:: execute_arrow_naive;
2845
2946/// Matches the encodings [`to_arrow_dictionary`] requires for export.
@@ -55,6 +72,13 @@ pub(super) fn to_arrow_dictionary(
5572 Err ( array) => array,
5673 } ;
5774
75+ if let DType :: Primitive ( ptype, _) = array. dtype ( )
76+ && ptype_from_arrow ( values_type) . ok ( ) == Some ( * ptype)
77+ {
78+ let array = array. execute :: < VortexPrimitiveArray > ( ctx) ?;
79+ return primitive_to_dict ( array, codes_type, values_type, exporter, ctx) ;
80+ }
81+
5882 // Otherwise, we should try and build a dictionary.
5983 // Arrow hides this functionality inside the cast module!
6084 let array = execute_arrow_naive ( array, Some ( values_type) , exporter, ctx) ?;
@@ -65,6 +89,74 @@ pub(super) fn to_arrow_dictionary(
6589 . map_err ( VortexError :: from)
6690}
6791
92+ /// Dictionary-encode a primitive array into the dictionary `arrow_cast` builds: values in order of
93+ /// first occurrence and null rows as null keys. Arrow's builder hashes with SipHash and looks up
94+ /// every new value twice, which dominated exporting columns that are stored without a dictionary.
95+ fn primitive_to_dict (
96+ array : VortexPrimitiveArray ,
97+ codes_type : & DataType ,
98+ values_type : & DataType ,
99+ exporter : & ArrowExporter < ' _ > ,
100+ ctx : & mut ExecutionCtx ,
101+ ) -> VortexResult < ArrowArrayRef > {
102+ let codes_ptype = ptype_from_arrow ( codes_type) ?;
103+ let max_code = codes_ptype. max_value_as_u64 ( ) ;
104+ let dict = match_each_unsigned_integer_ptype ! ( codes_ptype. to_unsigned( ) , |C | {
105+ match_each_native_ptype!( array. ptype( ) , |T | {
106+ encode_primitive:: <T , C >( & array, max_code, ctx) ?
107+ } )
108+ } ) ;
109+ dict_to_dict ( dict, codes_type, values_type, exporter, ctx)
110+ }
111+
112+ fn encode_primitive < T : NativePType , C : UnsignedPType > (
113+ array : & VortexPrimitiveArray ,
114+ max_code : u64 ,
115+ ctx : & mut ExecutionCtx ,
116+ ) -> VortexResult < DictArray >
117+ where
118+ NativeValue < T > : Hash + Eq ,
119+ {
120+ let len = array. len ( ) ;
121+ let validity = array. validity ( ) ?;
122+ // Sized for all-distinct input like Arrow's builder: growing the table rehashes every entry.
123+ let mut lookup = HashMap :: < NativeValue < T > , C , _ > :: with_capacity_and_hasher ( len, FxBuildHasher ) ;
124+ let mut values = BufferMut :: < T > :: with_capacity ( len) ;
125+ let mut code = |value : T | -> VortexResult < C > {
126+ match lookup. entry ( NativeValue ( value) ) {
127+ Entry :: Occupied ( entry) => Ok ( * entry. get ( ) ) ,
128+ Entry :: Vacant ( entry) => {
129+ if values. len ( ) as u64 > max_code {
130+ return Err ( ArrowError :: DictionaryKeyOverflowError . into ( ) ) ;
131+ }
132+ let code = C :: from_usize ( values. len ( ) ) . vortex_expect ( "code fits the key type" ) ;
133+ values. push ( value) ;
134+ Ok ( * entry. insert ( code) )
135+ }
136+ }
137+ } ;
138+
139+ let mut codes = BufferMut :: < C > :: with_capacity ( len) ;
140+ match validity. execute_mask ( len, ctx) ? {
141+ Mask :: AllTrue ( _) => {
142+ for & value in array. as_slice :: < T > ( ) {
143+ codes. push ( code ( value) ?) ;
144+ }
145+ }
146+ Mask :: AllFalse ( _) => codes. push_n ( C :: default ( ) , len) ,
147+ Mask :: Values ( mask) => {
148+ for ( & value, valid) in array. as_slice :: < T > ( ) . iter ( ) . zip ( mask. bit_buffer ( ) . iter ( ) ) {
149+ codes. push ( if valid { code ( value) ? } else { C :: default ( ) } ) ;
150+ }
151+ }
152+ }
153+
154+ let codes = VortexPrimitiveArray :: new ( codes. freeze ( ) , validity) ;
155+ let values = VortexPrimitiveArray :: new ( values. freeze ( ) , Validity :: NonNullable ) ;
156+ // SAFETY: every code indexes a value pushed above.
157+ Ok ( unsafe { DictArray :: new_unchecked ( codes. into_array ( ) , values. into_array ( ) ) } )
158+ }
159+
68160/// Convert a constant array to a dictionary with a single entry.
69161fn constant_to_dict (
70162 array : ConstantArray ,
@@ -249,6 +341,42 @@ mod tests {
249341 Ok ( ( ) )
250342 }
251343
344+ /// Primitive arrays stored without a dictionary build exactly the dictionary `arrow_cast` does.
345+ #[ rstest]
346+ #[ case:: u64_with_nulls(
347+ PrimitiveArray :: from_option_iter( [ Some ( 7u64 ) , None , Some ( 9 ) , Some ( 7 ) , None , Some ( 1 ) ] ) . into_array( ) ,
348+ DataType :: UInt32 ,
349+ DataType :: UInt64 ,
350+ ) ]
351+ #[ case:: signed_keys( buffer![ -1i32 , 5 , -1 , 5 , 5 , 2 ] . into_array( ) , DataType :: Int8 , DataType :: Int32 ) ]
352+ #[ case:: float_bits(
353+ buffer![ 0.0f64 , -0.0 , f64 :: NAN , 0.0 , f64 :: NAN ] . into_array( ) ,
354+ DataType :: UInt16 ,
355+ DataType :: Float64 ,
356+ ) ]
357+ #[ case:: all_null(
358+ PrimitiveArray :: from_option_iter( [ None :: <u32 >, None ] ) . into_array( ) ,
359+ DataType :: UInt32 ,
360+ DataType :: UInt32 ,
361+ ) ]
362+ fn primitive_matches_arrow_cast (
363+ #[ case] array : vortex_array:: ArrayRef ,
364+ #[ case] codes_type : DataType ,
365+ #[ case] values_type : DataType ,
366+ ) -> VortexResult < ( ) > {
367+ let target_type = dict_type ( codes_type, values_type. clone ( ) ) ;
368+ let expected = arrow_cast:: cast ( & execute ( array. clone ( ) , & values_type) ?, & target_type) ?;
369+ let actual = execute ( array, & target_type) ?;
370+ assert_eq ! ( actual. as_ref( ) , expected. as_ref( ) ) ;
371+ Ok ( ( ) )
372+ }
373+
374+ #[ test]
375+ fn primitive_overflowing_key_type_errors ( ) {
376+ let array = PrimitiveArray :: from_iter ( 0u32 ..300 ) . into_array ( ) ;
377+ assert ! ( execute( array, & dict_type( DataType :: UInt8 , DataType :: UInt32 ) ) . is_err( ) ) ;
378+ }
379+
252380 #[ test]
253381 fn mask_wrapped_dict_exports ( ) -> VortexResult < ( ) > {
254382 // Dictionary behind a lazy `mask` scalar-fn — the shape a scan produces when a row
0 commit comments