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(ArrayParts::new(
235            Dict,
236            dtype,
237            len,
238            data,
239            smallvec![Some(codes), Some(values)],
240        ))
241    }
242
243    /// Build a new `DictArray` without validating the codes or values.
244    ///
245    /// # Safety
246    ///
247    /// See [`DictData::new_unchecked`].
248    pub unsafe fn new_unchecked(codes: ArrayRef, values: ArrayRef) -> Self {
249        let dtype = values
250            .dtype()
251            .union_nullability(codes.dtype().nullability());
252        let len = codes.len();
253        let data = unsafe { DictData::new_unchecked() };
254        unsafe {
255            Array::from_parts_unchecked(ArrayParts::new(
256                Dict,
257                dtype,
258                len,
259                data,
260                smallvec![Some(codes), Some(values)],
261            ))
262        }
263    }
264
265    /// Set whether all values in the dictionary are referenced by at least one code.
266    ///
267    /// # Safety
268    ///
269    /// See [`DictData::set_all_values_referenced`].
270    pub unsafe fn set_all_values_referenced(self, all_values_referenced: bool) -> Self {
271        let dtype = self.dtype().clone();
272        let len = self.len();
273        let slots: ArraySlots = self.slots().iter().cloned().collect();
274        let data = unsafe {
275            self.into_data()
276                .set_all_values_referenced(all_values_referenced)
277        };
278        unsafe { Array::from_parts_unchecked(ArrayParts::new(Dict, dtype, len, data, slots)) }
279    }
280}
281
282#[cfg(test)]
283mod test {
284    use rand::RngExt;
285    use rand::SeedableRng;
286    use rand::distr::Distribution;
287    use rand::distr::StandardUniform;
288    use rand::prelude::StdRng;
289    use vortex_buffer::BitBuffer;
290    use vortex_buffer::buffer;
291    use vortex_error::VortexExpect;
292    use vortex_error::VortexResult;
293    use vortex_error::vortex_panic;
294    use vortex_mask::AllOr;
295
296    use crate::ArrayRef;
297    use crate::IntoArray;
298    use crate::VortexSessionExecute;
299    use crate::array_session;
300    use crate::arrays::ChunkedArray;
301    use crate::arrays::DictArray;
302    use crate::arrays::PrimitiveArray;
303    use crate::arrays::VarBinViewArray;
304    use crate::assert_arrays_eq;
305    use crate::builders::VarBinBuilder;
306    use crate::builders::builder_with_capacity_in;
307    use crate::dtype::DType;
308    use crate::dtype::NativePType;
309    use crate::dtype::Nullability::NonNullable;
310    use crate::dtype::PType;
311    use crate::dtype::UnsignedPType;
312    use crate::validity::Validity;
313
314    #[test]
315    fn nullable_codes_validity() {
316        let dict = DictArray::try_new(
317            PrimitiveArray::new(
318                buffer![0u32, 1, 2, 2, 1],
319                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
320            )
321            .into_array(),
322            PrimitiveArray::new(buffer![3, 6, 9], Validity::AllValid).into_array(),
323        )
324        .unwrap();
325        let mask = dict
326            .as_ref()
327            .validity()
328            .unwrap()
329            .execute_mask(
330                dict.as_ref().len(),
331                &mut array_session().create_execution_ctx(),
332            )
333            .unwrap();
334        let AllOr::Some(indices) = mask.indices() else {
335            vortex_panic!("Expected indices from mask")
336        };
337        assert_eq!(indices, [0, 2, 4]);
338    }
339
340    #[test]
341    fn nullable_values_validity() {
342        let dict = DictArray::try_new(
343            buffer![0u32, 1, 2, 2, 1].into_array(),
344            PrimitiveArray::new(
345                buffer![3, 6, 9],
346                Validity::from(BitBuffer::from(vec![true, false, false])),
347            )
348            .into_array(),
349        )
350        .unwrap();
351        let mask = dict
352            .as_ref()
353            .validity()
354            .unwrap()
355            .execute_mask(
356                dict.as_ref().len(),
357                &mut array_session().create_execution_ctx(),
358            )
359            .unwrap();
360        let AllOr::Some(indices) = mask.indices() else {
361            vortex_panic!("Expected indices from mask")
362        };
363        assert_eq!(indices, [0]);
364    }
365
366    #[test]
367    fn nullable_codes_and_values() {
368        let dict = DictArray::try_new(
369            PrimitiveArray::new(
370                buffer![0u32, 1, 2, 2, 1],
371                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
372            )
373            .into_array(),
374            PrimitiveArray::new(
375                buffer![3, 6, 9],
376                Validity::from(BitBuffer::from(vec![false, true, true])),
377            )
378            .into_array(),
379        )
380        .unwrap();
381        let mask = dict
382            .as_ref()
383            .validity()
384            .unwrap()
385            .execute_mask(
386                dict.as_ref().len(),
387                &mut array_session().create_execution_ctx(),
388            )
389            .unwrap();
390        let AllOr::Some(indices) = mask.indices() else {
391            vortex_panic!("Expected indices from mask")
392        };
393        assert_eq!(indices, [2, 4]);
394    }
395
396    #[test]
397    fn nullable_codes_and_non_null_values() {
398        let dict = DictArray::try_new(
399            PrimitiveArray::new(
400                buffer![0u32, 1, 2, 2, 1],
401                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
402            )
403            .into_array(),
404            PrimitiveArray::new(buffer![3, 6, 9], Validity::NonNullable).into_array(),
405        )
406        .unwrap();
407        let mask = dict
408            .as_ref()
409            .validity()
410            .unwrap()
411            .execute_mask(
412                dict.as_ref().len(),
413                &mut array_session().create_execution_ctx(),
414            )
415            .unwrap();
416        let AllOr::Some(indices) = mask.indices() else {
417            vortex_panic!("Expected indices from mask")
418        };
419        assert_eq!(indices, [0, 2, 4]);
420    }
421
422    fn make_dict_primitive_chunks<T: NativePType, Code: UnsignedPType>(
423        len: usize,
424        unique_values: usize,
425        chunk_count: usize,
426    ) -> ArrayRef
427    where
428        StandardUniform: Distribution<T>,
429    {
430        let mut rng = StdRng::seed_from_u64(0);
431
432        (0..chunk_count)
433            .map(|_| {
434                let values = (0..unique_values)
435                    .map(|_| rng.random::<T>())
436                    .collect::<PrimitiveArray>();
437                let codes = (0..len)
438                    .map(|_| {
439                        Code::from(rng.random_range(0..unique_values)).vortex_expect("valid value")
440                    })
441                    .collect::<PrimitiveArray>();
442
443                DictArray::try_new(codes.into_array(), values.into_array())
444                    .vortex_expect("DictArray creation should succeed in arbitrary impl")
445                    .into_array()
446            })
447            .collect::<ChunkedArray>()
448            .into_array()
449    }
450
451    #[test]
452    fn test_dict_utf8_append_to_varbin_builder() -> VortexResult<()> {
453        let values = VarBinViewArray::from_iter_str(["zero", "one", "two"]);
454        let dict = DictArray::try_new(buffer![2u8, 0, 2, 1].into_array(), values.into_array())?;
455        let expected = VarBinViewArray::from_iter_str(["two", "zero", "two", "one"]);
456        let mut ctx = array_session().create_execution_ctx();
457        let mut builder = VarBinBuilder::<i32>::with_capacity_in(
458            dict.dtype().clone(),
459            dict.len(),
460            ctx.allocator(),
461        );
462
463        dict.into_array()
464            .append_to_builder(&mut builder, &mut ctx)?;
465
466        assert_arrays_eq!(builder.finish_into_varbin(), expected, &mut ctx);
467        Ok(())
468    }
469
470    #[test]
471    fn test_dict_array_from_primitive_chunks() -> VortexResult<()> {
472        let mut ctx = array_session().create_execution_ctx();
473        let len = 2;
474        let chunk_count = 2;
475        let array = make_dict_primitive_chunks::<u64, u64>(len, 2, chunk_count);
476
477        let mut builder = builder_with_capacity_in(
478            &DType::Primitive(PType::U64, NonNullable),
479            len * chunk_count,
480            ctx.allocator(),
481        );
482        array.append_to_builder(
483            builder.as_mut(),
484            &mut array_session().create_execution_ctx(),
485        )?;
486
487        let into_prim = array.execute::<PrimitiveArray>(&mut ctx)?;
488        let prim_into = builder.finish_into_canonical(&mut ctx).into_primitive();
489
490        assert_arrays_eq!(into_prim, prim_into, &mut ctx);
491        Ok(())
492    }
493
494    #[cfg_attr(miri, ignore)]
495    #[test]
496    fn test_dict_metadata() {
497        use prost::Message;
498
499        use super::DictMetadata;
500        use crate::test_harness::check_metadata;
501
502        check_metadata(
503            "dict.metadata",
504            &DictMetadata {
505                codes_ptype: PType::U64 as i32,
506                values_len: u32::MAX,
507                is_nullable_codes: None,
508                all_values_referenced: None,
509            }
510            .encode_to_vec(),
511        );
512    }
513}