diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF index f6d915f92..ac399ad45 100644 --- a/META-INF/MANIFEST.MF +++ b/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: DuckDB JDBC Driver Bundle-SymbolicName: org.duckdb.duckdb_jdbc -Bundle-Version: 2.0.0.0 +Bundle-Version: 2.0.0.1 Bundle-Vendor: DuckDB Labs Bundle-Description: A JDBC-compliant driver for the DuckDB data management system Bundle-License: https://raw.githubusercontent.com/duckdb/duckdb/main/LICENSE diff --git a/src/jni/duckdb_java.cpp b/src/jni/duckdb_java.cpp index 659f6878a..9f5eda067 100644 --- a/src/jni/duckdb_java.cpp +++ b/src/jni/duckdb_java.cpp @@ -689,6 +689,8 @@ jobject ProcessVector(JNIEnv *env, Connection *conn_ref, Vector &vec, idx_t row_ jobject constlen_data = nullptr; jobjectArray varlen_data = nullptr; + Vector *string_vec = &vec; + duckdb::unique_ptr cast_vec; switch (vec.GetType().id()) { case LogicalTypeId::BOOLEAN: @@ -783,7 +785,8 @@ jobject ProcessVector(JNIEnv *env, Connection *conn_ref, Vector &vec, idx_t row_ } break; case LogicalTypeId::UNION: - case LogicalTypeId::STRUCT: { + case LogicalTypeId::STRUCT: + case LogicalTypeId::TUPLE: { varlen_data = env->NewObjectArray(row_count, J_DuckStruct, nullptr); auto &entries = StructVector::GetEntries(vec); @@ -794,7 +797,10 @@ jobject ProcessVector(JNIEnv *env, Connection *conn_ref, Vector &vec, idx_t row_ jobject j_vec = ProcessVector(env, conn_ref, entries[entry_i], row_count); env->SetObjectArrayElement(columns, entry_i, j_vec); env->DeleteLocalRef(j_vec); - jstring jstr = env->NewStringUTF(StructType::GetChildName(vec.GetType(), entry_i).c_str()); + // TUPLE members are unnamed, use the positional names synthesized by DuckDB: "element1", "element2", ... + auto &child_name = StructType::GetChildName(vec.GetType(), entry_i); + std::string name = child_name.empty() ? TupleType::GetChildName(entry_i) : child_name.GetIdentifierName(); + jstring jstr = env->NewStringUTF(name.c_str()); env->SetObjectArrayElement(names, entry_i, jstr); env->DeleteLocalRef(jstr); } @@ -904,21 +910,23 @@ jobject ProcessVector(JNIEnv *env, Connection *conn_ref, Vector &vec, idx_t row_ break; } default: { - Vector string_vec(LogicalType::VARCHAR); - VectorOperations::Cast(*conn_ref->context, vec, string_vec, row_count); - vec.ReferenceAndSetType(string_vec); - if (vec.GetVectorType() != VectorType::FLAT_VECTOR) { - vec.Flatten(); + // the source vector must not be modified, it is a part of the result chunk that can be cast again + // in getString() (with this vector as a child of a nested type) + cast_vec = make_uniq(LogicalType::VARCHAR); + VectorOperations::Cast(*conn_ref->context, vec, *cast_vec, row_count); + if (cast_vec->GetVectorType() != VectorType::FLAT_VECTOR) { + cast_vec->Flatten(); } + string_vec = cast_vec.get(); // fall through on purpose } case LogicalTypeId::VARCHAR: varlen_data = env->NewObjectArray(row_count, J_String, nullptr); for (idx_t row_idx = 0; row_idx < row_count; row_idx++) { - if (FlatVector::IsNull(vec, row_idx)) { + if (FlatVector::IsNull(*string_vec, row_idx)) { continue; } - const string_t *vec_data = FlatVector::GetData(vec); + const string_t *vec_data = FlatVector::GetData(*string_vec); const string_t d_str = vec_data[row_idx]; auto j_str = decode_charbuffer_to_jstring(env, d_str.GetData(), d_str.GetSize()); env->SetObjectArrayElement(varlen_data, row_idx, j_str); diff --git a/src/main/java/org/duckdb/DuckDBResultSetMetaData.java b/src/main/java/org/duckdb/DuckDBResultSetMetaData.java index 7366ebbc1..2a57dc38f 100644 --- a/src/main/java/org/duckdb/DuckDBResultSetMetaData.java +++ b/src/main/java/org/duckdb/DuckDBResultSetMetaData.java @@ -78,7 +78,8 @@ public static DuckDBColumnType TypeNameToType(String type_name) { return DuckDBColumnType.TIME_WITH_TIME_ZONE; } else if (type_name.equals("TIMESTAMP WITH TIME ZONE")) { return DuckDBColumnType.TIMESTAMP_WITH_TIME_ZONE; - } else if (type_name.startsWith("STRUCT")) { + } else if (type_name.startsWith("STRUCT") || type_name.startsWith("TUPLE")) { + // TUPLE is an unnamed STRUCT return DuckDBColumnType.STRUCT; } else if (type_name.startsWith("MAP")) { return DuckDBColumnType.MAP; diff --git a/src/test/java/org/duckdb/TestClosure.java b/src/test/java/org/duckdb/TestClosure.java index f4e4c2a0d..6f81a034f 100644 --- a/src/test/java/org/duckdb/TestClosure.java +++ b/src/test/java/org/duckdb/TestClosure.java @@ -463,7 +463,7 @@ public static void test_results_fetch_no_hang() throws Exception { ResultSet rs = stmt.executeQuery("SELECT i, i::VARCHAR FROM range(0, " + rowsCount + ") AS t(i)")) { executor.submit(() -> { try { - Thread.sleep(100); + Thread.sleep(200); conn.close(); } catch (Exception e) { e.printStackTrace(); diff --git a/src/test/java/org/duckdb/TestResults.java b/src/test/java/org/duckdb/TestResults.java index c7125f16e..50d00b0a0 100644 --- a/src/test/java/org/duckdb/TestResults.java +++ b/src/test/java/org/duckdb/TestResults.java @@ -327,6 +327,57 @@ public static void test_results_strings_cast() throws Exception { } } + public static void test_results_strings_cast_nested() throws Exception { + String[] values = new String[] {// recently added types + "'123456789012345678901234567890'::BIGNUM", "'12:34:56.123456789'::TIME_NS", + "'POINT (1 2)'::GEOMETRY", "{'a': 1}::VARIANT", + "'2024-01-01 12:00:00.123456789+00'::TIMESTAMPTZ_NS", + // composite types + "row(1, 'x')", "[1, NULL]", "array_value(1, 2)", "{'a': 1, 'b': NULL}", + "MAP {1: 'x'}", "union_value(u := 1)"}; + String[] wrappers = new String[] { + "[%s, NULL]", "array_value(%s, NULL)", "{'k': %s, 'n': NULL}", "MAP {1: %s, 2: NULL}", + "union_value(u := %s)", "row(%s, NULL)", "[[%s], NULL]", "[{'k': %s}]"}; + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { + for (String value : values) { + for (String wrapper : wrappers) { + String expr = String.format(wrapper, value); + try (ResultSet rs = stmt.executeQuery("SELECT " + expr + ", CAST(" + expr + " AS VARCHAR)")) { + assertTrue(rs.next()); + assertNotNull(rs.getObject(1)); + assertEquals(rs.getString(1), rs.getString(2), expr); + } + } + } + } + } + + public static void test_results_tuple() throws Exception { + try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT row(1, 'x'), [row(1, 'x'), NULL]")) { + ResultSetMetaData meta = rs.getMetaData(); + assertEquals(meta.getColumnType(1), Types.STRUCT); + assertEquals(meta.getColumnTypeName(1), "TUPLE(INTEGER, VARCHAR)"); + assertEquals(meta.getColumnType(2), Types.ARRAY); + assertEquals(meta.getColumnTypeName(2), "TUPLE(INTEGER, VARCHAR)[]"); + assertTrue(rs.next()); + + DuckDBStruct struct = (DuckDBStruct) rs.getObject(1); + assertEquals(struct.getSQLTypeName(), "TUPLE(INTEGER, VARCHAR)"); + assertEquals(struct.getAttributes().length, 2); + assertEquals(struct.getMap().get("element1"), 1); + assertEquals(struct.getMap().get("element2"), "x"); + assertEquals(rs.getString(1), "(1, x)"); + + Object[] list = (Object[]) rs.getArray(2).getArray(); + assertEquals(list.length, 2); + assertEquals(((DuckDBStruct) list[0]).getMap().get("element1"), 1); + assertEquals(((DuckDBStruct) list[0]).getMap().get("element2"), "x"); + assertNull(list[1]); + assertEquals(rs.getString(2), "[(1, x), NULL]"); + } + } + public static void test_results_precision() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { try (ResultSet rs = stmt.executeQuery("SELECT TRUE::BOOLEAN")) {