Skip to main content

vortex_array/scalar_fn/
vtable.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::any::Any;
5use std::fmt;
6use std::fmt::Debug;
7use std::fmt::Display;
8use std::fmt::Formatter;
9use std::hash::Hash;
10use std::sync::Arc;
11
12use arcref::ArcRef;
13use vortex_error::VortexExpect;
14use vortex_error::VortexResult;
15use vortex_error::vortex_bail;
16use vortex_error::vortex_err;
17use vortex_session::VortexSession;
18
19use crate::ArrayRef;
20use crate::ExecutionCtx;
21use crate::dtype::DType;
22use crate::expr::BoundExpression;
23use crate::expr::Expression;
24use crate::expr::display::ExprDisplay;
25use crate::scalar_fn::ScalarFnId;
26use crate::scalar_fn::ScalarFnRef;
27use crate::scalar_fn::TypedScalarFnInstance;
28
29/// This trait defines the interface for scalar function vtables, including methods for
30/// serialization, deserialization, validation, child naming, return type computation,
31/// and evaluation.
32///
33/// This trait is non-object safe and allows the implementer to make use of associated types
34/// for improved type safety, while allowing Vortex to enforce runtime checks on the inputs and
35/// outputs of each function.
36///
37/// The [`ScalarFnVTable`] trait should be implemented for a struct that holds global data across
38/// all instances of the expression. In almost all cases, this struct will be an empty unit
39/// struct, since most expressions do not require any global state.
40pub trait ScalarFnVTable: 'static + Sized + Clone + Send + Sync {
41    /// Options for this expression.
42    type Options: 'static + Send + Sync + Clone + Debug + Display + PartialEq + Eq + Hash;
43
44    /// Returns the ID of the scalar function vtable.
45    fn id(&self) -> ScalarFnId;
46
47    /// Serialize the options for this expression.
48    ///
49    /// Should return `Ok(None)` if the expression is not serializable, and `Ok(vec![])` if it is
50    /// serializable but has no metadata.
51    fn serialize(&self, options: &Self::Options) -> VortexResult<Option<Vec<u8>>> {
52        _ = options;
53        Ok(None)
54    }
55
56    /// Deserialize the options of this expression.
57    fn deserialize(
58        &self,
59        _metadata: &[u8],
60        _session: &VortexSession,
61    ) -> VortexResult<Self::Options> {
62        vortex_bail!("Expression {} is not deserializable", self.id());
63    }
64
65    /// Returns the arity of this expression.
66    fn arity(&self, options: &Self::Options) -> Arity;
67
68    /// Returns the name of the nth child of the expr.
69    fn child_name(&self, options: &Self::Options, child_idx: usize) -> ChildName;
70
71    /// Format an expression tree in a human-readable SQL-style format.
72    ///
73    /// The expression may be either an [`Expression`] or a
74    /// [`bound expression`](crate::expr::BoundExpression).
75    fn fmt_sql(
76        &self,
77        options: &Self::Options,
78        expr: &dyn ExprDisplay,
79        f: &mut Formatter<'_>,
80    ) -> fmt::Result {
81        write!(f, "{}(", self.id())?;
82        let nchildren = expr.display_children_count();
83        for i in 0..nchildren {
84            Display::fmt(expr.display_child(i), f)?;
85            if i + 1 < nchildren {
86                write!(f, ", ")?;
87            }
88        }
89        let opts = format!("{}", options);
90        if !opts.is_empty() {
91            write!(f, ", opts={}", opts)?;
92        }
93        write!(f, ")")
94    }
95
96    /// Coerce the arguments of this function.
97    ///
98    /// This is optionally used by Vortex users when performing type coercion over a Vortex
99    /// expression. Note that direct Vortex query engine integrations (e.g. DuckDB, DataFusion,
100    /// etc.) do not perform type coercion and rely on the engine's own logical planner.
101    ///
102    /// Note that the default implementation simply returns the arguments without coercion, and it
103    /// is expected that the [`ScalarFnVTable::return_dtype`] call may still fail.
104    fn coerce_args(&self, options: &Self::Options, args: &[DType]) -> VortexResult<Vec<DType>> {
105        let _ = options;
106        Ok(args.to_vec())
107    }
108
109    /// Compute the return [`DType`] of the expression if evaluated over the given input types.
110    ///
111    /// # Preconditions
112    ///
113    /// The length of `args` must match the [`Arity`] of this function. Callers are responsible
114    /// for validating this (e.g., [`Expression::try_new`] checks arity at construction time).
115    /// Implementations may assume correct arity and will panic or return nonsensical results if
116    /// violated.
117    ///
118    /// [`Expression::try_new`]: crate::expr::Expression::try_new
119    fn return_dtype(&self, options: &Self::Options, args: &[DType]) -> VortexResult<DType>;
120
121    /// Execute the expression over the input arguments.
122    ///
123    /// Implementations are encouraged to check their inputs for constant arrays to perform
124    /// more optimized execution.
125    ///
126    /// If the input arguments cannot be directly used for execution (for example, an expression
127    /// may require canonical input arrays), then the implementation should perform a single
128    /// child execution and return a new [`crate::arrays::ScalarFnArray`] wrapping up the new child.
129    ///
130    /// This provides maximum opportunities for array-level optimizations using execute_parent
131    /// kernels.
132    fn execute(
133        &self,
134        options: &Self::Options,
135        args: &dyn ExecutionArgs,
136        ctx: &mut ExecutionCtx,
137    ) -> VortexResult<ArrayRef>;
138
139    /// Implement an abstract reduction rule over a tree of scalar functions.
140    ///
141    /// The [`ReduceNode`] can be used to traverse children, inspect their types, and
142    /// construct the result expression.
143    ///
144    /// Return `Ok(None)` if no reduction is possible.
145    fn reduce(
146        &self,
147        options: &Self::Options,
148        node: &dyn ReduceNode,
149        ctx: &dyn ReduceCtx,
150    ) -> VortexResult<Option<ReduceNodeRef>> {
151        _ = options;
152        _ = node;
153        _ = ctx;
154        Ok(None)
155    }
156
157    /// Simplify the expression if possible.
158    fn simplify(
159        &self,
160        options: &Self::Options,
161        expr: &Expression,
162        ctx: &dyn SimplifyCtx,
163    ) -> VortexResult<Option<Expression>> {
164        _ = options;
165        _ = expr;
166        _ = ctx;
167        Ok(None)
168    }
169
170    /// Simplify the expression if possible, without type information.
171    fn simplify_untyped(
172        &self,
173        options: &Self::Options,
174        expr: &Expression,
175    ) -> VortexResult<Option<Expression>> {
176        _ = options;
177        _ = expr;
178        Ok(None)
179    }
180
181    /// Returns an expression that evaluates to the validity of the result of this expression.
182    ///
183    /// If a validity expression cannot be constructed, returns `None` and the expression will
184    /// be evaluated as normal before extracting the validity mask from the result.
185    ///
186    /// This is essentially a specialized form of a `reduce_parent`
187    fn validity(
188        &self,
189        options: &Self::Options,
190        expression: &Expression,
191    ) -> VortexResult<Option<Expression>> {
192        _ = (options, expression);
193        Ok(None)
194    }
195
196    /// Returns whether this scalar function is strict.
197    ///
198    /// A strict function returns null for a row when any argument is null for that row. This
199    /// matches [PostgreSQL's `STRICT` convention](https://www.postgresql.org/docs/current/sql-createfunction.html)
200    /// for null propagation.
201    ///
202    /// Return `true` only when this holds for every argument. `add` is strict, but Kleene `AND`
203    /// is not because `false AND null` returns `false`. `is_null` is also not strict.
204    ///
205    /// Strictness does not require valid inputs to produce a valid output. For example,
206    /// [`crate::expr::list_sum`] returns null for a valid empty list. Implement
207    /// [`ScalarFnVTable::validity`] only when the output validity can be derived without
208    /// evaluation.
209    ///
210    /// [`ScalarFnVTable::return_dtype`] must return a nullable output dtype when any input dtype is
211    /// nullable. A `cast` that forces a non-nullable output dtype is therefore not strict.
212    ///
213    /// This property applies only to the scalar function, not its child expressions. Nullary
214    /// functions are vacuously strict. The default is conservatively `false`.
215    fn is_strict(&self, options: &Self::Options) -> bool {
216        _ = options;
217        false
218    }
219
220    /// Returns whether this scalar function can raise a semantic error.
221    ///
222    /// Return `true` if a well-typed call can error because of its values. `checked_add` is
223    /// fallible on integer overflow, and integer division is fallible when its divisor is zero.
224    /// A null result is not an error: [`crate::expr::list_sum`] is infallible for an empty list.
225    ///
226    /// Exclude incidental execution errors, such as canonicalization failures, allocation errors,
227    /// and encoding mismatches. They are not part of the function's semantics.
228    ///
229    /// Returning `false` permits optimizations that evaluate the function over values that no input
230    /// row references. Dictionary push-down, for example, evaluates every dictionary value, so a
231    /// fallible function could error on a value that row-wise evaluation would never reach.
232    ///
233    /// This applies only to the scalar function, not its child expressions, and only to inputs
234    /// accepted by [`ScalarFnVTable::return_dtype`]. The default is conservatively `true`.
235    fn is_fallible(&self, options: &Self::Options) -> bool {
236        _ = options;
237        true
238    }
239}
240
241/// Arguments for reduction rules.
242pub trait ReduceCtx {
243    /// Create a new reduction node from the given scalar function and children.
244    fn new_node(
245        &self,
246        scalar_fn: ScalarFnRef,
247        children: &[ReduceNodeRef],
248    ) -> VortexResult<ReduceNodeRef>;
249}
250
251pub type ReduceNodeRef = Arc<dyn ReduceNode>;
252
253/// A node used for implementing abstract reduction rules.
254pub trait ReduceNode {
255    /// Downcast to Any.
256    fn as_any(&self) -> &dyn Any;
257
258    /// Return the data type of this node.
259    fn node_dtype(&self) -> VortexResult<DType>;
260
261    /// Return this node's scalar function if it is indeed a scalar fn.
262    fn scalar_fn(&self) -> Option<&ScalarFnRef>;
263
264    /// Descend to the child of this handle.
265    fn child(&self, idx: usize) -> ReduceNodeRef;
266
267    /// Returns the number of children of this node.
268    fn child_count(&self) -> usize;
269}
270
271/// The arity (number of arguments) of a function.
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273pub enum Arity {
274    Exact(usize),
275    Variadic { min: usize, max: Option<usize> },
276}
277
278impl Display for Arity {
279    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
280        match self {
281            Arity::Exact(n) => write!(f, "{}", n),
282            Arity::Variadic { min, max } => match max {
283                Some(max) if min == max => write!(f, "{}", min),
284                Some(max) => write!(f, "{}..{}", min, max),
285                None => write!(f, "{}+", min),
286            },
287        }
288    }
289}
290
291impl Arity {
292    /// Whether the given argument count matches this arity.
293    pub fn matches(&self, arg_count: usize) -> bool {
294        match self {
295            Arity::Exact(m) => *m == arg_count,
296            Arity::Variadic { min, max } => {
297                if arg_count < *min {
298                    return false;
299                }
300                if let Some(max) = max
301                    && arg_count > *max
302                {
303                    return false;
304                }
305                true
306            }
307        }
308    }
309}
310
311/// Context for simplification.
312///
313/// Used to lazily compute input data types where simplification requires them.
314pub trait SimplifyCtx {
315    /// Get the data type of the given expression.
316    fn return_dtype(&self, expr: &Expression) -> VortexResult<DType>;
317}
318
319/// Arguments for expression execution.
320pub trait ExecutionArgs {
321    /// Returns the input array at the given index.
322    fn get(&self, index: usize) -> VortexResult<ArrayRef>;
323
324    /// Returns the number of inputs.
325    fn num_inputs(&self) -> usize;
326
327    /// Returns the row count of the execution scope.
328    fn row_count(&self) -> usize;
329}
330
331/// A concrete [`ExecutionArgs`] backed by a `Vec<ArrayRef>`.
332pub struct VecExecutionArgs {
333    inputs: Vec<ArrayRef>,
334    row_count: usize,
335}
336
337impl VecExecutionArgs {
338    /// Create a new `VecExecutionArgs`.
339    pub fn new(inputs: Vec<ArrayRef>, row_count: usize) -> Self {
340        Self { inputs, row_count }
341    }
342}
343
344impl ExecutionArgs for VecExecutionArgs {
345    fn get(&self, index: usize) -> VortexResult<ArrayRef> {
346        self.inputs.get(index).cloned().ok_or_else(|| {
347            vortex_err!(
348                "Input index {} out of bounds (num_inputs={})",
349                index,
350                self.inputs.len()
351            )
352        })
353    }
354
355    fn num_inputs(&self) -> usize {
356        self.inputs.len()
357    }
358
359    fn row_count(&self) -> usize {
360        self.row_count
361    }
362}
363
364#[derive(Clone, Debug, PartialEq, Eq, Hash)]
365pub struct EmptyOptions;
366impl Display for EmptyOptions {
367    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
368        write!(f, "")
369    }
370}
371
372/// Factory functions for vtables.
373pub trait ScalarFnVTableExt: ScalarFnVTable {
374    /// Bind this vtable with the given options into a [`ScalarFnRef`].
375    fn bind(&self, options: Self::Options) -> ScalarFnRef {
376        TypedScalarFnInstance::new(self.clone(), options).erased()
377    }
378
379    /// Create a new expression with this vtable and the given options and children.
380    fn new_expr(
381        &self,
382        options: Self::Options,
383        children: impl IntoIterator<Item = Expression>,
384    ) -> Expression {
385        Self::try_new_expr(self, options, children).vortex_expect("Failed to create expression")
386    }
387
388    /// Try to create a new expression with this vtable and the given options and children.
389    fn try_new_expr(
390        &self,
391        options: Self::Options,
392        children: impl IntoIterator<Item = Expression>,
393    ) -> VortexResult<Expression> {
394        Expression::try_new(self.bind(options), children)
395    }
396
397    /// Try to create a bound expression with this vtable, the given options, and bound children.
398    fn try_new_bound_expr(
399        &self,
400        options: Self::Options,
401        children: impl IntoIterator<Item = BoundExpression>,
402    ) -> VortexResult<BoundExpression> {
403        BoundExpression::try_new(self.bind(options), children)
404    }
405}
406impl<V: ScalarFnVTable> ScalarFnVTableExt for V {}
407
408/// A reference to the name of a child expression.
409pub type ChildName = ArcRef<str>;