diff --git a/src/jni/duckdb_java.cpp b/src/jni/duckdb_java.cpp index bf2d7523..58d39b06 100644 --- a/src/jni/duckdb_java.cpp +++ b/src/jni/duckdb_java.cpp @@ -40,6 +40,7 @@ static jclass J_Float; static jclass J_Double; static jclass J_String; static jclass J_Timestamp; +static jmethodID J_Timestamp_valueOf; static jclass J_TimestampTZ; static jclass J_Decimal; static jclass J_ByteArray; @@ -70,11 +71,22 @@ static jfieldID J_DuckVector_varlen; static jclass J_DuckArray; static jmethodID J_DuckArray_init; +static jclass J_Struct; +static jmethodID J_Struct_getSQLTypeName; +static jmethodID J_Struct_getAttributes; + +static jclass J_Array; +static jmethodID J_Array_getBaseTypeName; +static jmethodID J_Array_getArray; + static jclass J_DuckStruct; static jmethodID J_DuckStruct_init; static jclass J_ByteBuffer; +static jclass J_DuckMap; +static jmethodID J_DuckMap_getSQLTypeName; + static jmethodID J_Map_entrySet; static jmethodID J_Set_iterator; static jmethodID J_Iterator_hasNext; @@ -89,6 +101,7 @@ static jmethodID J_UUID_getLeastSignificantBits; static jclass J_DuckDBDate; static jmethodID J_DuckDBDate_getDaysSinceEpoch; +static jclass J_Object; static jmethodID J_Object_toString; static jclass J_DuckDBTime; @@ -163,9 +176,12 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { tmpLocalRef = env->FindClass("java/lang/String"); J_String = (jclass)env->NewGlobalRef(tmpLocalRef); env->DeleteLocalRef(tmpLocalRef); + tmpLocalRef = env->FindClass("org/duckdb/DuckDBTimestamp"); J_Timestamp = (jclass)env->NewGlobalRef(tmpLocalRef); env->DeleteLocalRef(tmpLocalRef); + J_Timestamp_valueOf = env->GetStaticMethodID(J_Timestamp, "valueOf", "(Ljava/lang/Object;)Ljava/lang/Object;"); + tmpLocalRef = env->FindClass("org/duckdb/DuckDBTimestampTZ"); J_TimestampTZ = (jclass)env->NewGlobalRef(tmpLocalRef); env->DeleteLocalRef(tmpLocalRef); @@ -183,6 +199,11 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { J_ByteArray = (jclass)env->NewGlobalRef(tmpLocalRef); env->DeleteLocalRef(tmpLocalRef); + J_DuckMap = GetClassRef(env, "org/duckdb/user/DuckDBMap"); + D_ASSERT(J_DuckMap); + J_DuckMap_getSQLTypeName = env->GetMethodID(J_DuckMap, "getSQLTypeName", "()Ljava/lang/String;"); + D_ASSERT(J_DuckMap_getSQLTypeName); + tmpLocalRef = env->FindClass("java/util/Map"); J_Map_entrySet = env->GetMethodID(tmpLocalRef, "entrySet", "()Ljava/util/Set;"); env->DeleteLocalRef(tmpLocalRef); @@ -209,13 +230,21 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { D_ASSERT(J_DuckArray_init); env->DeleteLocalRef(tmpLocalRef); - tmpLocalRef = env->FindClass("org/duckdb/DuckDBStruct"); - D_ASSERT(tmpLocalRef); - J_DuckStruct = (jclass)env->NewGlobalRef(tmpLocalRef); + J_DuckStruct = GetClassRef(env, "org/duckdb/DuckDBStruct"); J_DuckStruct_init = env->GetMethodID(J_DuckStruct, "", "([Ljava/lang/String;[Lorg/duckdb/DuckDBVector;ILjava/lang/String;)V"); D_ASSERT(J_DuckStruct_init); - env->DeleteLocalRef(tmpLocalRef); + + J_Struct = GetClassRef(env, "java/sql/Struct"); + J_Struct_getSQLTypeName = env->GetMethodID(J_Struct, "getSQLTypeName", "()Ljava/lang/String;"); + J_Struct_getAttributes = env->GetMethodID(J_Struct, "getAttributes", "()[Ljava/lang/Object;"); + + J_Array = GetClassRef(env, "java/sql/Array"); + J_Array_getArray = env->GetMethodID(J_Array, "getArray", "()Ljava/lang/Object;"); + J_Array_getBaseTypeName = env->GetMethodID(J_Array, "getBaseTypeName", "()Ljava/lang/String;"); + + J_Object = GetClassRef(env, "java/lang/Object"); + J_Object_toString = env->GetMethodID(J_Object, "toString", "()Ljava/lang/String;"); tmpLocalRef = env->FindClass("java/util/Map$Entry"); J_Entry_getKey = env->GetMethodID(tmpLocalRef, "getKey", "()Ljava/lang/Object;"); @@ -559,11 +588,120 @@ struct ResultHolder { duckdb::unique_ptr chunk; }; +Value ToValue(JNIEnv *env, jobject param, duckdb::shared_ptr context) { + param = env->CallStaticObjectMethod(J_Timestamp, J_Timestamp_valueOf, param); + + if (param == nullptr) { + return (Value()); + } else if (env->IsInstanceOf(param, J_Bool)) { + return (Value::BOOLEAN(env->CallBooleanMethod(param, J_Bool_booleanValue))); + } else if (env->IsInstanceOf(param, J_Byte)) { + return (Value::TINYINT(env->CallByteMethod(param, J_Byte_byteValue))); + } else if (env->IsInstanceOf(param, J_Short)) { + return (Value::SMALLINT(env->CallShortMethod(param, J_Short_shortValue))); + } else if (env->IsInstanceOf(param, J_Int)) { + return (Value::INTEGER(env->CallIntMethod(param, J_Int_intValue))); + } else if (env->IsInstanceOf(param, J_Long)) { + return (Value::BIGINT(env->CallLongMethod(param, J_Long_longValue))); + } else if (env->IsInstanceOf(param, J_TimestampTZ)) { // Check for subclass before superclass! + return ( + Value::TIMESTAMPTZ((timestamp_t)env->CallLongMethod(param, J_TimestampTZ_getMicrosEpoch))); + } else if (env->IsInstanceOf(param, J_DuckDBDate)) { + return ( + Value::DATE((date_t)env->CallLongMethod(param, J_DuckDBDate_getDaysSinceEpoch))); + + } else if (env->IsInstanceOf(param, J_DuckDBTime)) { + return (Value::TIME((dtime_t)env->CallLongMethod(param, J_Timestamp_getMicrosEpoch))); + } else if (env->IsInstanceOf(param, J_Timestamp)) { + return ( + Value::TIMESTAMP((timestamp_t)env->CallLongMethod(param, J_Timestamp_getMicrosEpoch))); + } else if (env->IsInstanceOf(param, J_Float)) { + return (Value::FLOAT(env->CallFloatMethod(param, J_Float_floatValue))); + } else if (env->IsInstanceOf(param, J_Double)) { + return (Value::DOUBLE(env->CallDoubleMethod(param, J_Double_doubleValue))); + } else if (env->IsInstanceOf(param, J_Decimal)) { + Value val = create_value_from_bigdecimal(env, param); + return (val); + } else if (env->IsInstanceOf(param, J_String)) { + auto param_string = jstring_to_string(env, (jstring)param); + return (Value(param_string)); + } else if (env->IsInstanceOf(param, J_ByteArray)) { + return (Value::BLOB_RAW(byte_array_to_string(env, (jbyteArray)param))); + } else if (env->IsInstanceOf(param, J_UUID)) { + auto most_significant = (jlong)env->CallObjectMethod(param, J_UUID_getMostSignificantBits); + auto least_significant = (jlong)env->CallObjectMethod(param, J_UUID_getLeastSignificantBits); + return (Value::UUID(hugeint_t(most_significant, least_significant))); + } else if (env->IsInstanceOf(param, J_DuckMap)) { + auto typeName = jstring_to_string(env, (jstring)env->CallObjectMethod(param, J_DuckMap_getSQLTypeName)); + + LogicalType type; + context->RunFunctionInTransaction([&]() { type = TransformStringToLogicalType(typeName, *context); }); + + auto entrySet = env->CallObjectMethod(param, J_Map_entrySet); + auto iterator = env->CallObjectMethod(entrySet, J_Set_iterator); + duckdb::vector entries; + while (env->CallBooleanMethod(iterator, J_Iterator_hasNext)) { + auto entry = env->CallObjectMethod(iterator, J_Iterator_next); + + auto key = env->CallObjectMethod(entry, J_Entry_getKey); + auto value = env->CallObjectMethod(entry, J_Entry_getValue); + D_ASSERT(key); + D_ASSERT(value); + + entries.push_back(Value::STRUCT({{"key", ToValue(env, key, context)}, {"value", ToValue(env, value, context)}})); + } + + return (Value::MAP(ListType::GetChildType(type), entries)); + + } else if (env->IsInstanceOf(param, J_Struct)) { + auto typeName = jstring_to_string(env, (jstring)env->CallObjectMethod(param, J_Struct_getSQLTypeName)); + + LogicalType type; + context->RunFunctionInTransaction([&]() { type = TransformStringToLogicalType(typeName, *context); }); + + auto jvalues = (jobjectArray)env->CallObjectMethod(param, J_Struct_getAttributes); + + int size = env->GetArrayLength(jvalues); + + child_list_t values; + + for (int i = 0; i < size; i++) { + auto name = StructType::GetChildName(type, i); + + auto value = env->GetObjectArrayElement(jvalues, i); + + values.emplace_back(name, ToValue(env, value, context)); + } + + return (Value::STRUCT(std::move(values))); + } else if (env->IsInstanceOf(param, J_Array)) { + auto typeName = jstring_to_string(env, (jstring)env->CallObjectMethod(param, J_Array_getBaseTypeName)); + auto jvalues = (jobjectArray)env->CallObjectMethod(param, J_Array_getArray); + int size = env->GetArrayLength(jvalues); + + LogicalType type; + context->RunFunctionInTransaction([&]() { type = TransformStringToLogicalType(typeName, *context); }); + + duckdb::vector values; + for (int i = 0; i < size; i++) { + auto value = env->GetObjectArrayElement(jvalues, i); + + values.emplace_back(ToValue(env, value, context)); + } + + return (Value::LIST(type, values)); + + } else { + throw InvalidInputException("Unsupported parameter type"); + } +} + jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectArray params) { auto stmt_ref = (StatementHolder *)env->GetDirectBufferAddress(stmt_ref_buf); if (!stmt_ref) { throw InvalidInputException("Invalid statement"); } + auto res_ref = make_uniq(); duckdb::vector duckdb_params; @@ -572,64 +710,12 @@ jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectA throw InvalidInputException("Parameter count mismatch"); } + auto &context = stmt_ref->stmt->context; + if (param_len > 0) { for (idx_t i = 0; i < param_len; i++) { auto param = env->GetObjectArrayElement(params, i); - if (param == nullptr) { - duckdb_params.push_back(Value()); - continue; - } else if (env->IsInstanceOf(param, J_Bool)) { - duckdb_params.push_back(Value::BOOLEAN(env->CallBooleanMethod(param, J_Bool_booleanValue))); - continue; - } else if (env->IsInstanceOf(param, J_Byte)) { - duckdb_params.push_back(Value::TINYINT(env->CallByteMethod(param, J_Byte_byteValue))); - continue; - } else if (env->IsInstanceOf(param, J_Short)) { - duckdb_params.push_back(Value::SMALLINT(env->CallShortMethod(param, J_Short_shortValue))); - continue; - } else if (env->IsInstanceOf(param, J_Int)) { - duckdb_params.push_back(Value::INTEGER(env->CallIntMethod(param, J_Int_intValue))); - continue; - } else if (env->IsInstanceOf(param, J_Long)) { - duckdb_params.push_back(Value::BIGINT(env->CallLongMethod(param, J_Long_longValue))); - continue; - } else if (env->IsInstanceOf(param, J_TimestampTZ)) { // Check for subclass before superclass! - duckdb_params.push_back( - Value::TIMESTAMPTZ((timestamp_t)env->CallLongMethod(param, J_TimestampTZ_getMicrosEpoch))); - continue; - } else if (env->IsInstanceOf(param, J_DuckDBDate)) { - duckdb_params.push_back( - Value::DATE((date_t)env->CallLongMethod(param, J_DuckDBDate_getDaysSinceEpoch))); - - } else if (env->IsInstanceOf(param, J_DuckDBTime)) { - duckdb_params.push_back(Value::TIME((dtime_t)env->CallLongMethod(param, J_Timestamp_getMicrosEpoch))); - - } else if (env->IsInstanceOf(param, J_Timestamp)) { - duckdb_params.push_back( - Value::TIMESTAMP((timestamp_t)env->CallLongMethod(param, J_Timestamp_getMicrosEpoch))); - continue; - } else if (env->IsInstanceOf(param, J_Float)) { - duckdb_params.push_back(Value::FLOAT(env->CallFloatMethod(param, J_Float_floatValue))); - continue; - } else if (env->IsInstanceOf(param, J_Double)) { - duckdb_params.push_back(Value::DOUBLE(env->CallDoubleMethod(param, J_Double_doubleValue))); - continue; - } else if (env->IsInstanceOf(param, J_Decimal)) { - Value val = create_value_from_bigdecimal(env, param); - duckdb_params.push_back(val); - continue; - } else if (env->IsInstanceOf(param, J_String)) { - auto param_string = jstring_to_string(env, (jstring)param); - duckdb_params.push_back(Value(param_string)); - } else if (env->IsInstanceOf(param, J_ByteArray)) { - duckdb_params.push_back(Value::BLOB_RAW(byte_array_to_string(env, (jbyteArray)param))); - } else if (env->IsInstanceOf(param, J_UUID)) { - auto most_significant = (jlong)env->CallObjectMethod(param, J_UUID_getMostSignificantBits); - auto least_significant = (jlong)env->CallObjectMethod(param, J_UUID_getLeastSignificantBits); - duckdb_params.push_back(Value::UUID(hugeint_t(most_significant, least_significant))); - } else { - throw InvalidInputException("Unsupported parameter type"); - } + duckdb_params.push_back(ToValue(env, param, context)); } } diff --git a/src/main/java/org/duckdb/DuckDBConnection.java b/src/main/java/org/duckdb/DuckDBConnection.java index 1a5b0e53..e058bdf5 100644 --- a/src/main/java/org/duckdb/DuckDBConnection.java +++ b/src/main/java/org/duckdb/DuckDBConnection.java @@ -1,5 +1,9 @@ package org.duckdb; +import org.duckdb.user.DuckDBMap; +import org.duckdb.user.DuckDBUserArray; +import org.duckdb.user.DuckDBUserStruct; + import java.lang.reflect.InvocationTargetException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -327,11 +331,15 @@ public Properties getClientInfo() throws SQLException { } public Array createArrayOf(String typeName, Object[] elements) throws SQLException { - throw new SQLFeatureNotSupportedException("createArrayOf"); + return new DuckDBUserArray(typeName, elements); } public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - throw new SQLFeatureNotSupportedException("createStruct"); + return new DuckDBUserStruct(typeName, attributes); + } + + public Map createMap(String typeName, Map map) { + return new DuckDBMap<>(typeName, map); } public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { diff --git a/src/main/java/org/duckdb/DuckDBPreparedStatement.java b/src/main/java/org/duckdb/DuckDBPreparedStatement.java index 6488ae0e..c7994651 100644 --- a/src/main/java/org/duckdb/DuckDBPreparedStatement.java +++ b/src/main/java/org/duckdb/DuckDBPreparedStatement.java @@ -239,18 +239,6 @@ public void setObject(int parameterIndex, Object x) throws SQLException { if (params.length == 0) { params = new Object[getParameterMetaData().getParameterCount()]; } - // Change sql.Timestamp to DuckDBTimestamp - if (x instanceof Timestamp) { - x = new DuckDBTimestamp((Timestamp) x); - } else if (x instanceof LocalDateTime) { - x = new DuckDBTimestamp((LocalDateTime) x); - } else if (x instanceof OffsetDateTime) { - x = new DuckDBTimestampTZ((OffsetDateTime) x); - } else if (x instanceof Date) { - x = new DuckDBDate((Date) x); - } else if (x instanceof Time) { - x = new DuckDBTime((Time) x); - } params[parameterIndex - 1] = x; } diff --git a/src/main/java/org/duckdb/DuckDBTimestamp.java b/src/main/java/org/duckdb/DuckDBTimestamp.java index b0b8f492..8bd4f217 100644 --- a/src/main/java/org/duckdb/DuckDBTimestamp.java +++ b/src/main/java/org/duckdb/DuckDBTimestamp.java @@ -1,6 +1,8 @@ package org.duckdb; import java.sql.Timestamp; +import java.sql.Time; +import java.sql.Date; import java.time.ZoneOffset; import java.time.Instant; import java.time.LocalDateTime; @@ -99,6 +101,23 @@ public static long localDateTime2Micros(LocalDateTime localDateTime) { return DuckDBTimestamp.RefLocalDateTime.until(localDateTime, ChronoUnit.MICROS); } + // TODO: move this to C++ side + public static Object valueOf(Object x) { + // Change sql.Timestamp to DuckDBTimestamp + if (x instanceof Timestamp) { + x = new DuckDBTimestamp((Timestamp) x); + } else if (x instanceof LocalDateTime) { + x = new DuckDBTimestamp((LocalDateTime) x); + } else if (x instanceof OffsetDateTime) { + x = new DuckDBTimestampTZ((OffsetDateTime) x); + } else if (x instanceof Date) { + x = new DuckDBDate((Date) x); + } else if (x instanceof Time) { + x = new DuckDBTime((Time) x); + } + return x; + } + public Timestamp toSqlTimestamp() { return Timestamp.valueOf(this.toLocalDateTime()); } diff --git a/src/main/java/org/duckdb/user/DuckDBMap.java b/src/main/java/org/duckdb/user/DuckDBMap.java new file mode 100644 index 00000000..b294f817 --- /dev/null +++ b/src/main/java/org/duckdb/user/DuckDBMap.java @@ -0,0 +1,17 @@ +package org.duckdb.user; + +import java.util.HashMap; +import java.util.Map; + +public class DuckDBMap extends HashMap { + private final String typeName; + + public DuckDBMap(String typeName, Map map) { + super(map); + this.typeName = typeName; + } + + public String getSQLTypeName() { + return typeName; + } +} diff --git a/src/main/java/org/duckdb/user/DuckDBUserArray.java b/src/main/java/org/duckdb/user/DuckDBUserArray.java new file mode 100644 index 00000000..8be20c4f --- /dev/null +++ b/src/main/java/org/duckdb/user/DuckDBUserArray.java @@ -0,0 +1,72 @@ +package org.duckdb.user; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Map; + +public class DuckDBUserArray implements Array { + private final String typeName; + private final Object[] elements; + + public DuckDBUserArray(String typeName, Object[] elements) { + this.typeName = typeName; + this.elements = elements; + } + + @Override + public String getBaseTypeName() throws SQLException { + return typeName; + } + + @Override + public int getBaseType() throws SQLException { + throw new SQLFeatureNotSupportedException("getBaseType"); + } + + @Override + public Object getArray() throws SQLException { + return elements; + } + + @Override + public Object getArray(Map> map) throws SQLException { + return getArray(); + } + + @Override + public Object getArray(long index, int count) throws SQLException { + throw new SQLFeatureNotSupportedException("getArray"); + } + + @Override + public Object getArray(long index, int count, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getArray"); + } + + @Override + public ResultSet getResultSet() throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet"); + } + + @Override + public ResultSet getResultSet(Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet"); + } + + @Override + public ResultSet getResultSet(long index, int count) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet"); + } + + @Override + public ResultSet getResultSet(long index, int count, Map> map) throws SQLException { + throw new SQLFeatureNotSupportedException("getResultSet"); + } + + @Override + public void free() throws SQLException { + // no-op + } +} diff --git a/src/main/java/org/duckdb/user/DuckDBUserStruct.java b/src/main/java/org/duckdb/user/DuckDBUserStruct.java new file mode 100644 index 00000000..379a24ae --- /dev/null +++ b/src/main/java/org/duckdb/user/DuckDBUserStruct.java @@ -0,0 +1,30 @@ +package org.duckdb.user; + +import java.sql.SQLException; +import java.sql.Struct; +import java.util.Map; + +public class DuckDBUserStruct implements Struct { + private final String typeName; + private final Object[] attributes; + + public DuckDBUserStruct(String typeName, Object[] attributes) { + this.typeName = typeName; + this.attributes = attributes; + } + + @Override + public String getSQLTypeName() throws SQLException { + return typeName; + } + + @Override + public Object[] getAttributes() throws SQLException { + return attributes; + } + + @Override + public Object[] getAttributes(Map> map) throws SQLException { + return getAttributes(); + } +} diff --git a/src/test/java/org/duckdb/TestDuckDBJDBC.java b/src/test/java/org/duckdb/TestDuckDBJDBC.java index 4e418e13..4621b541 100644 --- a/src/test/java/org/duckdb/TestDuckDBJDBC.java +++ b/src/test/java/org/duckdb/TestDuckDBJDBC.java @@ -8,7 +8,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; + +import java.util.Arrays; + import java.sql.*; + import java.util.concurrent.Future; import java.time.Instant; import java.time.LocalDate; @@ -3437,6 +3441,80 @@ public static void test_structs() throws Exception { Struct struct = (Struct) resultSet.getObject(1); assertEquals(toJavaObject(struct), mapOf("a", 1)); assertEquals(struct.getSQLTypeName(), "STRUCT(a INTEGER)"); + + String definition = "STRUCT(i INTEGER, j INTEGER)"; + String typeName = "POINT"; + try (PreparedStatement stmt = + connection.prepareStatement("CREATE TYPE " + typeName + " AS " + definition)) { + stmt.execute(); + } + + testStruct(connection, connection.createStruct(definition, new Object[] {1, 2})); + testStruct(connection, connection.createStruct(typeName, new Object[] {1, 2})); + } + } + + public static void test_struct_with_timestamp() throws Exception { + try (Connection connection = DriverManager.getConnection(JDBC_URL)) { + LocalDateTime now = LocalDateTime.of(LocalDate.of(2020, 5, 12), LocalTime.of(16, 20, 0, 0)); + Struct struct1 = connection.createStruct("STRUCT(start TIMESTAMP)", new Object[] {now}); + + try (PreparedStatement stmt = connection.prepareStatement("SELECT ?")) { + stmt.setObject(1, struct1); + + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + + Struct result = (Struct) rs.getObject(1); + + assertEquals(Timestamp.valueOf(now), result.getAttributes()[0]); + } + } + } + } + + public static void test_struct_with_bad_type() throws Exception { + try (Connection connection = DriverManager.getConnection(JDBC_URL)) { + Struct struct1 = connection.createStruct("BAD TYPE NAME", new Object[0]); + + try (PreparedStatement stmt = connection.prepareStatement("SELECT ?")) { + stmt.setObject(1, struct1); + String message = assertThrows(stmt::executeQuery, SQLException.class); + + assertTrue(message.contains("Parser Error: syntax error at or near \"TYPE\"")); + } + } + } + + private static void testStruct(Connection connection, Struct struct) throws SQLException, Exception { + try (PreparedStatement stmt = connection.prepareStatement("SELECT ?")) { + stmt.setObject(1, struct); + + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + + Struct result = (Struct) rs.getObject(1); + + assertEquals(Arrays.asList(1, 2), Arrays.asList(result.getAttributes())); + } + } + } + + public static void test_write_map() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class)) { + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE test (thing MAP(string, integer));"); + } + Map map = mapOf("hello", 42); + try (PreparedStatement stmt = conn.prepareStatement("INSERT INTO test VALUES (?)")) { + stmt.setObject(1, conn.createMap("MAP(string, integer)", map)); + assertEquals(stmt.executeUpdate(), 1); + } + try (PreparedStatement stmt = conn.prepareStatement("FROM test"); ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + assertEquals(rs.getObject(1), map); + } + } } } @@ -3488,6 +3566,16 @@ public static void test_list() throws Exception { assertTrue(rs.next()); assertEquals(arrayToList(rs.getArray(1)), singletonList(new BigDecimal("0.000"))); } + try (PreparedStatement stmt = connection.prepareStatement("select ?")) { + Array array = connection.createArrayOf("INTEGER", new Object[] {1}); + + stmt.setObject(1, array); + + try (ResultSet rs = stmt.executeQuery()) { + assertTrue(rs.next()); + assertEquals(singletonList(1), arrayToList(rs.getArray(1))); + } + } } }