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