DuckDB in Your Application
Embed DuckDB in a native Codon executable to query local data and register custom SQL functions in the same process. Query results convert to explicit Codon tuple types, with streaming and batch iteration available for large outputs.
The first example registers a calculation, calls it from SQL, and checks the result. The embedded SDK supplies the database engine.
Before you begin: install the all-SDK distribution. No DuckDB server, Python package, or separate DuckDB CLI is required.
Embed DuckDB in a Codon Application
For applications written in Codon, open an embedded connection and register functions directly:
import exaloop.databases.duckdb as duckdb
def expected_loss(balance: float, probability: float) -> float:
return balance * probability * 0.45
with duckdb.connect(threads=4) as connection:
connection.create_function(
"expected_loss", expected_loss, Tuple[float, float], float)
with connection.query(
"SELECT expected_loss(1000.0::DOUBLE, 0.02::DOUBLE)"
) as result:
assert result.fetchall(Tuple[float]) == [(9.0,)]
For this example in application.codon, run:
exaloop --with duckdb run -release application.codon
The program exits successfully when the query returns 9.0.
Use an individual SDK without the CLI
codon run -release -plugin /path/to/sdk/duckdb application.codon
The embedded plugin includes DuckDB v1.4.3 with Parquet and JSON support linked in, so these formats work without extension downloads. It does not require the Python DuckDB package or CLI.
Connection and Result API
Use context managers for connections and results. A normal query loads the
complete result; stream=True lets you start consuming it without first
materializing the whole output.
| API | Contract |
|---|---|
duckdb.connect(path=":memory:", threads=1) | Open a database and connection; positive thread count required |
duckdb.version() | Linked DuckDB version string |
connection.create_function(name, operation, Parameters, Returns, side_effects=False, mode="scalar") | Register a specialized UDF; mode is the literal "scalar" or "vectorized" |
connection.query(sql, stream=False) | Return a materialized result, or opt into streaming |
connection.execute(sql) | Execute SQL and close/discard its result |
connection.close() | Idempotent disconnect and database close; aliases become closed |
result.columns | Copied list of column names |
result.fetchmany(Row, size=2048) | Consume at most size rows into an owned list; size must be positive |
result.batches(Row, size=2048) | Iterate over owned row batches |
result.rows(Row) | Iterate over rows with bounded batch buffering |
result.fetchall(Row) | Consume all remaining rows into list[Row] |
result.close() | Idempotently release the result and buffered chunk |
Choose Row to match the query's SQL types, including Optional for nullable
columns. The type reference covers less obvious cases,
such as SUM(BIGINT) returning a 128-bit integer.
For large result sets, combine streaming with batch iteration:
import exaloop.databases.duckdb as duckdb
total = 0
with duckdb.connect() as connection:
with connection.query("SELECT range FROM range(10003)", stream=True) as result:
for batch in result.batches(Tuple[int], size=4096):
total += sum(row[0] for row in batch)
assert total == 10003 * 10002 // 2
Finish a streaming result before issuing another query on its connection. Batching limits client-side conversion memory, although SQL operators such as sorting may still need working memory.
The embedded API accepts SQL strings and does not support bound parameters. Interpolating untrusted values can introduce SQL injection, so this API requires queries whose text is controlled by the application.
Threads and Ownership
DuckDB may run functions on several workers. Read-only state can be shared, but mutable state needs synchronization. A connection does not support concurrent use or recursive queries from its own callback.
By default, a function is treated as deterministic, so DuckDB may reuse or
precompute its result. Register side_effects=True for a function that reads
mutable state or produces random values; this does not guarantee evaluation
order or make that state thread-safe.
Next Steps
- Use decimal or vectorized functions for calculations beyond scalar floating-point values.
- Package the executable for another machine.
- Profile the application to find expensive calculations.