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 /// Serialize encoding metadata into a byte buffer for IPC or file storage.
119 ///
120 /// Return `None` if the array cannot be serialized by this encoding. Buffers and children are
121 /// serialized separately through [`buffer`](Self::buffer), [`nbuffers`](Self::nbuffers), and
122 /// child traversal.
123 fn serialize(
124 array: ArrayView<'_, Self>,
125 session: &VortexSession,
126 ) -> VortexResult<Option<Vec<u8>>>;
127
128 /// Deserialize an array from serialized metadata, buffers, and children.
129 ///
130 /// The returned [`ArrayParts`] are still validated by the generic adapter.
131 /// Deserializers should use the provided `session` to resolve plugin-owned metadata instead of
132 /// relying on global state.
133 fn deserialize(
134 &self,
135 dtype: &DType,
136 len: usize,
137 metadata: &[u8],
138 buffers: &[BufferHandle],
139 children: &dyn ArrayChildren,
140 session: &VortexSession,
141 ) -> VortexResult<ArrayParts<Self>>;
142
143 /// Writes the array's logical values into a canonical builder.
144 ///
145 /// The default implementation executes the full array to [`Canonical`] and appends that result.
146 /// Encodings may override this to avoid materializing an intermediate canonical array.
147 fn append_to_builder(
148 array: ArrayView<'_, Self>,
149 builder: &mut dyn ArrayBuilder,
150 ctx: &mut ExecutionCtx,
151 ) -> VortexResult<()> {
152 let canonical = array
153 .array()
154 .clone()
155 .execute::<Canonical>(ctx)?
156 .into_array();
157 canonical.append_to_builder(builder, ctx)
158 }
159
160 /// Returns the name of the slot at the given index.
161 ///
162 /// # Panics
163 /// Panics if `idx >= slots(array).len()`.
164 fn slot_name(array: ArrayView<'_, Self>, idx: usize) -> String;
165
166 /// Execute this array by returning an [`ExecutionResult`].
167 ///
168 /// Execution is **iterative**, not recursive. Instead of recursively executing children,
169 /// implementations should return [`ExecutionResult::execute_slot`] to request that the
170 /// scheduler execute a slot first, or [`ExecutionResult::done`] when the encoding can
171 /// produce a result directly.
172 ///
173 /// For good examples of this pattern, see:
174 /// - [`Dict::execute`](crate::arrays::dict::vtable::Dict::execute) — demonstrates
175 /// requiring children via `require_child!` and producing a result once they are canonical.
176 /// - `BitPacked::execute` (in `vortex-fastlanes`) — demonstrates requiring patches and
177 /// validity via `require_patches!`/`require_validity!`.
178 ///
179 /// Array execution is designed such that repeated execution of an array will eventually
180 /// converge to a canonical representation. Implementations of this function should therefore
181 /// ensure they make progress towards that goal.
182 ///
183 /// The returned array (in `Done`) must be logically equivalent to the input array. In other
184 /// words, the recursively canonicalized forms of both arrays must be equal.
185 ///
186 /// Debug builds will panic if the returned array is of the wrong type, wrong length, or
187 /// incorrectly contains null values.
188 fn execute(array: Array<Self>, ctx: &mut ExecutionCtx) -> VortexResult<ExecutionResult>;
189
190 /// Attempt to reduce the array to a simpler representation without changing logical values.
191 ///
192 /// Reductions are opportunistic and may return `Ok(None)` when no cheaper representation is
193 /// known.
194 fn reduce(array: ArrayView<'_, Self>) -> VortexResult<Option<ArrayRef>> {
195 _ = array;
196 Ok(None)
197 }
198
199 /// Attempt to reduce `parent` after this array appears as one of its children.
200 ///
201 /// This is used by lazy arrays to let child execution unlock parent simplifications.
202 fn reduce_parent(
203 array: ArrayView<'_, Self>,
204 parent: &ArrayRef,
205 child_idx: usize,
206 ) -> VortexResult<Option<ArrayRef>> {
207 _ = (array, parent, child_idx);
208 Ok(None)
209 }
210}
211
212/// Alias for migration — downstream code can start using `ArrayVTable`.
213pub use VTable as ArrayVTable;
214
215use crate::array::ArrayId;
216use crate::array::ArrayParts;
217
218/// Empty array metadata struct for encodings with no per-array metadata.
219#[derive(Clone, Debug, Default)]
220pub struct EmptyArrayData;
221
222impl ArrayEq for EmptyArrayData {
223 fn array_eq(&self, _other: &Self, _accuracy: EqMode) -> bool {
224 true
225 }
226}
227impl ArrayHash for EmptyArrayData {
228 fn array_hash<H: Hasher>(&self, _state: &mut H, _accuracy: EqMode) {}
229}
230
231impl Display for EmptyArrayData {
232 fn fmt(&self, _f: &mut Formatter<'_>) -> std::fmt::Result {
233 Ok(())
234 }
235}
236
237/// Rebuild an array that has no top-level buffers.
238#[inline]
239pub fn with_empty_buffers<V: VTable>(
240 vtable: &V,
241 array: ArrayView<'_, V>,
242 buffers: &[BufferHandle],
243) -> VortexResult<ArrayParts<V>> {
244 vortex_ensure!(
245 buffers.is_empty(),
246 "Array {} expects 0 buffers, got {}",
247 array.encoding_id(),
248 buffers.len()
249 );
250 Ok(ArrayParts::new(
251 vtable.clone(),
252 array.dtype().clone(),
253 array.len(),
254 array.data().clone(),
255 )
256 .with_slots(array.slots().iter().cloned().collect()))
257}
258
259/// Reject buffer replacement for encodings whose exposed buffers are not runtime backing buffers.
260#[inline]
261pub fn unsupported_buffer_replacement<V: VTable>(
262 array: ArrayView<'_, V>,
263 _buffers: &[BufferHandle],
264) -> VortexResult<ArrayParts<V>> {
265 vortex_bail!(
266 "Array {} does not support in-memory buffer replacement",
267 array.encoding_id()
268 )
269}
270
271/// Placeholder type used to indicate when a particular vtable is not supported by the encoding.
272pub struct NotSupported;
273
274/// Returns the validity as a child array if it produces one.
275#[inline]
276pub fn validity_to_child(validity: &Validity, len: usize) -> Option<ArrayRef> {
277 match validity {
278 Validity::NonNullable | Validity::AllValid => None,
279 Validity::AllInvalid => Some(ConstantArray::new(false, len).into_array()),
280 Validity::Array(array) => Some(array.clone()),
281 }
282}
283
284/// Reconstruct a [`Validity`] from an optional child array and nullability.
285///
286/// This is the inverse of [`validity_to_child`].
287#[inline]
288pub fn child_to_validity(child: Option<&ArrayRef>, nullability: Nullability) -> Validity {
289 match child {
290 Some(arr) => {
291 // Detect constant bool arrays created by validity_to_child.
292 // Use direct ScalarValue matching to avoid expensive scalar conversion.
293 if let Some(c) = arr.as_opt::<Constant>()
294 && let Some(ScalarValue::Bool(val)) = c.scalar().value()
295 {
296 return if *val {
297 Validity::AllValid
298 } else {
299 Validity::AllInvalid
300 };
301 }
302 Validity::Array(arr.clone())
303 }
304 None => Validity::from(nullability),
305 }
306}
307
308/// Returns 1 if validity produces a child, 0 otherwise.
309#[inline]
310pub fn validity_nchildren(validity: &Validity) -> usize {
311 match validity {
312 Validity::NonNullable | Validity::AllValid => 0,
313 Validity::AllInvalid | Validity::Array(_) => 1,
314 }
315}
316
317/// Returns the number of children produced by patches.
318#[inline]
319pub fn patches_nchildren(patches: &Patches) -> usize {
320 2 + patches.chunk_offsets().is_some() as usize
321}
322
323/// Returns the child at the given index within a patches component.
324#[inline]
325pub fn patches_child(patches: &Patches, idx: usize) -> ArrayRef {
326 match idx {
327 0 => patches.indices().clone(),
328 1 => patches.values().clone(),
329 2 => patches
330 .chunk_offsets()
331 .as_ref()
332 .vortex_expect("patch_chunk_offsets child out of bounds")
333 .clone(),
334 _ => vortex_panic!("patches child index {idx} out of bounds"),
335 }
336}
337
338/// Returns the name of the child at the given index within a patches component.
339#[inline]
340pub fn patches_child_name(idx: usize) -> &'static str {
341 match idx {
342 0 => "patch_indices",
343 1 => "patch_values",
344 2 => "patch_chunk_offsets",
345 _ => vortex_panic!("patches child name index {idx} out of bounds"),
346 }
347}