DECIMAL, UUID, BIT, BIGNUM,
temporals, and the composite LIST / ARRAY / STRUCT / MAP / UNION / ENUM types.
better-duck covers all of it behind one Rust enum, DuckValue. This page is the complete mapping,
how NULL is represented, and how a few feature flags change which Rust type
you read.
Values arrive as better_duck_core::types::value::DuckValue. Bind values implement AppendAble
(see parameters); the two are the read and
write sides of the same type coverage.
The complete DuckValue mapping
DuckValue is #[non_exhaustive] — always include a _ arm when you match on it, so your code
keeps compiling as new variants are added. It is Clone + PartialEq + Eq + Hash (floats
canonicalized, maps hashed order-independently), so a DuckValue works as a HashMap key.A column type that read-support does not yet decode surfaces as a
DuckDBConversionError rather
than panicking. As of 0.1.0-beta.8 the DuckDB C API exposes no value accessor for GEOMETRY,
VARIANT, ANY, or INTEGER_LITERAL, so those are not yet readable as typed values — but
GEOMETRY physically arrives as its WKB Blob, which you can decode with a geometry library on
the Rust side or convert in SQL.Two beta write-path limitations to know. Multi-arm
UNION writes currently build a
single-member union (reads are unaffected), and a DECIMAL’s declared width/scale is read into the
value but not yet carried back on write. See
Versioning & compatibility.NULL and validity
NULL is first-class, not a sentinel:
- A missing value reads back as
DuckValue::Null. - In columnar access a column carries a validity bitmask — packed
u64words where bitr % 64of wordr / 64set means rowris valid. DuckDB elides the mask entirely for a column with no NULLs, sovalidity()returnsNonein that case. TRY_CASTandOption<T>parameters / returns propagateNULLexplicitly, so you can round-trip missing data without magic values.
Feature flags that change the read type
Two decisions change which Rust type a temporal / numeric column decodes to. They are read-type choices — the SQL and theDuckValue variant are unchanged, only the inner Rust type differs.
chrono and the native date/time path are mutually exclusive for the read type — only one set
of DuckValue temporal conversions compiles at a time. The separate time
feature is an independent bind/append path (time-crate Date, Time, OffsetDateTime, …)
that coexists with either read type. See feature flags.Inspecting types without reading values
Two APIs let you learn a column’s real type — including the detail a coarse type id loses — without decoding any rows:TypeInfo(better_duck_core::types::TypeInfo) is the lossless type descriptor:TypeInfo::Decimal { width, scale },TypeInfo::List(..),TypeInfo::Scalar(id), nestedLIST/STRUCT/MAPshapes, andENUMdictionaries all survive. On a result, read it withcolumn_logical_type(idx)orcolumn_schema(). The coarsercolumn_type(idx)returns just the DuckDB type id.TableDescription(better_duck_core::TableDescription) reads a catalog table’s column names, types, andDEFAULTflags without issuing a query:
Mapping Rust types to DuckDB
With thederive feature, map your own types instead of matching DuckValue
by hand:
#[derive(FromRow)]/#[derive(ToRow)]— read a struct from a row / write a struct as a row or parameters.#[derive(DuckStruct)]— a Rust struct ↔ a DuckDBSTRUCT.#[derive(DuckEnum)]— a Rust unit enum ↔ a DuckDBENUM(by label).
Related
SQL dialect & statements
Parameter binding and what each statement returns.
Queries, Results & Values
The
DuckValue type and result metadata in depth.Columnar access
Zero-copy column reads and the validity bitmask.
Data derives
Map Rust structs and enums to rows and types.