Skip to main content

vortex_array/scalar_fn/unstable/row/
vtable.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Adapts [`RowFn`] implementations to the scalar-function interface.
5//!
6//! The blanket [`ScalarFnVTable`] implementation supplies common arity, validity, fallibility, and
7//! execution behavior. The visitor layer validates and executes the concrete signature selected by
8//! dispatch. [`row_fn_return_dtype`] and [`execute_rows`] expose the same paths to public vtables
9//! that delegate to a private row kernel.
10
11use vortex_error::VortexResult;
12use vortex_error::vortex_ensure_eq;
13use vortex_mask::MaskValuesRef;
14use vortex_session::VortexSession;
15
16use super::batch::BorrowedRowFnArgs;
17use super::batch::RowFnExecutionArgs;
18use super::batch::finalize_kernel_output;
19use super::row_fn::RowFn;
20use super::visitor::BatchPlanner;
21use super::visitor::ExecuteDenseWithRetry;
22use super::visitor::ExecuteFilteredRows;
23use super::visitor::ExecuteRows;
24use super::visitor::ExecuteValidRows;
25use crate::ArrayRef;
26use crate::ExecutionCtx;
27use crate::dtype::DType;
28use crate::expr::Expression;
29use crate::expr::union_child_validities;
30use crate::scalar_fn::Arity;
31use crate::scalar_fn::ChildName;
32use crate::scalar_fn::ExecutionArgs;
33use crate::scalar_fn::ScalarFnId;
34use crate::scalar_fn::ScalarFnVTable;
35use crate::scalar_fn::unstable::row::execute::DenseAttempt;
36
37impl<F: RowFn> ScalarFnVTable for F {
38    type Options = F::Options;
39
40    fn id(&self) -> ScalarFnId {
41        RowFn::id(self)
42    }
43
44    fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
45        RowFn::serialize(self, options)
46    }
47
48    fn deserialize(&self, metadata: &[u8], session: &VortexSession) -> VortexResult<Self::Options> {
49        RowFn::deserialize(self, metadata, session)
50    }
51
52    fn arity(&self, _options: &Self::Options) -> Arity {
53        Arity::Exact(F::ARG_NAMES.len())
54    }
55
56    fn child_name(&self, _options: &Self::Options, child_index: usize) -> ChildName {
57        ChildName::from(F::ARG_NAMES[child_index])
58    }
59
60    fn return_dtype(&self, options: &Self::Options, args: &[DType]) -> VortexResult<DType> {
61        row_fn_return_dtype(self, options, args)
62    }
63
64    fn execute(
65        &self,
66        options: &Self::Options,
67        args: &dyn ExecutionArgs,
68        ctx: &mut ExecutionCtx,
69    ) -> VortexResult<ArrayRef> {
70        execute_rows(self, options, args, ctx)
71    }
72
73    fn validity(
74        &self,
75        _options: &Self::Options,
76        expression: &Expression,
77    ) -> VortexResult<Option<Expression>> {
78        union_child_validities(expression)
79    }
80
81    // `RowFn` is stricter than `ScalarFnVTable::is_strict`: its kernel cannot produce null from
82    // valid inputs, so batch execution derives output validity only from input validity.
83    fn is_strict(&self, _options: &Self::Options) -> bool {
84        true
85    }
86
87    fn is_infallible(&self, _options: &Self::Options) -> bool {
88        F::INFALLIBLE
89    }
90}
91
92/// Compute the return dtype of a [`RowFn`] kernel without invoking its blanket vtable.
93pub fn row_fn_return_dtype<F: RowFn>(
94    function: &F,
95    options: &F::Options,
96    args: &[DType],
97) -> VortexResult<DType> {
98    ensure_arity(function, args.len())?;
99
100    let plan = function.dispatch(options, args, BatchPlanner::<F>::new(args))?;
101
102    Ok(plan.result_dtype(args))
103}
104
105/// Execute a [`RowFn`] without using its blanket [`ScalarFnVTable`] implementation.
106///
107/// A type cannot implement both [`RowFn`] and [`ScalarFnVTable`] because every `RowFn` receives the
108/// standard vtable automatically. Existing vtables can keep their custom hooks on one type and
109/// delegate row execution to a private `RowFn` kernel through this function.
110///
111/// Nullary functions execute for `args.row_count()` rows without batch validity handling because
112/// they have no input validity to propagate.
113pub fn execute_rows<F: RowFn>(
114    function: &F,
115    options: &F::Options,
116    args: &dyn ExecutionArgs,
117    ctx: &mut ExecutionCtx,
118) -> VortexResult<ArrayRef> {
119    ensure_arity(function, args.num_inputs())?;
120
121    if args.num_inputs() == 0 {
122        return execute_nullary_rows(function, options, args.row_count(), ctx);
123    }
124
125    let batch = prepare_batch(function, options, args)?;
126    batch.execute(
127        |args, ctx| execute_row_kernel(function, options, args, ctx),
128        |args, ctx| execute_dense_attempt(function, options, args, ctx),
129        |args, valid, ctx| try_execute_valid_rows(function, options, args, valid, ctx),
130        |args, valid, ctx| execute_filtered_rows(function, options, args, valid, ctx),
131        ctx,
132    )
133}
134
135/// Execute a nullary kernel without batch validity or constant handling.
136fn execute_nullary_rows<F: RowFn>(
137    function: &F,
138    options: &F::Options,
139    row_count: usize,
140    ctx: &mut ExecutionCtx,
141) -> VortexResult<ArrayRef> {
142    let plan = function.dispatch(options, &[], BatchPlanner::<F>::new(&[]))?;
143    let result_dtype = plan.result_dtype(&[]);
144    let args = BorrowedRowFnArgs::new(&[], row_count, &[], &plan);
145
146    // A nullary function has no input validity to propagate, so its kernel output is the finished
147    // column and this path labels it directly.
148    let values = plan.relabel_output(execute_row_kernel(function, options, args, ctx)?)?;
149
150    finalize_kernel_output(RowFn::id(function), &result_dtype, row_count, values, ctx)
151}
152
153fn ensure_arity<F: RowFn>(function: &F, actual: usize) -> VortexResult<()> {
154    let expected = F::ARG_NAMES.len();
155    vortex_ensure_eq!(
156        actual,
157        expected,
158        "row function {} requires arity {expected}, got {actual}",
159        RowFn::id(function),
160    );
161
162    Ok(())
163}
164
165fn execute_row_kernel<F: RowFn>(
166    function: &F,
167    options: &F::Options,
168    args: BorrowedRowFnArgs<'_>,
169    ctx: &mut ExecutionCtx,
170) -> VortexResult<ArrayRef> {
171    function.dispatch(
172        options,
173        args.dtypes(),
174        ExecuteRows::<F>::new(&args, args.dtypes(), args.plan(), ctx),
175    )
176}
177
178fn execute_dense_attempt<F: RowFn>(
179    function: &F,
180    options: &F::Options,
181    args: BorrowedRowFnArgs<'_>,
182    ctx: &mut ExecutionCtx,
183) -> VortexResult<DenseAttempt> {
184    function.dispatch(
185        options,
186        args.dtypes(),
187        ExecuteDenseWithRetry::<F>::new(&args, ctx),
188    )
189}
190
191fn try_execute_valid_rows<F: RowFn>(
192    function: &F,
193    options: &F::Options,
194    args: BorrowedRowFnArgs<'_>,
195    valid: MaskValuesRef,
196    ctx: &mut ExecutionCtx,
197) -> VortexResult<Option<ArrayRef>> {
198    function.dispatch(
199        options,
200        args.dtypes(),
201        ExecuteValidRows::<F>::new(&args, args.dtypes(), args.plan(), valid, ctx),
202    )
203}
204
205/// Execute valid rows over `args` filtered to the valid row domain of `valid`.
206fn execute_filtered_rows<F: RowFn>(
207    function: &F,
208    options: &F::Options,
209    args: BorrowedRowFnArgs<'_>,
210    valid: MaskValuesRef,
211    ctx: &mut ExecutionCtx,
212) -> VortexResult<ArrayRef> {
213    function.dispatch(
214        options,
215        args.dtypes(),
216        ExecuteFilteredRows::<F>::new(&args, args.dtypes(), args.plan(), valid, ctx),
217    )
218}
219
220fn prepare_batch<F: RowFn>(
221    function: &F,
222    options: &F::Options,
223    args: &dyn ExecutionArgs,
224) -> VortexResult<RowFnExecutionArgs> {
225    RowFnExecutionArgs::new(RowFn::id(function), args, |arg_dtypes| {
226        function.dispatch(options, arg_dtypes, BatchPlanner::<F>::new(arg_dtypes))
227    })
228}
229
230#[cfg(test)]
231mod tests {
232    use std::sync::Arc;
233    use std::sync::atomic::AtomicUsize;
234    use std::sync::atomic::Ordering;
235
236    use rstest::rstest;
237    use vortex_error::VortexError;
238    use vortex_error::VortexResult;
239    use vortex_session::registry::CachedId;
240
241    use super::execute_rows;
242    use super::row_fn_return_dtype;
243    use crate::IntoArray;
244    use crate::VortexSessionExecute;
245    use crate::array_session;
246    use crate::arrays::PrimitiveArray;
247    use crate::assert_arrays_eq;
248    use crate::dtype::DType;
249    use crate::scalar_fn::EmptyOptions;
250    use crate::scalar_fn::ScalarFnId;
251    use crate::scalar_fn::VecExecutionArgs;
252    use crate::scalar_fn::unstable::row::RowFn;
253    use crate::scalar_fn::unstable::row::RowVisitor;
254    use crate::validity::Validity;
255
256    #[derive(Clone)]
257    struct IndexingRowFn;
258
259    #[derive(Clone)]
260    struct NullarySeven;
261
262    #[derive(Clone)]
263    struct ChangingDispatchRowFn {
264        dispatches: Arc<AtomicUsize>,
265        change: DispatchChange,
266    }
267
268    #[derive(Clone, Copy)]
269    enum DispatchChange {
270        Policy,
271        Element,
272    }
273
274    impl RowFn for NullarySeven {
275        type Options = EmptyOptions;
276
277        const ARG_NAMES: &'static [&'static str] = &[];
278        const INFALLIBLE: bool = true;
279
280        fn id(&self) -> ScalarFnId {
281            static ID: CachedId = CachedId::new("test.nullary_seven");
282            *ID
283        }
284
285        fn dispatch<V: RowVisitor>(
286            &self,
287            _options: &Self::Options,
288            _args: &[DType],
289            visitor: V,
290        ) -> VortexResult<V::VisitResult> {
291            visitor.visit::<(), i64>(|()| 7)
292        }
293    }
294
295    impl RowFn for IndexingRowFn {
296        type Options = EmptyOptions;
297
298        const ARG_NAMES: &'static [&'static str] = &["value"];
299
300        const INFALLIBLE: bool = true;
301
302        fn id(&self) -> ScalarFnId {
303            static ID: CachedId = CachedId::new("test.indexing_row_fn");
304            *ID
305        }
306
307        fn dispatch<V: RowVisitor>(
308            &self,
309            _options: &Self::Options,
310            args: &[DType],
311            visitor: V,
312        ) -> VortexResult<V::VisitResult> {
313            _ = &args[0];
314
315            visitor.visit::<(i64,), i64>(|(value,)| value)
316        }
317    }
318
319    impl RowFn for ChangingDispatchRowFn {
320        type Options = EmptyOptions;
321
322        const ARG_NAMES: &'static [&'static str] = &["value"];
323        const INFALLIBLE: bool = false;
324
325        fn id(&self) -> ScalarFnId {
326            static ID: CachedId = CachedId::new("test.changing_dispatch_row_fn");
327            *ID
328        }
329
330        fn dispatch<V: RowVisitor>(
331            &self,
332            _options: &Self::Options,
333            _args: &[DType],
334            visitor: V,
335        ) -> VortexResult<V::VisitResult> {
336            if self.dispatches.fetch_add(1, Ordering::Relaxed) == 0 {
337                visitor.visit::<(i64,), i64>(|(value,)| value)
338            } else {
339                match self.change {
340                    DispatchChange::Policy => visitor
341                        .visit_deferred::<(i64,), i64, bool>(|(value,)| (value, false), |_| Ok(())),
342                    DispatchChange::Element => visitor.visit::<(u64,), u64>(|(value,)| value),
343                }
344            }
345        }
346    }
347
348    #[test]
349    fn test_return_dtype_rejects_wrong_arity_before_dispatch() {
350        let error = row_fn_return_dtype(&IndexingRowFn, &EmptyOptions, &[])
351            .expect_err("wrong arity must fail before dispatch");
352
353        assert_arity_error(error);
354    }
355
356    #[test]
357    fn test_execute_rejects_wrong_arity_before_dispatch() {
358        let args = VecExecutionArgs::new(vec![], 0);
359        let mut ctx = array_session().create_execution_ctx();
360        let error = execute_rows(&IndexingRowFn, &EmptyOptions, &args, &mut ctx)
361            .expect_err("wrong arity must fail before dispatch");
362
363        assert_arity_error(error);
364    }
365
366    #[rstest]
367    #[case::empty(0)]
368    #[case::nonempty(3)]
369    fn test_execute_nullary_rows(#[case] row_count: usize) -> VortexResult<()> {
370        let args = VecExecutionArgs::new(vec![], row_count);
371        let mut ctx = array_session().create_execution_ctx();
372
373        let actual = execute_rows(&NullarySeven, &EmptyOptions, &args, &mut ctx)?;
374        let expected = PrimitiveArray::from_iter(vec![7_i64; row_count]).into_array();
375
376        assert_arrays_eq!(&actual, &expected, &mut ctx);
377        Ok(())
378    }
379
380    #[test]
381    fn test_execute_rejects_dispatch_that_changes_after_planning() -> VortexResult<()> {
382        let function = ChangingDispatchRowFn {
383            dispatches: Arc::new(AtomicUsize::new(0)),
384            change: DispatchChange::Policy,
385        };
386        let input = PrimitiveArray::new(vec![1_i64, 2], Validity::NonNullable).into_array();
387        let args = VecExecutionArgs::new(vec![input], 2);
388        let mut ctx = array_session().create_execution_ctx();
389
390        let error = match execute_rows(&function, &EmptyOptions, &args, &mut ctx) {
391            Err(error) => error,
392            Ok(_) => vortex_error::vortex_bail!("dispatch must not change after planning"),
393        };
394        let message = error.to_string();
395
396        assert!(
397            message.contains("row dispatch must select the planned nullable execution policy"),
398            "unexpected error: {error}",
399        );
400        assert!(
401            message.contains("planned Dense, got DenseWithRetry"),
402            "unexpected error: {error}",
403        );
404        Ok(())
405    }
406
407    #[test]
408    fn test_execute_revalidates_element_types_after_planning() -> VortexResult<()> {
409        let function = ChangingDispatchRowFn {
410            dispatches: Arc::new(AtomicUsize::new(0)),
411            change: DispatchChange::Element,
412        };
413        let input = PrimitiveArray::new(vec![1_i64, 2], Validity::NonNullable).into_array();
414        let args = VecExecutionArgs::new(vec![input], 2);
415        let mut ctx = array_session().create_execution_ctx();
416
417        let error = match execute_rows(&function, &EmptyOptions, &args, &mut ctx) {
418            Err(error) => error,
419            Ok(_) => vortex_error::vortex_bail!("dispatch must preserve its planned element types"),
420        };
421
422        assert!(
423            error.to_string().contains("expected a u64 column"),
424            "unexpected error: {error}",
425        );
426        Ok(())
427    }
428
429    #[track_caller]
430    fn assert_arity_error(error: VortexError) {
431        assert!(
432            error.to_string().contains("requires arity 1, got 0"),
433            "unexpected error: {error}",
434        );
435    }
436}