Skip to main content
Preparing a statement once and executing it many times avoids re-parsing. better-duck-core offers two wrappers with different lifetimes, plus incremental (“pending”) execution and multi-statement batches.
There is no Connection::prepare. The entry point is conn.db().prepare(sql) (which returns a borrowing Statement) or CachedStatement::prepare(conn.db(), sql) for a 'static one. conn.db() returns the underlying raw connection the statement is prepared on.

Statement — borrows the connection

conn.db().prepare(sql) returns a Statement<'a> that borrows conn. bind auto-increments the positional index; bind_at targets a specific 1-based index; clear_bindings resets for reuse.

CachedStatement — owns its connection

A CachedStatement holds its own handle to the connection, so it does not borrow conn, is 'static, and is freely reusable (this is the type Diesel’s statement cache stores). Binds are 1-based; bind_named targets $name; reset_bindings clears for the next run.
A CachedStatement must only ever be executed on the connection it was prepared on — DuckDB scopes transactions to a connection, so running it elsewhere would execute outside the caller’s BEGIN/ROLLBACK. The wrapper enforces this by retaining that exact connection, so the rule is a type invariant, not something you must track.

Statement metadata

Both wrappers expose introspection (CachedStatement shown; Statement has the same via its raw helpers):
  • statement_type() -> StatementType — see below.
  • parameter_count() -> u64, parameter_name(index) -> Option<String>, parameter_index(name) -> Result<u64>, parameter_type / parameter_logical_type.
  • column_count() -> u64, column_name(index), column_type / column_logical_type.
StatementType (better_duck_core::StatementType) is #[non_exhaustive] and mirrors duckdb_statement_type — Select, Insert, Update, Delete, Create, Drop, Alter, Explain, Prepare, Execute, Transaction, Copy, Pragma, Call, Attach, Detach, Multi, …, and Unknown(raw) for a kind this build doesn’t recognise. Match with a _ arm.

Multi-statement batches — ExtractedStatements

conn.extract_statements(sql) parses a ;-separated batch into better_duck_core::ExtractedStatements; DuckDB does the parsing, so a semicolon inside a string literal is not a split. Prepare a given index at most once; an out-of-range index is rejected.
  • len() -> u64 — statements parsed; is_empty() is effectively always false (a zero-statement extract is an error at extraction time).
  • prepare(index) -> Result<CachedStatement> — prepare the statement at index.
A parse failure surfaces as Error::Engine carrying DuckDB’s message; an interior NUL as Error::NulError.

Incremental (task-at-a-time) execution

CachedStatement::pending() returns a PendingResult that runs the query one task at a time — useful for cooperative scheduling or progress reporting. into_pending() yields an OwnedPending that is Send + 'static (it owns the statement, so it carries no borrow — this is what the async facade steps across spawn_blocking dispatches).
PendingState (better_duck_core::PendingState) is #[non_exhaustive]:
Step until Ready, Error, or NoTasksAvailable — the remaining work may be owned by background workers, so stepping alone would not necessarily reach Ready. execute() finalizes the query either way. PendingResult::check_state() reads the current state without stepping; is_finished() on a state uses DuckDB’s own definition.
Both PendingResult and OwnedPending expose execute_task(), error() -> Option<String>, and execute(self) -> Result<DuckResult>.

See also