DuckDB type support
Foundations JDBC supports DuckDB's type system, including nested types (LIST, STRUCT, MAP, UNION) and extended integer types.
DuckDB's JDBC driver doesn't report column nullability or parameter metadata, so QueryChecker's .opt() and wrong-parameter-type checks silently pass on DuckDB. Column type mismatches are still caught. If you test against DuckDB in-memory and deploy against PostgreSQL or another dialect, run analysis against the production dialect too — it will find issues DuckDB can't. See Query Analysis: Database Behavior for the full matrix.
Integer types (signed)
| DuckDB Type | Java Type | Range |
|---|---|---|
TINYINT | Byte | -128 to 127 |
SMALLINT | Short | -32,768 to 32,767 |
INTEGER / INT | Integer | -2^31 to 2^31-1 |
BIGINT | Long | -2^63 to 2^63-1 |
HUGEINT | BigInteger | -2^127 to 2^127-1 |
- Kotlin
- Java
- Scala
val tinyType: DuckDbType<Byte> = DuckDbTypes.tinyint
val intType: DuckDbType<Int> = DuckDbTypes.integer
val hugeType: DuckDbType<BigInteger> = DuckDbTypes.hugeint
DuckDbType<Byte> tinyType = DuckDbTypes.tinyint;
DuckDbType<Integer> intType = DuckDbTypes.integer;
DuckDbType<BigInteger> hugeType = DuckDbTypes.hugeint;
val tinyType: DuckDbType[Byte] = DuckDbTypes.tinyint
val intType: DuckDbType[Int] = DuckDbTypes.integer
val hugeType: DuckDbType[java.math.BigInteger] = DuckDbTypes.hugeint
Integer types (unsigned)
| DuckDB Type | Java Type | Range |
|---|---|---|
UTINYINT | Uint1 | 0 to 255 |
USMALLINT | Uint2 | 0 to 65,535 |
UINTEGER | Uint4 | 0 to 2^32-1 |
UBIGINT | Uint8 | 0 to 2^64-1 |
UHUGEINT | BigInteger | 0 to 2^128-1 |
- Kotlin
- Java
- Scala
val utinyType: DuckDbType<Uint1> = DuckDbTypes.utinyint
val uintType: DuckDbType<Uint4> = DuckDbTypes.uinteger
val uhugeType: DuckDbType<BigInteger> = DuckDbTypes.uhugeint
DuckDbType<Uint1> utinyType = DuckDbTypes.utinyint;
DuckDbType<Uint4> uintType = DuckDbTypes.uinteger;
DuckDbType<BigInteger> uhugeType = DuckDbTypes.uhugeint;
val utinyType: DuckDbType[Uint1] = DuckDbTypes.utinyint
val uintType: DuckDbType[Uint4] = DuckDbTypes.uinteger
val uhugeType: DuckDbType[BigInteger] = DuckDbTypes.uhugeint
Floating-point types
| DuckDB Type | Java Type | Notes |
|---|---|---|
FLOAT / FLOAT4 / REAL | Float | 32-bit IEEE 754 |
DOUBLE / FLOAT8 | Double | 64-bit IEEE 754 |
- Kotlin
- Java
- Scala
val floatType: DuckDbType<Float> = DuckDbTypes.float_
val doubleType: DuckDbType<Double> = DuckDbTypes.double_
DuckDbType<Float> floatType = DuckDbTypes.float_;
DuckDbType<Double> doubleType = DuckDbTypes.double_;
val floatType: DuckDbType[Float] = DuckDbTypes.float_
val doubleType: DuckDbType[Double] = DuckDbTypes.double_
Fixed-point types
| DuckDB Type | Java Type | Notes |
|---|---|---|
DECIMAL(p,s) | BigDecimal | Arbitrary precision |
NUMERIC(p,s) | BigDecimal | Alias for DECIMAL |
- Kotlin
- Java
- Scala
val decimalType: DuckDbType<BigDecimal> = DuckDbTypes.decimal
val precise: DuckDbType<BigDecimal> = DuckDbTypes.decimalOf(18, 6) // DECIMAL(18,6)
DuckDbType<BigDecimal> decimalType = DuckDbTypes.decimal;
DuckDbType<BigDecimal> precise = DuckDbTypes.decimalOf(18, 6); // DECIMAL(18,6)
val decimalType: DuckDbType[BigDecimal] = DuckDbTypes.decimal
val precise: DuckDbType[BigDecimal] = DuckDbTypes.decimalOf(18, 6) // DECIMAL(18,6)
Boolean type
| DuckDB Type | Java Type |
|---|---|
BOOLEAN / BOOL | Boolean |
- Kotlin
- Java
- Scala
val boolType: DuckDbType<Boolean> = DuckDbTypes.boolean_
DuckDbType<Boolean> boolType = DuckDbTypes.boolean_;
val boolType: DuckDbType[Boolean] = DuckDbTypes.boolean_
String types
| DuckDB Type | Java Type | Notes |
|---|---|---|
VARCHAR / STRING / TEXT | String | Variable length |
CHAR(n) | String | Fixed length |
- Kotlin
- Java
- Scala
val varcharType: DuckDbType<String> = DuckDbTypes.varchar
val charType: DuckDbType<String> = DuckDbTypes.char_Of(10)
DuckDbType<String> varcharType = DuckDbTypes.varchar;
DuckDbType<String> charType = DuckDbTypes.char_Of(10);
val varcharType: DuckDbType[String] = DuckDbTypes.varchar
val charType: DuckDbType[String] = DuckDbTypes.char_Of(10)
Binary types
| DuckDB Type | Java Type |
|---|---|
BLOB / BYTEA / BINARY / VARBINARY | byte[] |
- Kotlin
- Java
- Scala
val blobType: DuckDbType<ByteArray> = DuckDbTypes.blob
DuckDbType<byte[]> blobType = DuckDbTypes.blob;
val blobType: DuckDbType[Array[Byte]] = DuckDbTypes.blob
Bit string type
| DuckDB Type | Java Type | Notes |
|---|---|---|
BIT / BITSTRING | String | String of 0s and 1s |
- Kotlin
- Java
- Scala
val bitType: DuckDbType<String> = DuckDbTypes.bit
val bit8: DuckDbType<String> = DuckDbTypes.bitOf(8) // BIT(8)
DuckDbType<String> bitType = DuckDbTypes.bit;
DuckDbType<String> bit8 = DuckDbTypes.bitOf(8); // BIT(8)
val bitType: DuckDbType[String] = DuckDbTypes.bit
val bit8: DuckDbType[String] = DuckDbTypes.bitOf(8) // BIT(8)
Date/time types
| DuckDB Type | Java Type | Notes |
|---|---|---|
DATE | LocalDate | Naive date, no zone |
TIME | LocalTime | Naive time, no zone |
TIMESTAMP / DATETIME | LocalDateTime | Naive timestamp, no zone |
TIMESTAMP WITH TIME ZONE | Instant | UTC instant — see note below |
TIME WITH TIME ZONE | OffsetTime | Time of day with offset preserved |
INTERVAL | Duration | Time duration |
- Kotlin
- Java
- Scala
val dateType: DuckDbType<LocalDate> = DuckDbTypes.date
val tsType: DuckDbType<LocalDateTime> = DuckDbTypes.timestamp
val tstzType: DuckDbType<Instant> = DuckDbTypes.timestamptz
val intervalType: DuckDbType<Duration> = DuckDbTypes.interval
DuckDbType<LocalDate> dateType = DuckDbTypes.date;
DuckDbType<LocalDateTime> tsType = DuckDbTypes.timestamp;
DuckDbType<Instant> tstzType = DuckDbTypes.timestamptz;
DuckDbType<Duration> intervalType = DuckDbTypes.interval;
val dateType: DuckDbType[LocalDate] = DuckDbTypes.date
val tsType: DuckDbType[LocalDateTime] = DuckDbTypes.timestamp
val tstzType: DuckDbType[Instant] = DuckDbTypes.timestamptz
val intervalType: DuckDbType[Duration] = DuckDbTypes.interval
TIMESTAMP WITH TIME ZONE is not what the name suggestsDespite the SQL keyword, DuckDB does not store any zone or offset with a TIMESTAMPTZ value — it stores an INT64 count of microseconds since the Unix epoch (see the DuckDB timestamp docs). The original offset or region is used only for parsing on input, then discarded. Reads render the instant in the session timezone, which is a display convenience, not persisted state.
Because the storage is a universal instant, the library maps this column to java.time.Instant — the Java type with the same semantics. Using OffsetDateTime would surface the JDBC driver's cosmetic "render in session offset" output as if it were data. Instant is identical in spirit to PostgreSQL's timestamptz mapping (see postgresql.md).
Note: TIME WITH TIME ZONE (TIMETZ) is a distinct case: DuckDB's JDBC driver does preserve the original offset on round-trip (verified empirically), so that column maps to OffsetTime, not Instant. The DuckDB CLI renders TIMETZ in session-TZ which can make it look normalized, but that's display-only.
Timestamp precision variants
| DuckDB Type | Java Type | Precision |
|---|---|---|
TIMESTAMP_S | LocalDateTime | Seconds |
TIMESTAMP_MS | LocalDateTime | Milliseconds |
TIMESTAMP | LocalDateTime | Microseconds (default) |
TIMESTAMP_NS | LocalDateTime | Nanoseconds |
- Kotlin
- Java
- Scala
val tsSeconds: DuckDbType<LocalDateTime> = DuckDbTypes.timestamp_s
val tsMillis: DuckDbType<LocalDateTime> = DuckDbTypes.timestamp_ms
val tsNanos: DuckDbType<LocalDateTime> = DuckDbTypes.timestamp_ns
DuckDbType<LocalDateTime> tsSeconds = DuckDbTypes.timestamp_s;
DuckDbType<LocalDateTime> tsMillis = DuckDbTypes.timestamp_ms;
DuckDbType<LocalDateTime> tsNanos = DuckDbTypes.timestamp_ns;
val tsSeconds: DuckDbType[LocalDateTime] = DuckDbTypes.timestamp_s
val tsMillis: DuckDbType[LocalDateTime] = DuckDbTypes.timestamp_ms
val tsNanos: DuckDbType[LocalDateTime] = DuckDbTypes.timestamp_ns
UUID type
| DuckDB Type | Java Type |
|---|---|
UUID | java.util.UUID |
- Kotlin
- Java
- Scala
val uuidType: DuckDbType<UUID> = DuckDbTypes.uuid
DuckDbType<UUID> uuidType = DuckDbTypes.uuid;
val uuidType: DuckDbType[UUID] = DuckDbTypes.uuid
JSON type
| DuckDB Type | Java Type |
|---|---|
JSON | Json |
- Kotlin
- Java
- Scala
val jsonType: DuckDbType<Json> = DuckDbTypes.json
val data: Json = Json("{\"name\": \"DuckDB\"}")
DuckDbType<Json> jsonType = DuckDbTypes.json;
Json data = new Json("{\"name\": \"DuckDB\"}");
val jsonType: DuckDbType[Json] = DuckDbTypes.json
val data: Json = new Json("{\"name\": \"DuckDB\"}")
Enum type
- Kotlin
- Java
- Scala
// Define your Kotlin enum
enum class Status { PENDING, ACTIVE, COMPLETED }
// Create DuckDbType — reified, no arguments beyond the SQL type name
val statusType: DuckDbType<Status> = DuckDbTypes.ofEnum<Status>("status")
// Define your Java enum
public enum Status {
PENDING,
ACTIVE,
COMPLETED
}
// Create DuckDbType — pass values(), no reflection
DuckDbType<Status> statusType = DuckDbTypes.ofEnum("status", Status.values());
// Plain Scala 3 enum — no extends java.lang.Enum needed
enum Status:
case PENDING, ACTIVE, COMPLETED
// Create DuckDbType — just pass .values
val statusType: DuckDbType[Status] =
DuckDbTypes.ofEnum("status", Status.values)
sqlType must match the declared type nameThe first argument to ofEnum(sqlType, ...) is used to cast bound parameters (e.g. CAST(? AS status)) and must match the name used in CREATE TYPE status AS ENUM(...) and in the column's declared type. If the column is typed status_t but you call ofEnum("status", ...), inserts fail with Type with name status does not exist.
LIST types
Any type can be made into a variable-length list with .list(). DuckDB renders as T[] and the Java representation is List<T>:
| DuckDB Type | Java Type | Created via |
|---|---|---|
INTEGER[] | List<Integer> | integer.list() |
VARCHAR[] | List<String> | varchar.list() |
DATE[] | List<LocalDate> | date.list() |
| ... | ... | anyType.list() |
- Kotlin
- Java
- Scala
// Any type can be made into a list with .list()
val listInt: DuckDbType<List<Int>> = DuckDbTypes.integer.list()
val listStr: DuckDbType<List<String>> = DuckDbTypes.varchar.list()
val listDouble: DuckDbType<List<Double>> = DuckDbTypes.double_.list()
val listUuid: DuckDbType<List<UUID>> = DuckDbTypes.uuid.list()
val listDate: DuckDbType<List<LocalDate>> = DuckDbTypes.date.list()
val listDecimal: DuckDbType<List<BigDecimal>> = DuckDbTypes.decimal.list()
// Any type can be made into a list with .list()
DuckDbType<List<Integer>> listInt = DuckDbTypes.integer.list();
DuckDbType<List<String>> listStr = DuckDbTypes.varchar.list();
DuckDbType<List<Double>> listDouble = DuckDbTypes.double_.list();
DuckDbType<List<UUID>> listUuid = DuckDbTypes.uuid.list();
DuckDbType<List<LocalDate>> listDate = DuckDbTypes.date.list();
DuckDbType<List<BigDecimal>> listDecimal = DuckDbTypes.decimal.list();
// Any type can be made into a list with .list()
val listInt = DuckDbTypes.integer.list
val listStr = DuckDbTypes.varchar.list
val listDouble = DuckDbTypes.double_.list
val listUuid = DuckDbTypes.uuid.list
val listDate = DuckDbTypes.date.list
val listDecimal = DuckDbTypes.decimal.list
ARRAY types
Fixed-size arrays use .array(size). DuckDB enforces that every row has exactly size elements — ideal for embeddings, RGB colors, or any dense fixed-shape tensor. The Java representation is still List<T>:
| DuckDB Type | Java Type | Created via |
|---|---|---|
FLOAT[1536] | List<Float> | float_.array(1536) |
INTEGER[3] | List<Integer> | integer.array(3) |
- Kotlin
- Java
- Scala
// Fixed-size ARRAY — every row must have exactly `size` elements.
val embedding: DuckDbType<List<Float>> = DuckDbTypes.float_.array(1536)
val rgb: DuckDbType<List<Int>> = DuckDbTypes.integer.array(3)
// Fixed-size ARRAY — every row must have exactly `size` elements.
DuckDbType<List<Float>> embedding = DuckDbTypes.float_.array(1536);
DuckDbType<List<Integer>> rgb = DuckDbTypes.integer.array(3);
// Fixed-size ARRAY — every row must have exactly `size` elements.
val embedding: DuckDbType[List[Float]] = DuckDbTypes.float_.array(1536)
val rgb: DuckDbType[List[Int]] = DuckDbTypes.integer.array(3)
Nested collections
LIST and ARRAY compose freely in any combination:
| SQL Type | Java Type | Created via |
|---|---|---|
T[][] | List<List<T>> | t.list().list() |
T[m][n] | List<List<T>> | t.array(n).array(m) |
T[][n] | List<List<T>> | t.list().array(n) |
T[m][] | List<List<T>> | t.array(m).list() |
- Kotlin
- Java
- Scala
// Nested collections compose — any combination is legal.
val grid: DuckDbType<List<List<Int>>> = DuckDbTypes.integer.list().list()
val matrix: DuckDbType<List<List<Float>>> = DuckDbTypes.float_.array(3).array(3)
val variableRows: DuckDbType<List<List<String>>> = DuckDbTypes.varchar.list().array(4)
// Nested collections compose — any combination is legal.
DuckDbType<List<List<Integer>>> grid = DuckDbTypes.integer.list().list();
DuckDbType<List<List<Float>>> matrix = DuckDbTypes.float_.array(3).array(3);
DuckDbType<List<List<String>>> variableRows = DuckDbTypes.varchar.list().array(4);
// Nested collections compose — any combination is legal.
val grid: DuckDbType[List[List[Int]]] = DuckDbTypes.integer.list.list
val matrix: DuckDbType[List[List[Float]]] = DuckDbTypes.float_.array(3).array(3)
val variableRows: DuckDbType[List[List[String]]] = DuckDbTypes.varchar.list.array(4)
MAP types
DuckDB's MAP type for key-value pairs:
| DuckDB Type | Java Type |
|---|---|
MAP(VARCHAR, INTEGER) | Map<String, Integer> |
MAP(VARCHAR, VARCHAR) | Map<String, String> |
- Kotlin
- Java
- Scala
// Create map types using the mapTo() combinator
val mapStrInt: DuckDbType<Map<String, Int>> = DuckDbTypes.varchar.mapTo(DuckDbTypes.integer)
val mapStrStr: DuckDbType<Map<String, String>> = DuckDbTypes.varchar.mapTo(DuckDbTypes.varchar)
val mapUuidTime: DuckDbType<Map<UUID, LocalTime>> = DuckDbTypes.uuid.mapTo(DuckDbTypes.time)
// Works with any combination of types
val mapStrDate: DuckDbType<Map<String, LocalDate>> = DuckDbTypes.varchar.mapTo(DuckDbTypes.date)
// Create map types using the mapTo() combinator
DuckDbType<Map<String, Integer>> mapStrInt = DuckDbTypes.varchar.mapTo(DuckDbTypes.integer);
DuckDbType<Map<String, String>> mapStrStr = DuckDbTypes.varchar.mapTo(DuckDbTypes.varchar);
DuckDbType<Map<UUID, LocalTime>> mapUuidTime = DuckDbTypes.uuid.mapTo(DuckDbTypes.time);
// Works with any combination of types
DuckDbType<Map<String, LocalDate>> mapStrDate = DuckDbTypes.varchar.mapTo(DuckDbTypes.date);
// Create map types using the mapTo() combinator
val mapStrInt: DuckDbType[Map[String, Int]] = DuckDbTypes.varchar.mapTo(DuckDbTypes.integer)
val mapStrStr: DuckDbType[Map[String, String]] = DuckDbTypes.varchar.mapTo(DuckDbTypes.varchar)
val mapUuidTime: DuckDbType[Map[UUID, LocalTime]] = DuckDbTypes.uuid.mapTo(DuckDbTypes.time)
// Works with any combination of types
val mapStrDate: DuckDbType[Map[String, LocalDate]] = DuckDbTypes.varchar.mapTo(DuckDbTypes.date)
Struct types
DuckDB STRUCT types are built from a RowCodecNamed via compositeOf:
DuckDbType<Person> personType = DuckDbTypes.compositeOf("person",
RowCodec.<Person>namedBuilder()
.field("name", DuckDbTypes.varchar, Person::name)
.field("age", DuckDbTypes.integer, Person::age)
.build(Person::new));
// List of structs (variable-length)
DuckDbType<List<Person>> personListType = personType.list();
// Fixed-size array of structs (e.g. always 3 members)
DuckDbType<List<Person>> trioType = personType.array(3);
UNION types
DuckDB's UNION type for tagged unions:
// Unions are typically handled via generated code
// The variants are defined by the table schema
Nullable types
Any type can be made nullable using .opt():
- Kotlin
- Java
- Scala
val notNull: DuckDbType<Int> = DuckDbTypes.integer
val nullable: DuckDbType<Int?> = DuckDbTypes.integer.opt()
DuckDbType<Integer> notNull = DuckDbTypes.integer;
DuckDbType<Optional<Integer>> nullable = DuckDbTypes.integer.opt();
val notNull: DuckDbType[Int] = DuckDbTypes.integer
val nullable: DuckDbType[Option[Int]] = DuckDbTypes.integer.opt
Custom domain types
Wrap base types with custom Java types using transform:
- Kotlin
- Java
- Scala
// Wrapper type
data class ProductId(val value: Long)
// Create DuckDbType from bigint
val productIdType: DuckDbType<ProductId> = DuckDbTypes.bigint.transform(::ProductId, ProductId::value)
// Wrapper type
public record ProductId(Long value) {}
// Create DuckDbType from bigint
DuckDbType<ProductId> productIdType =
DuckDbTypes.bigint.transform(ProductId::new, ProductId::value);
// Wrapper type
case class ProductId(value: Long)
// Create DuckDbType from bigint
val productIdType: DuckDbType[ProductId] = DuckDbTypes.bigint.transform(ProductId.apply, _.value)