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 mut values_vec = vec![!referenced; values_len];
158        let saturated = match codes_validity.bit_buffer() {
159            AllOr::All => match_each_integer_ptype!(codes_primitive.ptype(), |P| {
160                mark_codes(&mut values_vec, referenced, codes_primitive.as_slice::<P>())
161            }),
162            AllOr::None => false,
163            AllOr::Some(mask) => match_each_integer_ptype!(codes_primitive.ptype(), |P| {
164                mark_masked_codes(
165                    &mut values_vec,
166                    referenced,
167                    codes_primitive.as_slice::<P>(),
168                    mask,
169                    codes_validity.true_count(),
170                )
171            }),
172        };
173        if saturated {
174            return Ok(BitBuffer::full(referenced, values_len));
175        }
176
177        Ok(BitBuffer::from(values_vec))
178    }
179}
180impl<T: TypedArrayRef<Dict>> DictArrayExt for T {}
181
182/// Number of codes visited between checks for whether every dictionary value is referenced.
183const EARLY_EXIT_CHUNK: usize = 2048;
184
185/// Sets `values[code]` to `referenced` for every code, returning `true` early once every entry
186/// of `values` is `referenced`, since further codes cannot change the result.
187///
188/// With fewer codes than values not every value can be referenced, so the saturation checks are
189/// skipped entirely.
190fn mark_codes<P: AsPrimitive<usize>>(values: &mut [bool], referenced: bool, codes: &[P]) -> bool {
191    if codes.len() < values.len() {
192        for code in codes {
193            values[code.as_()] = referenced;
194        }
195        return false;
196    }
197
198    let mut cursor = 0;
199    for chunk in codes.chunks(EARLY_EXIT_CHUNK) {
200        for code in chunk {
201            values[code.as_()] = referenced;
202        }
203        if is_saturated(values, &mut cursor, referenced) {
204            return true;
205        }
206    }
207    false
208}
209
210/// Like [`mark_codes`], but only for the codes at the `valid_count` set positions of `mask`.
211fn mark_masked_codes<P: AsPrimitive<usize>>(
212    values: &mut [bool],
213    referenced: bool,
214    codes: &[P],
215    mask: &BitBuffer,
216    valid_count: usize,
217) -> bool {
218    if valid_count < values.len() {
219        mask.set_indices().for_each(|idx| {
220            values[codes[idx].as_()] = referenced;
221        });
222        return false;
223    }
224
225    let mut cursor = 0;
226    for start in (0..codes.len()).step_by(EARLY_EXIT_CHUNK) {
227        let end = (start + EARLY_EXIT_CHUNK).min(codes.len());
228        let codes = &codes[start..end];
229        mask.slice(start..end).set_indices().for_each(|idx| {
230            values[codes[idx].as_()] = referenced;
231        });
232        if is_saturated(values, &mut cursor, referenced) {
233            return true;
234        }
235    }
236    false
237}
238
239/// Advances `cursor` past the entries of `values` equal to `referenced` and returns whether that
240/// is all of them. Callers share one cursor that only moves forward, so repeated checks cost
241/// `O(values.len())` in total.
242#[inline]
243fn is_saturated(values: &[bool], cursor: &mut usize, referenced: bool) -> bool {
244    // Skip whole blocks with a branch-free comparison the compiler can vectorize, then find the
245    // exact stopping point within the first block that is not fully marked.
246    const BLOCK: usize = 32;
247    let rest = &values[*cursor..];
248    let full_blocks = rest
249        .as_chunks::<BLOCK>()
250        .0
251        .iter()
252        .take_while(|block| block.iter().fold(true, |acc, &v| acc & (v == referenced)))
253        .count();
254    *cursor += full_blocks * BLOCK;
255    *cursor += values[*cursor..]
256        .iter()
257        .take_while(|&&v| v == referenced)
258        .count();
259    *cursor == values.len()
260}
261
262/// Concrete parts of a [`DictArray`](super::DictArray) after iterative execution.
263pub struct DictParts {
264    pub dtype: DType,
265    pub codes: ArrayRef,
266    pub values: ArrayRef,
267}
268
269pub trait DictOwnedExt {
270    fn into_parts(self) -> DictParts;
271}
272
273impl DictOwnedExt for Array<Dict> {
274    fn into_parts(self) -> DictParts {
275        match self.try_into_parts() {
276            Ok(array_parts) => {
277                let slots = DictSlots::from_slots(array_parts.slots);
278                DictParts {
279                    dtype: array_parts.dtype,
280                    codes: slots.codes,
281                    values: slots.values,
282                }
283            }
284            Err(array) => {
285                let slots = DictSlotsView::from_slots(array.slots());
286                DictParts {
287                    dtype: array.dtype().clone(),
288                    codes: slots.codes.clone(),
289                    values: slots.values.clone(),
290                }
291            }
292        }
293    }
294}
295
296impl Array<Dict> {
297    /// Build a new `DictArray` from its components, `codes` and `values`.
298    pub fn new(codes: ArrayRef, values: ArrayRef) -> Self {
299        Self::try_new(codes, values).vortex_expect("DictArray new")
300    }
301
302    /// Build a new `DictArray` from its components, `codes` and `values`.
303    pub fn try_new(codes: ArrayRef, values: ArrayRef) -> VortexResult<Self> {
304        let dtype = values
305            .dtype()
306            .union_nullability(codes.dtype().nullability());
307        let len = codes.len();
308        let data = DictData::try_new(codes.dtype())?;
309        Array::try_from_parts(ArrayParts::new(
310            Dict,
311            dtype,
312            len,
313            data,
314            smallvec![Some(codes), Some(values)],
315        ))
316    }
317
318    /// Build a new `DictArray` without validating the codes or values.
319    ///
320    /// # Safety
321    ///
322    /// See [`DictData::new_unchecked`].
323    pub unsafe fn new_unchecked(codes: ArrayRef, values: ArrayRef) -> Self {
324        let dtype = values
325            .dtype()
326            .union_nullability(codes.dtype().nullability());
327        let len = codes.len();
328        let data = unsafe { DictData::new_unchecked() };
329        unsafe {
330            Array::from_parts_unchecked(ArrayParts::new(
331                Dict,
332                dtype,
333                len,
334                data,
335                smallvec![Some(codes), Some(values)],
336            ))
337        }
338    }
339
340    /// Set whether all values in the dictionary are referenced by at least one code.
341    ///
342    /// # Safety
343    ///
344    /// See [`DictData::set_all_values_referenced`].
345    pub unsafe fn set_all_values_referenced(self, all_values_referenced: bool) -> Self {
346        let dtype = self.dtype().clone();
347        let len = self.len();
348        let slots: ArraySlots = self.slots().iter().cloned().collect();
349        let data = unsafe {
350            self.into_data()
351                .set_all_values_referenced(all_values_referenced)
352        };
353        unsafe { Array::from_parts_unchecked(ArrayParts::new(Dict, dtype, len, data, slots)) }
354    }
355}
356
357#[cfg(test)]
358mod test {
359    use rand::RngExt;
360    use rand::SeedableRng;
361    use rand::distr::Distribution;
362    use rand::distr::StandardUniform;
363    use rand::prelude::StdRng;
364    use vortex_buffer::BitBuffer;
365    use vortex_buffer::buffer;
366    use vortex_error::VortexExpect;
367    use vortex_error::VortexResult;
368    use vortex_error::vortex_panic;
369    use vortex_mask::AllOr;
370
371    use crate::ArrayRef;
372    use crate::IntoArray;
373    use crate::VortexSessionExecute;
374    use crate::array_session;
375    use crate::arrays::ChunkedArray;
376    use crate::arrays::DictArray;
377    use crate::arrays::PrimitiveArray;
378    use crate::arrays::VarBinViewArray;
379    use crate::arrays::dict::DictArrayExt;
380    use crate::assert_arrays_eq;
381    use crate::builders::VarBinBuilder;
382    use crate::builders::builder_with_capacity_in;
383    use crate::dtype::DType;
384    use crate::dtype::NativePType;
385    use crate::dtype::Nullability::NonNullable;
386    use crate::dtype::PType;
387    use crate::dtype::UnsignedPType;
388    use crate::validity::Validity;
389
390    #[test]
391    fn nullable_codes_validity() {
392        let dict = DictArray::try_new(
393            PrimitiveArray::new(
394                buffer![0u32, 1, 2, 2, 1],
395                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
396            )
397            .into_array(),
398            PrimitiveArray::new(buffer![3, 6, 9], Validity::AllValid).into_array(),
399        )
400        .unwrap();
401        let mask = dict
402            .as_ref()
403            .validity()
404            .unwrap()
405            .execute_mask(
406                dict.as_ref().len(),
407                &mut array_session().create_execution_ctx(),
408            )
409            .unwrap();
410        let AllOr::Some(indices) = mask.indices() else {
411            vortex_panic!("Expected indices from mask")
412        };
413        assert_eq!(indices, [0, 2, 4]);
414    }
415
416    #[test]
417    fn nullable_values_validity() {
418        let dict = DictArray::try_new(
419            buffer![0u32, 1, 2, 2, 1].into_array(),
420            PrimitiveArray::new(
421                buffer![3, 6, 9],
422                Validity::from(BitBuffer::from(vec![true, false, false])),
423            )
424            .into_array(),
425        )
426        .unwrap();
427        let mask = dict
428            .as_ref()
429            .validity()
430            .unwrap()
431            .execute_mask(
432                dict.as_ref().len(),
433                &mut array_session().create_execution_ctx(),
434            )
435            .unwrap();
436        let AllOr::Some(indices) = mask.indices() else {
437            vortex_panic!("Expected indices from mask")
438        };
439        assert_eq!(indices, [0]);
440    }
441
442    #[test]
443    fn nullable_codes_and_values() {
444        let dict = DictArray::try_new(
445            PrimitiveArray::new(
446                buffer![0u32, 1, 2, 2, 1],
447                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
448            )
449            .into_array(),
450            PrimitiveArray::new(
451                buffer![3, 6, 9],
452                Validity::from(BitBuffer::from(vec![false, true, true])),
453            )
454            .into_array(),
455        )
456        .unwrap();
457        let mask = dict
458            .as_ref()
459            .validity()
460            .unwrap()
461            .execute_mask(
462                dict.as_ref().len(),
463                &mut array_session().create_execution_ctx(),
464            )
465            .unwrap();
466        let AllOr::Some(indices) = mask.indices() else {
467            vortex_panic!("Expected indices from mask")
468        };
469        assert_eq!(indices, [2, 4]);
470    }
471
472    #[test]
473    fn nullable_codes_and_non_null_values() {
474        let dict = DictArray::try_new(
475            PrimitiveArray::new(
476                buffer![0u32, 1, 2, 2, 1],
477                Validity::from(BitBuffer::from(vec![true, false, true, false, true])),
478            )
479            .into_array(),
480            PrimitiveArray::new(buffer![3, 6, 9], Validity::NonNullable).into_array(),
481        )
482        .unwrap();
483        let mask = dict
484            .as_ref()
485            .validity()
486            .unwrap()
487            .execute_mask(
488                dict.as_ref().len(),
489                &mut array_session().create_execution_ctx(),
490            )
491            .unwrap();
492        let AllOr::Some(indices) = mask.indices() else {
493            vortex_panic!("Expected indices from mask")
494        };
495        assert_eq!(indices, [0, 2, 4]);
496    }
497
498    fn make_dict_primitive_chunks<T: NativePType, Code: UnsignedPType>(
499        len: usize,
500        unique_values: usize,
501        chunk_count: usize,
502    ) -> (ArrayRef, Vec<T>)
503    where
504        StandardUniform: Distribution<T>,
505    {
506        let mut rng = StdRng::seed_from_u64(0);
507
508        let mut expected = Vec::with_capacity(len * chunk_count);
509        let array = (0..chunk_count)
510            .map(|_| {
511                let values = (0..unique_values)
512                    .map(|_| rng.random::<T>())
513                    .collect::<PrimitiveArray>();
514                let codes = (0..len)
515                    .map(|_| {
516                        Code::from(rng.random_range(0..unique_values)).vortex_expect("valid value")
517                    })
518                    .collect::<PrimitiveArray>();
519
520                expected.extend(codes.as_slice::<Code>().iter().map(|code| {
521                    values.as_slice::<T>()[<Code as num_traits::AsPrimitive<usize>>::as_(*code)]
522                }));
523                DictArray::try_new(codes.into_array(), values.into_array())
524                    .vortex_expect("DictArray creation should succeed in arbitrary impl")
525                    .into_array()
526            })
527            .collect::<ChunkedArray>()
528            .into_array();
529        (array, expected)
530    }
531
532    #[test]
533    fn test_dict_utf8_append_to_varbin_builder() -> VortexResult<()> {
534        let values = VarBinViewArray::from_iter_str(["zero", "one", "two"]);
535        let dict = DictArray::try_new(buffer![2u8, 0, 2, 1].into_array(), values.into_array())?;
536        let expected = VarBinViewArray::from_iter_str(["two", "zero", "two", "one"]);
537        let mut ctx = array_session().create_execution_ctx();
538        let mut builder = VarBinBuilder::<i32>::with_capacity_in(
539            dict.dtype().clone(),
540            dict.len(),
541            ctx.allocator(),
542        );
543
544        dict.into_array()
545            .append_to_builder(&mut builder, &mut ctx)?;
546
547        assert_arrays_eq!(builder.finish_into_varbin(), expected, &mut ctx);
548        Ok(())
549    }
550
551    #[test]
552    fn test_dict_array_from_primitive_chunks() -> VortexResult<()> {
553        let mut ctx = array_session().create_execution_ctx();
554        let len = 2;
555        let chunk_count = 2;
556        let (array, expected) = make_dict_primitive_chunks::<u64, u64>(len, 2, chunk_count);
557
558        let mut builder = builder_with_capacity_in(
559            &DType::Primitive(PType::U64, NonNullable),
560            len * chunk_count,
561            ctx.allocator(),
562        );
563        array.append_to_builder(
564            builder.as_mut(),
565            &mut array_session().create_execution_ctx(),
566        )?;
567
568        let prim_into = builder.finish_into_canonical(&mut ctx).into_primitive();
569
570        let expected = PrimitiveArray::from_iter(expected);
571        assert_arrays_eq!(array, expected, &mut ctx);
572        assert_arrays_eq!(prim_into, expected, &mut ctx);
573        Ok(())
574    }
575
576    #[cfg_attr(miri, ignore)]
577    #[test]
578    fn test_dict_metadata() {
579        use prost::Message;
580
581        use super::DictMetadata;
582        use crate::test_harness::check_metadata;
583
584        check_metadata(
585            "dict.metadata",
586            &DictMetadata {
587                codes_ptype: PType::U64 as i32,
588                values_len: u32::MAX,
589                is_nullable_codes: None,
590                all_values_referenced: None,
591            }
592            .encode_to_vec(),
593        );
594    }
595
596    /// Covers the early exit taken once every value is referenced, including when that happens
597    /// after the first chunk, alongside cases that never saturate.
598    #[rstest::rstest]
599    #[case::saturates_in_first_chunk(16, 5000, 16, None)]
600    #[case::saturates_in_later_chunk(4096, 60_000, 4096, None)]
601    #[case::saturates_unaligned_to_scan_block(4001, 60_000, 4001, None)]
602    #[case::never_saturates(1024, 10_000, 1000, None)]
603    #[case::fewer_codes_than_values(1024, 1000, 1024, None)]
604    #[case::fewer_valid_codes_than_values(1024, 1500, 1024, Some(0.5))]
605    #[case::saturates_with_nulls(64, 10_000, 64, Some(0.5))]
606    #[case::never_saturates_with_nulls(64, 10_000, 63, Some(0.5))]
607    fn referenced_values_mask_matches_reference(
608        #[case] num_values: u32,
609        #[case] num_codes: usize,
610        #[case] referenced_range: u32,
611        #[case] valid_fraction: Option<f64>,
612        #[values(true, false)] referenced: bool,
613    ) -> VortexResult<()> {
614        let mut rng = StdRng::seed_from_u64(0);
615        let codes: Vec<Option<u32>> = (0..num_codes)
616            .map(|_| {
617                valid_fraction
618                    .is_none_or(|f| rng.random_bool(f))
619                    .then(|| rng.random_range(0..referenced_range))
620            })
621            .collect();
622
623        let mut expected = vec![!referenced; num_values as usize];
624        for code in codes.iter().flatten() {
625            expected[*code as usize] = referenced;
626        }
627
628        let codes = match valid_fraction {
629            Some(_) => PrimitiveArray::from_option_iter(codes).into_array(),
630            None => PrimitiveArray::from_iter(codes.into_iter().flatten()).into_array(),
631        };
632        let dict =
633            DictArray::try_new(codes, PrimitiveArray::from_iter(0..num_values).into_array())?;
634        let mut ctx = array_session().create_execution_ctx();
635        let mask = dict.compute_referenced_values_mask(referenced, &mut ctx)?;
636
637        assert_eq!(mask, BitBuffer::from(expected));
638        Ok(())
639    }
640}