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    /// Execute this validity into a [`Mask`] of the given length.
267    ///
268    /// Resolving [`Validity::Array`] can execute and scan up to `length` values. Optimistic fast
269    /// paths that do not need the exact mask should use [`Self::definitely_no_nulls`] or
270    /// [`Self::definitely_all_null`] and delay this work until necessary.
271    #[inline]
272    pub fn execute_mask(&self, length: usize, ctx: &mut ExecutionCtx) -> VortexResult<Mask> {
273        match self {
274            Self::NonNullable | Self::AllValid => Ok(Mask::AllTrue(length)),
275            Self::AllInvalid => Ok(Mask::AllFalse(length)),
276            Self::Array(arr) => {
277                assert_eq!(
278                    arr.len(),
279                    length,
280                    "Validity::Array length must equal to_logical's argument: {}, {}.",
281                    arr.len(),
282                    length,
283                );
284                // TODO(ngates): I'm not sure execution should take arrays by ownership.
285                //  If so we should fix call sites to clone and this function takes self.
286                arr.clone().execute::<Mask>(ctx)
287            }
288        }
289    }
290
291    /// Compare the logical masks of two Validity values of the given length, executing them
292    /// into [`Mask`]s if necessary.
293    pub fn mask_eq(
294        &self,
295        other: &Validity,
296        length: usize,
297        ctx: &mut ExecutionCtx,
298    ) -> VortexResult<bool> {
299        match (self, other) {
300            // Fast paths that avoid executing: constant variants with known-equal masks.
301            (
302                Validity::NonNullable | Validity::AllValid,
303                Validity::NonNullable | Validity::AllValid,
304            )
305            | (Validity::AllInvalid, Validity::AllInvalid) => Ok(true),
306            _ => Ok(self.execute_mask(length, ctx)? == other.execute_mask(length, ctx)?),
307        }
308    }
309
310    /// Logically & two Validity values of the same length
311    #[inline]
312    pub fn and(self, rhs: Validity) -> VortexResult<Validity> {
313        Ok(match (self, rhs) {
314            // Should be pretty clear
315            (Validity::NonNullable, Validity::NonNullable) => Validity::NonNullable,
316            // Any `AllInvalid` makes the output all invalid values
317            (Validity::AllInvalid, _) | (_, Validity::AllInvalid) => Validity::AllInvalid,
318            // All truthy values on one side, which makes no effect on an `Array` variant
319            (Validity::Array(a), Validity::AllValid)
320            | (Validity::Array(a), Validity::NonNullable)
321            | (Validity::NonNullable, Validity::Array(a))
322            | (Validity::AllValid, Validity::Array(a)) => Validity::Array(a),
323            // Both sides are all valid
324            (Validity::NonNullable, Validity::AllValid)
325            | (Validity::AllValid, Validity::NonNullable)
326            | (Validity::AllValid, Validity::AllValid) => Validity::AllValid,
327            // Here we actually have to do some work
328            (Validity::Array(lhs), Validity::Array(rhs)) => Validity::Array(
329                Binary::try_new(lhs, rhs, Operator::And)?
330                    .into_array()
331                    .optimize()?,
332            ),
333        })
334    }
335
336    pub fn patch(
337        self,
338        len: usize,
339        indices_offset: usize,
340        indices: &ArrayRef,
341        patches: &Validity,
342        ctx: &mut ExecutionCtx,
343    ) -> VortexResult<Self> {
344        match (&self, patches) {
345            (Validity::NonNullable, Validity::NonNullable) => return Ok(Validity::NonNullable),
346            (Validity::NonNullable, _) => {
347                vortex_bail!("Can't patch a non-nullable validity with nullable validity")
348            }
349            (_, Validity::NonNullable) => {
350                vortex_bail!("Can't patch a nullable validity with non-nullable validity")
351            }
352            (Validity::AllValid, Validity::AllValid) => return Ok(Validity::AllValid),
353            (Validity::AllInvalid, Validity::AllInvalid) => return Ok(Validity::AllInvalid),
354            _ => {}
355        };
356
357        if matches!(self, Validity::NonNullable) {
358            return Ok(Self::NonNullable);
359        }
360
361        // From here on we know that the validity is nullable
362        let source = match self {
363            Validity::NonNullable => BoolArray::from(BitBuffer::new_set(len)),
364            Validity::AllValid => BoolArray::from(BitBuffer::new_set(len)),
365            Validity::AllInvalid => BoolArray::from(BitBuffer::new_unset(len)),
366            Validity::Array(a) => a.execute::<BoolArray>(ctx)?,
367        };
368
369        let patch_values = match patches {
370            Validity::NonNullable => BoolArray::from(BitBuffer::new_set(indices.len())),
371            Validity::AllValid => BoolArray::from(BitBuffer::new_set(indices.len())),
372            Validity::AllInvalid => BoolArray::from(BitBuffer::new_unset(indices.len())),
373            Validity::Array(a) => a.clone().execute::<BoolArray>(ctx)?,
374        };
375
376        let patches = Patches::new(
377            len,
378            indices_offset,
379            indices.clone(),
380            patch_values.into_array(),
381            // TODO(0ax1): chunk offsets
382            None,
383        )?;
384
385        Ok(Self::Array(source.patch(&patches, ctx)?.into_array()))
386    }
387
388    /// Convert into a nullable variant.
389    #[inline]
390    pub fn into_nullable(self) -> Validity {
391        match self {
392            Self::NonNullable => Self::AllValid,
393            Self::AllValid | Self::AllInvalid | Self::Array(_) => self,
394        }
395    }
396
397    /// Convert into a non-nullable variant, computing statistics if necessary.
398    ///
399    /// Returns `None` when the array contains invalid values (so the cast cannot be performed),
400    /// either because it is [`Validity::AllInvalid`] or because the validity array's minimum is
401    /// `false`.
402    #[inline]
403    pub fn into_non_nullable(self, len: usize, ctx: &mut ExecutionCtx) -> Option<Validity> {
404        match self {
405            _ if len == 0 => Some(Validity::NonNullable),
406            Self::NonNullable => Some(Self::NonNullable),
407            Self::AllValid => Some(Self::NonNullable),
408            Self::AllInvalid => None,
409            Self::Array(is_valid) => {
410                is_valid
411                    .statistics()
412                    .compute_min::<bool>(ctx)
413                    .vortex_expect("validity array must support min")
414                    .then(|| {
415                        // min true => all true
416                        Self::NonNullable
417                    })
418            }
419        }
420    }
421
422    /// Convert into a non-nullable variant without running execution.
423    ///
424    /// This is the cheap counterpart to [`Self::into_non_nullable`]: it inspects already-computed
425    /// statistics rather than triggering execution.
426    ///
427    /// Return values:
428    /// - `Ok(Some(NonNullable))` — the cast is provably safe.
429    /// - `Ok(None)` — We need to perform compute to determine whether cast is valid. Callers should fall back to [`Self::into_non_nullable`], typically by
430    ///   returning `Ok(None)` from a `CastReduce` rule so the corresponding `CastKernel` runs.
431    /// - `Err(_)` — we know the cast must fail (e.g. [`Validity::AllInvalid`]).
432    #[inline]
433    pub fn trivial_into_non_nullable(self, len: usize) -> VortexResult<Option<Validity>> {
434        match self {
435            _ if len == 0 => Ok(Some(Validity::NonNullable)),
436            Self::NonNullable => Ok(Some(Self::NonNullable)),
437            Self::AllValid => Ok(Some(Self::NonNullable)),
438            Self::AllInvalid => {
439                Err(vortex_err!(InvalidArgument: "Cannot cast AllInvalid to NonNullable"))
440            }
441            Self::Array(_) => Ok(None),
442        }
443    }
444
445    /// Convert into a variant compatible with the given nullability.
446    ///
447    /// This is the execution-time half of the nullability-cast pair. It is paired with
448    /// [`Self::trivially_cast_nullability`], which is used by `CastReduce` rules. The pattern is:
449    ///
450    /// - **`CastReduce` rules** (metadata-only rewrites in the optimizer) call
451    ///   [`Self::trivially_cast_nullability`]. If it returns `Ok(None)`, the rule returns `Ok(None)`
452    ///   and the cast is deferred to execution.
453    /// - **`CastKernel` impls** (executed via [`ExecuteParentKernel`]) call this method, which
454    ///   may run the underlying validity array to compute statistics.
455    ///
456    /// Returns `Err` when nullability cannot be cast (for example, casting to non-nullable while
457    /// invalid values are present).
458    ///
459    /// [`ExecuteParentKernel`]: crate::kernel::ExecuteParentKernel
460    #[inline]
461    pub fn cast_nullability(
462        self,
463        nullability: Nullability,
464        len: usize,
465        ctx: &mut ExecutionCtx,
466    ) -> VortexResult<Validity> {
467        match nullability {
468            Nullability::NonNullable => self.into_non_nullable(len, ctx).ok_or_else(|| {
469                vortex_err!(InvalidArgument: "Cannot cast array with invalid values to non-nullable type.")
470            }),
471            Nullability::Nullable => Ok(self.into_nullable()),
472        }
473    }
474
475    /// Best-effort, non-executing variant of [`Self::cast_nullability`].
476    ///
477    /// Use this from `CastReduce` rules — they run inside the optimizer where execution is not
478    /// available. The pairing with [`Self::cast_nullability`] is symmetric: every encoding that
479    /// implements `CastReduce` and inspects validity should also implement `CastKernel` so that
480    /// the harder cases (where statistics are not yet cached) can still be handled at execution
481    /// time.
482    ///
483    /// Return values:
484    /// - `Ok(Some(_))` — the cast is provably safe and the new [`Validity`] is returned.
485    /// - `Ok(None)` — the cast cannot be reduced cheaply (the `CastKernel` should be tried via
486    ///   [`Self::cast_nullability`]).
487    /// - `Err(_)` — the cast is provably impossible.
488    ///
489    /// Typical usage inside a `CastReduce`:
490    ///
491    /// ```ignore
492    /// let Some(new_validity) = array
493    ///     .validity()?
494    ///     .trivial_cast_nullability(dtype.nullability(), array.len())?
495    /// else {
496    ///     return Ok(None);
497    /// };
498    /// ```
499    #[inline]
500    pub fn trivially_cast_nullability(
501        self,
502        nullability: Nullability,
503        len: usize,
504    ) -> VortexResult<Option<Validity>> {
505        match nullability {
506            Nullability::NonNullable => self.trivial_into_non_nullable(len),
507            Nullability::Nullable => Ok(Some(self.into_nullable())),
508        }
509    }
510
511    /// Returns the length of the validity array, if it exists.
512    #[inline]
513    pub fn maybe_len(&self) -> Option<usize> {
514        match self {
515            Self::NonNullable | Self::AllValid | Self::AllInvalid => None,
516            Self::Array(a) => Some(a.len()),
517        }
518    }
519}
520
521impl From<BitBuffer> for Validity {
522    #[inline]
523    fn from(value: BitBuffer) -> Self {
524        let true_count = value.true_count();
525        if true_count == value.len() {
526            Self::AllValid
527        } else if true_count == 0 {
528            Self::AllInvalid
529        } else {
530            Self::Array(BoolArray::from(value).into_array())
531        }
532    }
533}
534
535impl FromIterator<Mask> for Validity {
536    #[inline]
537    fn from_iter<T: IntoIterator<Item = Mask>>(iter: T) -> Self {
538        Validity::from_mask(iter.into_iter().collect(), Nullability::Nullable)
539    }
540}
541
542impl FromIterator<bool> for Validity {
543    #[inline]
544    fn from_iter<T: IntoIterator<Item = bool>>(iter: T) -> Self {
545        Validity::from(BitBuffer::from_iter(iter))
546    }
547}
548
549impl From<Nullability> for Validity {
550    #[inline]
551    fn from(value: Nullability) -> Self {
552        Validity::from(&value)
553    }
554}
555
556impl From<&Nullability> for Validity {
557    #[inline]
558    fn from(value: &Nullability) -> Self {
559        match *value {
560            Nullability::NonNullable => Validity::NonNullable,
561            Nullability::Nullable => Validity::AllValid,
562        }
563    }
564}
565
566impl Validity {
567    /// Concatenate one or more validities together.
568    ///
569    /// Returns None if the vector is empty.
570    pub fn concat(validities: Vec<(Validity, usize)>) -> Option<Self> {
571        let mut validity_kinds = validities
572            .iter()
573            .map(|(v, _)| std::mem::discriminant(v))
574            .unique();
575        let validity_kind = validity_kinds.next()?;
576        if validity_kinds.next().is_none() {
577            // If there is only one kind of validity and its not Validity::Array, avoid constructing
578            // a Validity::Array.
579            if validity_kind == std::mem::discriminant(&Validity::AllValid) {
580                return Some(Validity::AllValid);
581            }
582            if validity_kind == std::mem::discriminant(&Validity::AllInvalid) {
583                return Some(Validity::AllInvalid);
584            }
585            if validity_kind == std::mem::discriminant(&Validity::NonNullable) {
586                return Some(Validity::NonNullable);
587            }
588        }
589
590        Some(Validity::Array(
591            unsafe {
592                ChunkedArray::new_unchecked(
593                    validities.into_iter().map(|(v, len)| v.to_array(len)),
594                    DType::Bool(Nullability::NonNullable),
595                )
596            }
597            .into_array(),
598        ))
599    }
600}
601
602impl Validity {
603    pub fn from_bit_buffer(buffer: BitBuffer, nullability: Nullability) -> Self {
604        if buffer.true_count() == buffer.len() {
605            nullability.into()
606        } else if buffer.true_count() == 0 {
607            Validity::AllInvalid
608        } else {
609            Validity::Array(BoolArray::new(buffer, Validity::NonNullable).into_array())
610        }
611    }
612
613    pub fn from_mask(mask: Mask, nullability: Nullability) -> Self {
614        assert!(
615            nullability == Nullability::Nullable || matches!(mask, Mask::AllTrue(_)),
616            "NonNullable validity must be AllValid",
617        );
618        match mask {
619            Mask::AllTrue(_) => match nullability {
620                Nullability::NonNullable => Validity::NonNullable,
621                Nullability::Nullable => Validity::AllValid,
622            },
623            Mask::AllFalse(_) => Validity::AllInvalid,
624            Mask::Values(values) => Validity::Array(values.into_array()),
625        }
626    }
627}
628
629impl IntoArray for Mask {
630    #[inline]
631    fn into_array(self) -> ArrayRef {
632        match self {
633            Self::AllTrue(len) => ConstantArray::new(true, len).into_array(),
634            Self::AllFalse(len) => ConstantArray::new(false, len).into_array(),
635            Self::Values(a) => a.into_array(),
636        }
637    }
638}
639
640impl IntoArray for &MaskValues {
641    #[inline]
642    fn into_array(self) -> ArrayRef {
643        BoolArray::new(self.bit_buffer().clone(), Validity::NonNullable).into_array()
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use rstest::rstest;
650    use vortex_buffer::Buffer;
651    use vortex_buffer::buffer;
652    use vortex_mask::Mask;
653
654    use crate::ArrayRef;
655    use crate::IntoArray;
656    use crate::VortexSessionExecute;
657    use crate::array_session;
658    use crate::arrays::PrimitiveArray;
659    use crate::dtype::Nullability;
660    use crate::validity::BoolArray;
661    use crate::validity::Validity;
662
663    #[rstest]
664    #[case(Validity::AllValid, 5, &[2, 4], Validity::AllValid, Validity::AllValid)]
665    #[case(
666        Validity::AllValid,
667        5,
668        &[2, 4],
669        Validity::AllInvalid,
670        Validity::Array(BoolArray::from_iter([true, true, false, true, false]).into_array())
671    )]
672    #[case(
673        Validity::AllValid,
674        5,
675        &[2, 4],
676        Validity::Array(BoolArray::from_iter([true, false]).into_array()),
677        Validity::Array(BoolArray::from_iter([true, true, true, true, false]).into_array())
678    )]
679    #[case(
680        Validity::AllInvalid,
681        5,
682        &[2, 4],
683        Validity::AllValid,
684        Validity::Array(BoolArray::from_iter([false, false, true, false, true]).into_array())
685    )]
686    #[case(Validity::AllInvalid, 5, &[2, 4], Validity::AllInvalid, Validity::AllInvalid)]
687    #[case(
688        Validity::AllInvalid,
689        5,
690        &[2, 4],
691        Validity::Array(BoolArray::from_iter([true, false]).into_array()),
692        Validity::Array(BoolArray::from_iter([false, false, true, false, false]).into_array())
693    )]
694    #[case(
695        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array()),
696        5,
697        &[2, 4],
698        Validity::AllValid,
699        Validity::Array(BoolArray::from_iter([false, true, true, true, true]).into_array())
700    )]
701    #[case(
702        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array()),
703        5,
704        &[2, 4],
705        Validity::AllInvalid,
706        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array())
707    )]
708    #[case(
709        Validity::Array(BoolArray::from_iter([false, true, false, true, false]).into_array()),
710        5,
711        &[2, 4],
712        Validity::Array(BoolArray::from_iter([true, false]).into_array()),
713        Validity::Array(BoolArray::from_iter([false, true, true, true, false]).into_array())
714    )]
715
716    fn patch_validity(
717        #[case] validity: Validity,
718        #[case] len: usize,
719        #[case] positions: &[u64],
720        #[case] patches: Validity,
721        #[case] expected: Validity,
722    ) {
723        let indices =
724            PrimitiveArray::new(Buffer::copy_from(positions), Validity::NonNullable).into_array();
725
726        let mut ctx = array_session().create_execution_ctx();
727
728        assert!(
729            validity
730                .patch(len, 0, &indices, &patches, &mut ctx,)
731                .unwrap()
732                .mask_eq(&expected, len, &mut ctx)
733                .unwrap()
734        );
735    }
736
737    #[test]
738    #[should_panic]
739    fn out_of_bounds_patch() {
740        let mut ctx = array_session().create_execution_ctx();
741        Validity::NonNullable
742            .patch(
743                2,
744                0,
745                &buffer![4].into_array(),
746                &Validity::AllInvalid,
747                &mut ctx,
748            )
749            .unwrap();
750    }
751
752    #[test]
753    #[should_panic]
754    fn into_validity_nullable() {
755        Validity::from_mask(Mask::AllFalse(10), Nullability::NonNullable);
756    }
757
758    #[test]
759    #[should_panic]
760    fn into_validity_nullable_array() {
761        Validity::from_mask(Mask::from_iter(vec![true, false]), Nullability::NonNullable);
762    }
763
764    #[rstest]
765    #[case(
766        Validity::AllValid,
767        PrimitiveArray::new(buffer![0, 1], Validity::from_iter(vec![true, false])).into_array(),
768        Validity::from_iter(vec![true, false])
769    )]
770    #[case(Validity::AllValid, buffer![0, 1].into_array(), Validity::AllValid)]
771    #[case(
772        Validity::AllValid,
773        PrimitiveArray::new(buffer![0, 1], Validity::AllInvalid).into_array(),
774        Validity::AllInvalid
775    )]
776    #[case(
777        Validity::NonNullable,
778        PrimitiveArray::new(buffer![0, 1], Validity::from_iter(vec![true, false])).into_array(),
779        Validity::from_iter(vec![true, false])
780    )]
781    #[case(Validity::NonNullable, buffer![0, 1].into_array(), Validity::NonNullable)]
782    #[case(
783        Validity::NonNullable,
784        PrimitiveArray::new(buffer![0, 1], Validity::AllInvalid).into_array(),
785        Validity::AllInvalid
786    )]
787    fn validity_take(
788        #[case] validity: Validity,
789        #[case] indices: ArrayRef,
790        #[case] expected: Validity,
791    ) {
792        let mut ctx = array_session().create_execution_ctx();
793        assert!(
794            validity
795                .take(&indices)
796                .unwrap()
797                .mask_eq(&expected, indices.len(), &mut ctx)
798                .unwrap()
799        );
800    }
801
802    #[rstest]
803    // Mixed constant variants with equal masks.
804    #[case(Validity::NonNullable, Validity::AllValid, true)]
805    #[case(Validity::AllValid, Validity::NonNullable, true)]
806    #[case(Validity::AllValid, Validity::AllInvalid, false)]
807    #[case(Validity::NonNullable, Validity::AllInvalid, false)]
808    // An array that resolves to a constant mask must equal the constant variant.
809    #[case(
810        Validity::Array(BoolArray::from_iter([true, true, true]).into_array()),
811        Validity::AllValid,
812        true
813    )]
814    #[case(
815        Validity::NonNullable,
816        Validity::Array(BoolArray::from_iter([true, true, true]).into_array()),
817        true
818    )]
819    #[case(
820        Validity::Array(BoolArray::from_iter([false, false, false]).into_array()),
821        Validity::AllInvalid,
822        true
823    )]
824    #[case(
825        Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
826        Validity::AllValid,
827        false
828    )]
829    #[case(
830        Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
831        Validity::AllInvalid,
832        false
833    )]
834    fn mask_eq_mixed_variants(
835        #[case] lhs: Validity,
836        #[case] rhs: Validity,
837        #[case] expected: bool,
838    ) -> vortex_error::VortexResult<()> {
839        let mut ctx = array_session().create_execution_ctx();
840        assert_eq!(lhs.mask_eq(&rhs, 3, &mut ctx)?, expected);
841        Ok(())
842    }
843}