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