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 CAST to/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:

ModeUser wroteExpected behaviour on error
CastMode::NormalCAST(x AS T)Call set_error and return false
CastMode::TryTRY_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 cast
  • CAST(DOUBLE AS BIGINT) — with implicit_cost(100) and extra_info for 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] — Normal vs Try cast mode