1use 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 #[prost(optional, bool, tag = "3")]
37 pub(super) is_nullable_codes: Option<bool>,
38 #[prost(optional, bool, tag = "4")]
42 pub(super) all_values_referenced: Option<bool>,
43}
44
45#[array_slots(Dict)]
46pub struct DictSlots {
47 #[slot(0)]
49 pub codes: ArrayRef,
50 #[slot(1)]
52 pub values: ArrayRef,
53}
54
55#[derive(Debug, Clone)]
56pub struct DictData {
57 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 pub unsafe fn new_unchecked() -> Self {
79 Self {
80 all_values_referenced: false,
81 }
82 }
83
84 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 pub fn new(codes_dtype: &DType) -> Self {
103 Self::try_new(codes_dtype).vortex_expect("DictArray new")
104 }
105
106 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
182const EARLY_EXIT_CHUNK: usize = 2048;
184
185fn 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
210fn 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#[inline]
243fn is_saturated(values: &[bool], cursor: &mut usize, referenced: bool) -> bool {
244 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
262pub 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 pub fn new(codes: ArrayRef, values: ArrayRef) -> Self {
299 Self::try_new(codes, values).vortex_expect("DictArray new")
300 }
301
302 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 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 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 #[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}