QuackDB decodes DuckDB Quack result vectors into Elixir values. The table below reflects the package's behavior; anything not listed as supported raises an explicit error rather than decoding to a lossy value.
Scalar types
| DuckDB type | Elixir value | Status | Notes |
|---|---|---|---|
BOOLEAN | boolean() | Supported | |
TINYINT | integer() | Supported | Signed 8-bit. |
UTINYINT | non_neg_integer() | Supported | Unsigned 8-bit. |
SMALLINT | integer() | Supported | Signed 16-bit. |
USMALLINT | non_neg_integer() | Supported | Unsigned 16-bit. |
INTEGER | integer() | Supported | Signed 32-bit. |
UINTEGER | non_neg_integer() | Supported | Unsigned 32-bit. |
BIGINT | integer() | Supported | Signed 64-bit. |
UBIGINT | non_neg_integer() | Supported | Unsigned 64-bit. |
HUGEINT | integer() | Supported | Signed 128-bit. |
UHUGEINT | non_neg_integer() | Supported | Unsigned 128-bit. |
FLOAT | float(), :nan, :infinity, :neg_infinity | Supported | 32-bit floating point; see Non-finite floats. |
DOUBLE | float(), :nan, :infinity, :neg_infinity | Supported | 64-bit floating point; see Non-finite floats. |
DECIMAL | Decimal.t() | Supported | Widths backed by 16-, 32-, 64-, and 128-bit storage are covered. |
VARCHAR / CHAR | String.t() | Supported | Invalid UTF-8 raises a protocol error. |
BLOB | binary() | Supported | Returned as raw bytes. |
UUID | UUID string | Supported | Returned in canonical lowercase UUID format. |
ENUM | String.t() | Supported | Returned as the enum label. |
BIT | String.t() | Supported | Returned as a string of 0 and 1 characters. |
BIGNUM | integer() | Supported | Decodes DuckDB's variable-length integer payload into an Elixir integer. |
GEOMETRY | binary() | Partial | Decoded as WKB-compatible bytes when DuckDB's spatial extension returns geometry values; semantic geometry structs are not implemented. |
Ecto schemas can use :binary_id or Ecto.UUID for UUID fields, including nullable fields and arrays. Schema reads return canonical UUID strings; SQL inserts and native append inserts preserve UUID values. Direct SQL results also remain canonical strings rather than Ecto's dumped 16-byte representation.
Non-finite floats
DuckDB FLOAT and DOUBLE columns can hold IEEE 754 infinities and NaN, and the BEAM has no float for them: <<x::float>> refuses to build one. QuackDB represents them with the same atoms Explorer uses, so a result column hands to a dataframe without translation:
| DuckDB value | Elixir value |
|---|---|
'inf' | :infinity |
'-inf' | :neg_infinity |
'nan' (any NaN payload, any sign) | :nan |
The atoms work in every direction:
QuackDB.query!(conn, "SELECT 1.0/0.0 AS x, 'nan'::DOUBLE AS y").rows
#=> [[:infinity, :nan]]
QuackDB.query!(conn, "SELECT isnan(?)", [:nan]).rows
#=> [[true]]
QuackDB.insert_rows!(conn, "measurements", [[id: 1, ratio: :neg_infinity]])As SQL parameters they are formatted as 'inf'::DOUBLE, '-inf'::DOUBLE, and 'nan'::DOUBLE; in native appends they are written as the IEEE bit patterns, with NaN as a quiet NaN.
Ecto's :float type accepts only numbers, so a schema field of type :float raises on load when the row holds one of these atoms, exactly as it does with Postgrex's :NaN and :inf. Direct SQL through the Repo returns the atoms unchanged. A schema that must carry non-finite values declares a custom type:
defmodule MyApp.Measure do
use Ecto.Type
def type, do: :float
def cast(value) when is_float(value) or value in [:nan, :infinity, :neg_infinity], do: {:ok, value}
def cast(_), do: :error
def load(value), do: cast(value)
def dump(value), do: cast(value)
endTemporal types
| DuckDB type | Elixir value | Status | Notes |
|---|---|---|---|
DATE | Date.t() | Supported | |
TIME | Time.t() | Supported | Microsecond precision. |
TIME_NS | QuackDB.NanosecondTime.t() | Supported | Preserves nanoseconds since midnight. |
TIMESTAMP_S | NaiveDateTime.t() | Supported | Second precision. |
TIMESTAMP_MS | NaiveDateTime.t() | Supported | Millisecond precision. |
TIMESTAMP | NaiveDateTime.t() | Supported | Microsecond precision. |
TIMESTAMP_NS | QuackDB.NanosecondTimestamp.t() | Supported | Preserves nanoseconds since Unix epoch. |
TIME WITH TIME ZONE | QuackDB.TimeWithTimeZone.t() | Supported | Preserves time-of-day and UTC offset seconds. |
TIMESTAMPTZ | DateTime.t() | Supported | Decoded as UTC. |
TIMESTAMPTZ_NS | QuackDB.NanosecondTimestamp.t() | Supported | The UTC instant in nanoseconds; Elixir's DateTime stops at microseconds. |
INTERVAL | QuackDB.Interval.t() | Supported | Preserves DuckDB month, day, and microsecond components. |
Nested types
| DuckDB type | Elixir value | Status | Notes |
|---|---|---|---|
LIST | list | Supported | Includes empty lists and null elements. |
STRUCT | map with string keys | Supported | Null child values are preserved. |
ARRAY | list | Supported | Fixed-size arrays are returned as Elixir lists. |
MAP | map | Supported | Map entries are converted to Elixir maps; duplicate-key policy follows Map.put/3. |
TUPLE | tuple | Supported | DuckDB 2.0's unnamed structs, such as SELECT (1, 'a'). |
VARIANT | any of the above | Supported | Objects become maps with string keys, arrays become lists, and scalars decode like their column type. Shredded VARIANT columns read from storage decode the same way. |
QuackDB.Ecto.Variant builds variant_extract/2, variant_type/1, variant_keys/1, and related expressions for querying inside VARIANT values.
Append encoding
QuackDB.insert_rows/4 supports scalar append values plus nested LIST, STRUCT, ARRAY, and MAP columns when explicit column specs are provided. Temporal append values use Elixir's Calendar conversion APIs and are encoded in DuckDB's ISO calendar representation.
Inferred types account for every value in a column: integers widen to BIGINT or HUGEINT as needed, integers mixed with floats become DOUBLE (while a DOUBLE holds them exactly), decimals and integers share one DECIMAL wide enough for all of them, and plain Elixir maps infer as DuckDB STRUCT values with the union of their keys. Any other mix fails with :mixed_append_types instead of truncating, dropping, or NULLing values. For explicit {:map, key_type, value_type} columns, QuackDB accepts either DuckDB-style key/value entries or ordinary Elixir maps:
QuackDB.insert_rows!(conn, "events", [[labels: %{env: "prod", region: "eu"}]],
columns: [labels: {:map, :varchar, :varchar}]
)
QuackDB.insert_rows!(conn, "events", [[labels: [%{key: "env", value: "prod"}]]],
columns: [labels: {:map, :varchar, :varchar}]
)VARIANT columns take any JSON-shaped value when declared as :variant; QuackDB sends JSON text and DuckDB casts it on insert, so objects, arrays, and scalars keep their shapes. Without the declaration, inference refuses to mix maps, lists, and scalars in one column, with a :mixed_append_types error. A VARIANT column with mixed shapes needs it:
QuackDB.insert_rows!(conn, "events", [[payload: %{kind: "duck"}], [payload: [1, 2]]],
columns: [payload: :variant]
)Both MAP forms encode as DuckDB MAP(VARCHAR, VARCHAR). Arbitrary mixed-key or mixed-value Elixir map semantics are not implied; DuckDB MAP columns still have one key type and one value type. Duplicate MAP keys decode with the later entry winning, matching Map.put/3. Keys and values are encoded through the declared DuckDB types, so atom keys in {:map, :varchar, :varchar} columns become strings.
Vector encodings
| DuckDB vector encoding | Status |
|---|---|
| Flat | Supported |
| Constant | Supported |
| Dictionary | Supported |
| Sequence | Supported |
Shredded (VARIANT) | Supported |
| FSST | Unsupported; QuackDB has an optional internal :fsst bridge, but current DuckDB Quack serialization flattens FSST vectors rather than exposing a compressed payload |
SQL parameter literals
QuackDB formats query parameters as DuckDB SQL literals client-side because the current Quack request path does not expose server-side bind parameters. Pass values separately rather than interpolating them into SQL.
For direct queries, use {:blob, bytes} whenever the value is binary data, even if those bytes are valid UTF-8. Plain valid UTF-8 binaries without NUL are treated as text; relying on DuckDB to cast text into BLOB can interpret backslash escapes or reject non-ASCII bytes. Ecto schema :binary fields are explicitly encoded as blobs, including in native append inserts. Ecto :naive_datetime_usec and :utc_datetime_usec preserve microseconds; TIMESTAMPTZ results normalize to UTC rather than retaining the original time-zone name.
Supported parameter values:
nil- booleans
- integers
- floats, and the non-finite atoms
:nan,:infinity, and:neg_infinity Decimal.t()- strings
{:blob, binary}{:json, map_or_list_or_scalar}Date.t()Time.t()NaiveDateTime.t()DateTime.t()QuackDB.Interval.t()Duration.t()values, converted to DuckDB interval literals and accepted by Ecto series/time-bucket helpers{:interval, months, days, micros}- lists containing supported parameter values
Unsupported parameter values raise explicit errors rather than being formatted lossy.
Notes
- Unsupported types should fail explicitly rather than silently returning lossy values.
- Result decoding supports row-shaped results and columnar fetch batches. Arrow IPC / zero-copy handoff remains future work.
- Type behavior is validated with gated real DuckDB Quack integration tests where DuckDB currently exposes the type through the Quack extension.