Skip to main content
DuckDB’s nested types map to DuckValue variants that contain other values. Three of them (Enum, Union) carry metadata-preserving wrappers so a read→write round trip keeps the full schema rather than degrading to an approximation.

LIST and ARRAY

List is variable-length; Array is fixed-length. On the write side, a Vec<T> maps to a LIST and a Box<[T]> to an ARRAY for any T: DuckLogicalType — including when the collection is empty, because the element type comes from T (known at compile time), not from the first element.
Reading a LIST/ARRAY into a Vec<T> goes element-by-element through FromDuckValue.

STRUCT — DuckStruct

DuckValue::Struct holds a HashMap<String, DuckValue> of named fields with a fixed schema. The DuckStruct newtype (pub struct DuckStruct(pub HashMap<String, DuckValue>)) exists so a plain HashMap<String, DuckValue> is not forced to mean STRUCT — leaving HashMap<K, V> free for MAP (the same reasoning Blob applies to Vec<u8>).
For a typed, named-field mapping, derive #[derive(DuckStruct)] on a Rust struct.

MAP

DuckValue::Map holds a HashMap<DuckValue, DuckValue> of arbitrary key→value pairs with a dynamic schema (any key type, unlike STRUCT’s fixed string keys). A plain HashMap<K, V> binds as a MAP.
See the Map helpers (get / get_mut / contains_key).

ENUM — DuckEnum

DuckValue::Enum preserves the ordered dictionary of labels and the selected index via better_duck_core::types::DuckEnum. The dictionary is Arc-shared, so cloning a value — or many rows of the same enum column — does not re-allocate the labels.
  • new(dict, index) / from_label(dict, label) — construct; errors if dict is empty or the index/label is invalid.
  • label(), index(), dictionary() — accessors.
The runtime DuckEnum value type is distinct from the #[derive(DuckEnum)] macro, which maps a Rust unit enum to a DuckDB ENUM.

UNION — DuckUnion

DuckValue::Union preserves the full ordered member schema, the selected tag, and the active value via better_duck_core::types::DuckUnion. The schema is Arc-shared.
  • new(members, tag, value) — full schema; errors if members is empty or tag is out of range.
  • single(value) — a one-member union derived from the value (used e.g. for a bare DuckValue bound through Diesel). A value read from a real union column keeps its full schema.
  • members(), tag(), active_name(), value() — accessors.

LogicalType and TypeInfo

Column type descriptors come in two forms:
  • better_duck_core::types::LogicalType — an owned DuckDB logical-type handle with recursive introspection. Build one for a Rust type with LogicalType::of::<T>(), read parts with list_child, array_child / array_size, map_key / map_value, struct_child / struct_child_name / struct_child_count, union_member / union_member_name / union_member_count, and enum_dictionary. set_alias / alias name a custom type (then register it to use the alias in SQL).
  • better_duck_core::types::TypeInfo — a lossless, owned, recursive descriptor produced by LogicalType::describe(). It is Clone + Send, so it outlives the FFI handles it came from. This is what DuckResult::column_schema() returns.
TypeInfo is #[non_exhaustive]: TypeInfo::to_logical_type() rebuilds an owned LogicalType — the inverse of describe() — so a typed value can reconstruct its exact declared type on write.

See also