Skip to main content

vortex_array/arrays/dict/
array.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Display;
5use std::fmt::Formatter;
6
7use num_traits::AsPrimitive;
8use smallvec::smallvec;
9use vortex_buffer::BitBuffer;
10use vortex_error::VortexExpect;
11use vortex_error::VortexResult;
12use vortex_error::vortex_bail;
13use vortex_error::vortex_ensure;
14use vortex_mask::AllOr;
15
16use crate::ArrayRef;
17use crate::ArraySlots;
18use crate::ExecutionCtx;
19use crate::array::Array;
20use crate::array::ArrayParts;
21use crate::array::TypedArrayRef;
22use crate::array_slots;
23use crate::arrays::Dict;
24use crate::arrays::PrimitiveArray;
25use crate::dtype::DType;
26use crate::dtype::PType;
27use crate::match_each_integer_ptype;
28
29#[derive(Clone, prost::Message)]
30pub struct DictMetadata {
31    #[prost(uint32, tag = "1")]
32    pub(super) values_len: u32,
33    #[prost(enumeration = "PType", tag = "2")]
34    pub(super) codes_ptype: i32,
35    // nullable codes are optional since they were added after stabilisation.
36    #[prost(optional, bool, tag = "3")]
37    pub(super) is_nullable_codes: Option<bool>,
38    // all_values_referenced is optional for backward compatibility.
39    // true = all dictionary values are definitely referenced by at least one code.
40    // false/None = unknown whether all values are referenced (conservative default).
41    #[prost(optional, bool, tag = "4")]
42    pub(super) all_values_referenced: Option<bool>,
43}
44
45#[array_slots(Dict)]
46pub struct DictSlots {
47    /// The codes array mapping each element to a dictionary entry.
48    pub codes: ArrayRef,
49    /// The dictionary values array containing the unique values.
50    pub values: ArrayRef,
51}
52
53#[derive(Debug, Clone)]
54pub struct DictData {
55    /// Indicates whether all dictionary values are definitely referenced by at least one code.
56    /// `true` = all values are referenced (computed during encoding).
57    /// `false` = unknown/might have unreferenced values.
58    /// In case this is incorrect never use this to enable memory unsafe behaviour just semantically
59    /// incorrect behaviour.
60    pub(super) all_values_referenced: bool,
61}
62
63impl Display for DictData {
64    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65        write!(f, "all_values_referenced: {}", self.all_values_referenced)
66    }
67}
68
69impl DictData {
70    /// Build a new `DictArray` without validating the codes or values.
71    ///
72    /// # Safety
73    /// This should be called only when you can guarantee the invariants checked
74    /// by the safe `DictArray::try_new` constructor are valid, for example when
75    /// you are filtering or slicing an existing valid `DictArray`.
76    pub unsafe fn new_unchecked() -> Self {
77        Self {
78            all_values_referenced: false,
79        }
80    }
81
82    /// Set whether all dictionary values are definitely referenced.
83    ///
84    /// # Safety
85    /// The caller must ensure that when setting `all_values_referenced = true`, ALL dictionary
86    /// values are actually referenced by at least one valid code. Setting this incorrectly can
87    /// lead to incorrect query results in operations like min/max.
88    ///
89    /// This is typically only set to `true` during dictionary encoding when we know for certain
90    /// that all values are referenced.
91    pub unsafe fn set_all_values_referenced(mut self, all_values_referenced: bool) -> Self {
92        self.all_values_referenced = all_values_referenced;
93        self
94    }
95
96    /// Build a new `DictArray` from its components, `codes` and `values`.
97    ///
98    /// This constructor will panic if `codes` or `values` do not pass validation for building
99    /// a new `DictArray`. See `DictArray::try_new` for a description of the error conditions.
100    pub fn new(codes_dtype: &DType) -> Self {
101        Self::try_new(codes_dtype).vortex_expect("DictArray new")
102    }
103
104    /// Build a new `DictArray` from its components, `codes` and `values`.
105    ///
106    /// The codes must be integers, and may be nullable. Values can be any
107    /// type, and may also be nullable. This mirrors the nullability of the Arrow `DictionaryArray`.
108    ///
109    /// # Errors
110    ///
111    /// The `codes` **must** be integers, and the maximum code must be less than the length
112    /// of the `values` array. Otherwise, this constructor returns an error.
113    ///
114    /// It is an error to provide a nullable `codes` with non-nullable `values`.
115    pub(crate) fn try_new(codes_dtype: &DType) -> VortexResult<Self> {
116        if !codes_dtype.is_int() {
117            vortex_bail!(MismatchedTypes: "int", codes_dtype);
118        }
119
120        Ok(unsafe { Self::new_unchecked() })
121    }
122}
123
124pub trait DictArrayExt: TypedArrayRef<Dict> + DictArraySlotsExt {
125    #[inline]
126    fn has_all_values_referenced(&self) -> bool {
127        self.all_values_referenced
128    }
129
130    fn validate_all_values_referenced(&self, ctx: &mut ExecutionCtx) -> VortexResult<()> {
131        if self.has_all_values_referenced() {
132            if !self.codes().is_host() {
133                return Ok(());
134            }
135
136            let referenced_mask = self.compute_referenced_values_mask(true, ctx)?;
137            let all_referenced = referenced_mask.true_count() == referenced_mask.len();
138
139            vortex_ensure!(all_referenced, "value in dict not referenced");
140        }
141
142        Ok(())
143    }
144
145    fn compute_referenced_values_mask(
146        &self,
147        referenced: bool,
148        ctx: &mut ExecutionCtx,
149    ) -> VortexResult<BitBuffer> {
150        let codes = self.codes();
151        let codes_validity = codes.validity()?.execute_mask(codes.len(), ctx)?;
152        let codes_primitive = codes.clone().execute::<PrimitiveArray>(ctx)?;
153        let values_len = self.values().len();
154
155        let init_value = !referenced;
156        let referenced_value = referenced;
157
158        let mut values_vec = vec![init_value; values_len];
159        match codes_validity.bit_buffer() {
160            AllOr::All => {
161                match_each_integer_ptype!(codes_primitive.ptype(), |P| {
162                    for idx in codes_primitive.as_slice::<P>() {
163                        let idxu: usize = idx.as_();
164                        values_vec[idxu] = referenced_value;
165                    }
166                });
167            }
168            AllOr::None => {}
169            AllOr::Some(mask) => {
170                match_each_integer_ptype!(codes_primitive.ptype(), |P| {
171                    let codes = codes_primitive.as_slice::<P>();
172                    mask.set_indices().for_each(|idx| {
173                        let idxu: usize = codes[idx].as_();
174                        values_vec[idxu] = referenced_value;
175                    });
176                });
177            }
178        }
179
180        Ok(BitBuffer::from(values_vec))
181    }
182}
183impl<T: TypedArrayRef<Dict>> DictArrayExt for T {}
184
185/// Concrete parts of a [`DictArray`](super::DictArray) after iterative execution.
186pub struct DictParts {
187    pub dtype: DType,
188    pub codes: ArrayRef,
189    pub values: ArrayRef,
190}
191
192pub trait DictOwnedExt {
193    fn into_parts(self) -> DictParts;
194}
195
196impl DictOwnedExt for Array<Dict> {
197    fn into_parts(self) -> DictParts {
198        match self.try_into_parts() {
199            Ok(array_parts) => {
200                let slots = DictSlots::from_slots(array_parts.slots);
201                DictParts {
202                    dtype: array_parts.dtype,
203                    codes: slots.codes,
204                    values: slots.values,
205                }
206            }
207            Err(array) => {
208                let slots = DictSlotsView::from_slots(array.slots());
209                DictParts {
210                    dtype: array.dtype().clone(),
211                    codes: slots.codes.clone(),
212                    values: slots.values.clone(),
213                }
214            }
215        }
216    }
217}
218
219impl Array<Dict> {
220    /// Build a new `DictArray` from its components, `codes` and `values`.
221    pub fn new(codes: ArrayRef, values: ArrayRef) -> Self {
222        Self::try_new(codes, values).vortex_expect("DictArray new")
223    }
224
225    /// Build a new `DictArray` from its components, `codes` and `values`.
226    pub fn try_new(codes: ArrayRef, values: ArrayRef) -> VortexResult<Self> {
227        let dtype = values
228            .dtype()
229            .union_nullability(codes.dtype().nullability());
230        let len = codes.len();
231        let data = DictData::try_new(codes.dtype())?;
232        Array::try_from_parts(
233            ArrayParts::new(Dict, dtype, len, data)
234                .with_slots(smallvec![Some(codes), Some(values)]),
235        )
236    }
237
238    /// Build a new `DictArray` without validating the codes or values.
239    ///
240    /// # Safety
241    ///
242    /// See [`DictData::new_unchecked`].
243    pub unsafe fn new_unchecked(codes: ArrayRef, values: ArrayRef) -> Self {
244        let dtype = values
245            .dtype()
246            .union_nullability(codes.dtype().nullability());
247        let len = codes.len();
248        let data = unsafe { DictData::new_unchecked() };
249        unsafe {
250            Array::from_parts_unchecked(
251                ArrayParts::new(Dict, dtype, len, data)
252                    .with_slots(smallvec![Some(codes), Some(values)]),
253            )
254        }
255    }
256
257    /// Set whether all values in the dictionary are referenced by at least one code.
258    ///
259    /// # Safety
260    ///
261    /// See [`DictData::set_all_values_referenced`].
262    pub unsafe fn set_all_values_referenced(self, all_values_referenced: bool) -> Self {
263        let dtype = self.dtype().clone();
264        let len = self.len();
265        let slots: ArraySlots = self.slots().iter().cloned().collect();
266        let data = unsafe {
267            self.into_data()
268                .set_all_values_referenced(all_values_referenced)
269        };
270        unsafe {
271            Array::from_parts_unchecked(ArrayParts::new(Dict, dtype, len, data).with_slots(slots))
272        }
273    }
274}
275
276#[cfg(test)]
277mod test {
278    use rand::RngExt;
279    use rand::SeedableRng;
280    use rand::distr::Distribution;
281    use rand::distr::StandardUniform;
282    use rand::prelude::StdRng;
283    use vortex_buffer::BitBuffer;
284    use vortex_buffer::buffer;
285    use vortex_error::VortexExpect;
286    use vortex_error::VortexResult;
287    use vortex_error::vortex_panic;
288    use vortex_mask::AllOr;
289
290    use crate::ArrayRef;
291    use crate::IntoArray;
292    use crate::VortexSessionExecute;
293    use crate::array_session;
294    use crate::arrays::ChunkedArray;
295    use crate::arrays::DictArray;
296    use crate::arrays::PrimitiveArray;
297    use crate::assert_arrays_eq;
298    use crate::builders::builder_with_capacity;
299    use crate::dtype::DType;
300    use crate::dtype::NativePType;
301    use crate::dtype::Nullability::NonNullable;
302    use crate::dtype::PType;
303    use crate::dtype::UnsignedPType;
304    use crate::validity::Validity;
305
306    #[test]
307    fn nullable_codes_validity() {
308        let dict = DictArray::try_new(
309            PrimitiveArray::new(
310                buffer![0u32, 1, 2, 2, 1],
311                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
312            )
313            .into_array(),
314            PrimitiveArray::new(buffer![3, 6, 9], Validity::AllValid).into_array(),
315        )
316        .unwrap();
317        let mask = dict
318            .as_ref()
319            .validity()
320            .unwrap()
321            .execute_mask(
322                dict.as_ref().len(),
323                &mut array_session().create_execution_ctx(),
324            )
325            .unwrap();
326        let AllOr::Some(indices) = mask.indices() else {
327            vortex_panic!("Expected indices from mask")
328        };
329        assert_eq!(indices, [0, 2, 4]);
330    }
331
332    #[test]
333    fn nullable_values_validity() {
334        let dict = DictArray::try_new(
335            buffer![0u32, 1, 2, 2, 1].into_array(),
336            PrimitiveArray::new(
337                buffer![3, 6, 9],
338                Validity::from(BitBuffer::from(vec![true, false, false])),
339            )
340            .into_array(),
341        )
342        .unwrap();
343        let mask = dict
344            .as_ref()
345            .validity()
346            .unwrap()
347            .execute_mask(
348                dict.as_ref().len(),
349                &mut array_session().create_execution_ctx(),
350            )
351            .unwrap();
352        let AllOr::Some(indices) = mask.indices() else {
353            vortex_panic!("Expected indices from mask")
354        };
355        assert_eq!(indices, [0]);
356    }
357
358    #[test]
359    fn nullable_codes_and_values() {
360        let dict = DictArray::try_new(
361            PrimitiveArray::new(
362                buffer![0u32, 1, 2, 2, 1],
363                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
364            )
365            .into_array(),
366            PrimitiveArray::new(
367                buffer![3, 6, 9],
368                Validity::from(BitBuffer::from(vec![false, true, true])),
369            )
370            .into_array(),
371        )
372        .unwrap();
373        let mask = dict
374            .as_ref()
375            .validity()
376            .unwrap()
377            .execute_mask(
378                dict.as_ref().len(),
379                &mut array_session().create_execution_ctx(),
380            )
381            .unwrap();
382        let AllOr::Some(indices) = mask.indices() else {
383            vortex_panic!("Expected indices from mask")
384        };
385        assert_eq!(indices, [2, 4]);
386    }
387
388    #[test]
389    fn nullable_codes_and_non_null_values() {
390        let dict = DictArray::try_new(
391            PrimitiveArray::new(
392                buffer![0u32, 1, 2, 2, 1],
393                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
394            )
395            .into_array(),
396            PrimitiveArray::new(buffer![3, 6, 9], Validity::NonNullable).into_array(),
397        )
398        .unwrap();
399        let mask = dict
400            .as_ref()
401            .validity()
402            .unwrap()
403            .execute_mask(
404                dict.as_ref().len(),
405                &mut array_session().create_execution_ctx(),
406            )
407            .unwrap();
408        let AllOr::Some(indices) = mask.indices() else {
409            vortex_panic!("Expected indices from mask")
410        };
411        assert_eq!(indices, [0, 2, 4]);
412    }
413
414    fn make_dict_primitive_chunks<T: NativePType, Code: UnsignedPType>(
415        len: usize,
416        unique_values: usize,
417        chunk_count: usize,
418    ) -> ArrayRef
419    where
420        StandardUniform: Distribution<T>,
421    {
422        let mut rng = StdRng::seed_from_u64(0);
423
424        (0..chunk_count)
425            .map(|_| {
426                let values = (0..unique_values)
427                    .map(|_| rng.random::<T>())
428                    .collect::<PrimitiveArray>();
429                let codes = (0..len)
430                    .map(|_| {
431                        Code::from(rng.random_range(0..unique_values)).vortex_expect("valid value")
432                    })
433                    .collect::<PrimitiveArray>();
434
435                DictArray::try_new(codes.into_array(), values.into_array())
436                    .vortex_expect("DictArray creation should succeed in arbitrary impl")
437                    .into_array()
438            })
439            .collect::<ChunkedArray>()
440            .into_array()
441    }
442
443    #[test]
444    fn test_dict_array_from_primitive_chunks() -> VortexResult<()> {
445        let mut ctx = array_session().create_execution_ctx();
446        let len = 2;
447        let chunk_count = 2;
448        let array = make_dict_primitive_chunks::<u64, u64>(len, 2, chunk_count);
449
450        let mut builder = builder_with_capacity(
451            &DType::Primitive(PType::U64, NonNullable),
452            len * chunk_count,
453        );
454        array.append_to_builder(
455            builder.as_mut(),
456            &mut array_session().create_execution_ctx(),
457        )?;
458
459        let into_prim = array.execute::<PrimitiveArray>(&mut ctx)?;
460        let prim_into = builder.finish_into_canonical(&mut ctx).into_primitive();
461
462        assert_arrays_eq!(into_prim, prim_into, &mut ctx);
463        Ok(())
464    }
465
466    #[cfg_attr(miri, ignore)]
467    #[test]
468    fn test_dict_metadata() {
469        use prost::Message;
470
471        use super::DictMetadata;
472        use crate::test_harness::check_metadata;
473
474        check_metadata(
475            "dict.metadata",
476            &DictMetadata {
477                codes_ptype: PType::U64 as i32,
478                values_len: u32::MAX,
479                is_nullable_codes: None,
480                all_values_referenced: None,
481            }
482            .encode_to_vec(),
483        );
484    }
485}