Skip to main content

vortex_runend/
array.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;
11use vortex_array::Array;
12use vortex_array::ArrayEq;
13use vortex_array::ArrayHash;
14use vortex_array::ArrayId;
15use vortex_array::ArrayParts;
16use vortex_array::ArrayRef;
17use vortex_array::ArrayView;
18use vortex_array::EqMode;
19use vortex_array::ExecutionCtx;
20use vortex_array::ExecutionResult;
21use vortex_array::IntoArray;
22use vortex_array::TypedArrayRef;
23use vortex_array::VortexSessionExecute;
24use vortex_array::arrays::Primitive;
25use vortex_array::arrays::VarBinViewArray;
26use vortex_array::buffer::BufferHandle;
27use vortex_array::dtype::DType;
28use vortex_array::dtype::Nullability;
29use vortex_array::dtype::PType;
30use vortex_array::legacy_session;
31use vortex_array::serde::ArrayChildren;
32use vortex_array::smallvec::smallvec;
33use vortex_array::validity::Validity;
34use vortex_array::vtable::VTable;
35use vortex_array::vtable::ValidityVTable;
36use vortex_error::VortexExpect as _;
37use vortex_error::VortexResult;
38use vortex_error::vortex_bail;
39use vortex_error::vortex_ensure;
40use vortex_error::vortex_panic;
41use vortex_session::VortexSession;
42use vortex_session::registry::CachedId;
43
44use crate::compress::runend_decode_primitive;
45use crate::compress::runend_decode_varbinview;
46use crate::compress::runend_encode;
47use crate::decompress_bool::runend_decode_bools;
48use crate::ops::find_physical_index;
49use crate::ops::find_slice_end_index;
50use crate::rules::RULES;
51
52/// A [`RunEnd`]-encoded Vortex array.
53pub type RunEndArray = Array<RunEnd>;
54
55#[derive(Clone, prost::Message)]
56pub struct RunEndMetadata {
57    #[prost(enumeration = "PType", tag = "1")]
58    pub ends_ptype: i32,
59    #[prost(uint64, tag = "2")]
60    pub num_runs: u64,
61    #[prost(uint64, tag = "3")]
62    pub offset: u64,
63}
64
65impl ArrayHash for RunEndData {
66    fn array_hash<H: Hasher>(&self, state: &mut H, _accuracy: EqMode) {
67        self.offset.hash(state);
68    }
69}
70
71impl ArrayEq for RunEndData {
72    fn array_eq(&self, other: &Self, _accuracy: EqMode) -> bool {
73        self.offset == other.offset
74    }
75}
76
77impl VTable for RunEnd {
78    type TypedArrayData = RunEndData;
79
80    type OperationsVTable = Self;
81    type ValidityVTable = Self;
82
83    fn id(&self) -> ArrayId {
84        static ID: CachedId = CachedId::new("vortex.runend");
85        *ID
86    }
87
88    #[allow(clippy::disallowed_methods)]
89    fn validate(
90        &self,
91        data: &Self::TypedArrayData,
92        dtype: &DType,
93        len: usize,
94        slots: &[Option<ArrayRef>],
95    ) -> VortexResult<()> {
96        let ends = slots[ENDS_SLOT]
97            .as_ref()
98            .vortex_expect("RunEndArray ends slot");
99        let values = slots[VALUES_SLOT]
100            .as_ref()
101            .vortex_expect("RunEndArray values slot");
102        // TODO(ctx): trait fixes - VTable::validate has a fixed signature.
103        let mut ctx = legacy_session().create_execution_ctx();
104        RunEndData::validate_parts(ends, values, data.offset, len, &mut ctx)?;
105        vortex_ensure!(
106            values.dtype() == dtype,
107            "expected dtype {}, got {}",
108            dtype,
109            values.dtype()
110        );
111        Ok(())
112    }
113
114    fn nbuffers(_array: ArrayView<'_, Self>) -> usize {
115        0
116    }
117
118    fn buffer(_array: ArrayView<'_, Self>, idx: usize) -> BufferHandle {
119        vortex_panic!("RunEndArray buffer index {idx} out of bounds")
120    }
121
122    fn buffer_name(_array: ArrayView<'_, Self>, idx: usize) -> Option<String> {
123        vortex_panic!("RunEndArray buffer_name index {idx} out of bounds")
124    }
125
126    fn with_buffers(
127        &self,
128        array: ArrayView<'_, Self>,
129        buffers: &[BufferHandle],
130    ) -> VortexResult<ArrayParts<Self>> {
131        vortex_array::vtable::with_empty_buffers(self, array, buffers)
132    }
133
134    fn serialize(
135        array: ArrayView<'_, Self>,
136        _session: &VortexSession,
137    ) -> VortexResult<Option<Vec<u8>>> {
138        Ok(Some(
139            RunEndMetadata {
140                ends_ptype: PType::try_from(array.ends().dtype())
141                    .vortex_expect("Must be a valid PType") as i32,
142                num_runs: array.ends().len() as u64,
143                offset: array.offset() as u64,
144            }
145            .encode_to_vec(),
146        ))
147    }
148
149    fn deserialize(
150        &self,
151        dtype: &DType,
152        len: usize,
153        metadata: &[u8],
154        _buffers: &[BufferHandle],
155        children: &dyn ArrayChildren,
156        _session: &VortexSession,
157    ) -> VortexResult<ArrayParts<Self>> {
158        let metadata = RunEndMetadata::decode(metadata)?;
159        let ends_dtype = DType::Primitive(metadata.ends_ptype(), Nullability::NonNullable);
160        let runs = usize::try_from(metadata.num_runs).vortex_expect("Must be a valid usize");
161        let ends = children.get(0, &ends_dtype, runs)?;
162
163        let values = children.get(1, dtype, runs)?;
164        let offset = usize::try_from(metadata.offset).vortex_expect("Offset must be a valid usize");
165        let slots = smallvec![Some(ends), Some(values)];
166        let data = RunEndData::new(offset);
167        Ok(ArrayParts::new(self.clone(), dtype.clone(), len, data).with_slots(slots))
168    }
169
170    fn slot_name(_array: ArrayView<'_, Self>, idx: usize) -> String {
171        SLOT_NAMES[idx].to_string()
172    }
173
174    fn reduce_parent(
175        array: ArrayView<'_, Self>,
176        parent: &ArrayRef,
177        child_idx: usize,
178    ) -> VortexResult<Option<ArrayRef>> {
179        RULES.evaluate(array, parent, child_idx)
180    }
181
182    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult> {
183        run_end_canonicalize(&array, ctx).map(ExecutionResult::done)
184    }
185}
186
187/// The run-end positions marking where each run terminates.
188pub(super) const ENDS_SLOT: usize = 0;
189/// The values for each run.
190pub(super) const VALUES_SLOT: usize = 1;
191pub(super) const NUM_SLOTS: usize = 2;
192pub(super) const SLOT_NAMES: [&str; NUM_SLOTS] = ["ends", "values"];
193
194#[derive(Clone, Debug)]
195pub struct RunEndData {
196    offset: usize,
197}
198
199impl Display for RunEndData {
200    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
201        write!(f, "offset: {}", self.offset)
202    }
203}
204
205pub struct RunEndDataParts {
206    pub ends: ArrayRef,
207    pub values: ArrayRef,
208    pub offset: usize,
209}
210
211pub trait RunEndArrayExt: TypedArrayRef<RunEnd> {
212    fn offset(&self) -> usize {
213        self.offset
214    }
215
216    fn ends(&self) -> &ArrayRef {
217        self.as_ref().slots()[ENDS_SLOT]
218            .as_ref()
219            .vortex_expect("RunEndArray ends slot")
220    }
221
222    fn values(&self) -> &ArrayRef {
223        self.as_ref().slots()[VALUES_SLOT]
224            .as_ref()
225            .vortex_expect("RunEndArray values slot")
226    }
227
228    fn dtype(&self) -> &DType {
229        self.values().dtype()
230    }
231
232    fn find_physical_index(&self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
233        find_physical_index(self.ends(), index + self.offset(), ctx)
234    }
235
236    fn find_slice_end_index(&self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
237        find_slice_end_index(self.ends(), index + self.offset(), ctx)
238    }
239}
240
241impl<T: TypedArrayRef<RunEnd>> RunEndArrayExt for T {}
242
243#[derive(Clone, Debug)]
244pub struct RunEnd;
245
246impl RunEnd {
247    /// Build a new [`RunEndArray`] without validation.
248    ///
249    /// # Safety
250    /// See [`RunEndData::new_unchecked`] for preconditions.
251    pub unsafe fn new_unchecked(
252        ends: ArrayRef,
253        values: ArrayRef,
254        offset: usize,
255        length: usize,
256    ) -> RunEndArray {
257        let dtype = values.dtype().clone();
258        let slots = smallvec![Some(ends), Some(values)];
259        let data = unsafe { RunEndData::new_unchecked(offset) };
260        unsafe {
261            Array::from_parts_unchecked(
262                ArrayParts::new(RunEnd, dtype, length, data).with_slots(slots),
263            )
264        }
265    }
266
267    /// Build a new [`RunEndArray`] from ends and values.
268    pub fn try_new(
269        ends: ArrayRef,
270        values: ArrayRef,
271        ctx: &mut ExecutionCtx,
272    ) -> VortexResult<RunEndArray> {
273        let len = RunEndData::logical_len_from_ends(&ends, ctx)?;
274        RunEndData::validate_parts(&ends, &values, 0, len, ctx)?;
275        let dtype = values.dtype().clone();
276        let slots = smallvec![Some(ends), Some(values)];
277        let data = RunEndData::new(0);
278        Array::try_from_parts(ArrayParts::new(RunEnd, dtype, len, data).with_slots(slots))
279    }
280
281    /// Build a new [`RunEndArray`] from ends, values, offset, and length.
282    pub fn try_new_offset_length(
283        ends: ArrayRef,
284        values: ArrayRef,
285        offset: usize,
286        length: usize,
287        ctx: &mut ExecutionCtx,
288    ) -> VortexResult<RunEndArray> {
289        RunEndData::validate_parts(&ends, &values, offset, length, ctx)?;
290        let dtype = values.dtype().clone();
291        let slots = smallvec![Some(ends), Some(values)];
292        let data = RunEndData::new(offset);
293        Array::try_from_parts(ArrayParts::new(RunEnd, dtype, length, data).with_slots(slots))
294    }
295
296    /// Build a new [`RunEndArray`] from ends and values (panics on invalid input).
297    pub fn new(ends: ArrayRef, values: ArrayRef, ctx: &mut ExecutionCtx) -> RunEndArray {
298        Self::try_new(ends, values, ctx).vortex_expect("RunEndData is always valid")
299    }
300
301    /// Run the array through run-end encoding.
302    pub fn encode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<RunEndArray> {
303        if let Some(parray) = array.as_opt::<Primitive>() {
304            let (ends, values) = runend_encode(parray, ctx);
305            let ends = ends.into_array();
306            let len = array.len();
307            let dtype = values.dtype().clone();
308            let slots = smallvec![Some(ends), Some(values)];
309            let data = unsafe { RunEndData::new_unchecked(0) };
310            Array::try_from_parts(ArrayParts::new(RunEnd, dtype, len, data).with_slots(slots))
311        } else {
312            vortex_bail!("REE can only encode primitive arrays")
313        }
314    }
315}
316
317impl RunEndData {
318    fn logical_len_from_ends(ends: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
319        if ends.is_empty() {
320            Ok(0)
321        } else {
322            usize::try_from(&ends.execute_scalar(ends.len() - 1, ctx)?)
323        }
324    }
325
326    pub(crate) fn validate_parts(
327        ends: &ArrayRef,
328        values: &ArrayRef,
329        offset: usize,
330        length: usize,
331        ctx: &mut ExecutionCtx,
332    ) -> VortexResult<()> {
333        // DType validation
334        vortex_ensure!(
335            ends.dtype().is_unsigned_int(),
336            "run ends must be unsigned integers, was {}",
337            ends.dtype(),
338        );
339        vortex_ensure!(
340            ends.len() == values.len(),
341            "run ends len != run values len, {} != {}",
342            ends.len(),
343            values.len()
344        );
345
346        // Handle empty run-ends
347        if ends.is_empty() {
348            vortex_ensure!(
349                offset == 0,
350                "non-zero offset provided for empty RunEndArray"
351            );
352            return Ok(());
353        }
354
355        // Zero-length logical slices may retain run metadata from the source array.
356        if length == 0 {
357            return Ok(());
358        }
359
360        #[cfg(debug_assertions)]
361        {
362            // Run ends must be strictly sorted for binary search to work correctly.
363            let pre_validation = ends.statistics().to_owned();
364
365            let is_sorted = ends
366                .statistics()
367                .compute_is_strict_sorted(ctx)
368                .unwrap_or(false);
369
370            // Preserve the original statistics since compute_is_strict_sorted may have mutated them.
371            // We don't want to run with different stats in debug mode and outside.
372            ends.statistics().inherit(pre_validation.iter());
373            debug_assert!(is_sorted);
374        }
375
376        // Skip host-only validation when ends are not host-resident.
377        if !ends.is_host() {
378            return Ok(());
379        }
380
381        // Validate the offset and length are valid for the given ends and values
382        if offset != 0 && length != 0 {
383            let first_run_end = usize::try_from(&ends.execute_scalar(0, ctx)?)?;
384            if first_run_end < offset {
385                vortex_bail!("First run end {first_run_end} must be >= offset {offset}");
386            }
387        }
388
389        let last_run_end = usize::try_from(&ends.execute_scalar(ends.len() - 1, ctx)?)?;
390        let min_required_end = offset + length;
391        if last_run_end < min_required_end {
392            vortex_bail!("Last run end {last_run_end} must be >= offset+length {min_required_end}");
393        }
394
395        Ok(())
396    }
397}
398
399impl RunEndData {
400    /// Build a new `RunEndArray` from an array of run `ends` and an array of `values`.
401    ///
402    /// Panics if any of the validation conditions described in [`RunEnd::try_new`] is
403    /// not satisfied.
404    ///
405    /// # Examples
406    ///
407    /// ```
408    /// # use vortex_array::arrays::BoolArray;
409    /// # use vortex_array::IntoArray;
410    /// # use vortex_array::VortexSessionExecute;
411    /// # use vortex_buffer::buffer;
412    /// # use vortex_error::VortexResult;
413    /// # use vortex_runend::RunEnd;
414    /// # fn main() -> VortexResult<()> {
415    /// let session = vortex_array::array_session();
416    /// vortex_runend::initialize(&session);
417    /// let mut ctx = session.create_execution_ctx();
418    /// let ends = buffer![2u8, 3u8].into_array();
419    /// let values = BoolArray::from_iter([false, true]).into_array();
420    /// let run_end = RunEnd::new(ends, values, &mut ctx);
421    ///
422    /// // Array encodes
423    /// assert_eq!(run_end.execute_scalar(0, &mut ctx)?, false.into());
424    /// assert_eq!(run_end.execute_scalar(1, &mut ctx)?, false.into());
425    /// assert_eq!(run_end.execute_scalar(2, &mut ctx)?, true.into());
426    /// # Ok(())
427    /// # }
428    /// ```
429    pub fn new(offset: usize) -> Self {
430        Self { offset }
431    }
432
433    /// Build a new `RunEndArray` without validation.
434    ///
435    /// # Safety
436    ///
437    /// The caller must ensure that all the validation performed in
438    /// [`RunEnd::try_new_offset_length`] is
439    /// satisfied before calling this function.
440    ///
441    /// See [`RunEnd::try_new_offset_length`] for the preconditions needed to build a new array.
442    pub unsafe fn new_unchecked(offset: usize) -> Self {
443        Self { offset }
444    }
445
446    /// Run the array through run-end encoding.
447    pub fn encode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
448        if let Some(parray) = array.as_opt::<Primitive>() {
449            let (_ends, _values) = runend_encode(parray, ctx);
450            // SAFETY: runend_encode handles this
451            unsafe { Ok(Self::new_unchecked(0)) }
452        } else {
453            vortex_bail!("REE can only encode primitive arrays")
454        }
455    }
456
457    pub fn into_parts(self, ends: ArrayRef, values: ArrayRef) -> RunEndDataParts {
458        RunEndDataParts {
459            ends,
460            values,
461            offset: self.offset,
462        }
463    }
464}
465
466impl ValidityVTable<RunEnd> for RunEnd {
467    fn validity(array: ArrayView<'_, RunEnd>) -> VortexResult<Validity> {
468        Ok(match array.values().validity()? {
469            Validity::NonNullable | Validity::AllValid => Validity::AllValid,
470            Validity::AllInvalid => Validity::AllInvalid,
471            Validity::Array(values_validity) => Validity::Array(unsafe {
472                RunEnd::new_unchecked(
473                    array.ends().clone(),
474                    values_validity,
475                    array.offset(),
476                    array.len(),
477                )
478                .into_array()
479            }),
480        })
481    }
482}
483
484pub(super) fn run_end_canonicalize(
485    array: &RunEndArray,
486    ctx: &mut ExecutionCtx,
487) -> VortexResult<ArrayRef> {
488    let pends = array.ends().clone().execute_as("ends", ctx)?;
489
490    Ok(match array.dtype() {
491        DType::Bool(_) => {
492            let bools = array.values().clone().execute_as("values", ctx)?;
493            runend_decode_bools(pends, bools, array.offset(), array.len(), ctx)?
494        }
495        DType::Primitive(..) => {
496            let pvalues = array.values().clone().execute_as("values", ctx)?;
497            runend_decode_primitive(pends, pvalues, array.offset(), array.len(), ctx)?.into_array()
498        }
499        DType::Utf8(_) | DType::Binary(_) => {
500            let values = array
501                .values()
502                .clone()
503                .execute_as::<VarBinViewArray>("values", ctx)?;
504            runend_decode_varbinview(pends, values, array.offset(), array.len(), ctx)?.into_array()
505        }
506        _ => vortex_bail!("Unsupported RunEnd value type: {}", array.dtype()),
507    })
508}
509
510#[cfg(test)]
511mod tests {
512    use std::sync::LazyLock;
513
514    use vortex_array::IntoArray;
515    use vortex_array::VortexSessionExecute;
516    use vortex_array::arrays::DictArray;
517    use vortex_array::arrays::VarBinViewArray;
518    use vortex_array::assert_arrays_eq;
519    use vortex_array::dtype::DType;
520    use vortex_array::dtype::Nullability;
521    use vortex_array::dtype::PType;
522    use vortex_buffer::buffer;
523    use vortex_session::VortexSession;
524
525    use crate::RunEnd;
526
527    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
528        let session = vortex_array::array_session();
529        crate::initialize(&session);
530        session
531    });
532
533    #[test]
534    fn test_runend_constructor() {
535        let mut ctx = SESSION.create_execution_ctx();
536        let arr = RunEnd::new(
537            buffer![2u32, 5, 10].into_array(),
538            buffer![1i32, 2, 3].into_array(),
539            &mut ctx,
540        );
541        assert_eq!(arr.len(), 10);
542        assert_eq!(
543            arr.dtype(),
544            &DType::Primitive(PType::I32, Nullability::NonNullable)
545        );
546
547        // 0, 1 => 1
548        // 2, 3, 4 => 2
549        // 5, 6, 7, 8, 9 => 3
550        let expected = buffer![1, 1, 2, 2, 2, 3, 3, 3, 3, 3].into_array();
551        assert_arrays_eq!(arr.into_array(), expected, &mut ctx);
552    }
553
554    #[test]
555    fn test_runend_utf8() {
556        let mut ctx = SESSION.create_execution_ctx();
557        let values = VarBinViewArray::from_iter_str(["a", "b", "c"]).into_array();
558        let arr = RunEnd::new(buffer![2u32, 5, 10].into_array(), values, &mut ctx);
559        assert_eq!(arr.len(), 10);
560        assert_eq!(arr.dtype(), &DType::Utf8(Nullability::NonNullable));
561
562        let expected =
563            VarBinViewArray::from_iter_str(["a", "a", "b", "b", "b", "c", "c", "c", "c", "c"])
564                .into_array();
565        assert_arrays_eq!(arr.into_array(), expected, &mut ctx);
566    }
567
568    #[test]
569    fn test_runend_dict() {
570        let mut ctx = SESSION.create_execution_ctx();
571        let dict_values = VarBinViewArray::from_iter_str(["x", "y", "z"]).into_array();
572        let dict_codes = buffer![0u32, 1, 2].into_array();
573        let dict = DictArray::try_new(dict_codes, dict_values).unwrap();
574
575        let arr = RunEnd::try_new(
576            buffer![2u32, 5, 10].into_array(),
577            dict.into_array(),
578            &mut ctx,
579        )
580        .unwrap();
581        assert_eq!(arr.len(), 10);
582
583        let expected =
584            VarBinViewArray::from_iter_str(["x", "x", "y", "y", "y", "z", "z", "z", "z", "z"])
585                .into_array();
586        assert_arrays_eq!(arr.into_array(), expected, &mut ctx);
587    }
588}