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