doc/developer/sqlfunc.md
#[sqlfunc] macroThe #[sqlfunc] attribute macro generates boilerplate for SQL scalar functions.
It creates a unit struct, a trait implementation (EagerUnaryFunc, EagerBinaryFunc, or EagerVariadicFunc), and a Display impl from a plain Rust function.
The macro lives in src/expr-derive-impl/src/sqlfunc.rs.
The generated trait implementations live in src/expr/src/scalar/func/{unary,binary,variadic}.rs.
Annotate a function with #[sqlfunc]:
#[sqlfunc]
fn negate_int32(a: i32) -> i32 {
-a
}
This generates:
NegateInt32 (camel-cased from the function name).EagerUnaryFunc implementation that calls the function.Display implementation that prints "negate_int32".The macro determines function arity from parameter count and types, after excluding &self receivers and trailing &RowArena parameters:
| Effective params | Dispatches to | Notes |
|---|---|---|
| 0 | Error | Nullary functions are not supported. |
| 1 | EagerUnaryFunc | Does not support &RowArena. |
| 2 | EagerBinaryFunc | Supports &RowArena. |
| 3+ | EagerVariadicFunc | Supports &RowArena and &self. |
Exception: If any parameter uses Variadic<T> or OptionalArg<T>, the function is always treated as variadic, regardless of parameter count.
Pass modifiers as key-value pairs in the attribute:
#[sqlfunc(sqlname = "+", is_monotone = (true, true), is_infix_op = true)]
fn add_int16<'a>(a: Datum<'a>, b: Datum<'a>) -> Result<Datum<'a>, EvalError> {
// ...
}
sqlnameThe SQL-visible name of the function.
stringify!)propagates_nullsWhether a NULL input produces a NULL output, skipping the function body entirely.
When true, the evaluation layer short-circuits on NULL inputs and returns NULL without calling the function.
This also affects output_type: if propagates_nulls is true and any input column is nullable, the output column is marked nullable.
!Input::nullable().
If the input type accepts Option or Datum (which can represent NULL), it is nullable, so propagates_nulls defaults to false.
If the input type is a non-nullable type like &str or i32, nulls are propagated (the function never sees them).introduces_nullsWhether the function can produce NULL from non-NULL inputs. The optimizer uses this to reason about column nullability.
Output::nullable().
A function returning Option<T> or Datum introduces nulls.
A function returning String or i32 does not.output_type_expr (because the output type is not statically known).could_errorWhether the function can produce an error at runtime. The optimizer uses this to avoid certain rewrites that might change error behavior.
Output::fallible().
A function returning Result<T, EvalError> is fallible.
A function returning T directly is not.is_monotoneWhether the function is monotone (non-strict; either non-decreasing or non-increasing). Monotone functions map ranges to ranges: given a range of possible inputs, the range of possible outputs can be determined by mapping the endpoints.
(bool, bool) tuple for binary (one per argument)false for unary and variadic; (false, false) for binarypreserves_uniquenessWhether the function is injective: if f(x) = f(y) then x = y.
falseinverseThe inverse function, if it exists.
Option<crate::UnaryFunc>NonenegateThe logical negation of a comparison function.
For example, < negates to >=.
Option<crate::BinaryFunc>Noneis_infix_opWhether the function is an infix operator (e.g., +, =, AND).
Affects how the function is displayed in EXPLAIN output.
falseis_associativeWhether the function is associative: f(a, f(b, c)) = f(f(a, b), c).
falseoutput_typeAn explicit type path for computing the output column type.
When set, the macro generates output_type and introduces_nulls based on this type instead of inferring from the return type.
i16, String)Self::Output::as_column_type()output_type_exproutput_type_exprAn expression that computes the output column type at runtime.
Use this for functions whose output type depends on input types or struct fields (e.g., ArrayCreate where the element type is stored on the struct).
SqlColumnTypeintroduces_nulls (must be specified explicitly)output_typetestGenerate a snapshot test for the macro expansion.
boolfalseSnapshot files are stored in src/expr-derive-impl/src/snapshots/.
Update them with cargo insta accept after running cargo test -p mz-expr-derive-impl.
For variadic functions, the struct name can be specified as the first positional argument.
This is required when a &self receiver is present (the struct is defined externally):
#[sqlfunc(ArrayFill, sqlname = "array_fill")]
fn array_fill_variadic<'a>(&self, fill: Datum<'a>, dims: Datum<'a>, temp_storage: &'a RowArena) -> Result<Datum<'a>, EvalError> {
// ...
}
Without a &self receiver, the struct name defaults to the camel-cased function name.
It can still be overridden with the first positional argument:
#[sqlfunc(Replace, sqlname = "replace")]
fn replace(text: &str, from: &str, to: &str) -> Result<String, EvalError> {
// ...
}
&self receiverWhen a &self receiver is present, the macro assumes the struct is defined externally and generates:
impl StructName { fn ... } containing the function body.EagerVariadicFunc trait implementation that delegates to the method.Display implementation.Without &self, the macro generates the struct itself (with standard derives) in addition to the trait and display implementations.
Variadic<T> and OptionalArg<T>These wrapper types affect both arity detection and null handling.
Variadic<T> consumes all remaining arguments from the iterator.
It wraps a Vec<T> and is used for functions with a truly variable number of arguments:
#[sqlfunc(is_associative = true)]
fn concat(strs: Variadic<Option<&str>>) -> Result<String, EvalError> {
// strs is a Vec<Option<&str>>, one entry per SQL argument
}
OptionalArg<T> consumes one argument if present, or produces None if the iterator is exhausted.
It wraps an Option<T> and is used for functions with optional trailing arguments:
#[sqlfunc(sqlname = "lpad")]
fn pad_leading(string: &str, raw_len: i32, pad: OptionalArg<&str>) -> Result<String, EvalError> {
let pad = pad.unwrap_or(" ");
// ...
}
Both are defined in src/repr/src/scalar.rs.
&RowArenaA trailing &RowArena parameter gives the function access to temporary storage for allocating return values that borrow from the arena.
It is excluded from arity detection and from the generated Input type.
The arena is always passed to binary and variadic call implementations (the trait requires it); for functions that don't use it, the parameter is simply unused.
Unary functions do not support &RowArena.
For variadic functions, the generated Input type depends on parameter count:
Variadic<Option<&'a str>>).(&'a str, &'a str, &'a str)).The interplay between propagates_nulls, introduces_nulls, and input/output types determines the nullability of the output column.
The output_type method on each generated struct computes the output SqlColumnType as:
output.nullable = output.nullable || (propagates_nulls && any_input_nullable)
Where:
output.nullable comes from introduces_nulls (or is inferred from the output type).propagates_nulls indicates whether NULL passes through.any_input_nullable is true if any input column is nullable.The evaluation layer (LazyUnaryFunc, LazyBinaryFunc, LazyVariadicFunc) handles the actual null short-circuiting at runtime through the InputDatumType::try_from_result/try_from_iter methods.
If the input type does not accept NULL (is non-nullable), the evaluation layer returns NULL directly without calling the function.
#[sqlfunc(
sqlname = "uint2_to_real",
preserves_uniqueness = true,
inverse = to_unary!(super::CastFloat32ToUint16),
is_monotone = true
)]
fn cast_uint16_to_float32(a: u16) -> f32 {
f32::from(a)
}
#[sqlfunc(
is_monotone = (true, true),
output_type = i16,
is_infix_op = true,
sqlname = "+",
propagates_nulls = true,
)]
fn add_int16<'a>(a: Datum<'a>, b: Datum<'a>) -> Result<Datum<'a>, EvalError> {
a.unwrap_int16()
.checked_add(b.unwrap_int16())
.ok_or(EvalError::NumericFieldOverflow)
.map(Datum::from)
}
#[sqlfunc(sqlname = "datediff")]
fn date_diff_date(unit_str: &str, a: Date, b: Date) -> Result<i64, EvalError> {
// Three fixed parameters → variadic with tuple input (&str, Date, Date)
}
#[sqlfunc(is_associative = true)]
fn concat(strs: Variadic<Option<&str>>) -> Result<String, EvalError> {
// Variadic<T> detected → variadic dispatch regardless of parameter count
}
&self and dynamic output type#[sqlfunc(
ArrayCreate,
sqlname = "array_create",
output_type_expr = "SqlScalarType::Array(Box::new(self.elem_type.clone())).nullable(false)",
introduces_nulls = false
)]
fn array_create<'a>(&self, datums: Variadic<Datum<'a>>, temp_storage: &'a RowArena) -> Array<'a> {
// &self: struct defined externally with an elem_type field
// output_type_expr: output type depends on runtime struct fields
}