Cast Functions
Cast functions let your extension define how DuckDB converts values from one type to
another. Once registered, both explicit CAST(x AS T) syntax and (optionally) implicit
coercions will use your callback.
When to use cast functions
- Your extension introduces a new logical type and needs
CASTto/from standard types. - You want to override DuckDB's built-in cast behaviour for a specific type pair.
- You need to control implicit cast priority relative to other registered casts.
Registering a cast
#![allow(unused)] fn main() { use quack_rs::cast::{CastFunctionBuilder, CastFunctionInfo, CastMode}; use quack_rs::types::TypeId; use quack_rs::vector::{VectorReader, VectorWriter}; use libduckdb_sys::{duckdb_function_info, duckdb_vector, idx_t}; unsafe extern "C" fn varchar_to_int( info: duckdb_function_info, count: idx_t, input: duckdb_vector, output: duckdb_vector, ) -> bool { let cast_info = unsafe { CastFunctionInfo::new(info) }; let reader = unsafe { VectorReader::from_vector(input, count as usize) }; let mut writer = unsafe { VectorWriter::new(output) }; for row in 0..count as usize { if !unsafe { reader.is_valid(row) } { unsafe { writer.set_null(row) }; continue; } let s = unsafe { reader.read_str(row) }; match s.parse::<i32>() { Ok(v) => unsafe { writer.write_i32(row, v) }, Err(e) => { let msg = format!("cannot cast {:?} to INTEGER: {e}", s); if cast_info.cast_mode() == CastMode::Try { // TRY_CAST: write NULL and record a per-row error unsafe { cast_info.set_row_error(&msg, row as idx_t, output) }; unsafe { writer.set_null(row) }; } else { // Regular CAST: abort the whole query unsafe { cast_info.set_error(&msg) }; return false; } } } } true } fn register(con: libduckdb_sys::duckdb_connection) -> Result<(), quack_rs::error::ExtensionError> { unsafe { CastFunctionBuilder::new(TypeId::Varchar, TypeId::Integer) .function(varchar_to_int) .register(con) } } }
Implicit casts
Provide an implicit_cost to allow DuckDB to use the cast automatically in
expressions where the types do not match:
#![allow(unused)] fn main() { use quack_rs::cast::CastFunctionBuilder; use quack_rs::types::TypeId; use libduckdb_sys::{duckdb_function_info, duckdb_vector, idx_t}; unsafe extern "C" fn my_cast(_: duckdb_function_info, _: idx_t, _: duckdb_vector, _: duckdb_vector) -> bool { true } fn register(con: libduckdb_sys::duckdb_connection) -> Result<(), quack_rs::error::ExtensionError> { unsafe { CastFunctionBuilder::new(TypeId::Varchar, TypeId::Integer) .function(my_cast) .implicit_cost(100) // lower = higher priority .register(con) } } }
Extra info
Attach arbitrary data to a cast function using extra_info. This is useful for
parameterising the cast behaviour (e.g., a rounding mode):
#![allow(unused)] fn main() { use quack_rs::cast::CastFunctionBuilder; use quack_rs::types::TypeId; use libduckdb_sys::{duckdb_function_info, duckdb_vector, idx_t}; use std::os::raw::c_void; unsafe extern "C" fn my_cast(_: duckdb_function_info, _: idx_t, _: duckdb_vector, _: duckdb_vector) -> bool { true } unsafe extern "C" fn my_destroy(_: *mut c_void) {} fn register(con: libduckdb_sys::duckdb_connection) -> Result<(), quack_rs::error::ExtensionError> { let mode = Box::into_raw(Box::new("round".to_string())).cast::<c_void>(); unsafe { CastFunctionBuilder::new(TypeId::Double, TypeId::BigInt) .function(my_cast) .implicit_cost(100) .extra_info(mode, Some(my_destroy)) .register(con) } } }
Inside the cast callback, retrieve the extra info with
CastFunctionInfo::get_extra_info().
TRY_CAST vs CAST
Inside your callback, check [CastFunctionInfo::cast_mode()] to distinguish between
the two modes:
| Mode | User wrote | Expected behaviour on error |
|---|---|---|
CastMode::Normal | CAST(x AS T) | Call set_error and return false |
CastMode::Try | TRY_CAST(x AS T) | Call set_row_error, write NULL, continue |
Working example
The examples/hello-ext extension registers two cast functions:
CAST(VARCHAR AS INTEGER)/TRY_CAST(VARCHAR AS INTEGER)— basic castCAST(DOUBLE AS BIGINT)— withimplicit_cost(100)andextra_infofor rounding mode
See examples/hello-ext/src/lib.rs for complete, copy-paste-ready references.
Complex source and target types
For casts involving complex types like DECIMAL(18, 3) or LIST(VARCHAR), use
the new_logical constructor instead of new:
#![allow(unused)] fn main() { use quack_rs::cast::CastFunctionBuilder; use quack_rs::types::{LogicalType, TypeId}; use libduckdb_sys::{duckdb_function_info, duckdb_vector, idx_t}; unsafe extern "C" fn my_cast(_: duckdb_function_info, _: idx_t, _: duckdb_vector, _: duckdb_vector) -> bool { true } fn register(con: libduckdb_sys::duckdb_connection) -> Result<(), quack_rs::error::ExtensionError> { unsafe { CastFunctionBuilder::new_logical( LogicalType::list(TypeId::Varchar), // LIST(VARCHAR) source LogicalType::list(TypeId::Integer), // LIST(INTEGER) target ) .function(my_cast) .register(con) } } }
The source() and target() accessor methods return Option<TypeId> — they
return None when the type was set via new_logical (since a LogicalType
cannot always be expressed as a simple TypeId).
API reference
- [
CastFunctionBuilder][quack_rs::cast::CastFunctionBuilder] — the main builder - [
CastFunctionInfo][quack_rs::cast::CastFunctionInfo] — info handle inside callbacks - [
CastMode][quack_rs::cast::CastMode] —NormalvsTrycast mode