Replacement Scans
A replacement scan lets users write:
SELECT * FROM 'myfile.myformat'
and have DuckDB automatically invoke your extension's table-valued scan instead of trying to open the path as a built-in file type. This is how DuckDB's built-in CSV, Parquet, and JSON readers work.
quack-rs provides ReplacementScanBuilder (a static registration helper) and
ReplacementScanInfo (an ergonomic wrapper for callbacks).
Registration API
Unlike the other builders in quack-rs, ReplacementScanBuilder uses a single
static call because the DuckDB C API takes all arguments at once:
#![allow(unused)] fn main() { use quack_rs::replacement_scan::ReplacementScanBuilder; // Low-level: pass raw extra_data and an optional delete callback. unsafe { ReplacementScanBuilder::register( db, // duckdb_database my_scan_callback, // ReplacementScanFn std::ptr::null_mut(), // extra_data (or a raw pointer) None, // delete_callback ); } // Ergonomic: pass owned Rust data; boxing and destructor are handled for you. unsafe { ReplacementScanBuilder::register_with_data(db, my_scan_callback, my_state); } }
Note: Replacement scans are registered on a database handle (
duckdb_database), not a connection. Register them before opening connections.
Callback signature
The raw callback receives duckdb_replacement_scan_info, but you can wrap it
with ReplacementScanInfo for ergonomic, safe access:
#![allow(unused)] fn main() { use quack_rs::replacement_scan::ReplacementScanInfo; unsafe extern "C" fn my_scan_callback( info: duckdb_replacement_scan_info, table_name: *const ::std::os::raw::c_char, _data: *mut ::std::os::raw::c_void, ) { let path = unsafe { std::ffi::CStr::from_ptr(table_name) } .to_str() .unwrap_or(""); if !path.ends_with(".myformat") { return; // pass — DuckDB will try other handlers } // Use ReplacementScanInfo for ergonomic access unsafe { ReplacementScanInfo::new(info) .set_function("read_myformat") .add_varchar_parameter(path); } } }
ReplacementScanInfo methods
| Method | Description |
|---|---|
set_function(name) | Redirect to the named table function |
add_varchar_parameter(value) | Add a VARCHAR parameter to the redirected call |
set_error(message) | Report an error (aborts this replacement scan) |
When to use replacement scans vs table functions
| Scenario | Use |
|---|---|
SELECT * FROM my_function('file.ext') | Table function |
SELECT * FROM 'file.ext' (bare path) | Replacement scan → delegates to a table function |
| File type auto-detection | Replacement scan |
Most extensions implement both: a table function that does the actual work, and a replacement scan that detects the file extension and transparently routes bare-path queries to the table function.
See also
replacement_scanmodule documentation- Table Functions