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