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