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