Internal site. Jolli authentication required to view.
Skip to Content

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 TypeRust Type
INTEGERi32
BIGINTi64
REALF32
DOUBLEF64
DECIMAL(p,s)Decimal
VARCHARString or &str
BOOLEANbool
TIMESTAMPTimestamp
DATEDate
ARRAY<T>Vec<T>

Nullable types use Option<T>.

Best Practices

  1. Keep UDFs pure โ€” Avoid side effects and external state
  2. Handle errors gracefully โ€” Return Option or Result for fallible operations
  3. Optimize for performance โ€” UDFs are called for every row
  4. Document your UDFs โ€” Add comments explaining parameters and return values
  5. 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