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
2 changes: 1 addition & 1 deletion META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 17 additions & 9 deletions src/jni/duckdb_java.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vector> cast_vec;

switch (vec.GetType().id()) {
case LogicalTypeId::BOOLEAN:
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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<Vector>(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<string_t>(vec);
const string_t *vec_data = FlatVector::GetData<string_t>(*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);
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/duckdb/DuckDBResultSetMetaData.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/org/duckdb/TestClosure.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
51 changes: 51 additions & 0 deletions src/test/java/org/duckdb/TestResults.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down
Loading