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::LEGACY_SESSION;
23use vortex_array::TypedArrayRef;
24use vortex_array::VortexSessionExecute;
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::scalar::PValue;
32use vortex_array::search_sorted::SearchSorted;
33use vortex_array::search_sorted::SearchSortedSide;
34use vortex_array::serde::ArrayChildren;
35use vortex_array::smallvec::smallvec;
36use vortex_array::validity::Validity;
37use vortex_array::vtable::VTable;
38use vortex_array::vtable::ValidityVTable;
39use vortex_error::VortexExpect as _;
40use vortex_error::VortexResult;
41use vortex_error::vortex_bail;
42use vortex_error::vortex_ensure;
43use vortex_error::vortex_panic;
44use vortex_session::VortexSession;
45use vortex_session::registry::CachedId;
46
47use crate::compress::runend_decode_primitive;
48use crate::compress::runend_decode_varbinview;
49use crate::compress::runend_encode;
50use crate::decompress_bool::runend_decode_bools;
51use crate::rules::RULES;
52
53/// A [`RunEnd`]-encoded Vortex array.
54pub type RunEndArray = Array<RunEnd>;
55
56#[derive(Clone, prost::Message)]
57pub struct RunEndMetadata {
58    #[prost(enumeration = "PType", tag = "1")]
59    pub ends_ptype: i32,
60    #[prost(uint64, tag = "2")]
61    pub num_runs: u64,
62    #[prost(uint64, tag = "3")]
63    pub offset: u64,
64}
65
66impl ArrayHash for RunEndData {
67    fn array_hash<H: Hasher>(&self, state: &mut H, _accuracy: EqMode) {
68        self.offset.hash(state);
69    }
70}
71
72impl ArrayEq for RunEndData {
73    fn array_eq(&self, other: &Self, _accuracy: EqMode) -> bool {
74        self.offset == other.offset
75    }
76}
77
78impl VTable for RunEnd {
79    type TypedArrayData = RunEndData;
80
81    type OperationsVTable = Self;
82    type ValidityVTable = Self;
83
84    fn id(&self) -> ArrayId {
85        static ID: CachedId = CachedId::new("vortex.runend");
86        *ID
87    }
88
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) -> VortexResult<usize> {
233        Ok(self
234            .ends()
235            .as_primitive_typed()
236            .search_sorted(
237                &PValue::from(index + self.offset()),
238                SearchSortedSide::Right,
239            )?
240            .to_ends_index(self.ends().len()))
241    }
242}
243impl<T: TypedArrayRef<RunEnd>> RunEndArrayExt for T {}
244
245#[derive(Clone, Debug)]
246pub struct RunEnd;
247
248impl RunEnd {
249    /// Build a new [`RunEndArray`] without validation.
250    ///
251    /// # Safety
252    /// See [`RunEndData::new_unchecked`] for preconditions.
253    pub unsafe fn new_unchecked(
254        ends: ArrayRef,
255        values: ArrayRef,
256        offset: usize,
257        length: usize,
258    ) -> RunEndArray {
259        let dtype = values.dtype().clone();
260        let slots = smallvec![Some(ends), Some(values)];
261        let data = unsafe { RunEndData::new_unchecked(offset) };
262        unsafe {
263            Array::from_parts_unchecked(
264                ArrayParts::new(RunEnd, dtype, length, data).with_slots(slots),
265            )
266        }
267    }
268
269    /// Build a new [`RunEndArray`] from ends and values.
270    pub fn try_new(
271        ends: ArrayRef,
272        values: ArrayRef,
273        ctx: &mut ExecutionCtx,
274    ) -> VortexResult<RunEndArray> {
275        let len = RunEndData::logical_len_from_ends(&ends, ctx)?;
276        RunEndData::validate_parts(&ends, &values, 0, len, ctx)?;
277        let dtype = values.dtype().clone();
278        let slots = smallvec![Some(ends), Some(values)];
279        let data = RunEndData::new(0);
280        Array::try_from_parts(ArrayParts::new(RunEnd, dtype, len, data).with_slots(slots))
281    }
282
283    /// Build a new [`RunEndArray`] from ends, values, offset, and length.
284    pub fn try_new_offset_length(
285        ends: ArrayRef,
286        values: ArrayRef,
287        offset: usize,
288        length: usize,
289        ctx: &mut ExecutionCtx,
290    ) -> VortexResult<RunEndArray> {
291        RunEndData::validate_parts(&ends, &values, offset, length, ctx)?;
292        let dtype = values.dtype().clone();
293        let slots = smallvec![Some(ends), Some(values)];
294        let data = RunEndData::new(offset);
295        Array::try_from_parts(ArrayParts::new(RunEnd, dtype, length, data).with_slots(slots))
296    }
297
298    /// Build a new [`RunEndArray`] from ends and values (panics on invalid input).
299    pub fn new(ends: ArrayRef, values: ArrayRef, ctx: &mut ExecutionCtx) -> RunEndArray {
300        Self::try_new(ends, values, ctx).vortex_expect("RunEndData is always valid")
301    }
302
303    /// Run the array through run-end encoding.
304    pub fn encode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<RunEndArray> {
305        if let Some(parray) = array.as_opt::<Primitive>() {
306            let (ends, values) = runend_encode(parray, ctx);
307            let ends = ends.into_array();
308            let len = array.len();
309            let dtype = values.dtype().clone();
310            let slots = smallvec![Some(ends), Some(values)];
311            let data = unsafe { RunEndData::new_unchecked(0) };
312            Array::try_from_parts(ArrayParts::new(RunEnd, dtype, len, data).with_slots(slots))
313        } else {
314            vortex_bail!("REE can only encode primitive arrays")
315        }
316    }
317}
318
319impl RunEndData {
320    fn logical_len_from_ends(ends: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<usize> {
321        if ends.is_empty() {
322            Ok(0)
323        } else {
324            usize::try_from(&ends.execute_scalar(ends.len() - 1, ctx)?)
325        }
326    }
327
328    pub(crate) fn validate_parts(
329        ends: &ArrayRef,
330        values: &ArrayRef,
331        offset: usize,
332        length: usize,
333        ctx: &mut ExecutionCtx,
334    ) -> VortexResult<()> {
335        // DType validation
336        vortex_ensure!(
337            ends.dtype().is_unsigned_int(),
338            "run ends must be unsigned integers, was {}",
339            ends.dtype(),
340        );
341        vortex_ensure!(
342            ends.len() == values.len(),
343            "run ends len != run values len, {} != {}",
344            ends.len(),
345            values.len()
346        );
347
348        // Handle empty run-ends
349        if ends.is_empty() {
350            vortex_ensure!(
351                offset == 0,
352                "non-zero offset provided for empty RunEndArray"
353            );
354            return Ok(());
355        }
356
357        // Zero-length logical slices may retain run metadata from the source array.
358        if length == 0 {
359            return Ok(());
360        }
361
362        #[cfg(debug_assertions)]
363        {
364            // Run ends must be strictly sorted for binary search to work correctly.
365            let pre_validation = ends.statistics().to_owned();
366
367            let is_sorted = ends
368                .statistics()
369                .compute_is_strict_sorted(ctx)
370                .unwrap_or(false);
371
372            // Preserve the original statistics since compute_is_strict_sorted may have mutated them.
373            // We don't want to run with different stats in debug mode and outside.
374            ends.statistics().inherit(pre_validation.iter());
375            debug_assert!(is_sorted);
376        }
377
378        // Skip host-only validation when ends are not host-resident.
379        if !ends.is_host() {
380            return Ok(());
381        }
382
383        // Validate the offset and length are valid for the given ends and values
384        if offset != 0 && length != 0 {
385            let first_run_end = usize::try_from(&ends.execute_scalar(0, ctx)?)?;
386            if first_run_end < offset {
387                vortex_bail!("First run end {first_run_end} must be >= offset {offset}");
388            }
389        }
390
391        let last_run_end = usize::try_from(&ends.execute_scalar(ends.len() - 1, ctx)?)?;
392        let min_required_end = offset + length;
393        if last_run_end < min_required_end {
394            vortex_bail!("Last run end {last_run_end} must be >= offset+length {min_required_end}");
395        }
396
397        Ok(())
398    }
399}
400
401impl RunEndData {
402    /// Build a new `RunEndArray` from an array of run `ends` and an array of `values`.
403    ///
404    /// Panics if any of the validation conditions described in [`RunEnd::try_new`] is
405    /// not satisfied.
406    ///
407    /// # Examples
408    ///
409    /// ```
410    /// # use vortex_array::arrays::BoolArray;
411    /// # use vortex_array::IntoArray;
412    /// # use vortex_array::VortexSessionExecute;
413    /// # use vortex_buffer::buffer;
414    /// # use vortex_error::VortexResult;
415    /// # use vortex_runend::RunEnd;
416    /// # fn main() -> VortexResult<()> {
417    /// let session = vortex_array::array_session();
418    /// vortex_runend::initialize(&session);
419    /// let mut ctx = session.create_execution_ctx();
420    /// let ends = buffer![2u8, 3u8].into_array();
421    /// let values = BoolArray::from_iter([false, true]).into_array();
422    /// let run_end = RunEnd::new(ends, values, &mut ctx);
423    ///
424    /// // Array encodes
425    /// assert_eq!(run_end.execute_scalar(0, &mut ctx)?, false.into());
426    /// assert_eq!(run_end.execute_scalar(1, &mut ctx)?, false.into());
427    /// assert_eq!(run_end.execute_scalar(2, &mut ctx)?, true.into());
428    /// # Ok(())
429    /// # }
430    /// ```
431    pub fn new(offset: usize) -> Self {
432        Self { offset }
433    }
434
435    /// Build a new `RunEndArray` without validation.
436    ///
437    /// # Safety
438    ///
439    /// The caller must ensure that all the validation performed in
440    /// [`RunEnd::try_new_offset_length`] is
441    /// satisfied before calling this function.
442    ///
443    /// See [`RunEnd::try_new_offset_length`] for the preconditions needed to build a new array.
444    pub unsafe fn new_unchecked(offset: usize) -> Self {
445        Self { offset }
446    }
447
448    /// Run the array through run-end encoding.
449    pub fn encode(array: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<Self> {
450        if let Some(parray) = array.as_opt::<Primitive>() {
451            let (_ends, _values) = runend_encode(parray, ctx);
452            // SAFETY: runend_encode handles this
453            unsafe { Ok(Self::new_unchecked(0)) }
454        } else {
455            vortex_bail!("REE can only encode primitive arrays")
456        }
457    }
458
459    pub fn into_parts(self, ends: ArrayRef, values: ArrayRef) -> RunEndDataParts {
460        RunEndDataParts {
461            ends,
462            values,
463            offset: self.offset,
464        }
465    }
466}
467
468impl ValidityVTable<RunEnd> for RunEnd {
469    fn validity(array: ArrayView<'_, RunEnd>) -> VortexResult<Validity> {
470        Ok(match array.values().validity()? {
471            Validity::NonNullable | Validity::AllValid => Validity::AllValid,
472            Validity::AllInvalid => Validity::AllInvalid,
473            Validity::Array(values_validity) => Validity::Array(unsafe {
474                RunEnd::new_unchecked(
475                    array.ends().clone(),
476                    values_validity,
477                    array.offset(),
478                    array.len(),
479                )
480                .into_array()
481            }),
482        })
483    }
484}
485
486pub(super) fn run_end_canonicalize(
487    array: &RunEndArray,
488    ctx: &mut ExecutionCtx,
489) -> VortexResult<ArrayRef> {
490    let pends = array.ends().clone().execute_as("ends", ctx)?;
491
492    Ok(match array.dtype() {
493        DType::Bool(_) => {
494            let bools = array.values().clone().execute_as("values", ctx)?;
495            runend_decode_bools(pends, bools, array.offset(), array.len(), ctx)?
496        }
497        DType::Primitive(..) => {
498            let pvalues = array.values().clone().execute_as("values", ctx)?;
499            runend_decode_primitive(pends, pvalues, array.offset(), array.len(), ctx)?.into_array()
500        }
501        DType::Utf8(_) | DType::Binary(_) => {
502            let values = array
503                .values()
504                .clone()
505                .execute_as::<VarBinViewArray>("values", ctx)?;
506            runend_decode_varbinview(pends, values, array.offset(), array.len(), ctx)?.into_array()
507        }
508        _ => vortex_bail!("Unsupported RunEnd value type: {}", array.dtype()),
509    })
510}
511
512#[cfg(test)]
513mod tests {
514    use std::sync::LazyLock;
515
516    use vortex_array::IntoArray;
517    use vortex_array::VortexSessionExecute;
518    use vortex_array::arrays::DictArray;
519    use vortex_array::arrays::VarBinViewArray;
520    use vortex_array::assert_arrays_eq;
521    use vortex_array::dtype::DType;
522    use vortex_array::dtype::Nullability;
523    use vortex_array::dtype::PType;
524    use vortex_buffer::buffer;
525    use vortex_session::VortexSession;
526
527    use crate::RunEnd;
528
529    static SESSION: LazyLock<VortexSession> = LazyLock::new(|| {
530        let session = vortex_array::array_session();
531        crate::initialize(&session);
532        session
533    });
534
535    #[test]
536    fn test_runend_constructor() {
537        let mut ctx = SESSION.create_execution_ctx();
538        let arr = RunEnd::new(
539            buffer![2u32, 5, 10].into_array(),
540            buffer![1i32, 2, 3].into_array(),
541            &mut ctx,
542        );
543        assert_eq!(arr.len(), 10);
544        assert_eq!(
545            arr.dtype(),
546            &DType::Primitive(PType::I32, Nullability::NonNullable)
547        );
548
549        // 0, 1 => 1
550        // 2, 3, 4 => 2
551        // 5, 6, 7, 8, 9 => 3
552        let expected = buffer![1, 1, 2, 2, 2, 3, 3, 3, 3, 3].into_array();
553        assert_arrays_eq!(arr.into_array(), expected, &mut ctx);
554    }
555
556    #[test]
557    fn test_runend_utf8() {
558        let mut ctx = SESSION.create_execution_ctx();
559        let values = VarBinViewArray::from_iter_str(["a", "b", "c"]).into_array();
560        let arr = RunEnd::new(buffer![2u32, 5, 10].into_array(), values, &mut ctx);
561        assert_eq!(arr.len(), 10);
562        assert_eq!(arr.dtype(), &DType::Utf8(Nullability::NonNullable));
563
564        let expected =
565            VarBinViewArray::from_iter_str(["a", "a", "b", "b", "b", "c", "c", "c", "c", "c"])
566                .into_array();
567        assert_arrays_eq!(arr.into_array(), expected, &mut ctx);
568    }
569
570    #[test]
571    fn test_runend_dict() {
572        let mut ctx = SESSION.create_execution_ctx();
573        let dict_values = VarBinViewArray::from_iter_str(["x", "y", "z"]).into_array();
574        let dict_codes = buffer![0u32, 1, 2].into_array();
575        let dict = DictArray::try_new(dict_codes, dict_values).unwrap();
576
577        let arr = RunEnd::try_new(
578            buffer![2u32, 5, 10].into_array(),
579            dict.into_array(),
580            &mut ctx,
581        )
582        .unwrap();
583        assert_eq!(arr.len(), 10);
584
585        let expected =
586            VarBinViewArray::from_iter_str(["x", "x", "y", "y", "y", "z", "z", "z", "z", "z"])
587                .into_array();
588        assert_arrays_eq!(arr.into_array(), expected, &mut ctx);
589    }
590}