Skip to main content

vortex_array/array/vtable/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! This module contains the VTable definitions for a Vortex encoding.
5//!
6//! A Vortex array encoding is implemented by a small static vtable type plus an associated
7//! `TypedArrayData` value stored in each array instance. The vtable owns behavior such as
8//! validation, serialization, execution, child traversal, scalar access, and validity access.
9//!
10//! The public [`ArrayRef`] API performs common precondition checks before calling
11//! into these traits. Implementations should focus on encoding-specific work and uphold the
12//! documented postconditions.
13
14mod operations;
15mod validity;
16
17use std::fmt::Debug;
18use std::fmt::Display;
19use std::fmt::Formatter;
20use std::hash::Hasher;
21
22pub use operations::*;
23pub use validity::*;
24use vortex_error::VortexExpect;
25use vortex_error::VortexResult;
26use vortex_error::vortex_bail;
27use vortex_error::vortex_ensure;
28use vortex_error::vortex_panic;
29use vortex_session::VortexSession;
30
31use crate::Array;
32use crate::ArrayRef;
33use crate::ArrayView;
34use crate::Canonical;
35use crate::EqMode;
36use crate::ExecutionResult;
37use crate::IntoArray;
38pub use crate::array::plugin::*;
39use crate::arrays::ConstantArray;
40use crate::arrays::constant::Constant;
41use crate::buffer::BufferHandle;
42use crate::builders::ArrayBuilder;
43use crate::dtype::DType;
44use crate::dtype::Nullability;
45use crate::executor::ExecutionCtx;
46use crate::hash::ArrayEq;
47use crate::hash::ArrayHash;
48use crate::patches::Patches;
49use crate::scalar::ScalarValue;
50use crate::serde::ArrayChildren;
51use crate::validity::Validity;
52
53/// The array [`VTable`] encapsulates logic for an Array type within Vortex.
54///
55/// The logic is split across several "VTable" traits to enable easier code organization than
56/// simply lumping everything into a single trait.
57///
58/// From this [`VTable`] trait, we derive implementations for the sealed `DynArrayData` trait and the
59/// public [`ArrayPlugin`] registry trait.
60///
61/// The functions defined in these vtable traits will typically document their pre- and
62/// post-conditions. The pre-conditions are validated inside the `DynArrayData` and [`ArrayRef`]
63/// implementations so do not need to be checked in the vtable implementations (for example, index
64/// out of bounds). Post-conditions are validated after invocation of the vtable function and will
65/// panic if violated.
66pub trait VTable: 'static + Clone + Sized + Send + Sync + Debug {
67    /// Per-array data owned by this encoding, excluding child arrays.
68    ///
69    /// Child arrays belong in [`ArrayParts::slots`](crate::ArrayParts::slots) so traversal,
70    /// serialization, and layout writers can discover them generically.
71    type TypedArrayData: 'static + Send + Sync + Clone + Debug + Display + ArrayHash + ArrayEq;
72
73    /// Scalar and element-wise operation hooks for this encoding.
74    type OperationsVTable: OperationsVTable<Self>;
75    /// Validity hook for nullable instances of this encoding.
76    type ValidityVTable: ValidityVTable<Self>;
77
78    /// Returns the ID of the array.
79    fn id(&self) -> ArrayId;
80
81    /// Validates that externally supplied logical metadata matches the array data.
82    ///
83    /// This is called by [`Array::try_from_parts`](crate::Array::try_from_parts) before the array
84    /// is published. Implementations should check dtype, length, slot count, child dtypes/lengths,
85    /// metadata bounds, and any buffer shape invariants that unsafe accessors depend on.
86    fn validate(
87        &self,
88        data: &Self::TypedArrayData,
89        dtype: &DType,
90        len: usize,
91        slots: &[Option<ArrayRef>],
92    ) -> VortexResult<()>;
93
94    /// Returns the number of top-level buffers in the array.
95    fn nbuffers(array: ArrayView<'_, Self>) -> usize;
96
97    /// Returns the buffer at the given index.
98    ///
99    /// # Panics
100    /// Panics if `idx >= nbuffers(array)`.
101    fn buffer(array: ArrayView<'_, Self>, idx: usize) -> BufferHandle;
102
103    /// Returns the name of the buffer at the given index, or `None` if unnamed.
104    fn buffer_name(array: ArrayView<'_, Self>, idx: usize) -> Option<String>;
105
106    /// Rebuild this array with replacement top-level buffers.
107    ///
108    /// This is for physical rewrites that preserve `dtype`, `len`, child slots, buffer count, and
109    /// buffer lengths. The caller checks the generic invariants before dispatching here;
110    /// implementations should interpret the replacement buffers for their encoding-specific
111    /// in-memory representation.
112    fn with_buffers(
113        &self,
114        array: ArrayView<'_, Self>,
115        buffers: &[BufferHandle],
116    ) -> VortexResult<ArrayParts<Self>>;
117
118    /// Returns the number of children in the array.
119    ///
120    /// The default counts non-None slots.
121    fn nchildren(array: ArrayView<'_, Self>) -> usize {
122        array.slots().iter().filter(|s| s.is_some()).count()
123    }
124
125    /// Returns the child at the given index.
126    ///
127    /// The default returns the `idx`-th non-None slot.
128    ///
129    /// # Panics
130    /// Panics if `idx >= nchildren(array)`.
131    fn child(array: ArrayView<'_, Self>, idx: usize) -> ArrayRef {
132        array
133            .slots()
134            .iter()
135            .filter_map(|s| s.clone())
136            .nth(idx)
137            .vortex_expect("child index out of bounds")
138    }
139
140    /// Returns the name of the child at the given index.
141    ///
142    /// The default returns the slot name of the `idx`-th non-None slot.
143    ///
144    /// # Panics
145    /// Panics if `idx >= nchildren(array)`.
146    fn child_name(array: ArrayView<'_, Self>, idx: usize) -> String {
147        array
148            .slots()
149            .iter()
150            .enumerate()
151            .filter(|(_, s)| s.is_some())
152            .nth(idx)
153            .map(|(slot_idx, _)| Self::slot_name(array, slot_idx))
154            .vortex_expect("child_name index out of bounds")
155    }
156
157    /// Serialize encoding metadata into a byte buffer for IPC or file storage.
158    ///
159    /// Return `None` if the array cannot be serialized by this encoding. Buffers and children are
160    /// serialized separately through [`buffer`](Self::buffer), [`nbuffers`](Self::nbuffers), and
161    /// child traversal.
162    fn serialize(
163        array: ArrayView<'_, Self>,
164        session: &VortexSession,
165    ) -> VortexResult<Option<Vec<u8>>>;
166
167    /// Deserialize an array from serialized metadata, buffers, and children.
168    ///
169    /// The returned [`ArrayParts`] are still validated by the generic adapter.
170    /// Deserializers should use the provided `session` to resolve plugin-owned metadata instead of
171    /// relying on global state.
172    fn deserialize(
173        &self,
174        dtype: &DType,
175        len: usize,
176        metadata: &[u8],
177        buffers: &[BufferHandle],
178        children: &dyn ArrayChildren,
179        session: &VortexSession,
180    ) -> VortexResult<ArrayParts<Self>>;
181
182    /// Writes the array's logical values into a canonical builder.
183    ///
184    /// The default implementation executes the full array to [`Canonical`] and appends that result.
185    /// Encodings may override this to avoid materializing an intermediate canonical array.
186    fn append_to_builder(
187        array: ArrayView<'_, Self>,
188        builder: &mut dyn ArrayBuilder,
189        ctx: &mut ExecutionCtx,
190    ) -> VortexResult<()> {
191        let canonical = array
192            .array()
193            .clone()
194            .execute::<Canonical>(ctx)?
195            .into_array();
196        canonical.append_to_builder(builder, ctx)
197    }
198
199    /// Returns the name of the slot at the given index.
200    ///
201    /// # Panics
202    /// Panics if `idx >= slots(array).len()`.
203    fn slot_name(array: ArrayView<'_, Self>, idx: usize) -> String;
204
205    /// Execute this array by returning an [`ExecutionResult`].
206    ///
207    /// Execution is **iterative**, not recursive. Instead of recursively executing children,
208    /// implementations should return [`ExecutionResult::execute_slot`] to request that the
209    /// scheduler execute a slot first, or [`ExecutionResult::done`] when the encoding can
210    /// produce a result directly.
211    ///
212    /// For good examples of this pattern, see:
213    /// - [`Dict::execute`](crate::arrays::dict::vtable::Dict::execute) — demonstrates
214    ///   requiring children via `require_child!` and producing a result once they are canonical.
215    /// - `BitPacked::execute` (in `vortex-fastlanes`) — demonstrates requiring patches and
216    ///   validity via `require_patches!`/`require_validity!`.
217    ///
218    /// Array execution is designed such that repeated execution of an array will eventually
219    /// converge to a canonical representation. Implementations of this function should therefore
220    /// ensure they make progress towards that goal.
221    ///
222    /// The returned array (in `Done`) must be logically equivalent to the input array. In other
223    /// words, the recursively canonicalized forms of both arrays must be equal.
224    ///
225    /// Debug builds will panic if the returned array is of the wrong type, wrong length, or
226    /// incorrectly contains null values.
227    fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult>;
228
229    /// Attempt to reduce the array to a simpler representation without changing logical values.
230    ///
231    /// Reductions are opportunistic and may return `Ok(None)` when no cheaper representation is
232    /// known.
233    fn reduce(array: ArrayView<'_, Self>) -> VortexResult<Option<ArrayRef>> {
234        _ = array;
235        Ok(None)
236    }
237
238    /// Attempt to reduce `parent` after this array appears as one of its children.
239    ///
240    /// This is used by lazy arrays to let child execution unlock parent simplifications.
241    fn reduce_parent(
242        array: ArrayView<'_, Self>,
243        parent: &ArrayRef,
244        child_idx: usize,
245    ) -> VortexResult<Option<ArrayRef>> {
246        _ = (array, parent, child_idx);
247        Ok(None)
248    }
249}
250
251/// Alias for migration — downstream code can start using `ArrayVTable`.
252pub use VTable as ArrayVTable;
253
254use crate::array::ArrayId;
255use crate::array::ArrayParts;
256
257/// Empty array metadata struct for encodings with no per-array metadata.
258#[derive(Clone, Debug, Default)]
259pub struct EmptyArrayData;
260
261impl ArrayEq for EmptyArrayData {
262    fn array_eq(&self, _other: &Self, _accuracy: EqMode) -> bool {
263        true
264    }
265}
266impl ArrayHash for EmptyArrayData {
267    fn array_hash<H: Hasher>(&self, _state: &mut H, _accuracy: EqMode) {}
268}
269
270impl Display for EmptyArrayData {
271    fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
272        Ok(())
273    }
274}
275
276/// Rebuild an array that has no top-level buffers.
277#[inline]
278pub fn with_empty_buffers<V: VTable>(
279    vtable: &V,
280    array: ArrayView<'_, V>,
281    buffers: &[BufferHandle],
282) -> VortexResult<ArrayParts<V>> {
283    vortex_ensure!(
284        buffers.is_empty(),
285        "Array {} expects 0 buffers, got {}",
286        array.encoding_id(),
287        buffers.len()
288    );
289    Ok(ArrayParts::new(
290        vtable.clone(),
291        array.dtype().clone(),
292        array.len(),
293        array.data().clone(),
294    )
295    .with_slots(array.slots().iter().cloned().collect()))
296}
297
298/// Reject buffer replacement for encodings whose exposed buffers are not runtime backing buffers.
299#[inline]
300pub fn unsupported_buffer_replacement<V: VTable>(
301    array: ArrayView<'_, V>,
302    _buffers: &[BufferHandle],
303) -> VortexResult<ArrayParts<V>> {
304    vortex_bail!(
305        "Array {} does not support in-memory buffer replacement",
306        array.encoding_id()
307    )
308}
309
310/// Placeholder type used to indicate when a particular vtable is not supported by the encoding.
311pub struct NotSupported;
312
313/// Returns the validity as a child array if it produces one.
314#[inline]
315pub fn validity_to_child(validity: &Validity, len: usize) -> Option<ArrayRef> {
316    match validity {
317        Validity::NonNullable | Validity::AllValid => None,
318        Validity::AllInvalid => Some(ConstantArray::new(false, len).into_array()),
319        Validity::Array(array) => Some(array.clone()),
320    }
321}
322
323/// Reconstruct a [`Validity`] from an optional child array and nullability.
324///
325/// This is the inverse of [`validity_to_child`].
326#[inline]
327pub fn child_to_validity(child: Option<&ArrayRef>, nullability: Nullability) -> Validity {
328    match child {
329        Some(arr) => {
330            // Detect constant bool arrays created by validity_to_child.
331            // Use direct ScalarValue matching to avoid expensive scalar conversion.
332            if let Some(c) = arr.as_opt::<Constant>()
333                && let Some(ScalarValue::Bool(val)) = c.scalar().value()
334            {
335                return if *val {
336                    Validity::AllValid
337                } else {
338                    Validity::AllInvalid
339                };
340            }
341            Validity::Array(arr.clone())
342        }
343        None => Validity::from(nullability),
344    }
345}
346
347/// Returns 1 if validity produces a child, 0 otherwise.
348#[inline]
349pub fn validity_nchildren(validity: &Validity) -> usize {
350    match validity {
351        Validity::NonNullable | Validity::AllValid => 0,
352        Validity::AllInvalid | Validity::Array(_) => 1,
353    }
354}
355
356/// Returns the number of children produced by patches.
357#[inline]
358pub fn patches_nchildren(patches: &Patches) -> usize {
359    2 + patches.chunk_offsets().is_some() as usize
360}
361
362/// Returns the child at the given index within a patches component.
363#[inline]
364pub fn patches_child(patches: &Patches, idx: usize) -> ArrayRef {
365    match idx {
366        0 => patches.indices().clone(),
367        1 => patches.values().clone(),
368        2 => patches
369            .chunk_offsets()
370            .as_ref()
371            .vortex_expect("patch_chunk_offsets child out of bounds")
372            .clone(),
373        _ => vortex_panic!("patches child index {idx} out of bounds"),
374    }
375}
376
377/// Returns the name of the child at the given index within a patches component.
378#[inline]
379pub fn patches_child_name(idx: usize) -> &'static str {
380    match idx {
381        0 => "patch_indices",
382        1 => "patch_values",
383        2 => "patch_chunk_offsets",
384        _ => vortex_panic!("patches child name index {idx} out of bounds"),
385    }
386}