Skip to main content

vortex_array/
validity.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Array validity and nullability behavior, used by arrays and compute functions.
5//!
6//! [`Validity`] describes which rows are logically present without forcing every array to carry a
7//! materialized boolean bitmap. Constant states (`NonNullable`, `AllValid`, `AllInvalid`) are cheap
8//! to clone and inspect. [`Validity::Array`] may itself be encoded or lazy, so APIs that need exact
9//! per-row answers take an [`ExecutionCtx`] and execute the validity array as needed.
10
11use std::fmt::Debug;
12use std::ops::Range;
13
14use itertools::Itertools as _;
15use vortex_buffer::BitBuffer;
16use vortex_error::VortexExpect as _;
17use vortex_error::VortexResult;
18use vortex_error::vortex_bail;
19use vortex_error::vortex_err;
20use vortex_mask::Mask;
21use vortex_mask::MaskValues;
22
23use crate::ArrayRef;
24use crate::Canonical;
25use crate::ExecutionCtx;
26use crate::IntoArray;
27use crate::arrays::BoolArray;
28use crate::arrays::ChunkedArray;
29use crate::arrays::ConstantArray;
30use crate::builtins::ArrayBuiltins;
31use crate::dtype::DType;
32use crate::dtype::Nullability;
33use crate::optimizer::ArrayOptimizer;
34use crate::patches::Patches;
35use crate::scalar::Scalar;
36use crate::scalar_fn::fns::binary::Binary;
37use crate::scalar_fn::fns::operators::Operator;
38
39/// Validity information for an array.
40#[derive(Clone)]
41pub enum Validity {
42    /// Items cannot be null because the dtype is non-nullable.
43    NonNullable,
44    /// The dtype is nullable, but every item is valid.
45    AllValid,
46    /// The dtype is nullable, and every item is null.
47    AllInvalid,
48    /// The validity of each position in the array is determined by a boolean array.
49    ///
50    /// True values are valid, false values are invalid ("null").
51    Array(ArrayRef),
52}
53
54impl Debug for Validity {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            Self::NonNullable => write!(f, "NonNullable"),
58            Self::AllValid => write!(f, "AllValid"),
59            Self::AllInvalid => write!(f, "AllInvalid"),
60            Self::Array(arr) => write!(f, "SomeValid({arr})"),
61        }
62    }
63}
64
65impl Validity {
66    /// Make a step towards canonicalising validity if necessary
67    pub fn execute(self, ctx: &mut ExecutionCtx) -> VortexResult<Validity> {
68        match self {
69            v @ Validity::NonNullable | v @ Validity::AllValid | v @ Validity::AllInvalid => Ok(v),
70            Validity::Array(a) => Ok(Validity::Array(a.execute::<Canonical>(ctx)?.into_array())),
71        }
72    }
73}
74
75impl Validity {
76    /// The [`DType`] of the underlying validity array (if it exists).
77    pub const DTYPE: DType = DType::Bool(Nullability::NonNullable);
78
79    /// Convert the validity to an array representation.
80    pub fn to_array(&self, len: usize) -> ArrayRef {
81        match self {
82            Self::NonNullable | Self::AllValid => ConstantArray::new(true, len).into_array(),
83            Self::AllInvalid => ConstantArray::new(false, len).into_array(),
84            Self::Array(a) => a.clone(),
85        }
86    }
87
88    /// If Validity is [`Validity::Array`], returns the array, otherwise returns `None`.
89    #[inline]
90    pub fn into_array(self) -> Option<ArrayRef> {
91        if let Self::Array(a) = self {
92            Some(a)
93        } else {
94            None
95        }
96    }
97
98    /// If Validity is [`Validity::Array`], returns a reference to the array array, otherwise returns `None`.
99    #[inline]
100    pub fn as_array(&self) -> Option<&ArrayRef> {
101        if let Self::Array(a) = self {
102            Some(a)
103        } else {
104            None
105        }
106    }
107
108    #[inline]
109    pub fn nullability(&self) -> Nullability {
110        if matches!(self, Self::NonNullable) {
111            Nullability::NonNullable
112        } else {
113            Nullability::Nullable
114        }
115    }
116
117    /// Returns `true` if this validity *definitely* contains no null values, i.e. it is either
118    /// [`Validity::NonNullable`] or [`Validity::AllValid`].
119    ///
120    /// Returning `false` does not prove the presence of nulls: a [`Validity::Array`] may still
121    /// resolve to all-valid once executed. Callers must treat `false` as "unknown without
122    /// compute" and either fall back to a null-handling path or execute the validity.
123    #[inline]
124    pub fn definitely_no_nulls(&self) -> bool {
125        matches!(self, Self::NonNullable | Self::AllValid)
126    }
127
128    /// Returns `true` if this validity is *definitely* all-null (every value is null), i.e. it
129    /// is [`Validity::AllInvalid`].
130    ///
131    /// Returning `false` does not prove that any value is valid: a [`Validity::Array`] may still
132    /// resolve to all-null once executed. Callers must treat `false` as "unknown without
133    /// compute". For a definitive answer, execute the validity with [`Self::execute_mask`] and
134    /// check whether the resulting [`Mask`] is all-false (`Mask::all_false`). This is the
135    /// all-null counterpart to [`Self::definitely_no_nulls`].
136    #[inline]
137    pub fn definitely_all_null(&self) -> bool {
138        matches!(self, Self::AllInvalid)
139    }
140
141    /// Returns whether this validity contains no null values, executing the validity array if
142    /// necessary.
143    ///
144    /// This is the exact counterpart to [`Self::definitely_no_nulls`]: use it when the caller
145    /// needs a definitive answer rather than a cheap, conservative one.
146    pub fn execute_no_nulls(&self, length: usize, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
147        match self {
148            Self::NonNullable | Self::AllValid => Ok(true),
149            Self::AllInvalid => Ok(length == 0),
150            Self::Array(_) => Ok(self.execute_mask(length, ctx)?.all_true()),
151        }
152    }
153
154    /// The union nullability and validity.
155    #[inline]
156    pub fn union_nullability(self, nullability: Nullability) -> Self {
157        match nullability {
158            Nullability::NonNullable => self,
159            Nullability::Nullable => self.into_nullable(),
160        }
161    }
162
163    /// Returns whether the `index` item is valid, using `ctx` to execute the validity array.
164    // Todo(joe): deprecate this
165    #[inline]
166    pub fn execute_is_valid(&self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
167        Ok(match self {
168            Self::NonNullable | Self::AllValid => true,
169            Self::AllInvalid => false,
170            Self::Array(a) => a
171                .execute_scalar(index, ctx)?
172                .as_bool()
173                .value()
174                .ok_or_else(|| vortex_err!("validity value at index {index} is null"))?,
175        })
176    }
177
178    /// Returns whether the `index` item is null, using `ctx` to execute the validity array.
179    // Todo(joe): deprecate this
180    #[inline]
181    pub fn execute_is_null(&self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<bool> {
182        Ok(!self.execute_is_valid(index, ctx)?)
183    }
184
185    #[inline]
186    pub fn slice(&self, range: Range<usize>) -> VortexResult<Self> {
187        match self {
188            Self::Array(a) => Ok(Self::Array(a.slice(range)?)),
189            Self::NonNullable | Self::AllValid | Self::AllInvalid => Ok(self.clone()),
190        }
191    }
192
193    pub fn take(&self, indices: &ArrayRef) -> VortexResult<Self> {
194        match self {
195            Self::NonNullable => indices.validity(),
196            Self::AllValid => Ok(match indices.validity()? {
197                Self::NonNullable => Self::AllValid,
198                v => v,
199            }),
200            Self::AllInvalid => Ok(Self::AllInvalid),
201            Self::Array(is_valid) => {
202                let maybe_is_valid = is_valid.take(indices.clone())?;
203                // Null indices invalidate that position.
204                let is_valid = maybe_is_valid.fill_null(Scalar::from(false))?;
205                Ok(Self::Array(is_valid))
206            }
207        }
208    }
209
210    // Invert the validity
211    pub fn not(&self) -> VortexResult<Self> {
212        match self {
213            Validity::NonNullable => Ok(Validity::NonNullable),
214            Validity::AllValid => Ok(Validity::AllInvalid),
215            Validity::AllInvalid => Ok(Validity::AllValid),
216            Validity::Array(arr) => Ok(Validity::Array(arr.not()?)),
217        }
218    }
219
220    /// Lazily filters a [`Validity`] with a selection mask, which keeps only the entries for which
221    /// the mask is true.
222    ///
223    /// The result has length equal to the number of true values in mask.
224    ///
225    /// If the validity is a [`Validity::Array`], then this lazily wraps it in a `FilterArray`
226    /// instead of eagerly filtering the values immediately.
227    pub fn filter(&self, mask: &Mask) -> VortexResult<Self> {
228        // NOTE(ngates): we take the mask as a reference to avoid the caller cloning unnecessarily
229        //  if we happen to be NonNullable, AllValid, or AllInvalid.
230        match self {
231            v @ (Validity::NonNullable | Validity::AllValid | Validity::AllInvalid) => {
232                Ok(v.clone())
233            }
234            Validity::Array(arr) => Ok(Validity::Array(arr.filter(mask.clone())?)),
235        }
236    }
237
238    /// Converts this validity into a [`Mask`] of the given length.
239    ///
240    /// Valid elements are `true` and invalid elements are `false`.
241    #[deprecated(note = "Use execute_mask")]
242    pub fn to_mask(&self, length: usize, ctx: &mut ExecutionCtx) -> VortexResult<Mask> {
243        match self {
244            Self::NonNullable | Self::AllValid => Ok(Mask::new_true(length)),
245            Self::AllInvalid => Ok(Mask::new_false(length)),
246            Self::Array(arr) => arr.clone().execute::<Mask>(ctx),
247        }
248    }
249
250    /// Execute this validity into a [`Mask`] of the given length.
251    ///
252    /// Resolving [`Validity::Array`] can execute and scan up to `length` values. Optimistic fast
253    /// paths that do not need the exact mask should use [`Self::definitely_no_nulls`] or
254    /// [`Self::definitely_all_null`] and delay this work until necessary.
255    #[inline]
256    pub fn execute_mask(&self, length: usize, ctx: &mut ExecutionCtx) -> VortexResult<Mask> {
257        match self {
258            Self::NonNullable | Self::AllValid => Ok(Mask::AllTrue(length)),
259            Self::AllInvalid => Ok(Mask::AllFalse(length)),
260            Self::Array(arr) => {
261                assert_eq!(
262                    arr.len(),
263                    length,
264                    "Validity::Array length must equal to_logical's argument: {}, {}.",
265                    arr.len(),
266                    length,
267                );
268                // TODO(ngates): I'm not sure execution should take arrays by ownership.
269                //  If so we should fix call sites to clone and this function takes self.
270                arr.clone().execute::<Mask>(ctx)
271            }
272        }
273    }
274
275    /// Compare the logical masks of two Validity values of the given length, executing them
276    /// into [`Mask`]s if necessary.
277    pub fn mask_eq(
278        &self,
279        other: &Validity,
280        length: usize,
281        ctx: &mut ExecutionCtx,
282    ) -> VortexResult<bool> {
283        match (self, other) {
284            // Fast paths that avoid executing: constant variants with known-equal masks.
285            (
286                Validity::NonNullable | Validity::AllValid,
287                Validity::NonNullable | Validity::AllValid,
288            )
289            | (Validity::AllInvalid, Validity::AllInvalid) => Ok(true),
290            _ => Ok(self.execute_mask(length, ctx)? == other.execute_mask(length, ctx)?),
291        }
292    }
293
294    /// Logically & two Validity values of the same length
295    #[inline]
296    pub fn and(self, rhs: Validity) -> VortexResult<Validity> {
297        Ok(match (self, rhs) {
298            // Should be pretty clear
299            (Validity::NonNullable, Validity::NonNullable) => Validity::NonNullable,
300            // Any `AllInvalid` makes the output all invalid values
301            (Validity::AllInvalid, _) | (_, Validity::AllInvalid) => Validity::AllInvalid,
302            // All truthy values on one side, which makes no effect on an `Array` variant
303            (Validity::Array(a), Validity::AllValid)
304            | (Validity::Array(a), Validity::NonNullable)
305            | (Validity::NonNullable, Validity::Array(a))
306            | (Validity::AllValid, Validity::Array(a)) => Validity::Array(a),
307            // Both sides are all valid
308            (Validity::NonNullable, Validity::AllValid)
309            | (Validity::AllValid, Validity::NonNullable)
310            | (Validity::AllValid, Validity::AllValid) => Validity::AllValid,
311            // Here we actually have to do some work
312            (Validity::Array(lhs), Validity::Array(rhs)) => Validity::Array(
313                Binary::try_new(lhs, rhs, Operator::And)?
314                    .into_array()
315                    .optimize()?,
316            ),
317        })
318    }
319
320    pub fn patch(
321        self,
322        len: usize,
323        indices_offset: usize,
324        indices: &ArrayRef,
325        patches: &Validity,
326        ctx: &mut ExecutionCtx,
327    ) -> VortexResult<Self> {
328        match (&self, patches) {
329            (Validity::NonNullable, Validity::NonNullable) => return Ok(Validity::NonNullable),
330            (Validity::NonNullable, _) => {
331                vortex_bail!("Can't patch a non-nullable validity with nullable validity")
332            }
333            (_, Validity::NonNullable) => {
334                vortex_bail!("Can't patch a nullable validity with non-nullable validity")
335            }
336            (Validity::AllValid, Validity::AllValid) => return Ok(Validity::AllValid),
337            (Validity::AllInvalid, Validity::AllInvalid) => return Ok(Validity::AllInvalid),
338            _ => {}
339        };
340
341        if matches!(self, Validity::NonNullable) {
342            return Ok(Self::NonNullable);
343        }
344
345        // From here on we know that the validity is nullable
346        let source = match self {
347            Validity::NonNullable => BoolArray::from(BitBuffer::new_set(len)),
348            Validity::AllValid => BoolArray::from(BitBuffer::new_set(len)),
349            Validity::AllInvalid => BoolArray::from(BitBuffer::new_unset(len)),
350            Validity::Array(a) => a.execute::<BoolArray>(ctx)?,
351        };
352
353        let patch_values = match patches {
354            Validity::NonNullable => BoolArray::from(BitBuffer::new_set(indices.len())),
355            Validity::AllValid => BoolArray::from(BitBuffer::new_set(indices.len())),
356            Validity::AllInvalid => BoolArray::from(BitBuffer::new_unset(indices.len())),
357            Validity::Array(a) => a.clone().execute::<BoolArray>(ctx)?,
358        };
359
360        let patches = Patches::new(
361            len,
362            indices_offset,
363            indices.clone(),
364            patch_values.into_array(),
365            // TODO(0ax1): chunk offsets
366            None,
367            ctx,
368        )?;
369
370        Ok(Self::Array(source.patch(&patches, ctx)?.into_array()))
371    }
372
373    /// Convert into a nullable variant.
374    #[inline]
375    pub fn into_nullable(self) -> Validity {
376        match self {
377            Self::NonNullable => Self::AllValid,
378            Self::AllValid | Self::AllInvalid | Self::Array(_) => self,
379        }
380    }
381
382    /// Convert into a non-nullable variant, computing statistics if necessary.
383    ///
384    /// Returns `None` when the array contains invalid values (so the cast cannot be performed),
385    /// either because it is [`Validity::AllInvalid`] or because the validity array's minimum is
386    /// `false`.
387    #[inline]
388    pub fn into_non_nullable(self, len: usize, ctx: &mut ExecutionCtx) -> Option<Validity> {
389        match self {
390            _ if len == 0 => Some(Validity::NonNullable),
391            Self::NonNullable => Some(Self::NonNullable),
392            Self::AllValid => Some(Self::NonNullable),
393            Self::AllInvalid => None,
394            Self::Array(is_valid) => {
395                is_valid
396                    .statistics()
397                    .compute_min::<bool>(ctx)
398                    .vortex_expect("validity array must support min")
399                    .then(|| {
400                        // min true => all true
401                        Self::NonNullable
402                    })
403            }
404        }
405    }
406
407    /// Convert into a non-nullable variant without running execution.
408    ///
409    /// This is the cheap counterpart to [`Self::into_non_nullable`]: it inspects already-computed
410    /// statistics rather than triggering execution.
411    ///
412    /// Return values:
413    /// - `Ok(Some(NonNullable))` — the cast is provably safe.
414    /// - `Ok(None)` — We need to perform compute to determine whether cast is valid. Callers should fall back to [`Self::into_non_nullable`], typically by
415    ///   returning `Ok(None)` from a `CastReduce` rule so the corresponding `CastKernel` runs.
416    /// - `Err(_)` — we know the cast must fail (e.g. [`Validity::AllInvalid`]).
417    #[inline]
418    pub fn trivial_into_non_nullable(self, len: usize) -> VortexResult<Option<Validity>> {
419        match self {
420            _ if len == 0 => Ok(Some(Validity::NonNullable)),
421            Self::NonNullable => Ok(Some(Self::NonNullable)),
422            Self::AllValid => Ok(Some(Self::NonNullable)),
423            Self::AllInvalid => {
424                Err(vortex_err!(InvalidArgument: "Cannot cast AllInvalid to NonNullable"))
425            }
426            Self::Array(_) => Ok(None),
427        }
428    }
429
430    /// Convert into a variant compatible with the given nullability.
431    ///
432    /// This is the execution-time half of the nullability-cast pair. It is paired with
433    /// [`Self::trivially_cast_nullability`], which is used by `CastReduce` rules. The pattern is:
434    ///
435    /// - **`CastReduce` rules** (metadata-only rewrites in the optimizer) call
436    ///   [`Self::trivially_cast_nullability`]. If it returns `Ok(None)`, the rule returns `Ok(None)`
437    ///   and the cast is deferred to execution.
438    /// - **`CastKernel` impls** (executed via [`ExecuteParentKernel`]) call this method, which
439    ///   may run the underlying validity array to compute statistics.
440    ///
441    /// Returns `Err` when nullability cannot be cast (for example, casting to non-nullable while
442    /// invalid values are present).
443    ///
444    /// [`ExecuteParentKernel`]: crate::kernel::ExecuteParentKernel
445    #[inline]
446    pub fn cast_nullability(
447        self,
448        nullability: Nullability,
449        len: usize,
450        ctx: &mut ExecutionCtx,
451    ) -> VortexResult<Validity> {
452        match nullability {
453            Nullability::NonNullable => self.into_non_nullable(len, ctx).ok_or_else(|| {
454                vortex_err!(InvalidArgument: "Cannot cast array with invalid values to non-nullable type.")
455            }),
456            Nullability::Nullable => Ok(self.into_nullable()),
457        }
458    }
459
460    /// Best-effort, non-executing variant of [`Self::cast_nullability`].
461    ///
462    /// Use this from `CastReduce` rules — they run inside the optimizer where execution is not
463    /// available. The pairing with [`Self::cast_nullability`] is symmetric: every encoding that
464    /// implements `CastReduce` and inspects validity should also implement `CastKernel` so that
465    /// the harder cases (where statistics are not yet cached) can still be handled at execution
466    /// time.
467    ///
468    /// Return values:
469    /// - `Ok(Some(_))` — the cast is provably safe and the new [`Validity`] is returned.
470    /// - `Ok(None)` — the cast cannot be reduced cheaply (the `CastKernel` should be tried via
471    ///   [`Self::cast_nullability`]).
472    /// - `Err(_)` — the cast is provably impossible.
473    ///
474    /// Typical usage inside a `CastReduce`:
475    ///
476    /// ```ignore
477    /// let Some(new_validity) = array
478    ///     .validity()?
479    ///     .trivial_cast_nullability(dtype.nullability(), array.len())?
480    /// else {
481    ///     return Ok(None);
482    /// };
483    /// ```
484    #[inline]
485    pub fn trivially_cast_nullability(
486        self,
487        nullability: Nullability,
488        len: usize,
489    ) -> VortexResult<Option<Validity>> {
490        match nullability {
491            Nullability::NonNullable => self.trivial_into_non_nullable(len),
492            Nullability::Nullable => Ok(Some(self.into_nullable())),
493        }
494    }
495
496    /// Returns the length of the validity array, if it exists.
497    #[inline]
498    pub fn maybe_len(&self) -> Option<usize> {
499        match self {
500            Self::NonNullable | Self::AllValid | Self::AllInvalid => None,
501            Self::Array(a) => Some(a.len()),
502        }
503    }
504}
505
506impl From<BitBuffer> for Validity {
507    #[inline]
508    fn from(value: BitBuffer) -> Self {
509        let true_count = value.true_count();
510        if true_count == value.len() {
511            Self::AllValid
512        } else if true_count == 0 {
513            Self::AllInvalid
514        } else {
515            Self::Array(BoolArray::from(value).into_array())
516        }
517    }
518}
519
520impl FromIterator<Mask> for Validity {
521    #[inline]
522    fn from_iter<T: IntoIterator<Item = Mask>>(iter: T) -> Self {
523        Validity::from_mask(iter.into_iter().collect(), Nullability::Nullable)
524    }
525}
526
527impl FromIterator<bool> for Validity {
528    #[inline]
529    fn from_iter<T: IntoIterator<Item = bool>>(iter: T) -> Self {
530        Validity::from(BitBuffer::from_iter(iter))
531    }
532}
533
534impl From<Nullability> for Validity {
535    #[inline]
536    fn from(value: Nullability) -> Self {
537        Validity::from(&value)
538    }
539}
540
541impl From<&Nullability> for Validity {
542    #[inline]
543    fn from(value: &Nullability) -> Self {
544        match *value {
545            Nullability::NonNullable => Validity::NonNullable,
546            Nullability::Nullable => Validity::AllValid,
547        }
548    }
549}
550
551impl Validity {
552    /// Concatenate one or more validities together.
553    ///
554    /// Returns None if the vector is empty.
555    pub fn concat(validities: Vec<(Validity, usize)>) -> Option<Self> {
556        let mut validity_kinds = validities
557            .iter()
558            .map(|(v, _)| std::mem::discriminant(v))
559            .unique();
560        let validity_kind = validity_kinds.next()?;
561        if validity_kinds.next().is_none() {
562            // If there is only one kind of validity and its not Validity::Array, avoid constructing
563            // a Validity::Array.
564            if validity_kind == std::mem::discriminant(&Validity::AllValid) {
565                return Some(Validity::AllValid);
566            }
567            if validity_kind == std::mem::discriminant(&Validity::AllInvalid) {
568                return Some(Validity::AllInvalid);
569            }
570            if validity_kind == std::mem::discriminant(&Validity::NonNullable) {
571                return Some(Validity::NonNullable);
572            }
573        }
574
575        Some(Validity::Array(
576            unsafe {
577                ChunkedArray::new_unchecked(
578                    validities.into_iter().map(|(v, len)| v.to_array(len)),
579                    DType::Bool(Nullability::NonNullable),
580                )
581            }
582            .into_array(),
583        ))
584    }
585}
586
587impl Validity {
588    pub fn from_bit_buffer(buffer: BitBuffer, nullability: Nullability) -> Self {
589        if buffer.true_count() == buffer.len() {
590            nullability.into()
591        } else if buffer.true_count() == 0 {
592            Validity::AllInvalid
593        } else {
594            Validity::Array(BoolArray::new(buffer, Validity::NonNullable).into_array())
595        }
596    }
597
598    pub fn from_mask(mask: Mask, nullability: Nullability) -> Self {
599        assert!(
600            nullability == Nullability::Nullable || matches!(mask, Mask::AllTrue(_)),
601            "NonNullable validity must be AllValid",
602        );
603        match mask {
604            Mask::AllTrue(_) => match nullability {
605                Nullability::NonNullable => Validity::NonNullable,
606                Nullability::Nullable => Validity::AllValid,
607            },
608            Mask::AllFalse(_) => Validity::AllInvalid,
609            Mask::Values(values) => Validity::Array(values.into_array()),
610        }
611    }
612}
613
614impl IntoArray for Mask {
615    #[inline]
616    fn into_array(self) -> ArrayRef {
617        match self {
618            Self::AllTrue(len) => ConstantArray::new(true, len).into_array(),
619            Self::AllFalse(len) => ConstantArray::new(false, len).into_array(),
620            Self::Values(a) => a.into_array(),
621        }
622    }
623}
624
625impl IntoArray for &MaskValues {
626    #[inline]
627    fn into_array(self) -> ArrayRef {
628        BoolArray::new(self.bit_buffer().clone(), Validity::NonNullable).into_array()
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use rstest::rstest;
635    use vortex_buffer::Buffer;
636    use vortex_buffer::buffer;
637    use vortex_mask::Mask;
638
639    use crate::ArrayRef;
640    use crate::IntoArray;
641    use crate::VortexSessionExecute;
642    use crate::array_session;
643    use crate::arrays::PrimitiveArray;
644    use crate::dtype::Nullability;
645    use crate::validity::BoolArray;
646    use crate::validity::Validity;
647
648    #[rstest]
649    #[case(Validity::AllValid, 5, &[2, 4], Validity::AllValid, Validity::AllValid)]
650    #[case(
651        Validity::AllValid,
652        5,
653        &[2, 4],
654        Validity::AllInvalid,
655        Validity::Array(BoolArray::from_iter([true, true, false, true, false]).into_array())
656    )]
657    #[case(
658        Validity::AllValid,
659        5,
660        &[2, 4],
661        Validity::Array(BoolArray::from_iter([true, false]).into_array()),
662        Validity::Array(BoolArray::from_iter([true, true, true, true, false]).into_array())
663    )]
664    #[case(
665        Validity::AllInvalid,
666        5,
667        &[2, 4],
668        Validity::AllValid,
669        Validity::Array(BoolArray::from_iter([false, false, true, false, true]).into_array())
670    )]
671    #[case(Validity::AllInvalid, 5, &[2, 4], Validity::AllInvalid, Validity::AllInvalid)]
672    #[case(
673        Validity::AllInvalid,
674        5,
675        &[2, 4],
676        Validity::Array(BoolArray::from_iter([true, false]).into_array()),
677        Validity::Array(BoolArray::from_iter([false, false, true, false, false]).into_array())
678    )]
679    #[case(
680        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array()),
681        5,
682        &[2, 4],
683        Validity::AllValid,
684        Validity::Array(BoolArray::from_iter([false, true, true, true, true]).into_array())
685    )]
686    #[case(
687        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array()),
688        5,
689        &[2, 4],
690        Validity::AllInvalid,
691        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array())
692    )]
693    #[case(
694        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array()),
695        5,
696        &[2, 4],
697        Validity::Array(BoolArray::from_iter([true, false]).into_array()),
698        Validity::Array(BoolArray::from_iter([false, true, true, true, false]).into_array())
699    )]
700
701    fn patch_validity(
702        #[case] validity: Validity,
703        #[case] len: usize,
704        #[case] positions: &[u64],
705        #[case] patches: Validity,
706        #[case] expected: Validity,
707    ) {
708        let indices =
709            PrimitiveArray::new(Buffer::copy_from(positions), Validity::NonNullable).into_array();
710
711        let mut ctx = array_session().create_execution_ctx();
712
713        assert!(
714            validity
715                .patch(len, 0, &indices, &patches, &mut ctx,)
716                .unwrap()
717                .mask_eq(&expected, len, &mut ctx)
718                .unwrap()
719        );
720    }
721
722    #[rstest]
723    #[case(
724        Validity::NonNullable,
725        1,
726        "Can't patch a non-nullable validity with nullable validity"
727    )]
728    #[case(
729        Validity::AllValid,
730        4,
731        "Patch indices 4, offset 0 are longer than the array length 2"
732    )]
733    fn rejects_invalid_patch(
734        #[case] validity: Validity,
735        #[case] index: u64,
736        #[case] message: &str,
737    ) {
738        let mut ctx = array_session().create_execution_ctx();
739        let error = validity
740            .patch(
741                2,
742                0,
743                &buffer![index].into_array(),
744                &Validity::AllInvalid,
745                &mut ctx,
746            )
747            .unwrap_err();
748        assert!(error.to_string().contains(message), "{error}");
749    }
750
751    #[test]
752    #[should_panic(expected = "NonNullable validity must be AllValid")]
753    fn into_validity_nullable() {
754        Validity::from_mask(Mask::AllFalse(10), Nullability::NonNullable);
755    }
756
757    #[test]
758    #[should_panic(expected = "NonNullable validity must be AllValid")]
759    fn into_validity_nullable_array() {
760        Validity::from_mask(Mask::from_iter(vec![true, false]), Nullability::NonNullable);
761    }
762
763    #[rstest]
764    #[case(
765        Validity::AllValid,
766        PrimitiveArray::new(buffer![0, 1], Validity::from_iter(vec![true, false])).into_array(),
767        Validity::from_iter(vec![true, false])
768    )]
769    #[case(Validity::AllValid, buffer![0, 1].into_array(), Validity::AllValid)]
770    #[case(
771        Validity::AllValid,
772        PrimitiveArray::new(buffer![0, 1], Validity::AllInvalid).into_array(),
773        Validity::AllInvalid
774    )]
775    #[case(
776        Validity::NonNullable,
777        PrimitiveArray::new(buffer![0, 1], Validity::from_iter(vec![true, false])).into_array(),
778        Validity::from_iter(vec![true, false])
779    )]
780    #[case(Validity::NonNullable, buffer![0, 1].into_array(), Validity::NonNullable)]
781    #[case(
782        Validity::NonNullable,
783        PrimitiveArray::new(buffer![0, 1], Validity::AllInvalid).into_array(),
784        Validity::AllInvalid
785    )]
786    fn validity_take(
787        #[case] validity: Validity,
788        #[case] indices: ArrayRef,
789        #[case] expected: Validity,
790    ) {
791        let mut ctx = array_session().create_execution_ctx();
792        let taken = validity.take(&indices).unwrap();
793        assert_eq!(taken.nullability(), expected.nullability());
794        assert!(taken.mask_eq(&expected, indices.len(), &mut ctx).unwrap());
795    }
796
797    #[rstest]
798    // Mixed constant variants with equal masks.
799    #[case(Validity::NonNullable, Validity::AllValid, true)]
800    #[case(Validity::AllValid, Validity::NonNullable, true)]
801    #[case(Validity::AllValid, Validity::AllInvalid, false)]
802    #[case(Validity::NonNullable, Validity::AllInvalid, false)]
803    // An array that resolves to a constant mask must equal the constant variant.
804    #[case(
805        Validity::Array(BoolArray::from_iter([true, true, true]).into_array()),
806        Validity::AllValid,
807        true
808    )]
809    #[case(
810        Validity::NonNullable,
811        Validity::Array(BoolArray::from_iter([true, true, true]).into_array()),
812        true
813    )]
814    #[case(
815        Validity::Array(BoolArray::from_iter([false, false, false]).into_array()),
816        Validity::AllInvalid,
817        true
818    )]
819    #[case(
820        Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
821        Validity::AllValid,
822        false
823    )]
824    #[case(
825        Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
826        Validity::AllInvalid,
827        false
828    )]
829    fn mask_eq_mixed_variants(
830        #[case] lhs: Validity,
831        #[case] rhs: Validity,
832        #[case] expected: bool,
833    ) -> vortex_error::VortexResult<()> {
834        let mut ctx = array_session().create_execution_ctx();
835        assert_eq!(lhs.mask_eq(&rhs, 3, &mut ctx)?, expected);
836        Ok(())
837    }
838}