From 8146a9933593bf18bc017433b72d239b998d5617 Mon Sep 17 00:00:00 2001 From: Joe Isaacs Date: Fri, 11 Sep 2026 14:26:57 +0100 Subject: [PATCH] feat(array): add scalar probes with lazy child contexts Signed-off-by: Joe Isaacs --- encodings/alp/src/alp/ops.rs | 2 + encodings/alp/src/alp_rd/ops.rs | 2 + encodings/bytebool/src/array.rs | 2 + encodings/datetime-parts/src/ops.rs | 2 + .../src/decimal_byte_parts/mod.rs | 2 + .../src/bitpacking/vtable/operations.rs | 2 + .../fastlanes/src/delta/vtable/operations.rs | 2 + .../fastlanes/src/for/vtable/operations.rs | 2 + .../fastlanes/src/rle/vtable/operations.rs | 2 + encodings/fastlanes/src/transposed_bool.rs | 2 + encodings/fsst/src/ops.rs | 2 + encodings/onpair/src/ops.rs | 2 + encodings/parquet-variant/src/operations.rs | 2 + encodings/pco/src/array.rs | 2 + encodings/runend/src/ops.rs | 2 + encodings/sequence/src/array.rs | 2 + encodings/sparse/src/ops.rs | 2 + encodings/zigzag/src/array.rs | 2 + encodings/zstd/src/array.rs | 2 + encodings/zstd/src/zstd_buffers.rs | 2 + vortex-array/PROBE_DESIGN.md | 130 ++++++++++ vortex-array/src/array/mod.rs | 38 +++ vortex-array/src/array/probe.rs | 241 ++++++++++++++++++ vortex-array/src/array/probe/tests.rs | 220 ++++++++++++++++ vortex-array/src/array/vtable/operations.rs | 27 ++ .../src/arrays/bool/vtable/operations.rs | 2 + .../src/arrays/chunked/vtable/operations.rs | 2 + .../src/arrays/constant/vtable/operations.rs | 2 + .../src/arrays/decimal/vtable/operations.rs | 2 + .../src/arrays/dict/vtable/operations.rs | 2 + .../src/arrays/extension/vtable/operations.rs | 2 + vortex-array/src/arrays/filter/vtable.rs | 2 + .../fixed_size_list/vtable/operations.rs | 2 + vortex-array/src/arrays/interleave/mod.rs | 2 + .../src/arrays/list/vtable/operations.rs | 2 + .../src/arrays/listview/vtable/operations.rs | 2 + .../src/arrays/map/vtable/operations.rs | 2 + .../src/arrays/masked/vtable/operations.rs | 2 + vortex-array/src/arrays/null/mod.rs | 2 + .../src/arrays/patched/vtable/operations.rs | 2 + .../src/arrays/piecewise_sequence/vtable.rs | 2 + .../src/arrays/primitive/vtable/operations.rs | 2 + .../src/arrays/scalar_fn/vtable/operations.rs | 2 + vortex-array/src/arrays/shared/vtable.rs | 2 + vortex-array/src/arrays/slice/vtable.rs | 2 + .../src/arrays/struct_/vtable/operations.rs | 2 + .../src/arrays/union/vtable/operations.rs | 2 + .../src/arrays/varbin/vtable/operations.rs | 2 + .../arrays/varbinview/vtable/operations.rs | 2 + .../src/arrays/variant/vtable/operations.rs | 2 + vortex-python/src/arrays/py/vtable.rs | 2 + 51 files changed, 748 insertions(+) create mode 100644 vortex-array/PROBE_DESIGN.md create mode 100644 vortex-array/src/array/probe.rs create mode 100644 vortex-array/src/array/probe/tests.rs diff --git a/encodings/alp/src/alp/ops.rs b/encodings/alp/src/alp/ops.rs index a8850744056..8b80ee9a875 100644 --- a/encodings/alp/src/alp/ops.rs +++ b/encodings/alp/src/alp/ops.rs @@ -15,6 +15,8 @@ use crate::ALPFloat; use crate::match_each_alp_float_ptype; impl OperationsVTable for ALP { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, ALP>, index: usize, diff --git a/encodings/alp/src/alp_rd/ops.rs b/encodings/alp/src/alp_rd/ops.rs index edb2fb21186..40749c51557 100644 --- a/encodings/alp/src/alp_rd/ops.rs +++ b/encodings/alp/src/alp_rd/ops.rs @@ -14,6 +14,8 @@ use crate::ALPRDArrayExt; use crate::ALPRDArraySlotsExt; impl OperationsVTable for ALPRD { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, ALPRD>, index: usize, diff --git a/encodings/bytebool/src/array.rs b/encodings/bytebool/src/array.rs index faa1fea81fa..1236ebdd5e7 100644 --- a/encodings/bytebool/src/array.rs +++ b/encodings/bytebool/src/array.rs @@ -312,6 +312,8 @@ impl ValidityVTable for ByteBool { } impl OperationsVTable for ByteBool { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, ByteBool>, index: usize, diff --git a/encodings/datetime-parts/src/ops.rs b/encodings/datetime-parts/src/ops.rs index d99e55c7542..e3be4a1b0ff 100644 --- a/encodings/datetime-parts/src/ops.rs +++ b/encodings/datetime-parts/src/ops.rs @@ -17,6 +17,8 @@ use crate::timestamp; use crate::timestamp::TimestampParts; impl OperationsVTable for DateTimeParts { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, DateTimeParts>, index: usize, diff --git a/encodings/decimal-byte-parts/src/decimal_byte_parts/mod.rs b/encodings/decimal-byte-parts/src/decimal_byte_parts/mod.rs index d5b0024f5b7..62c0eea25cb 100644 --- a/encodings/decimal-byte-parts/src/decimal_byte_parts/mod.rs +++ b/encodings/decimal-byte-parts/src/decimal_byte_parts/mod.rs @@ -289,6 +289,8 @@ fn to_canonical_decimal( } impl OperationsVTable for DecimalByteParts { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, DecimalByteParts>, index: usize, diff --git a/encodings/fastlanes/src/bitpacking/vtable/operations.rs b/encodings/fastlanes/src/bitpacking/vtable/operations.rs index e14b27323c1..4c4be1e746f 100644 --- a/encodings/fastlanes/src/bitpacking/vtable/operations.rs +++ b/encodings/fastlanes/src/bitpacking/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::BitPacked; use crate::bitpack_decompress; use crate::bitpacking::array::BitPackedArrayExt; impl OperationsVTable for BitPacked { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, BitPacked>, index: usize, diff --git a/encodings/fastlanes/src/delta/vtable/operations.rs b/encodings/fastlanes/src/delta/vtable/operations.rs index 7ed57a0886d..999fad1b33c 100644 --- a/encodings/fastlanes/src/delta/vtable/operations.rs +++ b/encodings/fastlanes/src/delta/vtable/operations.rs @@ -11,6 +11,8 @@ use vortex_error::VortexResult; use super::Delta; impl OperationsVTable for Delta { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Delta>, index: usize, diff --git a/encodings/fastlanes/src/for/vtable/operations.rs b/encodings/fastlanes/src/for/vtable/operations.rs index 36dac998cbe..3df84a704a9 100644 --- a/encodings/fastlanes/src/for/vtable/operations.rs +++ b/encodings/fastlanes/src/for/vtable/operations.rs @@ -13,6 +13,8 @@ use super::FoR; use crate::r#for::array::FoRArrayExt; use crate::r#for::array::FoRArraySlotsExt; impl OperationsVTable for FoR { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, FoR>, index: usize, diff --git a/encodings/fastlanes/src/rle/vtable/operations.rs b/encodings/fastlanes/src/rle/vtable/operations.rs index ca4d2d39545..8045fc550cc 100644 --- a/encodings/fastlanes/src/rle/vtable/operations.rs +++ b/encodings/fastlanes/src/rle/vtable/operations.rs @@ -14,6 +14,8 @@ use crate::rle::RLEArrayExt; use crate::rle::RLEArraySlotsExt; impl OperationsVTable for RLE { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, RLE>, index: usize, diff --git a/encodings/fastlanes/src/transposed_bool.rs b/encodings/fastlanes/src/transposed_bool.rs index efb3443cdfc..3acd47772b8 100644 --- a/encodings/fastlanes/src/transposed_bool.rs +++ b/encodings/fastlanes/src/transposed_bool.rs @@ -256,6 +256,8 @@ impl VTable for TransposedBool { } impl OperationsVTable for TransposedBool { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, TransposedBool>, index: usize, diff --git a/encodings/fsst/src/ops.rs b/encodings/fsst/src/ops.rs index b630508ed9e..615dc78d6db 100644 --- a/encodings/fsst/src/ops.rs +++ b/encodings/fsst/src/ops.rs @@ -14,6 +14,8 @@ use crate::FSST; use crate::FSSTArrayExt; impl OperationsVTable for FSST { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, FSST>, index: usize, diff --git a/encodings/onpair/src/ops.rs b/encodings/onpair/src/ops.rs index 728e5a0e6f2..a78df1d4b7d 100644 --- a/encodings/onpair/src/ops.rs +++ b/encodings/onpair/src/ops.rs @@ -18,6 +18,8 @@ use crate::decode::code_boundary_at; use crate::decode::collect_widened; impl OperationsVTable for OnPair { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, OnPair>, index: usize, diff --git a/encodings/parquet-variant/src/operations.rs b/encodings/parquet-variant/src/operations.rs index 8517df27f22..b8579516228 100644 --- a/encodings/parquet-variant/src/operations.rs +++ b/encodings/parquet-variant/src/operations.rs @@ -31,6 +31,8 @@ use crate::ParquetVariantArraySlotsExt; use crate::vtable::ParquetVariant; impl OperationsVTable for ParquetVariant { + type ProbeState<'a> = (); + /// Resolves one row according to the Parquet Variant shredding rules. /// /// For valid data, a row with both `value` and struct `typed_value` is a partially diff --git a/encodings/pco/src/array.rs b/encodings/pco/src/array.rs index 44a6a8e4045..48939f9efe8 100644 --- a/encodings/pco/src/array.rs +++ b/encodings/pco/src/array.rs @@ -778,6 +778,8 @@ impl ValidityVTable for Pco { } impl OperationsVTable for Pco { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Pco>, index: usize, diff --git a/encodings/runend/src/ops.rs b/encodings/runend/src/ops.rs index e2c2e3fc99b..14acef6c3e1 100644 --- a/encodings/runend/src/ops.rs +++ b/encodings/runend/src/ops.rs @@ -18,6 +18,8 @@ use crate::array::RunEndArrayExt; use crate::array::RunEndArraySlotsExt; impl OperationsVTable for RunEnd { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, RunEnd>, index: usize, diff --git a/encodings/sequence/src/array.rs b/encodings/sequence/src/array.rs index 10a222544b6..477da3191fe 100644 --- a/encodings/sequence/src/array.rs +++ b/encodings/sequence/src/array.rs @@ -426,6 +426,8 @@ impl VTable for Sequence { } impl OperationsVTable for Sequence { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Sequence>, index: usize, diff --git a/encodings/sparse/src/ops.rs b/encodings/sparse/src/ops.rs index 568d8d377d1..e5c45e47911 100644 --- a/encodings/sparse/src/ops.rs +++ b/encodings/sparse/src/ops.rs @@ -11,6 +11,8 @@ use crate::Sparse; use crate::SparseExt as _; impl OperationsVTable for Sparse { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Sparse>, index: usize, diff --git a/encodings/zigzag/src/array.rs b/encodings/zigzag/src/array.rs index ef3165132f4..4a7dc8b7436 100644 --- a/encodings/zigzag/src/array.rs +++ b/encodings/zigzag/src/array.rs @@ -231,6 +231,8 @@ impl Default for ZigZagData { } impl OperationsVTable for ZigZag { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, ZigZag>, index: usize, diff --git a/encodings/zstd/src/array.rs b/encodings/zstd/src/array.rs index fb3551e539b..66d0940de1e 100644 --- a/encodings/zstd/src/array.rs +++ b/encodings/zstd/src/array.rs @@ -1588,6 +1588,8 @@ impl ValidityVTable for Zstd { } impl OperationsVTable for Zstd { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Zstd>, index: usize, diff --git a/encodings/zstd/src/zstd_buffers.rs b/encodings/zstd/src/zstd_buffers.rs index f21deee6f64..912af56dd71 100644 --- a/encodings/zstd/src/zstd_buffers.rs +++ b/encodings/zstd/src/zstd_buffers.rs @@ -520,6 +520,8 @@ impl VTable for ZstdBuffers { } impl OperationsVTable for ZstdBuffers { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, ZstdBuffers>, index: usize, diff --git a/vortex-array/PROBE_DESIGN.md b/vortex-array/PROBE_DESIGN.md new file mode 100644 index 00000000000..f3cad7a5a08 --- /dev/null +++ b/vortex-array/PROBE_DESIGN.md @@ -0,0 +1,130 @@ +# Short-lived scalar probes + +Repeated scalar reads can redo validity evaluation, child dispatch, and decompression. +`ArrayProbe` gives an encoding a place to retain work for a short series of lookups, +without requiring callers to understand the encoding or execute the entire array. + +## Caller API + +```rust +let mut probe = array.probe(ProbeUsage::Repeated); +let first = probe.scalar_at(first_index, &mut ctx)?; +let second = probe.scalar_at(second_index, &mut ctx)?; + +let single = array.probe(ProbeUsage::Once).scalar_at(index, &mut ctx)?; +``` + +The probe borrows an `ArrayRef`. Construction allocates nothing and does not execute +anything. Scalar results own their values, include nullness, and can outlive the probe. +Bounds are checked before entering the encoding hook. A separate validity probe is not +needed: each encoding can resolve validity together with the value. + +`Repeated` permits preparation and reuse. `Once` requests no retained state; it is a +caching hint, so calling a one-off probe multiple times is still valid. Dropping the probe +releases its local state, child probes, and decoding resources. Probes are thread-local. + +## Vtable API + +The hook lives on `OperationsVTable`, selected by the array's existing `VTable`: + +```rust +type ProbeState<'a>: Default + 'a; + +fn probe_scalar<'a>( + array: ArrayView<'a, V>, + index: usize, + probe: Option<&mut ProbeCtx<'a, Self::ProbeState<'a>>>, + ctx: &mut ExecutionCtx, +) -> VortexResult { + array.array().execute_scalar(index, ctx) +} +``` + +All encodings initially select `ProbeState<'a> = ()` and use this default, including +primitive, PCO, FastLanes RLE, RunEnd, ScalarFn, and Zstd. The API change provides no +encoding-specific speedup on its own; optimized hooks are a separate follow-up. + +An encoding opting into preparation chooses its own concrete `ProbeState`. `Default` +should be cheap and avoid allocation or execution. Fallible preparation belongs in the +hook. The framework initializes one context on the first in-bounds repeated lookup and +passes that same context on subsequent calls. `Once` passes `None` without constructing +local state or child storage. The hook must preserve the logical dtype and handle nulls; +its contract differs from the legacy non-null `scalar_at` hook. + +## Local state and children + +```rust +pub struct ProbeCtx<'a, S> { + state: S, + children: ProbeChildren<'a>, +} +``` + +Encodings access local state with `probe.state_mut()` and request a retained child probe +with `probe.child(slot)?`. The framework binds child lookup to the original source's +slots and creates each child probe on its first request. Repeated requests reuse it; +creating the child does not initialize its encoding context or execute it. + +When local state and children must be borrowed simultaneously, use: + +```rust +let (state, children) = probe.parts(); +``` + +These are disjoint mutable borrows of the context's fields. Encoding-specific state does +not need to store child probes manually. A routing encoding can keep unit local state and +request the needed slots; a decoding encoding can keep owned indexes and decoded buffers. + +Each child has its own context and lazy child cache, so the API supports arbitrary nesting +when every parent routes through child probes. The cache key is `(parent probe, slot)`. +Separate roots have independent caches, and two slots referencing the same array still +have independent child state. Default hooks keep their existing scalar execution path. + +The child table is a `Vec>>`. It starts empty and allocates cells +for the source's slots on the first valid child request. Unrequested cells remain `None`. +Missing or out-of-bounds slots return an error before allocating a table. A dense table +reserves space for all slots, so wide arrays should be considered when measuring setup +cost. The vector provides the indirection required for recursive storage. + +## Lifetimes and ownership + +`'a` is the borrow of the root array. Every child slot is reachable for that lifetime; +child probes borrow the original slots without cloning array handles. Local state may +borrow views into the same source tree and own anything it prepares itself. + +A state cannot build a new array and store a probe borrowing that new array inside itself: +that would be self-referential. Owned decoded buffers can instead be read directly. +`use<'a>` controls capture for opaque `impl Trait` returns and does not replace the +associated state's outlives bound. + +## Storage and dispatch + +The entire typed `ProbeCtx<'a, S>` lives in erased storage owned by the probe. The holder +provides 128 inline bytes aligned to 16 bytes and a heap fallback for larger or more aligned +contexts. The child-cache header counts toward that capacity. Child tables, indexes, and +decoded buffers may allocate separately when an encoding needs them. + +```text +ArrayProbe::scalar_at + -> existing DynArrayData::probe_scalar dispatch + -> ArrayData retrieves ProbeCtx<'a, V's associated state> + -> OperationsVTable::probe_scalar +``` + +The adapter initializes the context with a source-bound factory, called once. Retrieving +it later uses a pointer cast with a fixed concrete type rather than an `Any` lookup. The +holder records a destructor for that exact type and drops it once, freeing a spilled +allocation and recursively dropping child probes. No pointer into inline storage is kept +across moves. + +All raw storage operations are confined to the holder. Its safety requires every access +to use the same concrete context type, including lifetimes. The probe's fixed source and +that source's vtable enforce this invariant. An invariant lifetime marker prevents erased +borrowed state from outliving the source. The holder is neither `Send` nor `Sync`, since +encoding state is not required to implement those traits. + +Tests cover inline and spilled storage, alignment, moves, borrowed state, exactly-once +initialization and destruction, default scalar behavior, bounds and nulls, lazy child +creation, repeated slot reuse, source binding, independent slots/contexts, and simultaneous +local-state and child access. Encoding follow-ups should test recursive cache reuse and +benchmark complete probe lifetimes, including preparation and teardown. diff --git a/vortex-array/src/array/mod.rs b/vortex-array/src/array/mod.rs index 8b9b2812bfa..2431a6a93cb 100644 --- a/vortex-array/src/array/mod.rs +++ b/vortex-array/src/array/mod.rs @@ -31,6 +31,13 @@ pub use erased::*; mod plugin; pub use plugin::*; +mod probe; +pub use probe::ArrayProbe; +pub use probe::ProbeChildren; +pub use probe::ProbeCtx; +use probe::ProbeStorage; +pub use probe::ProbeUsage; + mod foreign; pub(crate) use foreign::*; @@ -229,6 +236,14 @@ pub(crate) trait DynArrayData: 'static + private::Sealed + Send + Sync + Debug { index: usize, ctx: &mut ExecutionCtx, ) -> VortexResult; + + fn probe_scalar<'a>( + &'a self, + this: &'a ArrayRef, + index: usize, + state: Option<&mut ProbeStorage<'a>>, + ctx: &mut ExecutionCtx, + ) -> VortexResult; } /// Trait for converting a type into a Vortex [`ArrayRef`]. @@ -499,6 +514,29 @@ impl DynArrayData for ArrayData { let view = unsafe { ArrayView::new_unchecked(this, &self.data) }; >::scalar_at(view, index, ctx) } + + fn probe_scalar<'a>( + &'a self, + this: &'a ArrayRef, + index: usize, + state: Option<&mut ProbeStorage<'a>>, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + // SAFETY: this adapter belongs to the ArrayData stored in `this`. + let view = unsafe { ArrayView::new_unchecked(this, &self.data) }; + let state = state.map(|storage| { + // SAFETY: ArrayProbe fixes its source for its entire lifetime, so this storage + // is accessed only through this adapter with the same associated context type. + unsafe { + storage.get_or_init(|| { + ProbeCtx::<>::ProbeState<'a>>::new( + this, + ) + }) + } + }); + >::probe_scalar(view, index, state, ctx) + } } /// Wrapper around `&mut dyn Hasher` that implements `Hasher` (and is `Sized`). diff --git a/vortex-array/src/array/probe.rs b/vortex-array/src/array/probe.rs new file mode 100644 index 00000000000..0219460d96b --- /dev/null +++ b/vortex-array/src/array/probe.rs @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +//! Random scalar access with optional, encoding-specific retained state. + +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::ptr::NonNull; + +use vortex_error::VortexResult; +use vortex_error::vortex_ensure; +use vortex_error::vortex_err; + +use crate::ArrayRef; +use crate::ExecutionCtx; +use crate::scalar::Scalar; + +/// Whether scalar access should retain preparation for subsequent lookups. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProbeUsage { + /// Use temporary resources only, without initializing retained probe state. + Once, + /// Allow the encoding to retain preparation and decoded data between lookups. + Repeated, +} + +/// A borrowed scalar accessor that retains encoding-specific state until it is dropped. +/// +/// `'a` is the borrow of the root array. Its [`ProbeCtx`] retains a child probe for each +/// requested slot, so one lifetime covers the whole tree and no array handles are cloned. +/// Anything an encoding builds itself, such as a decoded page or validity mask, is owned +/// by its state. +/// +/// Construction never allocates or executes the array. Repeated access initializes the +/// encoding's context on the first in-bounds lookup. Small contexts live inline; larger or +/// more aligned contexts use a heap allocation. Child storage and decoding may allocate +/// separately, when first needed. +/// +/// `Once` is a caching hint, not a restriction on how many times the probe may be called. +/// Returned scalars own their values and can outlive the probe. Probes are local to a thread. +pub struct ArrayProbe<'a> { + array: &'a ArrayRef, + state: Option>, +} + +/// Local encoding state and lazy child probes retained for one source array. +/// +/// The framework initializes this context once for repeated access and passes it to +/// [`OperationsVTable::probe_scalar`](crate::vtable::OperationsVTable::probe_scalar). +/// One-off access receives `None` instead. `S` is the encoding's associated state type; +/// it may borrow the source tree for `'a` and own any prepared resources. +pub struct ProbeCtx<'a, S> { + state: S, + children: ProbeChildren<'a>, +} + +impl<'a, S: Default> ProbeCtx<'a, S> { + pub(crate) fn new(array: &'a ArrayRef) -> Self { + Self { + state: S::default(), + children: ProbeChildren { + array, + slots: Vec::new(), + }, + } + } +} + +impl<'a, S> ProbeCtx<'a, S> { + /// Access the encoding's retained local state. + pub fn state_mut(&mut self) -> &mut S { + &mut self.state + } + + /// Get the retained probe for a source slot, creating it on the first request. + /// + /// Returns an error for an absent or out-of-bounds slot. Creating a child probe does + /// not execute the child or initialize its encoding state. + pub fn child(&mut self, slot: usize) -> VortexResult<&mut ArrayProbe<'a>> { + self.children.child(slot) + } + + /// Borrow local state and child access together, allowing disjoint mutable access. + pub fn parts(&mut self) -> (&mut S, &mut ProbeChildren<'a>) { + (&mut self.state, &mut self.children) + } +} + +/// Lazy child probes bound to the slots of one source array. +/// +/// Obtain this through [`ProbeCtx::parts`] when retaining a mutable borrow of local state +/// while accessing children. Each slot has independent state, even if two slots reference +/// the same array. The slot table allocates on its first valid request; unrequested slots +/// remain empty. Dropping the parent context drops every created child probe. +pub struct ProbeChildren<'a> { + array: &'a ArrayRef, + slots: Vec>>, +} + +impl<'a> ProbeChildren<'a> { + /// Get or create a repeated-access probe for the given source slot. + /// + /// Returns an error for an absent or out-of-bounds slot without allocating a slot table. + pub fn child(&mut self, slot: usize) -> VortexResult<&mut ArrayProbe<'a>> { + let child = self + .array + .slots() + .get(slot) + .and_then(Option::as_ref) + .ok_or_else(|| vortex_err!("Probe child slot {slot} is missing"))?; + if self.slots.is_empty() { + self.slots.resize_with(self.array.slots().len(), || None); + } + Ok(self.slots[slot].get_or_insert_with(|| child.probe(ProbeUsage::Repeated))) + } +} + +impl ArrayRef { + /// Create an accessor with the requested policy for retaining state between scalar lookups. + /// + /// ``` + /// use vortex_array::{IntoArray, ProbeUsage, VortexSessionExecute}; + /// use vortex_array::arrays::PrimitiveArray; + /// + /// let array = PrimitiveArray::from_iter([10i32, 20, 30]).into_array(); + /// let mut ctx = vortex_array::array_session().create_execution_ctx(); + /// let mut probe = array.probe(ProbeUsage::Repeated); + /// assert_eq!(probe.scalar_at(2, &mut ctx)?, 30i32.into()); + /// assert_eq!(probe.scalar_at(0, &mut ctx)?, 10i32.into()); + /// # Ok::<(), vortex_error::VortexError>(()) + /// ``` + pub fn probe(&self, usage: ProbeUsage) -> ArrayProbe<'_> { + ArrayProbe { + array: self, + state: match usage { + ProbeUsage::Once => None, + ProbeUsage::Repeated => Some(ProbeStorage::new()), + }, + } + } +} + +impl<'a> ArrayProbe<'a> { + /// The array this probe reads from. + pub fn array(&self) -> &'a ArrayRef { + self.array + } + + /// Read a scalar, including its nullness, preparing and reusing state as appropriate. + pub fn scalar_at(&mut self, index: usize, ctx: &mut ExecutionCtx) -> VortexResult { + vortex_ensure!(index < self.array.len(), OutOfBounds: index, 0, self.array.len()); + let scalar = + self.array + .dyn_array() + .probe_scalar(self.array, index, self.state.as_mut(), ctx)?; + debug_assert_eq!(scalar.dtype(), self.array.dtype(), "Scalar dtype mismatch"); + Ok(scalar) + } +} + +// An open set of external state types cannot be represented by a fixed Rust enum. Erasing +// them inline avoids a mandatory Box allocation; all raw storage operations are confined here. +#[repr(align(16))] +struct InlineStorage(MaybeUninit<[u8; 128]>); + +pub(crate) struct ProbeStorage<'a> { + inline: InlineStorage, + heap: Option>, + drop_fn: Option, + // The erased state may borrow the root array tree, and the type system cannot see that + // borrow once erased. This marker ties the storage to `'a` (invariantly) so it cannot + // outlive the borrow. NonNull also keeps the erased storage !Send and !Sync, since we + // impose neither bound on state types. + _lifetime: PhantomData<&'a mut &'a ()>, +} + +impl<'a> ProbeStorage<'a> { + fn new() -> Self { + Self { + inline: InlineStorage(MaybeUninit::uninit()), + heap: None, + drop_fn: None, + _lifetime: PhantomData, + } + } + + /// # Safety + /// + /// Every call on this storage must use the same `T`, including its lifetime parameters. + /// The owning ArrayProbe fixes its source array, and only that array's erased adapter + /// may initialize/access this storage using its associated ProbeCtx type. + pub(crate) unsafe fn get_or_init(&mut self, init: impl FnOnce() -> T) -> &mut T { + if self.drop_fn.is_none() { + let value = init(); + if size_of::() <= size_of::() + && align_of::() <= align_of::() + { + // SAFETY: the size/alignment checks make the inline region suitable for T, + // and no value has been initialized in this storage yet. + unsafe { self.inline.0.as_mut_ptr().cast::().write(value) }; + self.drop_fn = Some(drop_inline::); + } else { + self.heap = Some(NonNull::from(Box::leak(Box::new(value))).cast()); + self.drop_fn = Some(drop_heap::); + } + } + // SAFETY: initialization above, or the caller's same-T invariant, establishes a + // live T at this pointer. The exclusive storage borrow provides exclusive access. + unsafe { &mut *self.as_mut_ptr().cast::() } + } + + fn as_mut_ptr(&mut self) -> *mut u8 { + match self.heap { + Some(ptr) => ptr.as_ptr(), + None => self.inline.0.as_mut_ptr().cast(), + } + } +} + +impl Drop for ProbeStorage<'_> { + fn drop(&mut self) { + if let Some(drop_fn) = self.drop_fn { + // SAFETY: the drop shim was installed only after its matching T was initialized. + // No pointer into inline storage is retained across moves of ProbeStorage. + unsafe { drop_fn(self.as_mut_ptr()) }; + } + } +} + +unsafe fn drop_inline(ptr: *mut u8) { + // SAFETY: the storage installed this shim for an initialized inline T. + unsafe { ptr.cast::().drop_in_place() }; +} + +unsafe fn drop_heap(ptr: *mut u8) { + // SAFETY: the storage installed this shim for a T allocated by Box::new and leaked once. + drop(unsafe { Box::from_raw(ptr.cast::()) }); +} + +#[cfg(test)] +mod tests; diff --git a/vortex-array/src/array/probe/tests.rs b/vortex-array/src/array/probe/tests.rs new file mode 100644 index 00000000000..6310e289fb3 --- /dev/null +++ b/vortex-array/src/array/probe/tests.rs @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +use std::cell::Cell; +use std::rc::Rc; + +use rstest::rstest; +use vortex_error::VortexResult; + +use super::ProbeCtx; +use super::ProbeStorage; +use super::ProbeUsage; +use crate::IntoArray; +use crate::VortexSessionExecute; +use crate::arrays::PrimitiveArray; +use crate::arrays::StructArray; + +#[derive(Default)] +struct Tracked { + drops: Rc>, +} + +impl Drop for Tracked { + fn drop(&mut self) { + self.drops.set(self.drops.get() + 1); + } +} + +#[derive(Default)] +struct Large { + tracked: Tracked, + padding: [[u8; 32]; 8], +} + +#[repr(align(64))] +#[derive(Default)] +struct Aligned(Tracked); + +#[derive(Default)] +struct Borrowed<'a>(Option<&'a usize>); + +#[test] +fn inline_state_survives_moves_and_drops_once() { + let mut storage = ProbeStorage::new(); + let initialized = Cell::new(0); + // SAFETY: every access to this storage uses Tracked. + let drops = Rc::clone( + &unsafe { + storage.get_or_init(|| { + initialized.set(initialized.get() + 1); + Tracked::default() + }) + } + .drops, + ); + assert!(storage.heap.is_none()); + let mut moved = (storage, 1); + // SAFETY: the moved storage still contains Tracked. + assert!(Rc::ptr_eq( + &unsafe { + moved.0.get_or_init(|| { + initialized.set(initialized.get() + 1); + Tracked::default() + }) + } + .drops, + &drops + )); + drop(moved); + assert_eq!(initialized.get(), 1); + assert_eq!(drops.get(), 1); +} + +#[test] +fn children_are_lazy_reused_and_bound_to_their_source_slots() -> VortexResult<()> { + let leaf = PrimitiveArray::from_iter([10i32, 20]).into_array(); + let array = StructArray::from_fields(&[("left", leaf.clone()), ("right", leaf)])?.into_array(); + let mut ctx = crate::array_session().create_execution_ctx(); + let mut probe = ProbeCtx::::new(&array); + assert_eq!(probe.children.slots.capacity(), 0); + // Slot zero is the absent struct validity; invalid requests must not allocate. + assert!(probe.child(0).is_err()); + assert!(probe.child(99).is_err()); + assert_eq!(probe.children.slots.capacity(), 0); + + let first = std::ptr::from_mut(probe.child(1)?); + assert!(probe.children.slots[2].is_none()); + let (state, children) = probe.parts(); + let child = children.child(1)?; + assert!( + child + .state + .as_ref() + .is_some_and(|state| state.drop_fn.is_none()) + ); + assert!(child.scalar_at(2, &mut ctx).is_err()); + assert!( + child + .state + .as_ref() + .is_some_and(|state| state.drop_fn.is_none()) + ); + assert_eq!(child.scalar_at(0, &mut ctx)?, 10i32.into()); + *state += 1; + assert_eq!(*probe.state_mut(), 1); + assert_eq!(std::ptr::from_mut(probe.child(1)?), first); + assert!( + probe + .child(1)? + .state + .as_ref() + .is_some_and(|state| state.drop_fn.is_some()) + ); + + // Identical sources in different slots still get independent probe state. + assert_ne!(std::ptr::from_mut(probe.child(2)?), first); + let source = array.slots()[1] + .as_ref() + .ok_or_else(|| vortex_error::vortex_err!("missing fixture slot"))?; + assert!(std::ptr::eq(probe.child(1)?.array(), source)); + assert!( + probe + .child(2)? + .state + .as_ref() + .is_some_and(|state| state.drop_fn.is_none()) + ); + let mut other = ProbeCtx::::new(&array); + assert!( + other + .child(1)? + .state + .as_ref() + .is_some_and(|state| state.drop_fn.is_none()) + ); + + let mut moved = (probe, ()); + assert_eq!(std::ptr::from_mut(moved.0.child(1)?), first); + assert_eq!(moved.0.child(1)?.scalar_at(1, &mut ctx)?, 20i32.into()); + Ok(()) +} + +#[test] +fn oversized_state_spills_and_drops_once() { + let mut storage = ProbeStorage::new(); + // SAFETY: every access to this storage uses Large. + let state = unsafe { storage.get_or_init::(Large::default) }; + state.padding[7][31] = 42; + let drops = Rc::clone(&state.tracked.drops); + assert!(storage.heap.is_some()); + let mut moved = (storage, 1); + // SAFETY: the moved storage still contains Large. + assert_eq!( + unsafe { moved.0.get_or_init::(Large::default) }.padding[7][31], + 42 + ); + drop(moved); + assert_eq!(drops.get(), 1); +} + +#[test] +fn over_aligned_state_spills() { + let mut storage = ProbeStorage::new(); + // SAFETY: this storage is only used for Aligned. + let state = unsafe { storage.get_or_init::(Aligned::default) }; + assert_eq!(std::ptr::from_ref(state).addr() % 64, 0); + let drops = Rc::clone(&state.0.drops); + assert!(storage.heap.is_some()); + drop(storage); + assert_eq!(drops.get(), 1); +} + +#[test] +fn state_can_borrow_and_unit_needs_no_allocation() { + let value = 42; + let mut borrowed = ProbeStorage::new(); + // SAFETY: the storage always contains Borrowed with the same source lifetime. + unsafe { borrowed.get_or_init::>(Borrowed::default) }.0 = Some(&value); + // SAFETY: same type and source lifetime as initialization. + assert_eq!( + unsafe { borrowed.get_or_init::>(Borrowed::default) }.0, + Some(&42) + ); + let mut unit = ProbeStorage::new(); + // SAFETY: this separate storage is only used for (). + unsafe { unit.get_or_init::<()>(Default::default) }; + assert!(unit.heap.is_none()); +} + +#[rstest] +#[case(ProbeUsage::Once)] +#[case(ProbeUsage::Repeated)] +fn access_checks_bounds_and_nulls(#[case] usage: ProbeUsage) -> VortexResult<()> { + let array = PrimitiveArray::from_option_iter([Some(10i32), None, Some(30)]).into_array(); + let mut ctx = crate::array_session().create_execution_ctx(); + let mut probe = array.probe(usage); + assert!( + probe + .state + .as_ref() + .is_none_or(|state| state.drop_fn.is_none()) + ); + assert!(probe.scalar_at(3, &mut ctx).is_err()); + assert!( + probe + .state + .as_ref() + .is_none_or(|state| state.drop_fn.is_none()) + ); + for index in [2, 1, 0, 2] { + assert_eq!( + probe.scalar_at(index, &mut ctx)?, + array.execute_scalar(index, &mut ctx)? + ); + } + if usage == ProbeUsage::Once { + assert!(probe.state.is_none()); + } + Ok(()) +} diff --git a/vortex-array/src/array/vtable/operations.rs b/vortex-array/src/array/vtable/operations.rs index 7f49e683640..883c70a1b47 100644 --- a/vortex-array/src/array/vtable/operations.rs +++ b/vortex-array/src/array/vtable/operations.rs @@ -5,6 +5,7 @@ use vortex_error::VortexResult; use vortex_error::vortex_bail; use crate::ExecutionCtx; +use crate::ProbeCtx; use crate::array::ArrayView; use crate::array::VTable; use crate::scalar::Scalar; @@ -17,6 +18,30 @@ use crate::vtable::NotSupported; /// [`ArrayRef`](crate::ArrayRef) /// methods perform common checks before dispatching here. pub trait OperationsVTable { + /// Encoding-specific state retained by repeated scalar access. + /// + /// Default construction should be cheap and avoid allocation or execution. Preparation + /// belongs in [`Self::probe_scalar`]. `'a` is the borrow of the root array, so state may + /// retain views into its source tree. Request retained child probes through [`ProbeCtx`]. + /// Use `()` when no local state is needed. + type ProbeState<'a>: Default + 'a; + + /// Read a scalar, handling nullness and optionally retaining state for subsequent reads. + /// + /// Bounds have been checked, but the row may be null. `None` requests one-off access and + /// never initializes a context; `Some` reuses local state and child probes for this source. + /// The scalar must retain the source's logical dtype, including nullability. + /// + /// The default preserves the existing scalar path without adding caching. + fn probe_scalar<'a>( + array: ArrayView<'a, V>, + index: usize, + _probe: Option<&mut ProbeCtx<'a, Self::ProbeState<'a>>>, + ctx: &mut ExecutionCtx, + ) -> VortexResult { + array.array().execute_scalar(index, ctx) + } + /// Fetch the scalar at the given index. /// /// ## Preconditions @@ -35,6 +60,8 @@ pub trait OperationsVTable { } impl OperationsVTable for NotSupported { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, V>, _index: usize, diff --git a/vortex-array/src/arrays/bool/vtable/operations.rs b/vortex-array/src/arrays/bool/vtable/operations.rs index c29ab20331b..5a91469eae5 100644 --- a/vortex-array/src/arrays/bool/vtable/operations.rs +++ b/vortex-array/src/arrays/bool/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::arrays::bool::BoolArrayExt; use crate::scalar::Scalar; impl OperationsVTable for Bool { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Bool>, index: usize, diff --git a/vortex-array/src/arrays/chunked/vtable/operations.rs b/vortex-array/src/arrays/chunked/vtable/operations.rs index 8f9e0867a88..d5484a3a538 100644 --- a/vortex-array/src/arrays/chunked/vtable/operations.rs +++ b/vortex-array/src/arrays/chunked/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::arrays::chunked::ChunkedArrayExt; use crate::scalar::Scalar; impl OperationsVTable for Chunked { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Chunked>, index: usize, diff --git a/vortex-array/src/arrays/constant/vtable/operations.rs b/vortex-array/src/arrays/constant/vtable/operations.rs index e3568a9c39f..d7ada7ee274 100644 --- a/vortex-array/src/arrays/constant/vtable/operations.rs +++ b/vortex-array/src/arrays/constant/vtable/operations.rs @@ -10,6 +10,8 @@ use crate::arrays::Constant; use crate::scalar::Scalar; impl OperationsVTable for Constant { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Constant>, _index: usize, diff --git a/vortex-array/src/arrays/decimal/vtable/operations.rs b/vortex-array/src/arrays/decimal/vtable/operations.rs index 257f26127ae..2eae2715b73 100644 --- a/vortex-array/src/arrays/decimal/vtable/operations.rs +++ b/vortex-array/src/arrays/decimal/vtable/operations.rs @@ -12,6 +12,8 @@ use crate::scalar::DecimalValue; use crate::scalar::Scalar; impl OperationsVTable for Decimal { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Decimal>, index: usize, diff --git a/vortex-array/src/arrays/dict/vtable/operations.rs b/vortex-array/src/arrays/dict/vtable/operations.rs index 1982a1e0870..a127c2febad 100644 --- a/vortex-array/src/arrays/dict/vtable/operations.rs +++ b/vortex-array/src/arrays/dict/vtable/operations.rs @@ -12,6 +12,8 @@ use crate::arrays::dict::DictArraySlotsExt; use crate::scalar::Scalar; impl OperationsVTable for Dict { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Dict>, index: usize, diff --git a/vortex-array/src/arrays/extension/vtable/operations.rs b/vortex-array/src/arrays/extension/vtable/operations.rs index 66de94b596a..b6843612955 100644 --- a/vortex-array/src/arrays/extension/vtable/operations.rs +++ b/vortex-array/src/arrays/extension/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::arrays::extension::ExtensionArrayExt; use crate::scalar::Scalar; impl OperationsVTable for Extension { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Extension>, index: usize, diff --git a/vortex-array/src/arrays/filter/vtable.rs b/vortex-array/src/arrays/filter/vtable.rs index 56fc112aa16..8301b682fd9 100644 --- a/vortex-array/src/arrays/filter/vtable.rs +++ b/vortex-array/src/arrays/filter/vtable.rs @@ -200,6 +200,8 @@ impl VTable for Filter { } } impl OperationsVTable for Filter { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Filter>, index: usize, diff --git a/vortex-array/src/arrays/fixed_size_list/vtable/operations.rs b/vortex-array/src/arrays/fixed_size_list/vtable/operations.rs index 9f4cf02fbf8..e31892e0215 100644 --- a/vortex-array/src/arrays/fixed_size_list/vtable/operations.rs +++ b/vortex-array/src/arrays/fixed_size_list/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::arrays::fixed_size_list::FixedSizeListArrayExt; use crate::scalar::Scalar; impl OperationsVTable for FixedSizeList { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, FixedSizeList>, index: usize, diff --git a/vortex-array/src/arrays/interleave/mod.rs b/vortex-array/src/arrays/interleave/mod.rs index d29981249d4..94fa0d77796 100644 --- a/vortex-array/src/arrays/interleave/mod.rs +++ b/vortex-array/src/arrays/interleave/mod.rs @@ -389,6 +389,8 @@ impl VTable for Interleave { } impl OperationsVTable for Interleave { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Interleave>, index: usize, diff --git a/vortex-array/src/arrays/list/vtable/operations.rs b/vortex-array/src/arrays/list/vtable/operations.rs index 02c686cd1f1..688e635c3b0 100644 --- a/vortex-array/src/arrays/list/vtable/operations.rs +++ b/vortex-array/src/arrays/list/vtable/operations.rs @@ -13,6 +13,8 @@ use crate::arrays::list::ListArrayExt; use crate::scalar::Scalar; impl OperationsVTable for List { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, List>, index: usize, diff --git a/vortex-array/src/arrays/listview/vtable/operations.rs b/vortex-array/src/arrays/listview/vtable/operations.rs index f0cb9539cc3..ec83518eae7 100644 --- a/vortex-array/src/arrays/listview/vtable/operations.rs +++ b/vortex-array/src/arrays/listview/vtable/operations.rs @@ -13,6 +13,8 @@ use crate::arrays::listview::ListViewArrayExt; use crate::scalar::Scalar; impl OperationsVTable for ListView { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, ListView>, index: usize, diff --git a/vortex-array/src/arrays/map/vtable/operations.rs b/vortex-array/src/arrays/map/vtable/operations.rs index d6e8fe87f12..bb7be8274d0 100644 --- a/vortex-array/src/arrays/map/vtable/operations.rs +++ b/vortex-array/src/arrays/map/vtable/operations.rs @@ -13,6 +13,8 @@ use crate::arrays::struct_::StructArrayExt; use crate::scalar::Scalar; impl OperationsVTable for Map { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Map>, index: usize, diff --git a/vortex-array/src/arrays/masked/vtable/operations.rs b/vortex-array/src/arrays/masked/vtable/operations.rs index c82d0bf03ed..673738520b2 100644 --- a/vortex-array/src/arrays/masked/vtable/operations.rs +++ b/vortex-array/src/arrays/masked/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::arrays::masked::MaskedArraySlotsExt; use crate::scalar::Scalar; impl OperationsVTable for Masked { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Masked>, index: usize, diff --git a/vortex-array/src/arrays/null/mod.rs b/vortex-array/src/arrays/null/mod.rs index 8479b7137cc..057188a2e16 100644 --- a/vortex-array/src/arrays/null/mod.rs +++ b/vortex-array/src/arrays/null/mod.rs @@ -175,6 +175,8 @@ impl Array { } impl OperationsVTable for Null { + type ProbeState<'a> = (); + fn scalar_at( _array: ArrayView<'_, Null>, _index: usize, diff --git a/vortex-array/src/arrays/patched/vtable/operations.rs b/vortex-array/src/arrays/patched/vtable/operations.rs index 51dd1fc9e3c..2135943164e 100644 --- a/vortex-array/src/arrays/patched/vtable/operations.rs +++ b/vortex-array/src/arrays/patched/vtable/operations.rs @@ -14,6 +14,8 @@ use crate::optimizer::ArrayOptimizer; use crate::scalar::Scalar; impl OperationsVTable for Patched { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Patched>, index: usize, diff --git a/vortex-array/src/arrays/piecewise_sequence/vtable.rs b/vortex-array/src/arrays/piecewise_sequence/vtable.rs index d68a00815f3..a40d8f1e8a1 100644 --- a/vortex-array/src/arrays/piecewise_sequence/vtable.rs +++ b/vortex-array/src/arrays/piecewise_sequence/vtable.rs @@ -145,6 +145,8 @@ impl VTable for PiecewiseSequence { } impl OperationsVTable for PiecewiseSequence { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, PiecewiseSequence>, index: usize, diff --git a/vortex-array/src/arrays/primitive/vtable/operations.rs b/vortex-array/src/arrays/primitive/vtable/operations.rs index ddeaa386485..7cc7e5a47a6 100644 --- a/vortex-array/src/arrays/primitive/vtable/operations.rs +++ b/vortex-array/src/arrays/primitive/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::match_each_native_ptype; use crate::scalar::Scalar; impl OperationsVTable for Primitive { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Primitive>, index: usize, diff --git a/vortex-array/src/arrays/scalar_fn/vtable/operations.rs b/vortex-array/src/arrays/scalar_fn/vtable/operations.rs index 40d75906356..75a2875cb82 100644 --- a/vortex-array/src/arrays/scalar_fn/vtable/operations.rs +++ b/vortex-array/src/arrays/scalar_fn/vtable/operations.rs @@ -15,6 +15,8 @@ use crate::scalar::Scalar; use crate::scalar_fn::VecExecutionArgs; impl OperationsVTable for ScalarFn { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, ScalarFn>, index: usize, diff --git a/vortex-array/src/arrays/shared/vtable.rs b/vortex-array/src/arrays/shared/vtable.rs index 758d091b557..9d097d7914f 100644 --- a/vortex-array/src/arrays/shared/vtable.rs +++ b/vortex-array/src/arrays/shared/vtable.rs @@ -125,6 +125,8 @@ impl VTable for Shared { } } impl OperationsVTable for Shared { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Shared>, index: usize, diff --git a/vortex-array/src/arrays/slice/vtable.rs b/vortex-array/src/arrays/slice/vtable.rs index c88c28a9a4d..8aa7d65f21e 100644 --- a/vortex-array/src/arrays/slice/vtable.rs +++ b/vortex-array/src/arrays/slice/vtable.rs @@ -169,6 +169,8 @@ impl VTable for Slice { } } impl OperationsVTable for Slice { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Slice>, index: usize, diff --git a/vortex-array/src/arrays/struct_/vtable/operations.rs b/vortex-array/src/arrays/struct_/vtable/operations.rs index b491e231520..35b70fbeef5 100644 --- a/vortex-array/src/arrays/struct_/vtable/operations.rs +++ b/vortex-array/src/arrays/struct_/vtable/operations.rs @@ -12,6 +12,8 @@ use crate::scalar::Scalar; use crate::scalar::ScalarValue; impl OperationsVTable for Struct { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Struct>, index: usize, diff --git a/vortex-array/src/arrays/union/vtable/operations.rs b/vortex-array/src/arrays/union/vtable/operations.rs index 39ba95ed4ee..2da1e623f90 100644 --- a/vortex-array/src/arrays/union/vtable/operations.rs +++ b/vortex-array/src/arrays/union/vtable/operations.rs @@ -14,6 +14,8 @@ use crate::arrays::union::UnionArraySlotsExt; use crate::scalar::Scalar; impl OperationsVTable for Union { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Union>, index: usize, diff --git a/vortex-array/src/arrays/varbin/vtable/operations.rs b/vortex-array/src/arrays/varbin/vtable/operations.rs index e11043e605a..4f2ada9c948 100644 --- a/vortex-array/src/arrays/varbin/vtable/operations.rs +++ b/vortex-array/src/arrays/varbin/vtable/operations.rs @@ -12,6 +12,8 @@ use crate::arrays::varbin::varbin_scalar; use crate::scalar::Scalar; impl OperationsVTable for VarBin { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, VarBin>, index: usize, diff --git a/vortex-array/src/arrays/varbinview/vtable/operations.rs b/vortex-array/src/arrays/varbinview/vtable/operations.rs index 1a1f20a0dbe..1e48be20c9f 100644 --- a/vortex-array/src/arrays/varbinview/vtable/operations.rs +++ b/vortex-array/src/arrays/varbinview/vtable/operations.rs @@ -11,6 +11,8 @@ use crate::arrays::varbin::varbin_scalar; use crate::scalar::Scalar; impl OperationsVTable for VarBinView { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, VarBinView>, index: usize, diff --git a/vortex-array/src/arrays/variant/vtable/operations.rs b/vortex-array/src/arrays/variant/vtable/operations.rs index c7f7d36fd97..eabf6ba842b 100644 --- a/vortex-array/src/arrays/variant/vtable/operations.rs +++ b/vortex-array/src/arrays/variant/vtable/operations.rs @@ -12,6 +12,8 @@ use crate::arrays::variant::VariantArraySlotsExt; use crate::scalar::Scalar; impl OperationsVTable for Variant { + type ProbeState<'a> = (); + fn scalar_at( array: ArrayView<'_, Variant>, index: usize, diff --git a/vortex-python/src/arrays/py/vtable.rs b/vortex-python/src/arrays/py/vtable.rs index 5ec749d3a5f..edb9156ccf0 100644 --- a/vortex-python/src/arrays/py/vtable.rs +++ b/vortex-python/src/arrays/py/vtable.rs @@ -122,6 +122,8 @@ impl VTable for PythonVTable { } impl OperationsVTable for PythonVTable { + type ProbeState<'a> = (); + fn scalar_at( _array: ArrayView<'_, PythonVTable>, _index: usize,