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 typeElixir valueStatusNotes
BOOLEANboolean()Supported
TINYINTinteger()SupportedSigned 8-bit.
UTINYINTnon_neg_integer()SupportedUnsigned 8-bit.
SMALLINTinteger()SupportedSigned 16-bit.
USMALLINTnon_neg_integer()SupportedUnsigned 16-bit.
INTEGERinteger()SupportedSigned 32-bit.
UINTEGERnon_neg_integer()SupportedUnsigned 32-bit.
BIGINTinteger()SupportedSigned 64-bit.
UBIGINTnon_neg_integer()SupportedUnsigned 64-bit.
HUGEINTinteger()SupportedSigned 128-bit.
UHUGEINTnon_neg_integer()SupportedUnsigned 128-bit.
FLOATfloat(), :nan, :infinity, :neg_infinitySupported32-bit floating point; see Non-finite floats.
DOUBLEfloat(), :nan, :infinity, :neg_infinitySupported64-bit floating point; see Non-finite floats.
DECIMALDecimal.t()SupportedWidths backed by 16-, 32-, 64-, and 128-bit storage are covered.
VARCHAR / CHARString.t()SupportedInvalid UTF-8 raises a protocol error.
BLOBbinary()SupportedReturned as raw bytes.
UUIDUUID stringSupportedReturned in canonical lowercase UUID format.
ENUMString.t()SupportedReturned as the enum label.
BITString.t()SupportedReturned as a string of 0 and 1 characters.
BIGNUMinteger()SupportedDecodes DuckDB's variable-length integer payload into an Elixir integer.
GEOMETRYbinary()PartialDecoded 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 valueElixir 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)
end

Temporal types

DuckDB typeElixir valueStatusNotes
DATEDate.t()Supported
TIMETime.t()SupportedMicrosecond precision.
TIME_NSQuackDB.NanosecondTime.t()SupportedPreserves nanoseconds since midnight.
TIMESTAMP_SNaiveDateTime.t()SupportedSecond precision.
TIMESTAMP_MSNaiveDateTime.t()SupportedMillisecond precision.
TIMESTAMPNaiveDateTime.t()SupportedMicrosecond precision.
TIMESTAMP_NSQuackDB.NanosecondTimestamp.t()SupportedPreserves nanoseconds since Unix epoch.
TIME WITH TIME ZONEQuackDB.TimeWithTimeZone.t()SupportedPreserves time-of-day and UTC offset seconds.
TIMESTAMPTZDateTime.t()SupportedDecoded as UTC.
TIMESTAMPTZ_NSQuackDB.NanosecondTimestamp.t()SupportedThe UTC instant in nanoseconds; Elixir's DateTime stops at microseconds.
INTERVALQuackDB.Interval.t()SupportedPreserves DuckDB month, day, and microsecond components.

Nested types

DuckDB typeElixir valueStatusNotes
LISTlistSupportedIncludes empty lists and null elements.
STRUCTmap with string keysSupportedNull child values are preserved.
ARRAYlistSupportedFixed-size arrays are returned as Elixir lists.
MAPmapSupportedMap entries are converted to Elixir maps; duplicate-key policy follows Map.put/3.
TUPLEtupleSupportedDuckDB 2.0's unnamed structs, such as SELECT (1, 'a').
VARIANTany of the aboveSupportedObjects 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 encodingStatus
FlatSupported
ConstantSupported
DictionarySupported
SequenceSupported
Shredded (VARIANT)Supported
FSSTUnsupported; 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.