Cast Functions
A DuckDB cast function defines how values of one type are converted to another.
This page shows how to register one from a Rust extension with quack-rs'
CastFunctionBuilder. Once registered, CAST(x AS T) and TRY_CAST(x AS T) use
your callback, and so do implicit conversions if you give the cast an implicit cost.
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: record a per-row error, which also sets the row to NULL unsafe { cast_info.set_row_error(&msg, row as idx_t, output) }; } else { // Regular CAST: fail the whole query 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(). The pointee must be Send + Sync: DuckDB
passes the same pointer to the callback on every thread that runs the cast, and the
destructor runs on whichever thread releases it. CastFunctionBuilder is Send, so
an unregistered builder may also run the destructor on the thread that drops it.
If register returns an error — a missing callback or type, a bare composite
TypeId (use new_logical), a null connection, or a source or target type that is
or contains ANY/INVALID, which DuckDB refuses — the builder still owns the extra
info and runs its destructor exactly once. These cases are checked in Rust before
DuckDB is called, because duckdb_register_cast_function rejects them before
taking ownership of the pointer.
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 for each failed row, continue |
set_row_error records the message and sets that row of the output to NULL
(FlatVector::SetNull inside the C API); row must be less than the callback's
count — DuckDB does not check it in release builds.
The return value does nothing in
TRY_CASTmode. DuckDB discards it (execute_cast.cppcalls the cast and ignores the result), so returningfalsedoes not turn the chunk intoNULLs: every row you did not null keeps whatever the output vector held — possibly a previous chunk's value. Null each failed row yourself. The one exception is a panic inside acast_callback!body: the macro then sets every row of the chunk toNULL, since nothing the body wrote can be trusted.
In Normal mode, set a message before returning false. A cast_callback! body
that sets none fails the query with "cast function failed without reporting an
error message"; a hand-written extern "C" callback that sets none makes DuckDB
report Conversion Error: followed by nothing. An empty message passed to
set_error / set_row_error is replaced by a placeholder.
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— the builderCastFunctionInfo— the info handle inside callbacksCastMode—NormalorTrycast_callback!— generates a panic-safe cast callback