Skip to main content
How Rust types map to DuckDB columns through Diesel: the standard types that work out of the box, the DuckDB-specific sql_types::Duck* markers and how to read/write them, and the values::* newtypes that make foreign Rust types bindable. Verified against crates/better-duck-diesel/src/types/duckdb_types.rs and src/values.rs.

Standard Diesel types

These work with no extra imports — Diesel’s own SQL types with DuckDB backend impls provided here:

DuckDB-specific SQL types

use better_duck_diesel::sql_types::*; — use these inside table! and in .bind::<T, _> / .into_sql::<T>(). Each is a zero-size type implementing Diesel’s SqlType + QueryId:
Date/time types map to chrono with the chrono feature, or to better_duck_core::types::date_native plain structs without it. Only one set compiles at a time — see Feature flags.

How Duck* columns move through Diesel

These marker types do not all behave the same through the query builder. There are two tiers:
DuckTinyInt, DuckUTinyInt, DuckUSmallInt, DuckUInt, DuckUBigInt (Rust i8/u8/u16/u32/u64) can be loaded through the typed .select(...) DSL — their Rust types satisfy Diesel’s Queryable blanket, so a .select((t::tiny, t::ubig)) into (i8, u64) works. They have no AsExpression, however, so they cannot be written with .values(...) — insert them with diesel::sql_query(...).bind::<DuckXxx, _>(v) or wrap them in the matching values::* newtype (which adds both bind and load).
DuckHugeInt/DuckUHugeInt (i128/u128) and DuckUuid, DuckEnum, DuckList, DuckArray, DuckStruct, DuckMap, DuckUnion, DuckBit, DuckBignum, DuckTimestamptz, DuckInterval, DuckTimeTz, DuckTimeNs do not flow through the typed builder at all. Round-trip them via raw SQL: diesel::sql_query(...).bind::<DuckXxx, _>(value) to write, and a #[derive(QueryableByName)] struct whose field carries #[diesel(sql_type = DuckXxx)] to read. Values are ordinary better_duck_core types (DuckValue, DuckUuid, DuckBit, …). The scalar ones (i128/u128/DuckUuid/DuckBit/DuckBignum) also become fully typed-DSL-usable through their values::* newtype.
DECIMAL (Numeric → rust_decimal::Decimal) is similar: Decimal has no AsExpression<Numeric>, so it binds through diesel::sql_query(...).bind::<Numeric, _>(d) rather than .values(...), though its FromSql/ToSql for the DuckDB backend still apply. INTERVAL can be bound either through DuckInterval or Diesel’s own diesel::sql_types::Interval (both map to chrono::Duration).

Reading & writing raw-SQL Duck* columns

For ENUM/STRUCT columns you can bind/read your own Rust types instead of DuckValue — see Data derives. The concrete Rust types the composite columns read into (verified in tests / example 22-diesel-types): DuckStruct → HashMap<String, DuckValue>, DuckMap → HashMap<DuckValue, DuckValue>, DuckUnion → Box<DuckValue>, DuckEnum → String, DuckUuid/DuckBit/DuckBignum → the core DuckUuid/DuckBit/DuckBignum, and DuckTimestamptz → chrono::DateTime<Utc>.

The values::* binding newtypes

Diesel’s blanket impl<T: Expression> AsExpression<T::SqlType> for T means a third-party crate can’t make a foreign value type (i128, u64, DuckUuid, …) usable as an expression at a custom Duck* SQL type — coherence rejects it. The thin local newtypes in better_duck_diesel::values::* fix this: wrap the value and it becomes bindable (.eq(…), .values(…), function arg, .into_sql::<T>()) and loadable (Queryable). The standard widths (i16/i32/i64/f32/f64) already work directly and have no wrapper. Each has From<inner> / From<newtype> for inner both ways. Round-trip (verified in tests/values_dsl.rs):

See also

Schema

Declaring columns with these types.

Data derives

Map your own types to ENUM / STRUCT.

Core values

The DuckValue type the raw path uses.

DSL overview

Binding values::* in expressions.