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