Skip to main content
A DuckResult borrows DuckDB FFI handles, so it is neither Send nor Clone. DuckResult::materialize() consumes it into a better_duck_core::ResultSet that holds no FFI handles — it is Send + Sync + Clone, so it can cross threads and outlive the connection.
The async facade returns a ResultSet from every query method — materializing is what makes an owned result safe to hand back out of a spawn_blocking closure.

API

All rows as a slice.
Consume into an owned Vec<DuckRow>.
The first row, if any.
Row count helpers.
Affected-row count (DML); 0 for SELECT.
Column names in order.
Lossless per-column TypeInfo.
The same metadata a DuckResult carries — see metadata.
ResultSet also implements IntoIterator (yielding owned DuckRows), so you can for row in set or set.into_iter().map(...).

Deserializing into structs

With the derive feature and a #[derive(FromRow)] type, to_structs::<T>() reads the whole result into a Vec<T>:
to_structs::<T: FromRow>() -> Result<Vec<T>, DuckDBConversionError> reads each row by column name; the first row that fails conversion surfaces its error. See FromRow / ToRow.

When to materialize

Materialize

Crossing threads, returning results from a function, caching, random access, or using to_structs. Pays an up-front copy; frees the connection immediately.

Stream (DuckResult)

Large results you fold once, or when you want to stop early. Zero extra copies; keeps the connection borrowed until dropped.

See also