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>;