Last Updated: 3/19/2026
SQL User-Defined Functions (UDFs)
Feldera allows you to extend SQL with custom functions written in Rust. UDFs enable complex computations, custom business logic, and integration with external libraries.
SQL UDFs
Simple functions can be defined directly in SQL:
CREATE FUNCTION add_tax(price DECIMAL(10,2), rate DECIMAL(5,4))
RETURNS DECIMAL(10,2)
AS price * (1 + rate);Use the function in queries:
SELECT
product_name,
price,
add_tax(price, 0.0825) as price_with_tax
FROM products;Rust UDFs
For more complex logic, write UDFs in Rust. Rust UDFs are compiled into the pipeline binary for maximum performance.
Declaring a Rust UDF
Declare the function signature in SQL:
CREATE FUNCTION validate_email(email VARCHAR)
RETURNS BOOLEAN
LANGUAGE RUST;Implementing the Rust UDF
Provide the Rust implementation in the pipelineโs UDF Rust code:
use regex::Regex;
pub fn validate_email(email: &str) -> bool {
let re = Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap();
re.is_match(email)
}Adding Dependencies
If your UDF needs external crates, add them to the UDF TOML configuration:
[dependencies]
regex = "1.10"
serde_json = "1.0"Using the UDF
SELECT
email,
validate_email(email) as is_valid
FROM users
WHERE NOT validate_email(email);UDF Examples
String Processing
CREATE FUNCTION extract_domain(email VARCHAR)
RETURNS VARCHAR
LANGUAGE RUST;pub fn extract_domain(email: &str) -> String {
email.split('@')
.nth(1)
.unwrap_or("")
.to_string()
}JSON Parsing
CREATE FUNCTION parse_json_field(json_str VARCHAR, field VARCHAR)
RETURNS VARCHAR
LANGUAGE RUST;use serde_json::Value;
pub fn parse_json_field(json_str: &str, field: &str) -> Option<String> {
let v: Value = serde_json::from_str(json_str).ok()?;
v.get(field)?.as_str().map(|s| s.to_string())
}Numeric Computations
CREATE FUNCTION haversine_distance(
lat1 DOUBLE, lon1 DOUBLE,
lat2 DOUBLE, lon2 DOUBLE
) RETURNS DOUBLE
LANGUAGE RUST;pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let r = 6371.0; // Earth radius in km
let dlat = (lat2 - lat1).to_radians();
let dlon = (lon2 - lon1).to_radians();
let a = (dlat / 2.0).sin().powi(2)
+ lat1.to_radians().cos()
* lat2.to_radians().cos()
* (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
r * c
}Handling NULL Values
UDFs can handle nullable arguments using Option:
CREATE FUNCTION safe_divide(a INTEGER, b INTEGER)
RETURNS INTEGER
LANGUAGE RUST;pub fn safe_divide(a: i32, b: Option<i32>) -> Option<i32> {
match b {
Some(divisor) if divisor != 0 => Some(a / divisor),
_ => None
}
}Type Mapping
SQL types map to Rust types:
| SQL Type | Rust Type |
|---|---|
| INTEGER | i32 |
| BIGINT | i64 |
| REAL | F32 |
| DOUBLE | F64 |
| DECIMAL(p,s) | Decimal |
| VARCHAR | String or &str |
| BOOLEAN | bool |
| TIMESTAMP | Timestamp |
| DATE | Date |
| ARRAY<T> | Vec<T> |
Nullable types use Option<T>.
Best Practices
- Keep UDFs pure โ Avoid side effects and external state
- Handle errors gracefully โ Return
OptionorResultfor fallible operations - Optimize for performance โ UDFs are called for every row
- Document your UDFs โ Add comments explaining parameters and return values
- Test thoroughly โ UDFs are compiled into the pipeline and hard to debug
Limitations
- UDFs cannot access external databases or APIs
- UDFs cannot maintain state between invocations
- UDFs must be deterministic for correct incremental computation
- Async operations are not supported
Whatโs Next
- Sql Data Types: Understand type mappings
- Materialized Views: Use UDFs in view definitions
- Time Series Sql: Apply UDFs in time-based queries