Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,24 @@
*/
public class DuckDbTypeTest {

// ==================== Wrapper Type Examples for bimap testing ====================
record UserId(int value) {}

record ProductCode(String value) {}

// A complex wrapper that contains a map internally
record Config(java.util.Map<String, Integer> settings) {}

// Bimapped types for testing MAP with wrapper keys/values
static DuckDbType<UserId> userIdType = DuckDbTypes.integer.bimap(UserId::new, UserId::value);

static DuckDbType<ProductCode> productCodeType =
DuckDbTypes.varchar.bimap(ProductCode::new, ProductCode::value);

// A type that wraps a map - bimapped from MAP(VARCHAR, INTEGER)
static DuckDbType<Config> configType =
DuckDbTypes.varchar.mapTo(DuckDbTypes.integer).bimap(Config::new, Config::settings);

// ==================== STRUCT Example ====================
record Person(String name, int age) {}

Expand Down Expand Up @@ -243,40 +261,102 @@ DuckDbTypes.json, new Json("{\"name\": \"DuckDB\", \"version\": 1.0}"))
.noIdentity(),

// ==================== MAP Types ====================
// MAP types don't support direct equality in WHERE clauses, so we mark noIdentity()
// MAP types use the mapTo() combinator. They don't support direct equality in WHERE
// clauses.
new DuckDbTypeAndExample<>(
DuckDbTypes.mapVarcharInteger(), java.util.Map.of("a", 1, "b", 2))
DuckDbTypes.varchar.mapTo(DuckDbTypes.integer), java.util.Map.of("a", 1, "b", 2))
.noIdentity(),
new DuckDbTypeAndExample<>(
DuckDbTypes.mapVarcharVarchar(),
DuckDbTypes.varchar.mapTo(DuckDbTypes.varchar),
java.util.Map.of("key1", "value1", "key2", "value2"))
.noIdentity(),
new DuckDbTypeAndExample<>(
DuckDbTypes.mapIntegerVarchar(), java.util.Map.of(1, "one", 2, "two"))
DuckDbTypes.integer.mapTo(DuckDbTypes.varchar),
java.util.Map.of(1, "one", 2, "two"))
.noIdentity(),
// MAP with UUID keys (natively supported)
// MAP with UUID keys
new DuckDbTypeAndExample<>(
DuckDbTypes.mapUuidVarchar(),
DuckDbTypes.uuid.mapTo(DuckDbTypes.varchar),
java.util.Map.of(
UUID.fromString("550e8400-e29b-41d4-a716-446655440000"), "value1",
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"), "value2"))
.noIdentity(),
// MAP with TIME values (requires String conversion)
// MAP with TIME values
new DuckDbTypeAndExample<>(
DuckDbTypes.mapVarcharTime(),
DuckDbTypes.varchar.mapTo(DuckDbTypes.time),
java.util.Map.of(
"morning", LocalTime.of(8, 15, 0),
"afternoon", LocalTime.of(14, 30, 45)))
.noIdentity(),
// MAP with UUID keys and TIME values (mixed conversion)
// MAP with UUID keys and TIME values
new DuckDbTypeAndExample<>(
DuckDbTypes.mapUuidTime(),
DuckDbTypes.uuid.mapTo(DuckDbTypes.time),
java.util.Map.of(
UUID.fromString("550e8400-e29b-41d4-a716-446655440000"),
LocalTime.of(14, 30, 45),
UUID.fromString("123e4567-e89b-12d3-a456-426614174000"),
LocalTime.of(8, 15, 0)))
.noIdentity(),
// More MAP type combinations
new DuckDbTypeAndExample<>(
DuckDbTypes.varchar.mapTo(DuckDbTypes.bigint),
java.util.Map.of("count", 100L, "total", 999999999999L))
.noIdentity(),
new DuckDbTypeAndExample<>(
DuckDbTypes.varchar.mapTo(DuckDbTypes.double_),
java.util.Map.of("pi", 3.14159, "e", 2.71828))
.noIdentity(),
new DuckDbTypeAndExample<>(
DuckDbTypes.varchar.mapTo(DuckDbTypes.boolean_),
java.util.Map.of("active", true, "verified", false))
.noIdentity(),
new DuckDbTypeAndExample<>(
DuckDbTypes.integer.mapTo(DuckDbTypes.integer),
java.util.Map.of(1, 100, 2, 200, 3, 300))
.noIdentity(),
new DuckDbTypeAndExample<>(
DuckDbTypes.varchar.mapTo(DuckDbTypes.date),
java.util.Map.of(
"start", LocalDate.of(2024, 1, 1), "end", LocalDate.of(2024, 12, 31)))
.noIdentity(),
new DuckDbTypeAndExample<>(
DuckDbTypes.varchar.mapTo(DuckDbTypes.uuid),
java.util.Map.of(
"user1", UUID.fromString("550e8400-e29b-41d4-a716-446655440000"),
"user2", UUID.fromString("123e4567-e89b-12d3-a456-426614174000")))
.noIdentity(),
// MAP with bimapped key type (UserId wrapping Integer)
new DuckDbTypeAndExample<>(
userIdType.mapTo(DuckDbTypes.varchar),
java.util.Map.of(new UserId(1), "admin", new UserId(2), "user"))
.noIdentity(),
// MAP with bimapped value type (ProductCode wrapping String)
new DuckDbTypeAndExample<>(
DuckDbTypes.integer.mapTo(productCodeType),
java.util.Map.of(1, new ProductCode("PROD-001"), 2, new ProductCode("PROD-002")))
.noIdentity(),
// MAP with bimapped key AND value types
new DuckDbTypeAndExample<>(
userIdType.mapTo(productCodeType),
java.util.Map.of(
new UserId(100), new ProductCode("SKU-A"),
new UserId(200), new ProductCode("SKU-B")))
.noIdentity(),
// MAP with Long keys
new DuckDbTypeAndExample<>(
DuckDbTypes.bigint.mapTo(DuckDbTypes.varchar),
java.util.Map.of(9999999999L, "large-key", 1L, "small-key"))
.noIdentity(),
// MAP with Double keys
new DuckDbTypeAndExample<>(
DuckDbTypes.double_.mapTo(DuckDbTypes.varchar),
java.util.Map.of(3.14, "pi", 2.71, "e"))
.noIdentity(),
// Config type directly (bimapped from MAP(VARCHAR, INTEGER))
// This tests that bimap works correctly with map types
new DuckDbTypeAndExample<>(
configType, new Config(java.util.Map.of("max_conn", 100, "min_conn", 5)))
.noIdentity(),

// ==================== LIST Types with complex element types ====================
// String-converted types (~33% overhead at 100k rows, but required for correctness)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package dev.typr.foundations;

import java.util.function.Function;

/**
* Handles conversion of values to/from DuckDB MAP entries. DuckDB JDBC returns maps with Object
* keys/values that need to be cast to the proper types, and when writing we may need to convert
* back.
*
* @param <A> the Java type
*/
public interface DuckDbMapSupport<A> {
/** Convert a raw value from a DuckDB MAP to the Java type. */
A fromMap(Object raw);

/** Convert the Java type to a value suitable for a DuckDB MAP. Usually identity. */
Object toMap(A value);

/** Create a support that just casts (for types DuckDB returns directly). */
@SuppressWarnings("unchecked")
static <A> DuckDbMapSupport<A> cast() {
return new DuckDbMapSupport<>() {
@Override
public A fromMap(Object raw) {
return (A) raw;
}

@Override
public Object toMap(A value) {
return value;
}
};
}

/** Create a support with custom conversion in both directions. */
static <A> DuckDbMapSupport<A> of(Function<Object, A> from, Function<A, Object> to) {
return new DuckDbMapSupport<>() {
@Override
public A fromMap(Object raw) {
return from.apply(raw);
}

@Override
public Object toMap(A value) {
return to.apply(value);
}
};
}

/** Create a support with custom read conversion, identity for write. */
static <A> DuckDbMapSupport<A> fromOnly(Function<Object, A> from) {
return new DuckDbMapSupport<>() {
@Override
public A fromMap(Object raw) {
return from.apply(raw);
}

@Override
public Object toMap(A value) {
return value;
}
};
}

/** Transform this support with a bijection (for bimap support). */
default <B> DuckDbMapSupport<B> bimap(Function<A, B> f, Function<B, A> g) {
DuckDbMapSupport<A> self = this;
return new DuckDbMapSupport<>() {
@Override
public B fromMap(Object raw) {
return f.apply(self.fromMap(raw));
}

@Override
public Object toMap(B value) {
return self.toMap(g.apply(value));
}
};
}
}
29 changes: 29 additions & 0 deletions foundations-jdbc/src/java/dev/typr/foundations/DuckDbRead.java
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,35 @@ static <K, V> DuckDbRead<java.util.Map<K, V>> readMap(
});
}

/**
* Read a MAP column using support objects to convert keys and values. DuckDB JDBC returns
* java.util.HashMap, and we use the support objects to convert each key/value pair.
*
* @param keySupport support for key type
* @param valueSupport support for value type
* @param <K> key type
* @param <V> value type
* @return reader for Map
*/
static <K, V> DuckDbRead<java.util.Map<K, V>> readMapWithSupport(
DuckDbMapSupport<K> keySupport, DuckDbMapSupport<V> valueSupport) {
return of(
(rs, idx) -> {
Object obj = rs.getObject(idx);
if (obj == null) return null;
if (obj instanceof java.util.Map<?, ?> rawMap) {
java.util.Map<K, V> result = new java.util.LinkedHashMap<>();
for (var entry : rawMap.entrySet()) {
K key = keySupport.fromMap(entry.getKey());
V value = valueSupport.fromMap(entry.getValue());
result.put(key, value);
}
return result;
}
throw new SQLException("Cannot convert " + obj.getClass() + " to Map");
});
}

/**
* Parse field names from a STRUCT type definition. e.g. STRUCT("name" VARCHAR, age INTEGER) ->
* ["name", "age"]
Expand Down
81 changes: 72 additions & 9 deletions foundations-jdbc/src/java/dev/typr/foundations/DuckDbType.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ public record DuckDbType<A>(
DuckDbWrite<A> write,
DuckDbStringifier<A> stringifier,
DuckDbJson<A> duckDbJson,
DuckDbText<A> duckDbText)
DuckDbText<A> duckDbText,
DuckDbMapSupport<A> mapSupport)
implements DbType<A> {
/** Constructor for backwards compatibility - uses stringifier-based text encoding. */
/** Constructor for backwards compatibility - uses cast-based map extraction. */
public DuckDbType(
DuckDbTypename<A> typename,
DuckDbRead<A> read,
Expand All @@ -28,7 +29,26 @@ public DuckDbType(
write,
stringifier,
duckDbJson,
DuckDbText.instance((a, sb) -> stringifier.unsafeEncode(a, sb, false)));
DuckDbText.instance((a, sb) -> stringifier.unsafeEncode(a, sb, false)),
DuckDbMapSupport.cast());
}

/** Constructor with custom map extractor. */
public DuckDbType(
DuckDbTypename<A> typename,
DuckDbRead<A> read,
DuckDbWrite<A> write,
DuckDbStringifier<A> stringifier,
DuckDbJson<A> duckDbJson,
DuckDbMapSupport<A> mapSupport) {
this(
typename,
read,
write,
stringifier,
duckDbJson,
DuckDbText.instance((a, sb) -> stringifier.unsafeEncode(a, sb, false)),
mapSupport);
}

@Override
Expand Down Expand Up @@ -148,11 +168,44 @@ public A[] fromJson(dev.typr.foundations.data.JsonValue json) {
}

/**
* Simple mapTo() method for DSL use - returns this type's info for use in MAP type references.
* This is used by generated DSL code for type signatures only (not actual JDBC operations).
* Create a MAP type from this key type and a value type. Uses the mapSupport to convert
* keys/values when reading from DuckDB JDBC, and SQL literal conversion for writing.
*
* @param valueType the value type for the map
* @param <V> the value type
* @return DuckDbType for Map with this type as keys and valueType as values
*/
public <V> DuckDbType<A> mapTo(DuckDbType<V> valueType) {
return this;
public <V> DuckDbType<java.util.Map<A, V>> mapTo(DuckDbType<V> valueType) {
DuckDbTypename<java.util.Map<A, V>> mapTypename = typename.mapTo(valueType.typename);
String sqlType = mapTypename.sqlType();
DuckDbRead<java.util.Map<A, V>> mapRead =
DuckDbRead.readMapWithSupport(mapSupport, valueType.mapSupport);
DuckDbWrite<java.util.Map<A, V>> mapWrite =
DuckDbWrite.writeMapViaSqlLiteral(sqlType, stringifier, valueType.stringifier);
DuckDbStringifier<java.util.Map<A, V>> mapStringifier =
DuckDbStringifier.instance(
(map, sb, quoted) -> {
if (map.isEmpty()) {
sb.append("{}");
return;
}
sb.append("{");
boolean first = true;
for (var entry : map.entrySet()) {
if (!first) sb.append(", ");
first = false;
stringifier.unsafeEncode(entry.getKey(), sb, true);
sb.append(": ");
valueType.stringifier.unsafeEncode(entry.getValue(), sb, true);
}
sb.append("}");
});
return new DuckDbType<>(
mapTypename,
mapRead,
mapWrite,
mapStringifier,
DuckDbTypes.mapJson(duckDbJson, valueType.duckDbJson));
}

/**
Expand Down Expand Up @@ -411,7 +464,16 @@ public <B> DuckDbType<B> bimap(SqlFunction<A, B> f, Function<B, A> g) {
read.map(f),
write.contramap(g),
stringifier.contramap(g),
duckDbJson.bimap(f, g));
duckDbJson.bimap(f, g),
mapSupport.bimap(
a -> {
try {
return f.apply(a);
} catch (java.sql.SQLException e) {
throw new RuntimeException(e);
}
},
g));
}

@Override
Expand All @@ -421,7 +483,8 @@ public <B> DuckDbType<B> to(dev.typr.foundations.dsl.Bijection<A, B> bijection)
read.map(bijection::underlying),
write.contramap(bijection::from),
stringifier.contramap(bijection::from),
duckDbJson.bimap(bijection::underlying, bijection::from));
duckDbJson.bimap(bijection::underlying, bijection::from),
mapSupport.bimap(bijection::underlying, bijection::from));
}

public static <A> DuckDbType<A> of(
Expand Down
Loading
Loading