From e6b2d6c587fbc9113173cf241356175355ed1801 Mon Sep 17 00:00:00 2001 From: Alex Kasko Date: Thu, 14 May 2026 17:19:48 +0100 Subject: [PATCH] Support fetching query results in chunks This PR allows to read a query result as a lazily-fetched sequence of [data chunks](https://github.com/duckdb/duckdb-java/blob/32d68a448d27f00e0e86f59e454dcf7a674e9cc8/src/duckdb/src/include/duckdb/common/types/data_chunk.hpp#L26-L30). It effectively exposes the [duckdb_fetch_chunk](https://github.com/duckdb/duckdb-java/blob/32d68a448d27f00e0e86f59e454dcf7a674e9cc8/src/duckdb/src/include/duckdb.h#L5376-L5386) call to Java allowing to read the results in batches avoiding the per-row overhead mandated by the JDBC specification. For accessing the chunks contents the same `DuckDBDataChunkReader` API is used as with [Java user-defined functions](https://github.com/duckdb/duckdb-java/blob/32d68a448d27f00e0e86f59e454dcf7a674e9cc8/UDF.MD). Usage example: ```java try (DuckDBConnection conn = DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class); DuckDBPreparedStatement ps = conn.prepare("SELECT ? AS col1")) { ps.setInt(1, 42); // statement parameters are still 1-based try (DuckDBChunkedResult res = ps.query()) { // advance to the next chunk, returns true on success while (res.nextChunk()) { // get the current chunk from the result DuckDBDataChunkReader chunk = res.chunk(); // iterate over the chunk columns, all indices are 0-based for (long columnIndex = 0; columnIndex < chunk.columnCount(); columnIndex++) { // get a vector for the specified column DuckDBReadableVector vector = chunk.vector(columnIndex); // iterate over vector rows for (long rowIndex = 0; rowIndex < chunk.rowCount(); rowIndex++) { // get a value in the vector on the specified row int val = vector.getInt(rowIndex); System.out.println(val); } } } } } ``` Note1: Currently it only supports basic data types, support for composite types (`LIST`, `STRUCT`) is going to be added in future. Note2: the `query()` method can only be used on prepared statements, currently there is no `query(String)` overload. --- CMakeLists.txt | 1 + CMakeLists.txt.in | 1 + duckdb_java.def | 8 + duckdb_java.exp | 8 + duckdb_java.map | 8 + src/jni/bindings_result.cpp | 144 ++++++++++++++++++ src/jni/duckdb_java.cpp | 39 +++-- src/jni/functions.cpp | 11 ++ src/jni/functions.hpp | 4 + src/main/java/org/duckdb/DuckDBBindings.java | 14 ++ .../java/org/duckdb/DuckDBChunkedResult.java | 133 ++++++++++++++++ .../java/org/duckdb/DuckDBConnection.java | 5 + .../org/duckdb/DuckDBDataChunkReader.java | 43 +++++- src/main/java/org/duckdb/DuckDBNative.java | 2 + .../org/duckdb/DuckDBPreparedStatement.java | 79 ++++++++-- .../java/org/duckdb/DuckDBReadableVector.java | 22 ++- .../duckdb/DuckDBScalarFunctionWrapper.java | 13 +- .../java/org/duckdb/DuckDBWritableVector.java | 42 ++++- src/test/java/org/duckdb/TestBindings.java | 16 +- .../java/org/duckdb/TestChunkedResult.java | 73 +++++++++ src/test/java/org/duckdb/TestClosure.java | 64 ++++++++ src/test/java/org/duckdb/TestDuckDBJDBC.java | 4 +- 22 files changed, 682 insertions(+), 52 deletions(-) create mode 100644 src/jni/bindings_result.cpp create mode 100644 src/main/java/org/duckdb/DuckDBChunkedResult.java create mode 100644 src/test/java/org/duckdb/TestChunkedResult.java diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c1d88e99..f968c4094 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -752,6 +752,7 @@ add_library(duckdb_java SHARED src/jni/bindings_common.cpp src/jni/bindings_data_chunk.cpp src/jni/bindings_logical_type.cpp + src/jni/bindings_result.cpp src/jni/bindings_scalar_function.cpp src/jni/bindings_table_function.cpp src/jni/bindings_table_function_bind.cpp diff --git a/CMakeLists.txt.in b/CMakeLists.txt.in index 15e7fbebf..65f979955 100644 --- a/CMakeLists.txt.in +++ b/CMakeLists.txt.in @@ -113,6 +113,7 @@ add_library(duckdb_java SHARED src/jni/bindings_common.cpp src/jni/bindings_data_chunk.cpp src/jni/bindings_logical_type.cpp + src/jni/bindings_result.cpp src/jni/bindings_scalar_function.cpp src/jni/bindings_table_function.cpp src/jni/bindings_table_function_bind.cpp diff --git a/duckdb_java.def b/duckdb_java.def index 4593c41f5..e1a03a34d 100644 --- a/duckdb_java.def +++ b/duckdb_java.def @@ -30,6 +30,7 @@ Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1db_1address Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute +Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1capi Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1pending Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1fetch Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1cast_1result_1to_1strings @@ -176,6 +177,13 @@ Java_org_duckdb_DuckDBBindings_duckdb_1get_1timestamp_1ms Java_org_duckdb_DuckDBBindings_duckdb_1get_1timestamp_1ns Java_org_duckdb_DuckDBBindings_duckdb_1get_1varchar +Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1result +Java_org_duckdb_DuckDBBindings_duckdb_1fetch_1chunk +Java_org_duckdb_DuckDBBindings_duckdb_1column_1name +Java_org_duckdb_DuckDBBindings_duckdb_1column_1type +Java_org_duckdb_DuckDBBindings_duckdb_1column_1count +Java_org_duckdb_DuckDBBindings_duckdb_1result_1error + duckdb_adbc_init duckdb_add_aggregate_function_to_set duckdb_add_replacement_scan diff --git a/duckdb_java.exp b/duckdb_java.exp index 8ca137161..530556bde 100644 --- a/duckdb_java.exp +++ b/duckdb_java.exp @@ -27,6 +27,7 @@ _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1db_1address _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute +_Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1capi _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1pending _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1fetch _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1cast_1result_1to_1strings @@ -173,6 +174,13 @@ _Java_org_duckdb_DuckDBBindings_duckdb_1get_1timestamp_1ms _Java_org_duckdb_DuckDBBindings_duckdb_1get_1timestamp_1ns _Java_org_duckdb_DuckDBBindings_duckdb_1get_1varchar +_Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1result +_Java_org_duckdb_DuckDBBindings_duckdb_1fetch_1chunk +_Java_org_duckdb_DuckDBBindings_duckdb_1column_1name +_Java_org_duckdb_DuckDBBindings_duckdb_1column_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1column_1count +_Java_org_duckdb_DuckDBBindings_duckdb_1result_1error + _duckdb_adbc_init _duckdb_add_aggregate_function_to_set _duckdb_add_replacement_scan diff --git a/duckdb_java.map b/duckdb_java.map index c780493dc..d5c29e9b8 100644 --- a/duckdb_java.map +++ b/duckdb_java.map @@ -29,6 +29,7 @@ DUCKDB_JAVA { Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1create_1extension_1type; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1disconnect; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute; + Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1capi; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1pending; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1fetch; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1cast_1result_1to_1strings; @@ -175,6 +176,13 @@ DUCKDB_JAVA { Java_org_duckdb_DuckDBBindings_duckdb_1get_1timestamp_1ns; Java_org_duckdb_DuckDBBindings_duckdb_1get_1varchar; + Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1result; + Java_org_duckdb_DuckDBBindings_duckdb_1fetch_1chunk; + Java_org_duckdb_DuckDBBindings_duckdb_1column_1name; + Java_org_duckdb_DuckDBBindings_duckdb_1column_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1column_1count; + Java_org_duckdb_DuckDBBindings_duckdb_1result_1error; + duckdb_adbc_init; duckdb_add_aggregate_function_to_set; duckdb_add_replacement_scan; diff --git a/src/jni/bindings_result.cpp b/src/jni/bindings_result.cpp new file mode 100644 index 000000000..a80791f03 --- /dev/null +++ b/src/jni/bindings_result.cpp @@ -0,0 +1,144 @@ +#include "bindings.hpp" +#include "refs.hpp" +#include "util.hpp" + +#include +#include + +duckdb_result *result_buf_to_result(JNIEnv *env, jobject result_buf) { + + if (result_buf == nullptr) { + env->ThrowNew(J_SQLException, "Invalid result buffer"); + return nullptr; + } + + duckdb_result *result = reinterpret_cast(env->GetDirectBufferAddress(result_buf)); + if (result == nullptr) { + env->ThrowNew(J_SQLException, "Invalid result"); + return nullptr; + } + + return result; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_destroy_result + * Signature: (Ljava/nio/ByteBuffer;)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1result(JNIEnv *env, jclass, jobject result) { + + duckdb_result *res = result_buf_to_result(env, result); + if (env->ExceptionCheck()) { + return; + } + + duckdb_result *ptr = res; + duckdb_destroy_result(res); + delete ptr; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_fetch_chunk + * Signature: (Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1fetch_1chunk(JNIEnv *env, jclass, jobject result) { + + duckdb_result *res = result_buf_to_result(env, result); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_data_chunk chunk = duckdb_fetch_chunk(*res); + + return make_ptr_buf(env, chunk); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_column_name + * Signature: (Ljava/nio/ByteBuffer;J)[B + */ +JNIEXPORT jbyteArray JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1column_1name(JNIEnv *env, jclass, jobject result, + jlong col) { + + duckdb_result *res = result_buf_to_result(env, result); + if (env->ExceptionCheck()) { + return nullptr; + } + idx_t col_idx = jlong_to_idx(env, col); + if (env->ExceptionCheck()) { + return nullptr; + } + + const char *name = duckdb_column_name(res, col_idx); + if (name == nullptr) { + return nullptr; + } + + idx_t len = static_cast(std::strlen(name)); + + return make_jbyteArray(env, name, len); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_column_type + * Signature: (Ljava/nio/ByteBuffer;J)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1column_1type(JNIEnv *env, jclass, jobject result, + jlong col) { + + duckdb_result *res = result_buf_to_result(env, result); + if (env->ExceptionCheck()) { + return -1; + } + idx_t col_idx = jlong_to_idx(env, col); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_type dt = duckdb_column_type(res, col_idx); + + return static_cast(dt); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_column_count + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1column_1count(JNIEnv *env, jclass, jobject result) { + + duckdb_result *res = result_buf_to_result(env, result); + if (env->ExceptionCheck()) { + return -1; + } + + idx_t count = duckdb_column_count(res); + + return static_cast(count); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_result_error + * Signature: (Ljava/nio/ByteBuffer;)[B + */ +JNIEXPORT jbyteArray JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1result_1error(JNIEnv *env, jclass, jobject result) { + + duckdb_result *res = result_buf_to_result(env, result); + if (env->ExceptionCheck()) { + return nullptr; + } + + const char *error_msg = duckdb_result_error(res); + if (error_msg == nullptr) { + return nullptr; + } + + idx_t len = static_cast(std::strlen(error_msg)); + + return make_jbyteArray(env, error_msg, len); +} \ No newline at end of file diff --git a/src/jni/duckdb_java.cpp b/src/jni/duckdb_java.cpp index 45d8a047d..43c96f887 100644 --- a/src/jni/duckdb_java.cpp +++ b/src/jni/duckdb_java.cpp @@ -13,6 +13,7 @@ extern "C" { #include "duckdb/function/scalar/variant_utils.hpp" #include "duckdb/function/table/arrow.hpp" #include "duckdb/main/appender.hpp" +#include "duckdb/main/capi/capi_internal.hpp" #include "duckdb/main/client_context.hpp" #include "duckdb/main/client_data.hpp" #include "duckdb/main/database_manager.hpp" @@ -268,13 +269,13 @@ jobject _duckdb_jdbc_pending_query(JNIEnv *env, jclass, jobject conn_ref_buf, jb return env->NewDirectByteBuffer(pending_ref.release(), 0); } -jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectArray params) { +static duckdb::unique_ptr execute_prepared_statement(JNIEnv *env, jobject stmt_ref_buf, + jobjectArray params, bool stream_results) { auto stmt_ref = reinterpret_cast(env->GetDirectBufferAddress(stmt_ref_buf)); if (!stmt_ref) { throw InvalidInputException("Invalid statement"); } - auto res_ref = make_uniq(); duckdb::vector duckdb_params; idx_t param_len = env->GetArrayLength(params); @@ -293,20 +294,38 @@ jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectA } } + auto res = stmt_ref->stmt->Execute(duckdb_params, stream_results); + if (res->HasError()) { + std::string error_msg = std::string(res->GetError()); + duckdb::ExceptionType error_type = res->GetErrorType(); + jclass exc_type = duckdb::ExceptionType::INTERRUPT == error_type ? J_SQLTimeoutException : J_SQLException; + env->ThrowNew(exc_type, error_msg.c_str()); + return nullptr; + } + return res; +} + +jobject _duckdb_jdbc_execute(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectArray params) { + auto stmt_ref = reinterpret_cast(env->GetDirectBufferAddress(stmt_ref_buf)); + if (!stmt_ref) { + throw InvalidInputException("Invalid statement"); + } Value result; bool stream_results = stmt_ref->stmt->context->TryGetCurrentSetting("jdbc_stream_results", result) ? result.GetValue() : false; + auto res_ref = make_uniq(); + res_ref->res = execute_prepared_statement(env, stmt_ref_buf, params, stream_results); + return env->NewDirectByteBuffer(res_ref.release(), 0); +} - res_ref->res = stmt_ref->stmt->Execute(duckdb_params, stream_results); - if (res_ref->res->HasError()) { - std::string error_msg = std::string(res_ref->res->GetError()); - duckdb::ExceptionType error_type = res_ref->res->GetErrorType(); - res_ref->res = nullptr; - jclass exc_type = duckdb::ExceptionType::INTERRUPT == error_type ? J_SQLTimeoutException : J_SQLException; - env->ThrowNew(exc_type, error_msg.c_str()); +jobject _duckdb_jdbc_execute_capi(JNIEnv *env, jclass, jobject stmt_ref_buf, jobjectArray params) { + auto res_ptr = execute_prepared_statement(env, stmt_ref_buf, params, true); + if (!res_ptr) { return nullptr; } - return env->NewDirectByteBuffer(res_ref.release(), 0); + auto out = make_uniq(); + DuckDBTranslateResult(std::move(res_ptr), out.get()); + return env->NewDirectByteBuffer(out.release(), 0); } jobject _duckdb_jdbc_execute_pending(JNIEnv *env, jclass, jobject pending_ref_buf) { diff --git a/src/jni/functions.cpp b/src/jni/functions.cpp index 1f8080ee6..14524a9ea 100644 --- a/src/jni/functions.cpp +++ b/src/jni/functions.cpp @@ -206,6 +206,17 @@ JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute(JNI } } +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1capi(JNIEnv * env, jclass param0, jobject param1, jobjectArray param2) { + try { + return _duckdb_jdbc_execute_capi(env, param0, param1, param2); + } catch (const std::exception &e) { + duckdb::ErrorData error(e); + ThrowJNI(env, error.Message().c_str()); + + return nullptr; + } +} + JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1pending(JNIEnv * env, jclass param0, jobject param1) { try { return _duckdb_jdbc_execute_pending(env, param0, param1); diff --git a/src/jni/functions.hpp b/src/jni/functions.hpp index d6b2c452f..e3714ed96 100644 --- a/src/jni/functions.hpp +++ b/src/jni/functions.hpp @@ -85,6 +85,10 @@ jobject _duckdb_jdbc_execute(JNIEnv * env, jclass param0, jobject param1, jobjec JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute(JNIEnv * env, jclass param0, jobject param1, jobjectArray param2); +jobject _duckdb_jdbc_execute_capi(JNIEnv * env, jclass param0, jobject param1, jobjectArray param2); + +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1capi(JNIEnv * env, jclass param0, jobject param1, jobjectArray param2); + jobject _duckdb_jdbc_execute_pending(JNIEnv * env, jclass param0, jobject param1); JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1execute_1pending(JNIEnv * env, jclass param0, jobject param1); diff --git a/src/main/java/org/duckdb/DuckDBBindings.java b/src/main/java/org/duckdb/DuckDBBindings.java index 18264648d..be5cdbba0 100644 --- a/src/main/java/org/duckdb/DuckDBBindings.java +++ b/src/main/java/org/duckdb/DuckDBBindings.java @@ -279,6 +279,20 @@ static native int duckdb_appender_create_ext(ByteBuffer connection, byte[] catal static native byte[] duckdb_get_varchar(ByteBuffer value); + // result + + static native void duckdb_destroy_result(ByteBuffer result); + + static native ByteBuffer duckdb_fetch_chunk(ByteBuffer result); + + static native byte[] duckdb_column_name(ByteBuffer result, long col); + + static native int duckdb_column_type(ByteBuffer result, long col); + + static native long duckdb_column_count(ByteBuffer result); + + static native byte[] duckdb_result_error(ByteBuffer result); + enum CAPIType { DUCKDB_TYPE_INVALID(0, 0), // bool diff --git a/src/main/java/org/duckdb/DuckDBChunkedResult.java b/src/main/java/org/duckdb/DuckDBChunkedResult.java new file mode 100644 index 000000000..8378d58c7 --- /dev/null +++ b/src/main/java/org/duckdb/DuckDBChunkedResult.java @@ -0,0 +1,133 @@ +package org.duckdb; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.duckdb.DuckDBBindings.*; + +import java.nio.ByteBuffer; +import java.sql.SQLException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class DuckDBChunkedResult implements AutoCloseable { + private final DuckDBPreparedStatement stmt; + private ByteBuffer resultRef; + private final Lock resultRefLock = new ReentrantLock(); + private DuckDBDataChunkReader currentChunk = null; + + public DuckDBChunkedResult(DuckDBPreparedStatement stmt, ByteBuffer resultRef) { + this.stmt = stmt; + this.resultRef = resultRef; + } + + public boolean nextChunk() { + checkOpen(); + resultRefLock.lock(); + try { + checkOpen(); + clearCurrentChunk(); + ByteBuffer chunkRef = duckdb_fetch_chunk(resultRef); + if (chunkRef == null) { + return false; + } + currentChunk = new DuckDBDataChunkReader(chunkRef); + return true; + } finally { + resultRefLock.unlock(); + } + } + + public DuckDBDataChunkReader chunk() { + return currentChunk; + } + + public String columnName(long columnIndex) { + checkOpen(); + resultRefLock.lock(); + try { + checkOpen(); + byte[] bytes = duckdb_column_name(resultRef, columnIndex); + if (null == bytes) { + return null; + } + return new String(bytes, UTF_8); + } finally { + resultRefLock.unlock(); + } + } + + public int columnTypeId(long columnIndex) { + checkOpen(); + resultRefLock.lock(); + try { + checkOpen(); + return duckdb_column_type(resultRef, columnIndex); + } finally { + resultRefLock.unlock(); + } + } + + public long columnCount() { + checkOpen(); + resultRefLock.lock(); + try { + checkOpen(); + return duckdb_column_count(resultRef); + } finally { + resultRefLock.unlock(); + } + } + + @Override + public void close() throws SQLException { + if (isClosed()) { + return; + } + resultRefLock.lock(); + try { + if (isClosed()) { + return; + } + clearCurrentChunk(); + duckdb_destroy_result(resultRef); + resultRef = null; + } finally { + resultRefLock.unlock(); + } + + // isCloseOnCompletion() throws if already closed, and we can't check for isClosed() because it could change + // between when we check and call isCloseOnCompletion, so access the field directly. + if (stmt.closeOnCompletion) { + stmt.close(); + } + } + + public boolean isClosed() { + return resultRef == null; + } + + void checkError() throws SQLException { + resultRefLock.lock(); + try { + byte[] error = duckdb_result_error(resultRef); + if (error != null) { + String errorStr = new String(error, UTF_8); + throw new SQLException("Query failed: " + errorStr); + } + } finally { + resultRefLock.unlock(); + } + } + + private void checkOpen() { + if (isClosed()) { + throw new IllegalStateException("Result was closed"); + } + } + + private void clearCurrentChunk() { + if (currentChunk != null) { + currentChunk.closeAndDestroy(); + currentChunk = null; + } + } +} diff --git a/src/main/java/org/duckdb/DuckDBConnection.java b/src/main/java/org/duckdb/DuckDBConnection.java index 0bc04e942..9e2c876c0 100644 --- a/src/main/java/org/duckdb/DuckDBConnection.java +++ b/src/main/java/org/duckdb/DuckDBConnection.java @@ -303,6 +303,11 @@ public PreparedStatement prepareStatement(String sql) throws SQLException { return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, 0); } + public DuckDBPreparedStatement prepare(String sql) throws SQLException { + PreparedStatement ps = prepareStatement(sql); + return ps.unwrap(DuckDBPreparedStatement.class); + } + public DatabaseMetaData getMetaData() throws SQLException { return new DuckDBDatabaseMetaData(this); } diff --git a/src/main/java/org/duckdb/DuckDBDataChunkReader.java b/src/main/java/org/duckdb/DuckDBDataChunkReader.java index d91b78b64..20edeab2d 100644 --- a/src/main/java/org/duckdb/DuckDBDataChunkReader.java +++ b/src/main/java/org/duckdb/DuckDBDataChunkReader.java @@ -3,6 +3,8 @@ import static org.duckdb.DuckDBBindings.*; import java.nio.ByteBuffer; +import java.sql.SQLException; +import java.util.concurrent.locks.ReentrantLock; import java.util.stream.LongStream; import org.duckdb.DuckDBFunctions.FunctionException; @@ -12,7 +14,8 @@ *

Column index violations throw {@link IndexOutOfBoundsException}. */ public final class DuckDBDataChunkReader { - private final ByteBuffer chunkRef; + private ByteBuffer chunkRef; + final ReentrantLock chunkRefLock = new ReentrantLock(); private final long rowCount; private final long columnCount; private final DuckDBReadableVector[] vectors; @@ -29,7 +32,7 @@ public final class DuckDBDataChunkReader { for (long columnIndex = 0; columnIndex < columnCount; columnIndex++) { ByteBuffer vectorRef = duckdb_data_chunk_get_vector(chunkRef, columnIndex); int arrayIndex = Math.toIntExact(columnIndex); - vectors[arrayIndex] = new DuckDBReadableVector(vectorRef, rowCount); + vectors[arrayIndex] = new DuckDBReadableVector(vectorRef, this, rowCount); } } @@ -52,4 +55,40 @@ public DuckDBReadableVector vector(long columnIndex) { int arrayIndex = Math.toIntExact(columnIndex); return vectors[arrayIndex]; } + + void close() { + closeInternal(false); + } + + void closeAndDestroy() { + closeInternal(true); + } + + private void closeInternal(boolean destroy) { + if (isClosed()) { + return; + } + chunkRefLock.lock(); + try { + if (isClosed()) { + return; + } + if (destroy) { + duckdb_destroy_data_chunk(chunkRef); + } + chunkRef = null; + } finally { + chunkRefLock.unlock(); + } + } + + boolean isClosed() { + return chunkRef == null; + } + + void checkOpen() { + if (isClosed()) { + throw new IllegalStateException("Chunk was closed"); + } + } } diff --git a/src/main/java/org/duckdb/DuckDBNative.java b/src/main/java/org/duckdb/DuckDBNative.java index 30b1c0e22..9f82af28d 100644 --- a/src/main/java/org/duckdb/DuckDBNative.java +++ b/src/main/java/org/duckdb/DuckDBNative.java @@ -178,6 +178,8 @@ private static void loadFromCurrentJarDir(String libName) throws Exception { // returns res_ref result reference object static native ByteBuffer duckdb_jdbc_execute(ByteBuffer stmt_ref, Object[] params) throws SQLException; + static native ByteBuffer duckdb_jdbc_execute_capi(ByteBuffer stmt_ref, Object[] params) throws SQLException; + static native ByteBuffer duckdb_jdbc_pending_query(ByteBuffer conn_ref, byte[] query) throws SQLException; static native ByteBuffer duckdb_jdbc_execute_pending(ByteBuffer pending_ref) throws SQLException; diff --git a/src/main/java/org/duckdb/DuckDBPreparedStatement.java b/src/main/java/org/duckdb/DuckDBPreparedStatement.java index 5c2015835..0d056057b 100644 --- a/src/main/java/org/duckdb/DuckDBPreparedStatement.java +++ b/src/main/java/org/duckdb/DuckDBPreparedStatement.java @@ -55,6 +55,8 @@ public class DuckDBPreparedStatement implements PreparedStatement { private DuckDBResultSet selectResult = null; private long updateResult = 0; + private DuckDBChunkedResult chunkedResult = null; + private boolean returnsChangedRows = false; private boolean returnsNothing = false; private boolean returnsResultSet = false; @@ -137,10 +139,7 @@ private void prepare(String sql) throws SQLException { meta = null; params = new Object[0]; - if (selectResult != null) { - selectResult.close(); - } - selectResult = null; + clearResults(); updateResult = 0; // Lock connection while still holding statement lock @@ -213,11 +212,7 @@ public boolean execute() throws SQLException { try { checkOpen(); checkPrepared(); - - if (selectResult != null) { - selectResult.close(); - } - selectResult = null; + clearResults(); if (!isConnAutoCommit()) { startTransaction(); @@ -251,9 +246,8 @@ public boolean execute() throws SQLException { pendingQuery.close(); } if (queryFailed) { - if (null != selectResult) { - selectResult.close(); - } else if (null != resultRef) { + clearResults(); + if (null != resultRef) { DuckDBNative.duckdb_jdbc_free_result(resultRef); } close(); @@ -270,6 +264,51 @@ public boolean execute() throws SQLException { return returnsResultSet; } + public DuckDBChunkedResult query() throws SQLException { + checkOpen(); + if (!isPreparedStatement) { + throw new SQLException("Data Chunk interface can only be used with prepared statements"); + } + + // Wait with dispatching a new query if connection is locked by cancel() call + Lock connLock = getConnRefLock(); + connLock.lock(); + connLock.unlock(); + + boolean queryFailed = false; + stmtRefLock.lock(); + try { + checkOpen(); + clearResults(); + + if (!isConnAutoCommit()) { + startTransaction(); + } + + scheduleCancelTask(); + + ByteBuffer chunkedResultRef = DuckDBNative.duckdb_jdbc_execute_capi(stmtRef, params); + + cleanupCancelQueryTask(); + + chunkedResult = new DuckDBChunkedResult(this, chunkedResultRef); + chunkedResult.checkError(); + return chunkedResult; + + } catch (SQLException e) { + clearResults(); + queryFailed = true; + throw e; + + } finally { + stmtRefLock.unlock(); + this.query = null; + if (queryFailed) { + close(); + } + } + } + @Override public ResultSet executeQuery() throws SQLException { requireNonBatch(); @@ -420,10 +459,7 @@ public void close() throws SQLException { cleanupCancelQueryTask(); - if (selectResult != null) { - selectResult.close(); - selectResult = null; - } + clearResults(); if (stmtRef != null) { // Delete prepared statement DuckDBNative.duckdb_jdbc_release(stmtRef); @@ -1334,6 +1370,17 @@ private Lock getConnRefLock() throws SQLException { } } + private void clearResults() throws SQLException { + if (selectResult != null) { + selectResult.close(); + selectResult = null; + } + if (chunkedResult != null) { + chunkedResult.close(); + chunkedResult = null; + } + } + private void cleanupCancelQueryTask() { if (cancelQueryFuture != null) { cancelQueryFuture.cancel(false); diff --git a/src/main/java/org/duckdb/DuckDBReadableVector.java b/src/main/java/org/duckdb/DuckDBReadableVector.java index 93a8c2ffb..6ba174b0c 100644 --- a/src/main/java/org/duckdb/DuckDBReadableVector.java +++ b/src/main/java/org/duckdb/DuckDBReadableVector.java @@ -24,16 +24,18 @@ public final class DuckDBReadableVector { private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); private final ByteBuffer vectorRef; + private final DuckDBDataChunkReader chunkNullable; private final long rowCount; private final DuckDBVectorTypeInfo typeInfo; private final ByteBuffer data; private final ByteBuffer validity; - DuckDBReadableVector(ByteBuffer vectorRef, long rowCount) { + DuckDBReadableVector(ByteBuffer vectorRef, DuckDBDataChunkReader chunkNullable, long rowCount) { if (vectorRef == null) { throw new FunctionException("Invalid vector reference"); } this.vectorRef = vectorRef; + this.chunkNullable = chunkNullable; this.rowCount = rowCount; try { this.typeInfo = DuckDBVectorTypeInfo.fromVector(vectorRef); @@ -316,17 +318,25 @@ public String getString(long row) { if (isNull(row)) { return null; } - byte[] bytes = duckdb_vector_get_string(data, row); + if (null != chunkNullable) { + chunkNullable.checkOpen(); + chunkNullable.chunkRefLock.lock(); + chunkNullable.checkOpen(); + } + byte[] bytes = null; + try { + bytes = duckdb_vector_get_string(data, row); + } finally { + if (null != chunkNullable) { + chunkNullable.chunkRefLock.unlock(); + } + } if (bytes == null) { return null; } return new String(bytes, UTF_8); } - ByteBuffer vectorRef() { - return vectorRef; - } - private void requireType(DuckDBColumnType expected) { if (typeInfo.columnType != expected) { throw new FunctionException("Expected vector type " + expected + ", found " + typeInfo.columnType); diff --git a/src/main/java/org/duckdb/DuckDBScalarFunctionWrapper.java b/src/main/java/org/duckdb/DuckDBScalarFunctionWrapper.java index e8877942f..be379b653 100644 --- a/src/main/java/org/duckdb/DuckDBScalarFunctionWrapper.java +++ b/src/main/java/org/duckdb/DuckDBScalarFunctionWrapper.java @@ -14,13 +14,22 @@ class DuckDBScalarFunctionWrapper { } public void execute(ByteBuffer functionInfo, ByteBuffer inputChunk, ByteBuffer outputVector) { + DuckDBDataChunkReader inputReader = null; + DuckDBWritableVector outputWriter = null; try { - DuckDBDataChunkReader inputReader = new DuckDBDataChunkReader(inputChunk); - DuckDBWritableVector outputWriter = new DuckDBWritableVector(outputVector, inputReader.rowCount()); + inputReader = new DuckDBDataChunkReader(inputChunk); + outputWriter = new DuckDBWritableVector(outputVector, inputReader.rowCount()); function.apply(inputReader, outputWriter); } catch (Throwable throwable) { String trace = collectStackTrace(throwable); duckdb_scalar_function_set_error(functionInfo, trace.getBytes(UTF_8)); + } finally { + if (null != inputReader) { + inputReader.close(); + } + if (null != outputWriter) { + outputWriter.close(); + } } } } diff --git a/src/main/java/org/duckdb/DuckDBWritableVector.java b/src/main/java/org/duckdb/DuckDBWritableVector.java index 4b46d7450..36bf041a2 100644 --- a/src/main/java/org/duckdb/DuckDBWritableVector.java +++ b/src/main/java/org/duckdb/DuckDBWritableVector.java @@ -14,13 +14,15 @@ import java.time.OffsetDateTime; import java.time.ZoneId; import java.time.ZoneOffset; +import java.util.concurrent.locks.ReentrantLock; import org.duckdb.DuckDBFunctions.FunctionException; public final class DuckDBWritableVector { private static final BigInteger UINT64_MAX = new BigInteger("18446744073709551615"); private static final ByteOrder NATIVE_ORDER = ByteOrder.nativeOrder(); - private final ByteBuffer vectorRef; + private ByteBuffer vectorRef; + private final ReentrantLock vectorRefLock = new ReentrantLock(); private final long rowCount; private final DuckDBVectorTypeInfo typeInfo; private final ByteBuffer data; @@ -469,14 +471,17 @@ public void setString(long row, String value) { setNull(row); return; } - duckdb_vector_assign_string_element_len(vectorRef, row, value.getBytes(UTF_8)); + checkOpen(); + vectorRefLock.lock(); + try { + checkOpen(); + duckdb_vector_assign_string_element_len(vectorRef, row, value.getBytes(UTF_8)); + } finally { + vectorRefLock.unlock(); + } markValid(row); } - ByteBuffer vectorRef() { - return vectorRef; - } - private void markValid(long row) { setRowValidity(row, true); } @@ -587,4 +592,29 @@ private int checkedRowIndex(long row) { private int checkedByteOffset(long row, int elementWidth) { return Math.toIntExact(Math.multiplyExact(row, (long) elementWidth)); } + + void close() { + if (isClosed()) { + return; + } + vectorRefLock.lock(); + try { + if (isClosed()) { + return; + } + vectorRef = null; + } finally { + vectorRefLock.unlock(); + } + } + + private boolean isClosed() { + return vectorRef == null; + } + + private void checkOpen() { + if (isClosed()) { + throw new IllegalStateException("Vector was closed"); + } + } } diff --git a/src/test/java/org/duckdb/TestBindings.java b/src/test/java/org/duckdb/TestBindings.java index 701d82880..ff0071c56 100644 --- a/src/test/java/org/duckdb/TestBindings.java +++ b/src/test/java/org/duckdb/TestBindings.java @@ -30,11 +30,11 @@ public static void test_bindings_vector_row_index_stream() throws Exception { input.setInt(1, 41); input.setInt(2, -5); - DuckDBReadableVector readable = new DuckDBReadableVector(inputVec, 3); + DuckDBReadableVector readable = new DuckDBReadableVector(inputVec, null, 3); DuckDBWritableVector output = new DuckDBWritableVector(outputVec, 3); readable.stream().forEachOrdered(row -> { output.setInt(row, readable.getInt(row) + 1); }); - DuckDBReadableVector result = new DuckDBReadableVector(outputVec, 3); + DuckDBReadableVector result = new DuckDBReadableVector(outputVec, null, 3); assertEquals(result.getInt(0), 2); assertEquals(result.getInt(1), 42); assertEquals(result.getInt(2), -4); @@ -161,7 +161,7 @@ public static void test_bindings_vector_get_string() throws Exception { writable.setNull(0); writable.setString(1, "duckdb"); - DuckDBReadableVector readable = new DuckDBReadableVector(vec, rowCount); + DuckDBReadableVector readable = new DuckDBReadableVector(vec, null, rowCount); assertNull(readable.getString(0)); assertEquals(readable.getString(1), "duckdb"); assertThrows(() -> { readable.getString(rowCount); }, IndexOutOfBoundsException.class); @@ -182,7 +182,7 @@ public static void test_bindings_vector_native_endian_roundtrip() throws Excepti ByteBuffer rawData = duckdb_vector_get_data(vec, (long) rowCount * Integer.BYTES); assertEquals(rawData.order(ByteOrder.nativeOrder()).getInt(0), expected); - DuckDBReadableVector readable = new DuckDBReadableVector(vec, rowCount); + DuckDBReadableVector readable = new DuckDBReadableVector(vec, null, rowCount); assertEquals(readable.getInt(0), expected); duckdb_destroy_vector(vec); @@ -237,7 +237,7 @@ public static void test_bindings_vector_ubigint_native_endian_roundtrip() throws assertEquals(nativeData.getLong(i * Long.BYTES), values[i].longValue()); } - DuckDBReadableVector readable = new DuckDBReadableVector(vec, rowCount); + DuckDBReadableVector readable = new DuckDBReadableVector(vec, null, rowCount); for (int i = 0; i < values.length; i++) { assertEquals(readable.getUint64(i), values[i]); } @@ -269,7 +269,7 @@ public static void test_bindings_vector_uhugeint_native_endian_roundtrip() throw assertEquals(nativeData.getLong(offset + Long.BYTES), values[i].shiftRight(Long.SIZE).longValue()); } - DuckDBReadableVector readable = new DuckDBReadableVector(vec, rowCount); + DuckDBReadableVector readable = new DuckDBReadableVector(vec, null, rowCount); for (int i = 0; i < values.length; i++) { assertEquals(readable.getUHugeInt(i), values[i]); } @@ -401,7 +401,7 @@ public static void test_bindings_writable_vector_validity_word_boundaries() thro writable.setNull(row); } - DuckDBReadableVector readable = new DuckDBReadableVector(vec, rowCount); + DuckDBReadableVector readable = new DuckDBReadableVector(vec, null, rowCount); for (long row : boundaryRows) { assertTrue(readable.isNull(row)); } @@ -414,7 +414,7 @@ public static void test_bindings_writable_vector_validity_word_boundaries() thro writable.setInt(row, (int) (row + 1000)); } - DuckDBReadableVector revalidated = new DuckDBReadableVector(vec, rowCount); + DuckDBReadableVector revalidated = new DuckDBReadableVector(vec, null, rowCount); for (long row : boundaryRows) { assertFalse(revalidated.isNull(row)); assertEquals(revalidated.getInt(row), (int) (row + 1000)); diff --git a/src/test/java/org/duckdb/TestChunkedResult.java b/src/test/java/org/duckdb/TestChunkedResult.java new file mode 100644 index 000000000..9b6e085e2 --- /dev/null +++ b/src/test/java/org/duckdb/TestChunkedResult.java @@ -0,0 +1,73 @@ +package org.duckdb; + +import static org.duckdb.TestDuckDBJDBC.JDBC_URL; +import static org.duckdb.test.Assertions.*; + +import java.sql.DriverManager; + +public class TestChunkedResult { + + public static void test_chunked_result_basic() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + DuckDBPreparedStatement ps = conn.prepare("SELECT 42"); DuckDBChunkedResult res = ps.query()) { + assertTrue(res.nextChunk()); + assertNotNull(res.chunk()); + assertEquals(res.chunk().vector(0).getInt(0), 42); + assertFalse(res.nextChunk()); + } + } + + public static void test_chunked_result_multiple_chunks() throws Exception { + long count = (1 << 16) + 7; + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + DuckDBPreparedStatement ps = conn.prepare("SELECT\n" + + " num::BIGINT AS col1,\n" + + " repeat(num::VARCHAR, 16) AS col2\n" + + "FROM range(" + count + ") t(num);"); + DuckDBChunkedResult res = ps.query()) { + assertEquals(res.columnCount(), (long) 2); + assertEquals(res.columnName(0), "col1"); + assertEquals(res.columnTypeId(0), 5); + long chunksCount = 0; + long num = 0; + while (res.nextChunk()) { + assertNotNull(res.chunk()); + for (long row = 0; row < res.chunk().rowCount(); row++) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 16; i++) { + sb.append(num); + } + assertEquals(res.chunk().vector(0).getLong(row), num); + assertEquals(res.chunk().vector(1).getString(row), sb.toString()); + num += 1; + } + chunksCount += 1; + } + assertFalse(res.nextChunk()); + assertFalse(res.nextChunk()); + assertEquals(chunksCount, count / 2048 + 1); + } + } + + public static void test_chunked_result_params() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + DuckDBPreparedStatement ps = conn.prepare("SELECT ?")) { + ps.setInt(1, 42); + try (DuckDBChunkedResult res = ps.query()) { + assertEquals(res.columnTypeId(0), 4); + assertTrue(res.nextChunk()); + assertNotNull(res.chunk()); + assertEquals(res.chunk().vector(0).getInt(0), 42); + assertFalse(res.nextChunk()); + } + ps.setLong(1, 43); + try (DuckDBChunkedResult res = ps.query()) { + assertEquals(res.columnTypeId(0), 5); + assertTrue(res.nextChunk()); + assertNotNull(res.chunk()); + assertEquals(res.chunk().vector(0).getLong(0), (long) 43); + assertFalse(res.nextChunk()); + } + } + } +} diff --git a/src/test/java/org/duckdb/TestClosure.java b/src/test/java/org/duckdb/TestClosure.java index af6fd83df..8f8787e2a 100644 --- a/src/test/java/org/duckdb/TestClosure.java +++ b/src/test/java/org/duckdb/TestClosure.java @@ -64,6 +64,17 @@ public static void test_result_set_auto_closed_prepared() throws Exception { } } + public static void test_result_set_auto_closed_chunked() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class)) { + DuckDBPreparedStatement ps = conn.prepare("select 42"); + DuckDBChunkedResult rs1 = ps.query(); + DuckDBChunkedResult rs2 = ps.query(); + assertTrue(rs1.isClosed()); + ps.close(); + assertTrue(rs2.isClosed()); + } + } + public static void test_statements_auto_closed_on_conn_close() throws Exception { Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt1 = conn.createStatement(); @@ -109,6 +120,16 @@ public static void test_results_auto_closed_on_conn_close_prepared() throws Exce assertTrue(ps.isClosed()); } + public static void test_result_chunked_auto_closed_on_conn_close_prepared() throws Exception { + DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + DuckDBPreparedStatement ps = conn.prepare("select 42"); + DuckDBChunkedResult rs = ps.query(); + rs.nextChunk(); + conn.close(); + assertTrue(rs.isClosed()); + assertTrue(ps.isClosed()); + } + public static void test_statement_auto_closed_on_completion() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL)) { Statement stmt = conn.createStatement(); @@ -133,6 +154,18 @@ public static void test_prepared_statement_auto_closed_on_completion() throws Ex } } + public static void test_prepared_statement_chunked_auto_closed_on_completion() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class)) { + DuckDBPreparedStatement ps = conn.prepare("select 42"); + ps.closeOnCompletion(); + assertTrue(ps.isCloseOnCompletion()); + try (DuckDBChunkedResult rs = ps.query()) { + rs.nextChunk(); + } + assertTrue(ps.isClosed()); + } + } + public static void test_long_query_conn_close() throws Exception { Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); @@ -460,6 +493,37 @@ public static void test_results_fetch_no_hang_prepared() throws Exception { } } + @SuppressWarnings("try") + public static void test_results_fetch_no_hang_prepared_chunked() throws Exception { + ExecutorService executor = Executors.newSingleThreadExecutor(); + long rowsCount = 1 << 24; + int iterations = 1; + for (int i = 0; i < iterations; i++) { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + DuckDBPreparedStatement ps = + conn.prepare("SELECT i, i::VARCHAR FROM range(0, " + rowsCount + ") AS t(i)"); + DuckDBChunkedResult rs = ps.query()) { + executor.submit(() -> { + try { + Thread.sleep(400); + conn.close(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + long[] resultsCount = new long[1]; + assertThrows(() -> { + while (rs.nextChunk()) { + resultsCount[0] += rs.chunk().rowCount(); + Thread.sleep(50); + } + }, IllegalStateException.class); + assertTrue(resultsCount[0] > 0); + assertTrue(resultsCount[0] < rowsCount); + } + } + } + public static void test_stmt_can_only_cancel_self() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt1 = conn.createStatement(); Statement stmt2 = conn.createStatement()) { diff --git a/src/test/java/org/duckdb/TestDuckDBJDBC.java b/src/test/java/org/duckdb/TestDuckDBJDBC.java index 7aea4bf74..ee72bf901 100644 --- a/src/test/java/org/duckdb/TestDuckDBJDBC.java +++ b/src/test/java/org/duckdb/TestDuckDBJDBC.java @@ -2290,8 +2290,8 @@ public static void main(String[] args) throws Exception { statusCode = runTests(args, TestDuckDBJDBC.class, TestAppender.class, TestAppenderCollection.class, TestAppenderCollection2D.class, TestAppenderComposite.class, TestSingleValueAppender.class, - TestBatch.class, TestBindings.class, TestClosure.class, TestExtensionTypes.class, - TestJfrEvents.class, TestMetadata.class, /* TestNoLib.class ,*/ + TestBatch.class, TestBindings.class, TestChunkedResult.class, TestClosure.class, + TestExtensionTypes.class, TestJfrEvents.class, TestMetadata.class, /* TestNoLib.class ,*/ /* TestSpatial.class,*/ TestParameterMetadata.class, TestPrepare.class, TestResults.class, TestScalarFunctions.class, TestSessionInit.class, TestTableFunctions.class, TestTimestamp.class, TestVariant.class);