Skip to main content

vortex_sparse/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::fmt::Debug;
5use std::fmt::Display;
6use std::fmt::Formatter;
7use std::hash::Hash;
8use std::hash::Hasher;
9
10use prost::Message as _;
11use vortex_array::AnyCanonical;
12use vortex_array::Array;
13use vortex_array::ArrayEq;
14use vortex_array::ArrayHash;
15use vortex_array::ArrayId;
16use vortex_array::ArrayParts;
17use vortex_array::ArrayRef;
18use vortex_array::ArraySlots;
19use vortex_array::ArrayView;
20use vortex_array::Canonical;
21use vortex_array::EqMode;
22use vortex_array::ExecutionCtx;
23use vortex_array::ExecutionResult;
24use vortex_array::IntoArray;
25use vortex_array::arrays::BoolArray;
26use vortex_array::arrays::ConstantArray;
27use vortex_array::arrays::Primitive;
28use vortex_array::arrays::PrimitiveArray;
29use vortex_array::arrays::bool::BoolArrayExt;
30use vortex_array::buffer::BufferHandle;
31use vortex_array::builtins::ArrayBuiltins;
32use vortex_array::dtype::DType;
33use vortex_array::dtype::Nullability;
34use vortex_array::patches::PatchSlotIndices;
35use vortex_array::patches::Patches;
36use vortex_array::patches::PatchesData;
37use vortex_array::patches::PatchesMetadata;
38use vortex_array::require_child;
39use vortex_array::require_opt_child;
40use vortex_array::scalar::Scalar;
41use vortex_array::scalar::ScalarValue;
42use vortex_array::scalar_fn::fns::operators::Operator;
43use vortex_array::serde::ArrayChildren;
44use vortex_array::validity::Validity;
45use vortex_array::vtable::VTable;
46use vortex_array::vtable::ValidityVTable;
47use vortex_buffer::Buffer;
48use vortex_buffer::ByteBufferMut;
49use vortex_error::VortexExpect as _;
50use vortex_error::VortexResult;
51use vortex_error::vortex_bail;
52use vortex_error::vortex_ensure;
53use vortex_error::vortex_ensure_eq;
54use vortex_error::vortex_panic;
55use vortex_mask::AllOr;
56use vortex_mask::Mask;
57use vortex_session::VortexSession;
58use vortex_session::registry::CachedId;
59
60use crate::canonical::execute_sparse;
61use crate::rules::RULES;
62
63mod canonical;
64mod compute;
65mod kernel;
66mod ops;
67mod rules;
68mod slice;
69
70use vortex_array::aggregate_fn::AggregateFnVTable as _;
71use vortex_array::aggregate_fn::fns::is_constant::IsConstant;
72use vortex_array::aggregate_fn::fns::min_max::MinMax;
73use vortex_array::aggregate_fn::fns::nan_count::NanCount;
74use vortex_array::aggregate_fn::fns::null_count::NullCount;
75use vortex_array::aggregate_fn::fns::sum::Sum;
76use vortex_array::aggregate_fn::session::AggregateFnSessionExt;
77use vortex_array::session::ArraySessionExt;
78
79/// Initialize Sparse encoding in the given session.
80///
81/// Registers the Sparse array vtable, parent execution kernels, and aggregate kernels
82/// (`IsConstant`, `Sum`, `MinMax`, `NullCount`, `NanCount`).
83pub fn initialize(session: &VortexSession) {
84    session.arrays().register(Sparse);
85    kernel::initialize(session);
86
87    let aggregate_fns = session.aggregate_fns();
88    aggregate_fns.register_aggregate_kernel(
89        Sparse.id(),
90        Some(IsConstant.id()),
91        &compute::is_constant::SparseIsConstantKernel,
92    );
93    aggregate_fns.register_aggregate_kernel(
94        Sparse.id(),
95        Some(Sum.id()),
96        &compute::sum::SparseSumKernel,
97    );
98    aggregate_fns.register_aggregate_kernel(
99        Sparse.id(),
100        Some(MinMax.id()),
101        &compute::min_max::SparseMinMaxKernel,
102    );
103    aggregate_fns.register_aggregate_kernel(
104        Sparse.id(),
105        Some(NullCount.id()),
106        &compute::null_count::SparseNullCountKernel,
107    );
108    aggregate_fns.register_aggregate_kernel(
109        Sparse.id(),
110        Some(NanCount.id()),
111        &compute::nan_count::SparseNanCountKernel,
112    );
113}
114
115/// A [`Sparse`]-encoded Vortex array.
116pub type SparseArray = Array<Sparse>;
117
118#[vortex_array::array_slots(Sparse)]
119pub struct SparseSlots {
120    pub patch_indices: ArrayRef,
121    pub patch_values: ArrayRef,
122    pub patch_chunk_offsets: Option<ArrayRef>,
123}
124
125/// Concrete parts of a [`SparseArray`] after iterative execution.
126pub(crate) struct SparseParts {
127    pub patches: Patches,
128    pub fill_value: Scalar,
129    pub dtype: DType,
130    pub len: usize,
131}
132
133pub(crate) trait SparseOwnedExt {
134    fn into_parts(self) -> VortexResult<SparseParts>;
135}
136
137impl SparseOwnedExt for Array<Sparse> {
138    fn into_parts(self) -> VortexResult<SparseParts> {
139        let patches = Patches::new(
140            self.len(),
141            self.patches().offset(),
142            self.as_ref().slots()[SparseSlots::PATCH_INDICES]
143                .clone()
144                .vortex_expect("indices"),
145            self.as_ref().slots()[SparseSlots::PATCH_VALUES]
146                .clone()
147                .vortex_expect("values"),
148            self.as_ref().slots()[SparseSlots::PATCH_CHUNK_OFFSETS].clone(),
149        )?;
150        Ok(SparseParts {
151            patches,
152            fill_value: self.fill_scalar().clone(),
153            dtype: self.dtype().clone(),
154            len: self.len(),
155        })
156    }
157}
158
159#[derive(Clone, prost::Message)]
160#[repr(C)]
161pub struct SparseMetadata {
162    #[prost(message, required, tag = "1")]
163    patches: PatchesMetadata,
164}
165
166impl ArrayHash for SparseData {
167    fn array_hash<H: Hasher>(&self, state: &mut H, _accuracy: EqMode) {
168        self.array_len.hash(state);
169        self.patches_data.hash(state);
170        self.fill_value.hash(state);
171    }
172}
173
174impl ArrayEq for SparseData {
175    fn array_eq(&self, other: &Self, _accuracy: EqMode) -> bool {
176        self.array_len == other.array_len
177            && self.patches_data == other.patches_data
178            && self.fill_value == other.fill_value
179    }
180}
181
182impl VTable for Sparse {
183    type TypedArrayData = SparseData;
184
185    type OperationsVTable = Self;
186    type ValidityVTable = Self;
187
188    fn id(&self) -> ArrayId {
189        static ID: CachedId = CachedId::new("vortex.sparse");
190        *ID
191    }
192
193    fn validate(
194        &self,
195        data: &Self::TypedArrayData,
196        dtype: &DType,
197        len: usize,
198        slots: &[Option<ArrayRef>],
199    ) -> VortexResult<()> {
200        let patches = SparseData::patches_from_slots(data, len, slots);
201        SparseData::validate(&patches, data.fill_scalar(), dtype, len)
202    }
203
204    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
205        1
206    }
207
208    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
209        match idx {
210            0 => {
211                let fill_value_buffer =
212                    ScalarValue::to_proto_bytes::<ByteBufferMut>(array.fill_value.value()).freeze();
213                BufferHandle::new_host(fill_value_buffer)
214            }
215            _ => vortex_panic!("SparseArray buffer index {idx} out of bounds"),
216        }
217    }
218
219    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
220        match idx {
221            0 => Some("fill_value".to_string()),
222            _ => vortex_panic!("SparseArray buffer_name index {idx} out of bounds"),
223        }
224    }
225
226    fn with_buffers(
227        &self,
228        array: ArrayView<'_, Self>,
229        buffers: &[BufferHandle],
230    ) -> VortexResult<ArrayParts<Self>> {
231        vortex_array::vtable::unsupported_buffer_replacement(array, buffers)
232    }
233
234    fn serialize(
235        array: ArrayView<'_, Self>,
236        _session: &VortexSession,
237    ) -> VortexResult<Option<Vec<u8>>> {
238        let patches = array.patches().to_metadata(array.len(), array.dtype())?;
239        let metadata = SparseMetadata { patches };
240
241        // Note that we DO NOT serialize the fill value since that is stored in the buffers.
242        Ok(Some(metadata.encode_to_vec()))
243    }
244
245    fn deserialize(
246        &self,
247        dtype: &DType,
248        len: usize,
249        metadata: &[u8],
250        buffers: &[BufferHandle],
251        children: &dyn ArrayChildren,
252        session: &VortexSession,
253    ) -> VortexResult<ArrayParts<Self>> {
254        let metadata = SparseMetadata::decode(metadata)?;
255
256        // Once we have the patches metadata, we need to get the fill value from the buffers.
257
258        if buffers.len() != 1 {
259            vortex_bail!("Expected 1 buffer, got {}", buffers.len());
260        }
261        let scalar_bytes: &[u8] = &buffers[0].clone().try_to_host_sync()?;
262
263        let scalar_value = ScalarValue::from_proto_bytes(scalar_bytes, dtype, session)?;
264        let fill_value = Scalar::try_new(dtype.clone(), scalar_value)?;
265
266        vortex_ensure_eq!(
267            children.len(),
268            2,
269            "SparseArray expects 2 children for sparse encoding, found {}",
270            children.len()
271        );
272
273        let patch_indices = children.get(
274            0,
275            &metadata.patches.indices_dtype()?,
276            metadata.patches.len()?,
277        )?;
278        let patch_values = children.get(1, dtype, metadata.patches.len()?)?;
279
280        let patches = Patches::new(
281            len,
282            metadata.patches.offset()?,
283            patch_indices,
284            patch_values,
285            None,
286        )?;
287        let slots = SparseData::make_slots(&patches);
288        let data = SparseData::from_patches(&patches, fill_value)?;
289        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
290    }
291
292    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
293        SparseSlots::NAMES[idx].to_string()
294    }
295
296    fn reduce_parent(
297        array: ArrayView<'_, Self>,
298        parent: &ArrayRef,
299        child_idx: usize,
300    ) -> VortexResult<Option<ArrayRef>> {
301        RULES.evaluate(array, parent, child_idx)
302    }
303
304    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
305        // Resolve offset first: wrap indices in Binary(indices, offset, Sub) and
306        // reassemble with offset=0. Uses slot children (not data) since the executor
307        // may have updated slots via reduce_parent/execute_parent.
308        let array = if array.patches().offset() != 0 {
309            let offset = array.patches().offset();
310            let indices = array.patch_indices();
311            let values = array.patch_values().clone();
312            let len = array.len();
313            let offset_scalar = Scalar::from(offset).cast(indices.dtype())?;
314            let resolved_indices = indices.binary(
315                ConstantArray::new(offset_scalar, indices.len()).into_array(),
316                Operator::Sub,
317            )?;
318            let patches = Patches::new(len, 0, resolved_indices.clone(), values, None)?;
319            // Decompose, update in place, and reassemble without re-validation.
320            match array.try_into_parts() {
321                Ok(mut parts) => {
322                    parts.data.patches_data = PatchesData::from_patches(&patches);
323                    parts.slots[SparseSlots::PATCH_INDICES] = Some(resolved_indices);
324                    parts.slots[SparseSlots::PATCH_CHUNK_OFFSETS] = None;
325                    unsafe { Array::from_parts_unchecked(parts) }
326                }
327                Err(array) => unsafe {
328                    Sparse::new_unchecked(patches, array.fill_scalar().clone())
329                },
330            }
331        } else {
332            array
333        };
334
335        // Require children to be executed through the scheduler,
336        // enabling cross-step optimization via reduce_parent rules.
337        let array = require_child!(
338            array, array.patch_indices(), SparseSlots::PATCH_INDICES => Primitive
339        );
340        let array = require_child!(
341            array, array.patch_values(), SparseSlots::PATCH_VALUES => AnyCanonical
342        );
343        require_opt_child!(
344            array,
345            array.patch_chunk_offsets(),
346            SparseSlots::PATCH_CHUNK_OFFSETS => Primitive
347        );
348
349        let parts = array.into_parts()?;
350        // TODO(joe): remove ctx from execute_sparse since all slots should be canonical.
351        execute_sparse(parts, ctx).map(ExecutionResult::done)
352    }
353}
354
355const PATCH_SLOTS: PatchSlotIndices = PatchSlotIndices {
356    indices: SparseSlots::PATCH_INDICES,
357    values: SparseSlots::PATCH_VALUES,
358    chunk_offsets: SparseSlots::PATCH_CHUNK_OFFSETS,
359};
360
361#[derive(Clone, Debug)]
362pub struct SparseData {
363    /// The total length of the sparse array.
364    array_len: usize,
365    /// Patch metadata (offset, offset_within_chunk) for reconstructing Patches from slots.
366    patches_data: PatchesData,
367    fill_value: Scalar,
368}
369
370impl Display for SparseData {
371    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
372        write!(f, "fill_value: {}", self.fill_value)
373    }
374}
375
376#[derive(Clone, Debug)]
377pub struct Sparse;
378
379impl Sparse {
380    /// Construct a new [`SparseArray`] from indices, values, length, and fill value.
381    pub fn try_new(
382        indices: ArrayRef,
383        values: ArrayRef,
384        len: usize,
385        fill_value: Scalar,
386    ) -> VortexResult<SparseArray> {
387        let dtype = fill_value.dtype().clone();
388        vortex_ensure!(
389            values.dtype() == &dtype,
390            "sparse values dtype {} must match fill value dtype {}",
391            values.dtype(),
392            dtype,
393        );
394        let patches = Patches::new(len, 0, indices, values, None)?;
395        let slots = SparseData::make_slots(&patches);
396        let data = SparseData::from_patches(&patches, fill_value)?;
397        Ok(unsafe {
398            Array::from_parts_unchecked(ArrayParts::new(Sparse, dtype, len, data).with_slots(slots))
399        })
400    }
401
402    pub fn try_new_from_patches(patches: Patches, fill_value: Scalar) -> VortexResult<SparseArray> {
403        let dtype = fill_value.dtype().clone();
404        let len = patches.array_len();
405        let slots = SparseData::make_slots(&patches);
406        let data = SparseData::from_patches(&patches, fill_value)?;
407        Ok(unsafe {
408            Array::from_parts_unchecked(ArrayParts::new(Sparse, dtype, len, data).with_slots(slots))
409        })
410    }
411
412    pub(crate) unsafe fn new_unchecked(patches: Patches, fill_value: Scalar) -> SparseArray {
413        let dtype = fill_value.dtype().clone();
414        let len = patches.array_len();
415        let slots = SparseData::make_slots(&patches);
416        let data = SparseData::from_patches_unchecked(&patches, fill_value);
417        unsafe {
418            Array::from_parts_unchecked(ArrayParts::new(Sparse, dtype, len, data).with_slots(slots))
419        }
420    }
421
422    /// Encode the given array as a [`SparseArray`].
423    pub fn encode(
424        array: &ArrayRef,
425        fill_value: Option<Scalar>,
426        ctx: &mut ExecutionCtx,
427    ) -> VortexResult<ArrayRef> {
428        SparseData::encode(array, fill_value, ctx)
429    }
430}
431
432impl SparseData {
433    pub fn validate(
434        patches: &Patches,
435        fill_value: &Scalar,
436        dtype: &DType,
437        len: usize,
438    ) -> VortexResult<()> {
439        vortex_ensure!(
440            fill_value.dtype() == dtype,
441            "fill value dtype {} does not match array dtype {}",
442            fill_value.dtype(),
443            dtype,
444        );
445        vortex_ensure!(
446            patches.array_len() == len,
447            "patches length {} does not match array length {}",
448            patches.array_len(),
449            len
450        );
451        vortex_ensure!(
452            patches.values().dtype() == dtype,
453            "patch values dtype {} does not match array dtype {}",
454            patches.values().dtype(),
455            dtype,
456        );
457        Ok(())
458    }
459
460    fn make_slots(patches: &Patches) -> ArraySlots {
461        let mut slots = ArraySlots::with_capacity(SparseSlots::COUNT);
462        PatchesData::push_slots(&mut slots, Some(patches));
463        slots
464    }
465
466    /// Reconstruct a [`Patches`] from the stored metadata and the array's slots.
467    fn patches_from_slots(data: &SparseData, len: usize, slots: &[Option<ArrayRef>]) -> Patches {
468        PatchesData::patches_from_slots(Some(&data.patches_data), len, slots, PATCH_SLOTS)
469            .vortex_expect("SparseArray patch slots must be present")
470    }
471
472    /// Build a new SparseData from an existing set of patches.
473    pub fn try_new_from_patches(patches: Patches, fill_value: Scalar) -> VortexResult<Self> {
474        Self::from_patches(&patches, fill_value)
475    }
476
477    /// Extract metadata from patches to create SparseData.
478    ///
479    /// Patch values must already match the fill dtype; callers are expected to construct patches
480    /// with the correct dtype rather than relying on this to normalize them.
481    fn from_patches(patches: &Patches, fill_value: Scalar) -> VortexResult<Self> {
482        vortex_ensure!(
483            patches.values().dtype() == fill_value.dtype(),
484            "patch values dtype {} must match fill dtype {}",
485            patches.values().dtype(),
486            fill_value.dtype(),
487        );
488        Ok(Self::from_patches_unchecked(patches, fill_value))
489    }
490
491    /// Extract metadata from patches to create SparseData, without validation.
492    fn from_patches_unchecked(patches: &Patches, fill_value: Scalar) -> Self {
493        Self {
494            array_len: patches.array_len(),
495            patches_data: PatchesData::from_patches(patches),
496            fill_value,
497        }
498    }
499
500    /// Returns the length of the array.
501    #[inline]
502    pub fn len(&self) -> usize {
503        self.array_len
504    }
505
506    /// Returns whether the array is empty.
507    #[inline]
508    pub fn is_empty(&self) -> bool {
509        self.array_len == 0
510    }
511
512    /// Returns the logical data type of the array.
513    #[inline]
514    pub fn dtype(&self) -> &DType {
515        self.fill_scalar().dtype()
516    }
517
518    /// Returns the offset of the patches within the parent array.
519    #[inline]
520    pub fn offset(&self) -> usize {
521        self.patches_data.offset()
522    }
523
524    #[inline]
525    pub fn fill_scalar(&self) -> &Scalar {
526        &self.fill_value
527    }
528
529    /// Encode given array as a SparseArray.
530    ///
531    /// Optionally provided fill value will be respected if the array is less than 90% null.
532    pub fn encode(
533        array: &ArrayRef,
534        fill_value: Option<Scalar>,
535        ctx: &mut ExecutionCtx,
536    ) -> VortexResult<ArrayRef> {
537        if let Some(fill_value) = fill_value.as_ref()
538            && !array.dtype().eq_ignore_nullability(fill_value.dtype())
539        {
540            vortex_bail!(
541                "Array and fill value types must have the same base type. got {} and {}",
542                array.dtype(),
543                fill_value.dtype()
544            )
545        }
546        let mask = array.validity()?.execute_mask(array.len(), ctx)?;
547
548        if mask.all_false() {
549            // Array is constant NULL
550            return Ok(
551                ConstantArray::new(Scalar::null(array.dtype().clone()), array.len()).into_array(),
552            );
553        } else if mask.false_count() as f64 > (0.9 * mask.len() as f64) {
554            // Array is dominated by NULL but has non-NULL values
555            let non_null_values = array
556                .filter(mask.clone())?
557                .execute::<Canonical>(ctx)?
558                .into_array();
559            let non_null_indices = match mask.indices() {
560                AllOr::All => {
561                    // We already know that the mask is 90%+ false
562                    unreachable!("Mask is mostly null")
563                }
564                AllOr::None => {
565                    // we know there are some non-NULL values
566                    unreachable!("Mask is mostly null but not all null")
567                }
568                AllOr::Some(values) => {
569                    let buffer: Buffer<u32> = values
570                        .iter()
571                        .map(|&v| v.try_into().vortex_expect("indices must fit in u32"))
572                        .collect();
573
574                    buffer.into_array()
575                }
576            };
577
578            return Sparse::try_new(
579                non_null_indices,
580                non_null_values,
581                array.len(),
582                Scalar::null(array.dtype().clone()),
583            )
584            .map(IntoArray::into_array);
585        }
586
587        let fill = if let Some(fill) = fill_value {
588            fill.cast(array.dtype())?
589        } else {
590            // TODO(robert): Support other dtypes, only thing missing is getting most common value out of the array
591            let primitive = array.clone().execute::<PrimitiveArray>(ctx)?;
592            let (top_pvalue, _) = primitive
593                .top_value(ctx)?
594                .vortex_expect("Non empty or all null array");
595
596            Scalar::primitive_value(top_pvalue, top_pvalue.ptype(), array.dtype().nullability())
597        };
598
599        let fill_array = ConstantArray::new(fill.clone(), array.len()).into_array();
600        let non_top_bool = array
601            .binary(fill_array.clone(), Operator::NotEq)?
602            .fill_null(Scalar::bool(true, Nullability::NonNullable))?
603            .execute::<BoolArray>(ctx)?;
604        let non_top_mask = Mask::from_buffer(non_top_bool.to_bit_buffer());
605
606        let non_top_values = array
607            .filter(non_top_mask.clone())?
608            .execute::<Canonical>(ctx)?
609            .into_array();
610
611        let indices: Buffer<u64> = match non_top_mask {
612            Mask::AllTrue(count) => {
613                // all true -> complete slice
614                (0u64..count as u64).collect()
615            }
616            Mask::AllFalse(_) => {
617                // All values are equal to the top value
618                return Ok(fill_array);
619            }
620            Mask::Values(values) => values.indices().iter().map(|v| *v as u64).collect(),
621        };
622
623        Sparse::try_new(indices.into_array(), non_top_values, array.len(), fill)
624            .map(IntoArray::into_array)
625    }
626}
627
628/// Extension trait for accessing patches on [`SparseArray`] and [`ArrayView<'_, Sparse>`].
629///
630/// Patches are reconstructed from the array's slots and stored metadata on each call.
631pub trait SparseExt {
632    /// Reconstruct patches from the array's slots and metadata.
633    fn patches(&self) -> Patches;
634
635    /// Return patches with offset-resolved indices (offset subtracted from each index).
636    fn resolved_patches(&self) -> VortexResult<Patches> {
637        let patches = self.patches();
638        let indices_offset = Scalar::from(patches.offset()).cast(patches.indices().dtype())?;
639        let indices = patches.indices().binary(
640            ConstantArray::new(indices_offset, patches.indices().len()).into_array(),
641            Operator::Sub,
642        )?;
643
644        Patches::new(
645            patches.array_len(),
646            0,
647            indices,
648            patches.values().clone(),
649            // TODO(0ax1): handle chunk offsets
650            None,
651        )
652    }
653}
654
655impl SparseExt for ArrayView<'_, Sparse> {
656    fn patches(&self) -> Patches {
657        SparseData::patches_from_slots(self.data(), self.len(), self.slots())
658    }
659}
660
661impl SparseExt for Array<Sparse> {
662    fn patches(&self) -> Patches {
663        SparseData::patches_from_slots(self.data(), self.as_array().len(), self.slots())
664    }
665}
666
667impl ValidityVTable<Sparse> for Sparse {
668    fn validity(array: ArrayView<'_, Sparse>) -> VortexResult<Validity> {
669        let orig_patches = array.patches();
670        let validity_patches = unsafe {
671            Patches::new_unchecked(
672                orig_patches.array_len(),
673                orig_patches.offset(),
674                orig_patches.indices().clone(),
675                orig_patches
676                    .values()
677                    .validity()?
678                    .to_array(orig_patches.values().len()),
679                orig_patches.chunk_offsets().clone(),
680                orig_patches.offset_within_chunk(),
681            )
682        };
683
684        Ok(Validity::Array(
685            unsafe { Sparse::new_unchecked(validity_patches, array.fill_value.is_valid().into()) }
686                .into_array(),
687        ))
688    }
689}
690
691#[cfg(test)]
692mod test {
693    use std::sync::LazyLock;
694
695    use itertools::Itertools;
696    use vortex_array::IntoArray;
697    use vortex_array::VortexSessionExecute;
698    use vortex_array::arrays::ConstantArray;
699    use vortex_array::arrays::PrimitiveArray;
700    use vortex_array::assert_arrays_eq;
701    use vortex_array::builtins::ArrayBuiltins;
702    use vortex_array::dtype::DType;
703    use vortex_array::dtype::Nullability;
704    use vortex_array::dtype::PType;
705    use vortex_array::scalar::Scalar;
706    use vortex_array::validity::Validity;
707    use vortex_buffer::buffer;
708    use vortex_error::VortexExpect;
709
710    use super::*;
711    use crate::Sparse;
712
713    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
714        let session = vortex_array::array_session();
715        initialize(&session);
716        session
717    });
718
719    fn nullable_fill() -> Scalar {
720        Scalar::null(DType::Primitive(PType::I32, Nullability::Nullable))
721    }
722
723    fn non_nullable_fill() -> Scalar {
724        Scalar::from(42i32)
725    }
726
727    fn sparse_array(fill_value: Scalar) -> ArrayRef {
728        // merged array: [null, null, 100, null, null, 200, null, null, 300, null]
729        let mut values = buffer![100i32, 200, 300].into_array();
730        values = values.cast(fill_value.dtype().clone()).unwrap();
731
732        Sparse::try_new(buffer![2u64, 5, 8].into_array(), values, 10, fill_value)
733            .unwrap()
734            .into_array()
735    }
736
737    #[test]
738    pub fn test_scalar_at() {
739        let array = sparse_array(nullable_fill());
740
741        assert_eq!(
742            array
743                .execute_scalar(0, &mut SESSION.create_execution_ctx())
744                .unwrap(),
745            nullable_fill()
746        );
747        assert_eq!(
748            array
749                .execute_scalar(2, &mut SESSION.create_execution_ctx())
750                .unwrap(),
751            Scalar::from(Some(100_i32))
752        );
753        assert_eq!(
754            array
755                .execute_scalar(5, &mut SESSION.create_execution_ctx())
756                .unwrap(),
757            Scalar::from(Some(200_i32))
758        );
759    }
760
761    #[test]
762    #[should_panic(expected = "out of bounds")]
763    fn test_scalar_at_oob() {
764        let array = sparse_array(nullable_fill());
765        array
766            .execute_scalar(10, &mut SESSION.create_execution_ctx())
767            .unwrap();
768    }
769
770    #[test]
771    pub fn test_scalar_at_again() {
772        let arr = Sparse::try_new(
773            ConstantArray::new(10u32, 1).into_array(),
774            ConstantArray::new(Scalar::primitive(1234u32, Nullability::Nullable), 1).into_array(),
775            100,
776            Scalar::null(DType::Primitive(PType::U32, Nullability::Nullable)),
777        )
778        .unwrap();
779
780        assert_eq!(
781            arr.execute_scalar(10, &mut SESSION.create_execution_ctx())
782                .unwrap()
783                .as_primitive()
784                .typed_value::<u32>(),
785            Some(1234)
786        );
787        assert!(
788            arr.execute_scalar(0, &mut SESSION.create_execution_ctx())
789                .unwrap()
790                .is_null()
791        );
792        assert!(
793            arr.execute_scalar(99, &mut SESSION.create_execution_ctx())
794                .unwrap()
795                .is_null()
796        );
797    }
798
799    #[test]
800    pub fn scalar_at_sliced() {
801        let sliced = sparse_array(nullable_fill()).slice(2..7).unwrap();
802        assert_eq!(
803            usize::try_from(
804                &sliced
805                    .execute_scalar(0, &mut SESSION.create_execution_ctx())
806                    .unwrap()
807            )
808            .unwrap(),
809            100
810        );
811    }
812
813    #[test]
814    pub fn validity_mask_sliced_null_fill() {
815        let sliced = sparse_array(nullable_fill()).slice(2..7).unwrap();
816        assert_eq!(
817            sliced
818                .validity()
819                .unwrap()
820                .execute_mask(sliced.len(), &mut SESSION.create_execution_ctx())
821                .unwrap(),
822            Mask::from_iter(vec![true, false, false, true, false])
823        );
824    }
825
826    #[test]
827    pub fn validity_mask_sliced_nonnull_fill() {
828        let sliced = Sparse::try_new(
829            buffer![2u64, 5, 8].into_array(),
830            ConstantArray::new(
831                Scalar::null(DType::Primitive(PType::F32, Nullability::Nullable)),
832                3,
833            )
834            .into_array(),
835            10,
836            Scalar::primitive(1.0f32, Nullability::Nullable),
837        )
838        .unwrap()
839        .slice(2..7)
840        .unwrap();
841
842        assert_eq!(
843            sliced
844                .validity()
845                .unwrap()
846                .execute_mask(sliced.len(), &mut SESSION.create_execution_ctx())
847                .unwrap(),
848            Mask::from_iter(vec![false, true, true, false, true])
849        );
850    }
851
852    #[test]
853    pub fn scalar_at_sliced_twice() {
854        let sliced_once = sparse_array(nullable_fill()).slice(1..8).unwrap();
855        assert_eq!(
856            usize::try_from(
857                &sliced_once
858                    .execute_scalar(1, &mut SESSION.create_execution_ctx())
859                    .unwrap()
860            )
861            .unwrap(),
862            100
863        );
864
865        let sliced_twice = sliced_once.slice(1..6).unwrap();
866        assert_eq!(
867            usize::try_from(
868                &sliced_twice
869                    .execute_scalar(3, &mut SESSION.create_execution_ctx())
870                    .unwrap()
871            )
872            .unwrap(),
873            200
874        );
875    }
876
877    #[test]
878    pub fn sparse_validity_mask() {
879        let array = sparse_array(nullable_fill());
880        assert_eq!(
881            array
882                .validity()
883                .unwrap()
884                .execute_mask(array.len(), &mut SESSION.create_execution_ctx())
885                .unwrap()
886                .to_bit_buffer()
887                .iter()
888                .collect_vec(),
889            [
890                false, false, true, false, false, true, false, false, true, false
891            ]
892        );
893    }
894
895    #[test]
896    fn sparse_validity_mask_non_null_fill() {
897        let array = sparse_array(non_nullable_fill());
898        assert!(
899            array
900                .validity()
901                .unwrap()
902                .execute_mask(array.len(), &mut SESSION.create_execution_ctx())
903                .unwrap()
904                .all_true()
905        );
906    }
907
908    #[test]
909    #[should_panic]
910    fn test_invalid_length() {
911        let values = buffer![15_u32, 135, 13531, 42].into_array();
912        let indices = buffer![10_u64, 11, 50, 100].into_array();
913
914        Sparse::try_new(indices, values, 100, 0_u32.into()).unwrap();
915    }
916
917    #[test]
918    fn test_valid_length() {
919        let values = buffer![15_u32, 135, 13531, 42].into_array();
920        let indices = buffer![10_u64, 11, 50, 100].into_array();
921
922        Sparse::try_new(indices, values, 101, 0_u32.into()).unwrap();
923    }
924
925    #[test]
926    fn encode_with_nulls() {
927        let mut ctx = SESSION.create_execution_ctx();
928        let original = PrimitiveArray::new(
929            buffer![0i32, 1, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4],
930            Validity::from_iter(vec![
931                true, true, false, true, false, true, false, true, true, false, true, false,
932            ]),
933        );
934        let sparse = Sparse::encode(&original.clone().into_array(), None, &mut ctx)
935            .vortex_expect("Sparse::encode should succeed for test data");
936        assert_eq!(
937            sparse
938                .validity()
939                .unwrap()
940                .execute_mask(sparse.len(), &mut ctx)
941                .unwrap(),
942            Mask::from_iter(vec![
943                true, true, false, true, false, true, false, true, true, false, true, false,
944            ])
945        );
946        let sparse_primitive = sparse.execute::<PrimitiveArray>(&mut ctx).unwrap();
947        assert_arrays_eq!(sparse_primitive, original, &mut ctx);
948    }
949
950    #[test]
951    fn validity_mask_includes_null_values_when_fill_is_null() {
952        let indices = buffer![0u8, 2, 4, 6, 8].into_array();
953        let values = PrimitiveArray::from_option_iter([Some(0i16), Some(1), None, None, Some(4)])
954            .into_array();
955        let array = Sparse::try_new(indices, values, 10, Scalar::null_native::<i16>()).unwrap();
956        let actual = array
957            .validity()
958            .unwrap()
959            .execute_mask(array.len(), &mut SESSION.create_execution_ctx())
960            .unwrap();
961        let expected = Mask::from_iter([
962            true, false, true, false, false, false, false, false, true, false,
963        ]);
964
965        assert_eq!(actual, expected);
966    }
967}