diff --git a/CMakeLists.txt b/CMakeLists.txt index 85b75b2ea..2c93bf351 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -553,7 +553,7 @@ endif() file(GLOB_RECURSE JAVA_SRC_FILES src/main/java/org/duckdb/*.java) file(GLOB_RECURSE JAVA_TEST_FILES src/test/java/org/duckdb/*.java) -set(CMAKE_JAVA_COMPILE_FLAGS -encoding utf-8 -g) +set(CMAKE_JAVA_COMPILE_FLAGS -encoding utf-8 -g -Xlint:all) add_jar(duckdb_jdbc ${JAVA_SRC_FILES} META-INF/services/java.sql.Driver MANIFEST META-INF/MANIFEST.MF @@ -570,6 +570,12 @@ else() endif() add_library(duckdb_java SHARED + src/jni/bindings_appender.cpp + src/jni/bindings_common.cpp + src/jni/bindings_data_chunk.cpp + src/jni/bindings_logical_type.cpp + src/jni/bindings_validity.cpp + src/jni/bindings_vector.cpp src/jni/config.cpp src/jni/duckdb_java.cpp src/jni/functions.cpp diff --git a/CMakeLists.txt.in b/CMakeLists.txt.in index 778a3abf9..e689a6680 100644 --- a/CMakeLists.txt.in +++ b/CMakeLists.txt.in @@ -83,7 +83,7 @@ endif() file(GLOB_RECURSE JAVA_SRC_FILES src/main/java/org/duckdb/*.java) file(GLOB_RECURSE JAVA_TEST_FILES src/test/java/org/duckdb/*.java) -set(CMAKE_JAVA_COMPILE_FLAGS -encoding utf-8 -g) +set(CMAKE_JAVA_COMPILE_FLAGS -encoding utf-8 -g -Xlint:all) add_jar(duckdb_jdbc ${JAVA_SRC_FILES} META-INF/services/java.sql.Driver MANIFEST META-INF/MANIFEST.MF @@ -100,6 +100,12 @@ else() endif() add_library(duckdb_java SHARED + src/jni/bindings_appender.cpp + src/jni/bindings_common.cpp + src/jni/bindings_data_chunk.cpp + src/jni/bindings_logical_type.cpp + src/jni/bindings_validity.cpp + src/jni/bindings_vector.cpp src/jni/config.cpp src/jni/duckdb_java.cpp src/jni/functions.cpp diff --git a/duckdb_java.def b/duckdb_java.def index 7e8f9e4b7..02aa51a66 100644 --- a/duckdb_java.def +++ b/duckdb_java.def @@ -47,6 +47,50 @@ Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1catalog Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1schema Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1startup +Java_org_duckdb_DuckDBBindings_duckdb_1vector_1size +Java_org_duckdb_DuckDBBindings_duckdb_1create_1logical_1type +Java_org_duckdb_DuckDBBindings_duckdb_1get_1type_1id +Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1width +Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1scale +Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1internal_1type +Java_org_duckdb_DuckDBBindings_duckdb_1create_1list_1type +Java_org_duckdb_DuckDBBindings_duckdb_1create_1array_1type +Java_org_duckdb_DuckDBBindings_duckdb_1create_1struct_1type +Java_org_duckdb_DuckDBBindings_duckdb_1struct_1type_1child_1count +Java_org_duckdb_DuckDBBindings_duckdb_1array_1type_1array_1size +Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1logical_1type +Java_org_duckdb_DuckDBBindings_duckdb_1create_1vector +Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1vector +Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1column_1type +Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1data +Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1validity +Java_org_duckdb_DuckDBBindings_duckdb_1vector_1ensure_1validity_1writable +Java_org_duckdb_DuckDBBindings_duckdb_1vector_1assign_1string_1element_1len +Java_org_duckdb_DuckDBBindings_duckdb_1validity_1row_1is_1valid +Java_org_duckdb_DuckDBBindings_duckdb_1validity_1set_1row_1validity +Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1child +Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1size +Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1set_1size +Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1reserve +Java_org_duckdb_DuckDBBindings_duckdb_1struct_1vector_1get_1child +Java_org_duckdb_DuckDBBindings_duckdb_1array_1vector_1get_1child +Java_org_duckdb_DuckDBBindings_duckdb_1create_1data_1chunk +Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1data_1chunk +Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1reset +Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1column_1count +Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1vector +Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1size +Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1set_1size +Java_org_duckdb_DuckDBBindings_duckdb_1appender_1create_1ext +Java_org_duckdb_DuckDBBindings_duckdb_1appender_1error +Java_org_duckdb_DuckDBBindings_duckdb_1appender_1flush +Java_org_duckdb_DuckDBBindings_duckdb_1appender_1close +Java_org_duckdb_DuckDBBindings_duckdb_1appender_1destroy +Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1count +Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1type +Java_org_duckdb_DuckDBBindings_duckdb_1append_1data_1chunk +Java_org_duckdb_DuckDBBindings_duckdb_1append_1default_1to_1chunk + duckdb_adbc_init duckdb_add_aggregate_function_to_set duckdb_add_replacement_scan diff --git a/duckdb_java.exp b/duckdb_java.exp index 91884e161..9a302e63d 100644 --- a/duckdb_java.exp +++ b/duckdb_java.exp @@ -44,6 +44,50 @@ _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1catalog _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1schema _Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1startup +_Java_org_duckdb_DuckDBBindings_duckdb_1vector_1size +_Java_org_duckdb_DuckDBBindings_duckdb_1create_1logical_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1get_1type_1id +_Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1width +_Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1scale +_Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1internal_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1create_1list_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1create_1array_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1create_1struct_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1struct_1type_1child_1count +_Java_org_duckdb_DuckDBBindings_duckdb_1array_1type_1array_1size +_Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1logical_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1create_1vector +_Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1vector +_Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1column_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1data +_Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1validity +_Java_org_duckdb_DuckDBBindings_duckdb_1vector_1ensure_1validity_1writable +_Java_org_duckdb_DuckDBBindings_duckdb_1vector_1assign_1string_1element_1len +_Java_org_duckdb_DuckDBBindings_duckdb_1validity_1row_1is_1valid +_Java_org_duckdb_DuckDBBindings_duckdb_1validity_1set_1row_1validity +_Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1child +_Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1size +_Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1set_1size +_Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1reserve +_Java_org_duckdb_DuckDBBindings_duckdb_1struct_1vector_1get_1child +_Java_org_duckdb_DuckDBBindings_duckdb_1array_1vector_1get_1child +_Java_org_duckdb_DuckDBBindings_duckdb_1create_1data_1chunk +_Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1data_1chunk +_Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1reset +_Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1column_1count +_Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1vector +_Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1size +_Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1set_1size +_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1create_1ext +_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1error +_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1flush +_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1close +_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1destroy +_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1count +_Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1type +_Java_org_duckdb_DuckDBBindings_duckdb_1append_1data_1chunk +_Java_org_duckdb_DuckDBBindings_duckdb_1append_1default_1to_1chunk + _duckdb_adbc_init _duckdb_add_aggregate_function_to_set _duckdb_add_replacement_scan diff --git a/duckdb_java.map b/duckdb_java.map index 18a345f58..48b9bee57 100644 --- a/duckdb_java.map +++ b/duckdb_java.map @@ -46,6 +46,50 @@ DUCKDB_JAVA { Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1set_1schema; Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1startup; + Java_org_duckdb_DuckDBBindings_duckdb_1vector_1size; + Java_org_duckdb_DuckDBBindings_duckdb_1create_1logical_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1get_1type_1id; + Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1width; + Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1scale; + Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1internal_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1create_1list_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1create_1array_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1create_1struct_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1struct_1type_1child_1count; + Java_org_duckdb_DuckDBBindings_duckdb_1array_1type_1array_1size; + Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1logical_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1create_1vector; + Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1vector; + Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1column_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1data; + Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1validity; + Java_org_duckdb_DuckDBBindings_duckdb_1vector_1ensure_1validity_1writable; + Java_org_duckdb_DuckDBBindings_duckdb_1vector_1assign_1string_1element_1len; + Java_org_duckdb_DuckDBBindings_duckdb_1validity_1row_1is_1valid; + Java_org_duckdb_DuckDBBindings_duckdb_1validity_1set_1row_1validity; + Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1child; + Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1size; + Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1set_1size; + Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1reserve; + Java_org_duckdb_DuckDBBindings_duckdb_1struct_1vector_1get_1child; + Java_org_duckdb_DuckDBBindings_duckdb_1array_1vector_1get_1child; + Java_org_duckdb_DuckDBBindings_duckdb_1create_1data_1chunk; + Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1data_1chunk; + Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1reset; + Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1column_1count; + Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1vector; + Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1size; + Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1set_1size; + Java_org_duckdb_DuckDBBindings_duckdb_1appender_1create_1ext; + Java_org_duckdb_DuckDBBindings_duckdb_1appender_1error; + Java_org_duckdb_DuckDBBindings_duckdb_1appender_1flush; + Java_org_duckdb_DuckDBBindings_duckdb_1appender_1close; + Java_org_duckdb_DuckDBBindings_duckdb_1appender_1destroy; + Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1count; + Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1type; + Java_org_duckdb_DuckDBBindings_duckdb_1append_1data_1chunk; + Java_org_duckdb_DuckDBBindings_duckdb_1append_1default_1to_1chunk; + duckdb_adbc_init; duckdb_add_aggregate_function_to_set; duckdb_add_replacement_scan; diff --git a/src/jni/bindings.hpp b/src/jni/bindings.hpp new file mode 100644 index 000000000..871077ddc --- /dev/null +++ b/src/jni/bindings.hpp @@ -0,0 +1,11 @@ +#pragma once + +extern "C" { +#include "duckdb.h" +} + +#include "org_duckdb_DuckDBBindings.h" + +duckdb_logical_type logical_type_buf_to_logical_type(JNIEnv *env, jobject logical_type_buf); + +duckdb_data_chunk chunk_buf_to_chunk(JNIEnv *env, jobject chunk_buf); diff --git a/src/jni/bindings_appender.cpp b/src/jni/bindings_appender.cpp new file mode 100644 index 000000000..be36ab82d --- /dev/null +++ b/src/jni/bindings_appender.cpp @@ -0,0 +1,241 @@ +#include "bindings.hpp" +#include "holders.hpp" +#include "util.hpp" + +static duckdb_appender appender_buf_to_appender(JNIEnv *env, jobject appender_buf) { + + if (appender_buf == nullptr) { + env->ThrowNew(J_SQLException, "Invalid appender buffer"); + return nullptr; + } + + duckdb_appender appender = reinterpret_cast(env->GetDirectBufferAddress(appender_buf)); + if (appender == nullptr) { + env->ThrowNew(J_SQLException, "Invalid appender"); + return nullptr; + } + + return appender; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_appender_create_ext + * Signature: (Ljava/nio/ByteBuffer;[B[B[B[Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1appender_1create_1ext(JNIEnv *env, jclass, + jobject connection, + jbyteArray catalog, + jbyteArray schema, jbyteArray table, + jobjectArray out_appender) { + + duckdb_connection conn = conn_ref_buf_to_conn(env, connection); + if (env->ExceptionCheck()) { + return -1; + } + + std::string catalog_str = jbyteArray_to_string(env, catalog); + if (env->ExceptionCheck()) { + return -1; + } + const char *catalog_ptr = nullptr != catalog ? catalog_str.c_str() : nullptr; + + std::string schema_str = jbyteArray_to_string(env, schema); + if (env->ExceptionCheck()) { + return -1; + } + const char *schema_ptr = nullptr != schema ? schema_str.c_str() : nullptr; + + std::string table_str = jbyteArray_to_string(env, table); + if (env->ExceptionCheck()) { + return -1; + } + const char *table_ptr = nullptr != table ? table_str.c_str() : nullptr; + + check_out_param(env, out_appender); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_appender appender = nullptr; + + duckdb_state state = duckdb_appender_create_ext(conn, catalog_ptr, schema_ptr, table_ptr, &appender); + + if (state == DuckDBSuccess) { + jobject appender_ref_buf = env->NewDirectByteBuffer(appender, 0); + set_out_param(env, out_appender, appender_ref_buf); + } + + return static_cast(state); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_appender_error + * Signature: (Ljava/nio/ByteBuffer;)[B + */ +JNIEXPORT jbyteArray JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1appender_1error(JNIEnv *env, jclass, + jobject appender) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return nullptr; + } + + const char *error_msg = duckdb_appender_error(app); + if (error_msg == nullptr) { + return nullptr; + } + + idx_t len = static_cast(std::strlen(error_msg)); + + return make_jbyteArray(env, error_msg, len); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_appender_flush + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1appender_1flush(JNIEnv *env, jclass, jobject appender) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_state state = duckdb_appender_flush(app); + + return static_cast(state); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_appender_close + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1appender_1close(JNIEnv *env, jclass, jobject appender) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_state state = duckdb_appender_close(app); + + return static_cast(state); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_appender_destroy + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1appender_1destroy(JNIEnv *env, jclass, jobject appender) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_state state = duckdb_appender_destroy(&app); + + return static_cast(state); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_appender_column_count + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1count(JNIEnv *env, jclass, + jobject appender) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return -1; + } + + idx_t count = duckdb_appender_column_count(app); + + return static_cast(count); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_appender_column_type + * Signature: (Ljava/nio/ByteBuffer;J)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1appender_1column_1type(JNIEnv *env, jclass, + jobject appender, + jlong col_idx) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return nullptr; + } + + idx_t idx = jlong_to_idx(env, col_idx); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_logical_type logical_type = duckdb_appender_column_type(app, idx); + + return make_ptr_buf(env, logical_type); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_append_data_chunk + * Signature: (Ljava/nio/ByteBuffer;Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1append_1data_1chunk(JNIEnv *env, jclass, jobject appender, + jobject chunk) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_state state = duckdb_append_data_chunk(app, dc); + + return static_cast(state); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_append_default_to_chunk + * Signature: (Ljava/nio/ByteBuffer;Ljava/nio/ByteBuffer;JJ)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1append_1default_1to_1chunk(JNIEnv *env, jclass, + jobject appender, + jobject chunk, jlong col, + jlong row) { + + duckdb_appender app = appender_buf_to_appender(env, appender); + if (env->ExceptionCheck()) { + return -1; + } + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return -1; + } + idx_t col_idx = jlong_to_idx(env, col); + if (env->ExceptionCheck()) { + return -1; + } + idx_t row_idx = jlong_to_idx(env, row); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_state state = duckdb_append_default_to_chunk(app, dc, col_idx, row_idx); + + return static_cast(state); +} diff --git a/src/jni/bindings_common.cpp b/src/jni/bindings_common.cpp new file mode 100644 index 000000000..4f34e8b88 --- /dev/null +++ b/src/jni/bindings_common.cpp @@ -0,0 +1,13 @@ +#include "bindings.hpp" + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_vector_size + * Signature: ()J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1vector_1size(JNIEnv *, jclass) { + + idx_t vector_size = duckdb_vector_size(); + + return static_cast(vector_size); +} diff --git a/src/jni/bindings_data_chunk.cpp b/src/jni/bindings_data_chunk.cpp new file mode 100644 index 000000000..7995e2e44 --- /dev/null +++ b/src/jni/bindings_data_chunk.cpp @@ -0,0 +1,166 @@ +#include "bindings.hpp" +#include "refs.hpp" +#include "util.hpp" + +#include + +duckdb_data_chunk chunk_buf_to_chunk(JNIEnv *env, jobject chunk_buf) { + + if (chunk_buf == nullptr) { + env->ThrowNew(J_SQLException, "Invalid data chunk buffer"); + return nullptr; + } + + duckdb_data_chunk chunk = reinterpret_cast(env->GetDirectBufferAddress(chunk_buf)); + + if (chunk == nullptr) { + env->ThrowNew(J_SQLException, "Invalid data chunk"); + return nullptr; + } + + return chunk; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_create_data_chunk + * Signature: ([Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1create_1data_1chunk(JNIEnv *env, jclass, + jobjectArray logical_types) { + + if (logical_types == nullptr) { + env->ThrowNew(J_SQLException, "Invalid logical type array"); + return nullptr; + } + + size_t column_count = static_cast(env->GetArrayLength(logical_types)); + + std::vector lt_vec; + lt_vec.reserve(column_count); + + for (size_t i = 0; i < column_count; i++) { + + jobject lt_buf = env->GetObjectArrayElement(logical_types, i); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, lt_buf); + if (env->ExceptionCheck()) { + return nullptr; + } + + lt_vec.push_back(lt); + } + + duckdb_data_chunk data_chunk = duckdb_create_data_chunk(lt_vec.data(), static_cast(column_count)); + + return make_ptr_buf(env, data_chunk); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_destroy_data_chunk + * Signature: (Ljava/nio/ByteBuffer;)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1data_1chunk(JNIEnv *env, jclass, jobject chunk) { + + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return; + } + + duckdb_destroy_data_chunk(&dc); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_data_chunk_reset + * Signature: (Ljava/nio/ByteBuffer;)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1reset(JNIEnv *env, jclass, jobject chunk) { + + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return; + } + + duckdb_data_chunk_reset(dc); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_data_chunk_get_column_count + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1column_1count(JNIEnv *env, jclass, + jobject chunk) { + + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return -1; + } + + idx_t count = duckdb_data_chunk_get_column_count(dc); + + return static_cast(count); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_data_chunk_get_vector + * Signature: (Ljava/nio/ByteBuffer;J)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1vector(JNIEnv *env, jclass, + jobject chunk, + jlong col_idx) { + + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return nullptr; + } + + idx_t idx = jlong_to_idx(env, col_idx); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_vector vec = duckdb_data_chunk_get_vector(dc, idx); + + return make_ptr_buf(env, vec); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_data_chunk_get_size + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1get_1size(JNIEnv *env, jclass, + jobject chunk) { + + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return -1; + } + + idx_t dc_size = duckdb_data_chunk_get_size(dc); + + return static_cast(dc_size); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_data_chunk_set_size + * Signature: (Ljava/nio/ByteBuffer;J)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1data_1chunk_1set_1size(JNIEnv *env, jclass, jobject chunk, + jlong size) { + + duckdb_data_chunk dc = chunk_buf_to_chunk(env, chunk); + if (env->ExceptionCheck()) { + return; + } + + duckdb_data_chunk_set_size(dc, static_cast(size)); +} diff --git a/src/jni/bindings_logical_type.cpp b/src/jni/bindings_logical_type.cpp new file mode 100644 index 000000000..bc45b2937 --- /dev/null +++ b/src/jni/bindings_logical_type.cpp @@ -0,0 +1,272 @@ +#include "bindings.hpp" +#include "refs.hpp" +#include "util.hpp" + +#include + +duckdb_logical_type logical_type_buf_to_logical_type(JNIEnv *env, jobject logical_type_buf) { + + if (logical_type_buf == nullptr) { + env->ThrowNew(J_SQLException, "Invalid logical type buffer"); + return nullptr; + } + + duckdb_logical_type logical_type = + reinterpret_cast(env->GetDirectBufferAddress(logical_type_buf)); + if (logical_type == nullptr) { + env->ThrowNew(J_SQLException, "Invalid logical type"); + return nullptr; + } + + return logical_type; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_create_logical_type + * Signature: (I)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1create_1logical_1type(JNIEnv *env, jclass, jint type) { + + duckdb_type dt = static_cast(type); + + duckdb_logical_type lt = duckdb_create_logical_type(dt); + + return make_ptr_buf(env, lt); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_get_type_id + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1get_1type_1id(JNIEnv *env, jclass, jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return 1; + } + + duckdb_type type_id = duckdb_get_type_id(lt); + + return static_cast(type_id); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_decimal_width + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1width(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return -1; + } + + uint8_t width = duckdb_decimal_width(lt); + + return static_cast(width); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_decimal_scale + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1scale(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return -1; + } + + uint8_t scale = duckdb_decimal_scale(lt); + + return static_cast(scale); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_decimal_internal_type + * Signature: (Ljava/nio/ByteBuffer;)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1decimal_1internal_1type(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_type type_id = duckdb_decimal_internal_type(lt); + + return static_cast(type_id); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_create_list_type + * Signature: (Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1create_1list_1type(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_logical_type list_type = duckdb_create_list_type(lt); + + return make_ptr_buf(env, list_type); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_create_array_type + * Signature: (Ljava/nio/ByteBuffer;J)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1create_1array_1type(JNIEnv *env, jclass, + jobject logical_type, + jlong array_size) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return nullptr; + } + idx_t size_idx = jlong_to_idx(env, array_size); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_logical_type array_type = duckdb_create_array_type(lt, size_idx); + + return make_ptr_buf(env, array_type); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_create_struct_type + * Signature: ([Ljava/nio/ByteBuffer;[[B)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1create_1struct_1type(JNIEnv *env, jclass, + jobjectArray member_types, + jobjectArray member_names) { + + if (member_types == nullptr) { + env->ThrowNew(J_SQLException, "Invalid member types array"); + return nullptr; + } + if (member_names == nullptr) { + env->ThrowNew(J_SQLException, "Invalid member names array"); + return nullptr; + } + + size_t member_count = static_cast(env->GetArrayLength(member_types)); + size_t names_count = static_cast(env->GetArrayLength(member_names)); + if (member_count != names_count) { + env->ThrowNew(J_SQLException, "Invalid member names array size"); + return nullptr; + } + + std::vector mt_vec; + mt_vec.reserve(member_count); + + for (size_t i = 0; i < member_count; i++) { + jobject lt_buf = env->GetObjectArrayElement(member_types, i); + if (env->ExceptionCheck()) { + return nullptr; + } + if (nullptr == lt_buf) { + env->ThrowNew(J_SQLException, "Invalid null type specified"); + return nullptr; + } + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, lt_buf); + if (env->ExceptionCheck()) { + return nullptr; + } + mt_vec.push_back(lt); + } + + std::vector names_vec; + names_vec.reserve(member_count); + std::vector names_cstr_vec; + names_cstr_vec.reserve(member_count); + + for (size_t i = 0; i < member_count; i++) { + jbyteArray jba = reinterpret_cast(env->GetObjectArrayElement(member_names, i)); + if (env->ExceptionCheck()) { + return nullptr; + } + if (nullptr == jba) { + env->ThrowNew(J_SQLException, "Invalid null name specified"); + return nullptr; + } + std::string str = jbyteArray_to_string(env, jba); + if (env->ExceptionCheck()) { + return nullptr; + } + names_cstr_vec.emplace_back(str.c_str()); + names_vec.emplace_back(std::move(str)); + } + + duckdb_logical_type struct_type = + duckdb_create_struct_type(mt_vec.data(), names_cstr_vec.data(), static_cast(member_count)); + + return make_ptr_buf(env, struct_type); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_struct_type_child_count + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1struct_1type_1child_1count(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return -1; + } + + idx_t count = duckdb_struct_type_child_count(lt); + + return static_cast(count); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_array_type_array_size + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1array_1type_1array_1size(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return -1; + } + + idx_t size = duckdb_array_type_array_size(lt); + + return static_cast(size); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_destroy_logical_type + * Signature: (Ljava/nio/ByteBuffer;)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1logical_1type(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return; + } + + duckdb_destroy_logical_type(<); +} diff --git a/src/jni/bindings_validity.cpp b/src/jni/bindings_validity.cpp new file mode 100644 index 000000000..35465d1ea --- /dev/null +++ b/src/jni/bindings_validity.cpp @@ -0,0 +1,70 @@ +#include "bindings.hpp" +#include "refs.hpp" +#include "util.hpp" + +#include + +static uint64_t *validity_buf_to_validity(JNIEnv *env, jobject validity_buf) { + + if (validity_buf == nullptr) { + env->ThrowNew(J_SQLException, "Invalid validity buffer"); + return nullptr; + } + + uint64_t *validity = reinterpret_cast(env->GetDirectBufferAddress(validity_buf)); + + if (validity == nullptr) { + env->ThrowNew(J_SQLException, "Invalid validity"); + return nullptr; + } + + return validity; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_validity_row_is_valid + * Signature: (Ljava/nio/ByteBuffer;J)Z + */ +JNIEXPORT jboolean JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1validity_1row_1is_1valid(JNIEnv *env, jclass, + jobject validity, + jlong row) { + + uint64_t *val = validity_buf_to_validity(env, validity); + if (env->ExceptionCheck()) { + return false; + } + + idx_t row_idx = jlong_to_idx(env, row); + if (env->ExceptionCheck()) { + return false; + } + + bool res = duckdb_validity_row_is_valid(val, row_idx); + + return static_cast(res); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_validity_set_row_validity + * Signature: (Ljava/nio/ByteBuffer;JZ)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1validity_1set_1row_1validity(JNIEnv *env, jclass, + jobject validity, jlong row, + jboolean valid) { + + uint64_t *val = validity_buf_to_validity(env, validity); + if (env->ExceptionCheck()) { + return; + } + + idx_t row_idx = jlong_to_idx(env, row); + if (env->ExceptionCheck()) { + return; + } + + bool valid_flag = static_cast(valid); + + duckdb_validity_set_row_validity(val, row_idx, valid_flag); +} diff --git a/src/jni/bindings_vector.cpp b/src/jni/bindings_vector.cpp new file mode 100644 index 000000000..c59bf2192 --- /dev/null +++ b/src/jni/bindings_vector.cpp @@ -0,0 +1,288 @@ +#include "bindings.hpp" +#include "refs.hpp" +#include "util.hpp" + +#include + +static duckdb_vector vector_buf_to_vector(JNIEnv *env, jobject vector_buf) { + + if (vector_buf == nullptr) { + env->ThrowNew(J_SQLException, "Invalid vector buffer"); + return nullptr; + } + + duckdb_vector vector = reinterpret_cast(env->GetDirectBufferAddress(vector_buf)); + + if (vector == nullptr) { + env->ThrowNew(J_SQLException, "Invalid vector"); + return nullptr; + } + + return vector; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_create_vector + * Signature: (Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1create_1vector(JNIEnv *env, jclass, + jobject logical_type) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return nullptr; + } + + idx_t cap = duckdb_vector_size(); + + duckdb_vector vec = duckdb_create_vector(lt, cap); + + return make_ptr_buf(env, vec); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_destroy_vector + * Signature: (Ljava/nio/ByteBuffer;)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1vector(JNIEnv *env, jclass, jobject vector) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return; + } + + duckdb_destroy_vector(&vec); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_vector_get_column_type + * Signature: (Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1column_1type(JNIEnv *env, jclass, + jobject vector) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_logical_type lt = duckdb_vector_get_column_type(vec); + + return make_ptr_buf(env, lt); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_vector_get_data + * Signature: (Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1data(JNIEnv *env, jclass, jobject vector, + jlong col_width_bytes) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return nullptr; + } + idx_t width = jlong_to_idx(env, col_width_bytes); + if (env->ExceptionCheck()) { + return nullptr; + } + + void *data = duckdb_vector_get_data(vec); + + if (data != nullptr) { + idx_t vector_size = duckdb_vector_size() * width; + return make_data_buf(env, data, vector_size); + } + + return nullptr; +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_vector_get_validity + * Signature: (Ljava/nio/ByteBuffer;J)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1vector_1get_1validity(JNIEnv *env, jclass, + jobject vector, + jlong array_size) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return nullptr; + } + + uint64_t *mask = duckdb_vector_get_validity(vec); + idx_t vec_len = duckdb_vector_size(); + idx_t mask_len = vec_len * sizeof(uint64_t) * array_size / 64; + + return make_data_buf(env, mask, mask_len); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_vector_ensure_validity_writable + * Signature: (Ljava/nio/ByteBuffer;)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1vector_1ensure_1validity_1writable(JNIEnv *env, jclass, + jobject vector) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return; + } + + duckdb_vector_ensure_validity_writable(vec); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_vector_assign_string_element_len + * Signature: (Ljava/nio/ByteBuffer;J[B)V + */ +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1vector_1assign_1string_1element_1len(JNIEnv *env, jclass, + jobject vector, + jlong index, + jbyteArray str) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return; + } + idx_t idx = jlong_to_idx(env, index); + if (env->ExceptionCheck()) { + return; + } + auto str_ptr = make_jbyteArray_ptr(env, str); + if (env->ExceptionCheck()) { + return; + } + + idx_t len = static_cast(env->GetArrayLength(str)); + + duckdb_vector_assign_string_element_len(vec, idx, str_ptr.get(), len); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_list_vector_get_child + * Signature: (Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1child(JNIEnv *env, jclass, + jobject vector) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_vector res = duckdb_list_vector_get_child(vec); + + return make_ptr_buf(env, res); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_list_vector_get_size + * Signature: (Ljava/nio/ByteBuffer;)J + */ +JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1get_1size(JNIEnv *env, jclass, + jobject vector) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return -1; + } + + idx_t size = duckdb_list_vector_get_size(vec); + + return uint64_to_jlong(size); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_list_vector_set_size + * Signature: (Ljava/nio/ByteBuffer;J)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1set_1size(JNIEnv *env, jclass, + jobject vector, jlong size) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return -1; + } + idx_t size_idx = jlong_to_idx(env, size); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_state state = duckdb_list_vector_set_size(vec, size_idx); + + return static_cast(state); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_list_vector_reserve + * Signature: (Ljava/nio/ByteBuffer;J)I + */ +JNIEXPORT jint JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1list_1vector_1reserve(JNIEnv *env, jclass, jobject vector, + jlong capacity) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return -1; + } + idx_t cap = jlong_to_idx(env, capacity); + if (env->ExceptionCheck()) { + return -1; + } + + duckdb_state state = duckdb_list_vector_reserve(vec, cap); + + return static_cast(state); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_struct_vector_get_child + * Signature: (Ljava/nio/ByteBuffer;J)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1struct_1vector_1get_1child(JNIEnv *env, jclass, + jobject vector, + jlong index) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return nullptr; + } + idx_t idx = jlong_to_idx(env, index); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_vector res = duckdb_struct_vector_get_child(vec, idx); + + return make_ptr_buf(env, res); +} + +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_array_vector_get_child + * Signature: (Ljava/nio/ByteBuffer;)Ljava/nio/ByteBuffer; + */ +JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1array_1vector_1get_1child(JNIEnv *env, jclass, + jobject vector) { + + duckdb_vector vec = vector_buf_to_vector(env, vector); + if (env->ExceptionCheck()) { + return nullptr; + } + + duckdb_vector res = duckdb_array_vector_get_child(vec); + + return make_ptr_buf(env, res); +} diff --git a/src/jni/duckdb_java.cpp b/src/jni/duckdb_java.cpp index 2ca49f6d7..0c425f04a 100644 --- a/src/jni/duckdb_java.cpp +++ b/src/jni/duckdb_java.cpp @@ -67,7 +67,7 @@ JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved) { duckdb::DBInstanceCache instance_cache; jobject _duckdb_jdbc_startup(JNIEnv *env, jclass, jbyteArray database_j, jboolean read_only, jobject props) { - auto database = byte_array_to_string(env, database_j); + auto database = jbyteArray_to_string(env, database_j); std::unique_ptr config = create_db_config(env, read_only, props); bool cache_instance = database != ":memory:" && !database.empty(); auto shared_db = instance_cache.GetOrCreateInstance(database, *config, cache_instance); @@ -196,7 +196,7 @@ jobject _duckdb_jdbc_prepare(JNIEnv *env, jclass, jobject conn_ref_buf, jbyteArr return nullptr; } - auto query = byte_array_to_string(env, query_j); + auto query = jbyteArray_to_string(env, query_j); auto statements = conn_ref->ExtractStatements(query.c_str()); if (statements.empty()) { @@ -625,8 +625,8 @@ jobject _duckdb_jdbc_create_appender(JNIEnv *env, jclass, jobject conn_ref_buf, if (!conn_ref) { return nullptr; } - auto schema_name = byte_array_to_string(env, schema_name_j); - auto table_name = byte_array_to_string(env, table_name_j); + auto schema_name = jbyteArray_to_string(env, schema_name_j); + auto table_name = jbyteArray_to_string(env, table_name_j); auto appender = new Appender(*conn_ref, schema_name, table_name); return env->NewDirectByteBuffer(appender, 0); } @@ -701,7 +701,7 @@ void _duckdb_jdbc_appender_append_string(JNIEnv *env, jclass, jobject appender_r return; } - auto string_value = byte_array_to_string(env, value); + auto string_value = jbyteArray_to_string(env, value); get_appender(env, appender_ref_buf)->Append(string_value.c_str()); } @@ -711,7 +711,7 @@ void _duckdb_jdbc_appender_append_bytes(JNIEnv *env, jclass, jobject appender_re return; } - auto string_value = byte_array_to_string(env, value); + auto string_value = jbyteArray_to_string(env, value); get_appender(env, appender_ref_buf)->Append(Value::BLOB_RAW(string_value)); } @@ -771,7 +771,7 @@ void _duckdb_jdbc_arrow_register(JNIEnv *env, jclass, jobject conn_ref_buf, jlon if (conn == nullptr) { return; } - auto name = byte_array_to_string(env, name_j); + auto name = jbyteArray_to_string(env, name_j); auto arrow_array_stream = (ArrowArrayStream *)(uintptr_t)arrow_array_stream_pointer; diff --git a/src/jni/holders.hpp b/src/jni/holders.hpp index b268b3197..d099dabb1 100644 --- a/src/jni/holders.hpp +++ b/src/jni/holders.hpp @@ -1,6 +1,10 @@ #pragma once +extern "C" { +#include "duckdb.h" +} #include "duckdb.hpp" +#include "refs.hpp" #include @@ -70,3 +74,22 @@ inline duckdb::Connection *get_connection(JNIEnv *env, jobject conn_ref_buf) { return conn_ref; } + +inline duckdb_connection conn_ref_buf_to_conn(JNIEnv *env, jobject conn_ref_buf) { + if (conn_ref_buf == nullptr) { + env->ThrowNew(J_SQLException, "Invalid connection buffer"); + return nullptr; + } + auto conn_holder = reinterpret_cast(env->GetDirectBufferAddress(conn_ref_buf)); + if (conn_holder == nullptr) { + env->ThrowNew(J_SQLException, "Invalid connection holder"); + return nullptr; + } + auto conn_ref = conn_holder->connection.get(); + if (conn_ref == nullptr || conn_ref->context == nullptr) { + env->ThrowNew(J_SQLException, "Invalid connection"); + return nullptr; + } + + return reinterpret_cast(conn_ref); +} diff --git a/src/jni/refs.cpp b/src/jni/refs.cpp index f6ae8146d..bc20f691f 100644 --- a/src/jni/refs.cpp +++ b/src/jni/refs.cpp @@ -75,6 +75,9 @@ jclass J_DuckStruct; jmethodID J_DuckStruct_init; jclass J_ByteBuffer; +jmethodID J_ByteBuffer_order; +jclass J_ByteOrder; +jobject J_ByteOrder_LITTLE_ENDIAN; jclass J_DuckMap; jmethodID J_DuckMap_getSQLTypeName; @@ -271,6 +274,9 @@ void create_refs(JNIEnv *env) { J_DuckVector_varlen = get_field_id(env, J_DuckVector, "varlen_data", "[Ljava/lang/Object;"); J_ByteBuffer = make_class_ref(env, "java/nio/ByteBuffer"); + J_ByteBuffer_order = get_method_id(env, J_ByteBuffer, "order", "(Ljava/nio/ByteOrder;)Ljava/nio/ByteBuffer;"); + J_ByteOrder = make_class_ref(env, "java/nio/ByteOrder"); + J_ByteOrder_LITTLE_ENDIAN = make_static_object_field_ref(env, J_ByteOrder, "LITTLE_ENDIAN", "Ljava/nio/ByteOrder;"); J_ProfilerPrintFormat = make_class_ref(env, "org/duckdb/ProfilerPrintFormat"); J_ProfilerPrintFormat_QUERY_TREE = diff --git a/src/jni/refs.hpp b/src/jni/refs.hpp index 5bfeb2639..006afa36d 100644 --- a/src/jni/refs.hpp +++ b/src/jni/refs.hpp @@ -72,6 +72,9 @@ extern jclass J_DuckStruct; extern jmethodID J_DuckStruct_init; extern jclass J_ByteBuffer; +extern jmethodID J_ByteBuffer_order; +extern jclass J_ByteOrder; +extern jobject J_ByteOrder_LITTLE_ENDIAN; extern jclass J_DuckMap; extern jmethodID J_DuckMap_getSQLTypeName; diff --git a/src/jni/types.cpp b/src/jni/types.cpp index 3789fc69c..257fc5594 100644 --- a/src/jni/types.cpp +++ b/src/jni/types.cpp @@ -197,7 +197,7 @@ duckdb::Value to_duckdb_value(JNIEnv *env, jobject param, duckdb::ClientContext auto param_string = jstring_to_string(env, (jstring)param); return (duckdb::Value(param_string)); } else if (env->IsInstanceOf(param, J_ByteArray)) { - return (duckdb::Value::BLOB_RAW(byte_array_to_string(env, (jbyteArray)param))); + return (duckdb::Value::BLOB_RAW(jbyteArray_to_string(env, (jbyteArray)param))); } else if (env->IsInstanceOf(param, J_UUID)) { return create_value_from_uuid(env, param); } else if (env->IsInstanceOf(param, J_DuckMap)) { diff --git a/src/jni/util.cpp b/src/jni/util.cpp index 8996c3cd9..c22d03c65 100644 --- a/src/jni/util.cpp +++ b/src/jni/util.cpp @@ -3,7 +3,9 @@ #include "refs.hpp" #include +#include #include +#include void check_java_exception_and_rethrow(JNIEnv *env) { if (env->ExceptionCheck()) { @@ -19,24 +21,33 @@ void check_java_exception_and_rethrow(JNIEnv *env) { } } -std::string byte_array_to_string(JNIEnv *env, jbyteArray ba_j) { - idx_t len = env->GetArrayLength(ba_j); +std::string jbyteArray_to_string(JNIEnv *env, jbyteArray ba_j) { + if (nullptr == ba_j) { + return std::string(); + } + size_t len = static_cast(env->GetArrayLength(ba_j)); + if (len == 0) { + return std::string(); + } std::string ret; ret.resize(len); - jbyte *bytes = (jbyte *)env->GetByteArrayElements(ba_j, NULL); - - for (idx_t i = 0; i < len; i++) { - ret[i] = bytes[i]; + jbyte *bytes = reinterpret_cast(env->GetByteArrayElements(ba_j, nullptr)); + if (bytes == nullptr) { + env->ThrowNew(J_SQLException, "GetByteArrayElements error"); + return std::string(); } + + std::memcpy(&ret[0], bytes, len); + env->ReleaseByteArrayElements(ba_j, bytes, 0); return ret; } -std::string jstring_to_string(JNIEnv *env, jstring string_j) { - jbyteArray bytes = (jbyteArray)env->CallObjectMethod(string_j, J_String_getBytes, J_Charset_UTF8); - return byte_array_to_string(env, bytes); +std::string jstring_to_string(JNIEnv *env, jstring jstr) { + jbyteArray bytes = reinterpret_cast(env->CallObjectMethod(jstr, J_String_getBytes, J_Charset_UTF8)); + return jbyteArray_to_string(env, bytes); } jobject decode_charbuffer_to_jstring(JNIEnv *env, const char *d_str, idx_t d_str_len) { @@ -52,3 +63,90 @@ jlong uint64_to_jlong(uint64_t value) { } return static_cast(std::numeric_limits::max()); } + +idx_t jlong_to_idx(JNIEnv *env, jlong value) { + if (value < 0) { + env->ThrowNew(J_SQLException, "Invalid index"); + return 0; + } + return static_cast(value); +} + +void check_out_param(JNIEnv *env, jobjectArray out_param) { + if (out_param == nullptr) { + env->ThrowNew(J_SQLException, "Invalid null output parameter"); + return; + } + if (env->GetArrayLength(out_param) != 1) { + env->ThrowNew(J_SQLException, "Invalid output parameter"); + return; + } +} + +void set_out_param(JNIEnv *env, jobjectArray out_param, jobject value) { + if (out_param == nullptr) { + env->ThrowNew(J_SQLException, "Invalid null output parameter"); + return; + } + env->SetObjectArrayElement(out_param, 0, value); +} + +jbyteArray_ptr make_jbyteArray_ptr(JNIEnv *env, jbyteArray jbytes) { + if (jbytes == nullptr) { + return jbyteArray_ptr(nullptr, [](char *) {}); + } + jbyte *bytes = env->GetByteArrayElements(jbytes, nullptr); + if (bytes == nullptr) { + env->ThrowNew(J_SQLException, "GetByteArrayElements error"); + return jbyteArray_ptr(nullptr, [](char *) {}); + } + + char *chars = reinterpret_cast(bytes); + + return jbyteArray_ptr(chars, [env, jbytes](char *ptr) { + jbyte *bytes = reinterpret_cast(ptr); + env->ReleaseByteArrayElements(jbytes, bytes, 0); + }); +} + +jbyteArray make_jbyteArray(JNIEnv *env, const char *data, idx_t len) { + if (data == nullptr) { + return nullptr; + } + + jbyteArray jbytes = env->NewByteArray(static_cast(len)); + if (jbytes == nullptr) { + env->ThrowNew(J_SQLException, "NewByteArray error"); + return nullptr; + } + + jbyte *bytes = env->GetByteArrayElements(jbytes, nullptr); + if (bytes == nullptr) { + env->ThrowNew(J_SQLException, "GetByteArrayElements error"); + return nullptr; + } + + std::memcpy(bytes, data, static_cast(len)); + + env->ReleaseByteArrayElements(jbytes, bytes, 0); + + return jbytes; +} + +jobject make_ptr_buf(JNIEnv *env, void *ptr) { + if (ptr != nullptr) { + return env->NewDirectByteBuffer(ptr, 0); + } + + return nullptr; +} + +jobject make_data_buf(JNIEnv *env, void *data, idx_t len) { + if (data != nullptr) { + jobject buf = env->NewDirectByteBuffer(data, uint64_to_jlong(len)); + env->CallObjectMethod(buf, J_ByteBuffer_order, J_ByteOrder_LITTLE_ENDIAN); + return buf; + } + + return nullptr; +} diff --git a/src/jni/util.hpp b/src/jni/util.hpp index 7a8374459..2c2d3c897 100644 --- a/src/jni/util.hpp +++ b/src/jni/util.hpp @@ -1,16 +1,38 @@ #pragma once -#include "duckdb.hpp" +extern "C" { +#include "duckdb.h" +} +#include #include +#include #include +using jstring_ptr = std::unique_ptr>; + +using jbyteArray_ptr = std::unique_ptr>; + void check_java_exception_and_rethrow(JNIEnv *env); -std::string byte_array_to_string(JNIEnv *env, jbyteArray ba_j); +std::string jbyteArray_to_string(JNIEnv *env, jbyteArray ba_j); std::string jstring_to_string(JNIEnv *env, jstring string_j); jobject decode_charbuffer_to_jstring(JNIEnv *env, const char *d_str, idx_t d_str_len); jlong uint64_to_jlong(uint64_t value); + +idx_t jlong_to_idx(JNIEnv *env, jlong value); + +void check_out_param(JNIEnv *env, jobjectArray out_param); + +void set_out_param(JNIEnv *env, jobjectArray out_param, jobject value); + +jbyteArray_ptr make_jbyteArray_ptr(JNIEnv *env, jbyteArray jbytes); + +jbyteArray make_jbyteArray(JNIEnv *env, const char *data, idx_t len); + +jobject make_ptr_buf(JNIEnv *env, void *ptr); + +jobject make_data_buf(JNIEnv *env, void *data, idx_t len); diff --git a/src/main/java/org/duckdb/DuckDBAppender.java b/src/main/java/org/duckdb/DuckDBAppender.java index fd79f9726..c591a427d 100644 --- a/src/main/java/org/duckdb/DuckDBAppender.java +++ b/src/main/java/org/duckdb/DuckDBAppender.java @@ -1,106 +1,1598 @@ package org.duckdb; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.time.ZoneOffset.UTC; +import static java.time.temporal.ChronoUnit.*; +import static org.duckdb.DuckDBBindings.*; +import static org.duckdb.DuckDBBindings.CAPIType.*; +import static org.duckdb.DuckDBHugeInt.HUGE_INT_MAX; +import static org.duckdb.DuckDBHugeInt.HUGE_INT_MIN; + import java.math.BigDecimal; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; +import java.math.BigInteger; +import java.nio.*; import java.sql.SQLException; -import java.time.LocalDateTime; -import org.duckdb.DuckDBTimestamp; +import java.time.*; +import java.util.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; public class DuckDBAppender implements AutoCloseable { - protected ByteBuffer appender_ref = null; + private static final Set supportedTypes = new LinkedHashSet<>(); + static { + supportedTypes.add(DUCKDB_TYPE_BOOLEAN.typeId); + supportedTypes.add(DUCKDB_TYPE_TINYINT.typeId); + supportedTypes.add(DUCKDB_TYPE_UTINYINT.typeId); + supportedTypes.add(DUCKDB_TYPE_SMALLINT.typeId); + supportedTypes.add(DUCKDB_TYPE_USMALLINT.typeId); + supportedTypes.add(DUCKDB_TYPE_INTEGER.typeId); + supportedTypes.add(DUCKDB_TYPE_UINTEGER.typeId); + supportedTypes.add(DUCKDB_TYPE_BIGINT.typeId); + supportedTypes.add(DUCKDB_TYPE_UBIGINT.typeId); + supportedTypes.add(DUCKDB_TYPE_HUGEINT.typeId); + supportedTypes.add(DUCKDB_TYPE_UHUGEINT.typeId); + + supportedTypes.add(DUCKDB_TYPE_FLOAT.typeId); + supportedTypes.add(DUCKDB_TYPE_DOUBLE.typeId); + + supportedTypes.add(DUCKDB_TYPE_DECIMAL.typeId); + + supportedTypes.add(DUCKDB_TYPE_VARCHAR.typeId); + supportedTypes.add(DUCKDB_TYPE_BLOB.typeId); + + supportedTypes.add(DUCKDB_TYPE_DATE.typeId); + supportedTypes.add(DUCKDB_TYPE_TIME.typeId); + supportedTypes.add(DUCKDB_TYPE_TIME_TZ.typeId); + supportedTypes.add(DUCKDB_TYPE_TIMESTAMP_S.typeId); + supportedTypes.add(DUCKDB_TYPE_TIMESTAMP_MS.typeId); + supportedTypes.add(DUCKDB_TYPE_TIMESTAMP.typeId); + supportedTypes.add(DUCKDB_TYPE_TIMESTAMP_TZ.typeId); + supportedTypes.add(DUCKDB_TYPE_TIMESTAMP_NS.typeId); + + supportedTypes.add(DUCKDB_TYPE_ARRAY.typeId); + supportedTypes.add(DUCKDB_TYPE_STRUCT.typeId); + } + private static final CAPIType[] int8Types = new CAPIType[] {DUCKDB_TYPE_TINYINT, DUCKDB_TYPE_UTINYINT}; + private static final CAPIType[] int16Types = new CAPIType[] {DUCKDB_TYPE_SMALLINT, DUCKDB_TYPE_USMALLINT}; + private static final CAPIType[] int32Types = new CAPIType[] {DUCKDB_TYPE_INTEGER, DUCKDB_TYPE_UINTEGER}; + private static final CAPIType[] int64Types = new CAPIType[] {DUCKDB_TYPE_BIGINT, DUCKDB_TYPE_UBIGINT}; + private static final CAPIType[] int128Types = new CAPIType[] {DUCKDB_TYPE_HUGEINT, DUCKDB_TYPE_UHUGEINT}; + private static final CAPIType[] timestampLocalTypes = new CAPIType[] { + DUCKDB_TYPE_TIMESTAMP_S, DUCKDB_TYPE_TIMESTAMP_MS, DUCKDB_TYPE_TIMESTAMP, DUCKDB_TYPE_TIMESTAMP_NS}; + private static final CAPIType[] timestampMicrosTypes = + new CAPIType[] {DUCKDB_TYPE_TIMESTAMP, DUCKDB_TYPE_TIMESTAMP_TZ}; + + private static final int STRING_MAX_INLINE_BYTES = 12; + + private static final LocalDateTime EPOCH_DATE_TIME = LocalDateTime.ofEpochSecond(0, 0, UTC); + + private final DuckDBConnection conn; + + private final String catalog; + private final String schema; + private final String table; + + private final long maxRows; + + private ByteBuffer appenderRef; + private final Lock appenderRefLock = new ReentrantLock(); + + private final ByteBuffer chunkRef; + private final Column[] columns; - public DuckDBAppender(DuckDBConnection con, String schemaName, String tableName) throws SQLException { - if (con == null) { - throw new SQLException("Invalid connection"); + private long rowIdx = 0; + private int colIdx = 0; + private int structFieldIdx = 0; + + private boolean appendingRow = false; + private boolean appendingStruct = false; + + private boolean writeInlinedStrings = true; + + DuckDBAppender(DuckDBConnection conn, String catalog, String schema, String table) throws SQLException { + this.conn = conn; + this.catalog = catalog; + this.schema = schema; + this.table = table; + + this.maxRows = duckdb_vector_size(); + + ByteBuffer appenderRef = null; + ByteBuffer[] colTypes = null; + ByteBuffer chunkRef = null; + Column[] vectors = null; + try { + appenderRef = createAppender(conn, catalog, schema, table); + colTypes = readTableTypes(appenderRef); + chunkRef = createChunk(colTypes); + vectors = createVectors(chunkRef, colTypes); + } catch (Exception e) { + if (null != chunkRef) { + duckdb_destroy_data_chunk(chunkRef); + } + if (null != colTypes) { + for (ByteBuffer ct : colTypes) { + if (null != ct) { + duckdb_destroy_logical_type(ct); + } + } + } + if (null != appenderRef) { + duckdb_appender_destroy(appenderRef); + } + throw new SQLException(createErrMsg(e.getMessage()), e); + } + + this.appenderRef = appenderRef; + this.chunkRef = chunkRef; + this.columns = vectors; + } + + public DuckDBAppender beginRow() throws SQLException { + checkOpen(); + checkAppendingRow(false); + checkAppendingStruct(false); + if (0 != colIdx) { + throw new SQLException(createErrMsg("'endRow' must be called before adding next row")); } - appender_ref = DuckDBNative.duckdb_jdbc_create_appender( - con.connRef, schemaName.getBytes(StandardCharsets.UTF_8), tableName.getBytes(StandardCharsets.UTF_8)); + this.appendingRow = true; + return this; } - public void beginRow() throws SQLException { - DuckDBNative.duckdb_jdbc_appender_begin_row(appender_ref); + public DuckDBAppender endRow() throws SQLException { + checkOpen(); + checkAppendingRow(true); + checkAppendingStruct(false); + + if (columns.length != colIdx) { + throw new SQLException(createErrMsg("'endRow' can be called only after adding all columns")); + } + + rowIdx++; + this.appendingRow = false; + if (rowIdx >= maxRows) { + try { + flush(); + } catch (SQLException e) { + this.appendingRow = true; + rowIdx--; + throw e; + } + } + + colIdx = 0; + return this; } - public void endRow() throws SQLException { - DuckDBNative.duckdb_jdbc_appender_end_row(appender_ref); + public DuckDBAppender beginStruct() throws Exception { + checkOpen(); + checkAppendingStruct(false); + this.appendingStruct = true; + return this; + } + + public DuckDBAppender endStruct() throws Exception { + checkOpen(); + checkAppendingStruct(true); + this.structFieldIdx = 0; + this.appendingStruct = false; + incrementColOrStructFieldIdx(); + return this; } public void flush() throws SQLException { - DuckDBNative.duckdb_jdbc_appender_flush(appender_ref); + checkOpen(); + checkAppendingRow(false); + checkAppendingStruct(false); + + if (0 == rowIdx) { + return; + } + + appenderRefLock.lock(); + try { + checkOpen(); + + duckdb_data_chunk_set_size(chunkRef, rowIdx); + + int appendState = duckdb_append_data_chunk(appenderRef, chunkRef); + if (0 != appendState) { + byte[] errorUTF8 = duckdb_appender_error(appenderRef); + String error = null != errorUTF8 ? strFromUTF8(errorUTF8) : ""; + throw new SQLException(createErrMsg(error)); + } + + int flushState = duckdb_appender_flush(appenderRef); + if (0 != flushState) { + byte[] errorUTF8 = duckdb_appender_error(appenderRef); + String error = null != errorUTF8 ? strFromUTF8(errorUTF8) : ""; + throw new SQLException(createErrMsg(error)); + } + + duckdb_data_chunk_reset(chunkRef); + try { + for (Column col : columns) { + col.reset(); + } + } catch (SQLException e) { + throw new SQLException(createErrMsg(e.getMessage()), e); + } + + rowIdx = 0; + } finally { + appenderRefLock.unlock(); + } + } + + @Override + public void close() throws SQLException { + if (isClosed()) { + return; + } + appenderRefLock.lock(); + try { + if (isClosed()) { + return; + } + if (rowIdx > 0) { + try { + flush(); + } catch (SQLException e) { + // suppress + } + } + for (Column col : columns) { + col.destroy(); + } + duckdb_destroy_data_chunk(chunkRef); + duckdb_appender_close(appenderRef); + duckdb_appender_destroy(appenderRef); + + // Untrack the appender from parent connection, + // if 'closing' flag is set it means that the parent connection itself + // is being closed and we don't need to untrack this instance from the connection. + if (!conn.closing) { + conn.connRefLock.lock(); + try { + conn.appenders.remove(this); + } finally { + conn.connRefLock.unlock(); + } + } + + appenderRef = null; + } finally { + appenderRefLock.unlock(); + } + } + + public boolean isClosed() throws SQLException { + return appenderRef == null; + } + + // append primitives + + public DuckDBAppender append(boolean value) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_BOOLEAN); + byte val = (byte) (value ? 1 : 0); + col.data.put(val); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(char value) throws SQLException { + String str = String.valueOf(value); + return append(str); + } + + public DuckDBAppender append(byte value) throws SQLException { + Column col = currentColumnWithRowPos(int8Types); + col.data.put(value); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(short value) throws SQLException { + Column col = currentColumnWithRowPos(int16Types); + col.data.putShort(value); + incrementColOrStructFieldIdx(); + return this; } - public void append(boolean value) throws SQLException { - DuckDBNative.duckdb_jdbc_appender_append_boolean(appender_ref, value); + public DuckDBAppender append(int value) throws SQLException { + Column col = currentColumnWithRowPos(int32Types); + col.data.putInt(value); + incrementColOrStructFieldIdx(); + return this; } - public void append(byte value) throws SQLException { - DuckDBNative.duckdb_jdbc_appender_append_byte(appender_ref, value); + public DuckDBAppender append(long value) throws SQLException { + Column col = currentColumnWithRowPos(int64Types); + col.data.putLong(value); + incrementColOrStructFieldIdx(); + return this; } - public void append(short value) throws SQLException { - DuckDBNative.duckdb_jdbc_appender_append_short(appender_ref, value); + public DuckDBAppender append(float value) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_FLOAT); + col.data.putFloat(value); + incrementColOrStructFieldIdx(); + return this; } - public void append(int value) throws SQLException { - DuckDBNative.duckdb_jdbc_appender_append_int(appender_ref, value); + public DuckDBAppender append(double value) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_DOUBLE); + col.data.putDouble(value); + incrementColOrStructFieldIdx(); + return this; } - public void append(long value) throws SQLException { - DuckDBNative.duckdb_jdbc_appender_append_long(appender_ref, value); + // append primitive wrappers, int128 and decimal + + public DuckDBAppender append(Boolean value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_BOOLEAN); + if (value == null) { + return appendNull(); + } + return append(value.booleanValue()); } - // New naming schema for object params to keep compatibility with calling "append(null)" - public void appendLocalDateTime(LocalDateTime value) throws SQLException { + public DuckDBAppender append(Character value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_VARCHAR); if (value == null) { - DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); - } else { - long timeInMicros = DuckDBTimestamp.localDateTime2Micros(value); - DuckDBNative.duckdb_jdbc_appender_append_timestamp(appender_ref, timeInMicros); + return appendNull(); } + return append(value.charValue()); } - public void appendBigDecimal(BigDecimal value) throws SQLException { + public DuckDBAppender append(Byte value) throws SQLException { + checkCurrentColumnType(int8Types); if (value == null) { - DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); - } else { - DuckDBNative.duckdb_jdbc_appender_append_decimal(appender_ref, value); + return appendNull(); } + return append(value.byteValue()); } - public void append(float value) throws SQLException { - DuckDBNative.duckdb_jdbc_appender_append_float(appender_ref, value); + public DuckDBAppender append(Short value) throws SQLException { + checkCurrentColumnType(int16Types); + if (value == null) { + return appendNull(); + } + return append(value.shortValue()); } - public void append(double value) throws SQLException { - DuckDBNative.duckdb_jdbc_appender_append_double(appender_ref, value); + public DuckDBAppender append(Integer value) throws SQLException { + checkCurrentColumnType(int32Types); + if (value == null) { + return appendNull(); + } + return append(value.intValue()); } - public void append(String value) throws SQLException { + public DuckDBAppender append(Long value) throws SQLException { + checkCurrentColumnType(int64Types); if (value == null) { - DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); - } else { - DuckDBNative.duckdb_jdbc_appender_append_string(appender_ref, value.getBytes(StandardCharsets.UTF_8)); + return appendNull(); + } + return append(value.longValue()); + } + + public DuckDBAppender append(Float value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_FLOAT); + if (value == null) { + return appendNull(); + } + return append(value.floatValue()); + } + + public DuckDBAppender append(Double value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_DOUBLE); + if (value == null) { + return appendNull(); + } + return append(value.doubleValue()); + } + + public DuckDBAppender appendHugeInt(long lower, long upper) throws SQLException { + Column col = currentColumnWithRowPos(int128Types); + col.data.putLong(lower); + col.data.putLong(upper); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(BigInteger value) throws SQLException { + checkCurrentColumnType(int128Types); + if (value == null) { + return appendNull(); + } + if (value.compareTo(HUGE_INT_MIN) < 0 || value.compareTo(HUGE_INT_MAX) > 0) { + throw new SQLException("Specified BigInteger value is out of range for HUGEINT field"); + } + long lower = value.longValue(); + long upper = value.shiftRight(64).longValue(); + return appendHugeInt(lower, upper); + } + + public DuckDBAppender appendDecimal(short value) throws SQLException { + Column col = currentDecimalColumnWithRowPos(DUCKDB_TYPE_SMALLINT); + col.data.putShort(value); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendDecimal(int value) throws SQLException { + Column col = currentDecimalColumnWithRowPos(DUCKDB_TYPE_INTEGER); + col.data.putInt(value); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendDecimal(long value) throws SQLException { + Column col = currentDecimalColumnWithRowPos(DUCKDB_TYPE_BIGINT); + col.data.putLong(value); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendDecimal(long lower, long upper) throws SQLException { + Column col = currentDecimalColumnWithRowPos(DUCKDB_TYPE_HUGEINT); + col.data.putLong(lower); + col.data.putLong(upper); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(BigDecimal value) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_DECIMAL); + if (value == null) { + return appendNull(); + } + if (value.precision() > col.decimalPrecision) { + throw new SQLException(createErrMsg("invalid decimal precision, max expected: " + col.decimalPrecision + + ", actual: " + value.precision())); + } + if (col.decimalScale != value.scale()) { + throw new SQLException( + createErrMsg("invalid decimal scale, expected: " + col.decimalScale + ", actual: " + value.scale())); + } + + switch (col.decimalInternalType) { + case DUCKDB_TYPE_SMALLINT: { + checkDecimalPrecision(value, DUCKDB_TYPE_SMALLINT, 4); + short shortValue = value.unscaledValue().shortValueExact(); + return appendDecimal(shortValue); + } + case DUCKDB_TYPE_INTEGER: { + checkDecimalPrecision(value, DUCKDB_TYPE_INTEGER, 9); + int intValue = value.unscaledValue().intValueExact(); + return appendDecimal(intValue); + } + case DUCKDB_TYPE_BIGINT: { + checkDecimalPrecision(value, DUCKDB_TYPE_BIGINT, 18); + long longValue = value.unscaledValue().longValueExact(); + return appendDecimal(longValue); + } + case DUCKDB_TYPE_HUGEINT: { + checkDecimalPrecision(value, DUCKDB_TYPE_HUGEINT, 38); + BigInteger unscaledValue = value.unscaledValue(); + long lower = unscaledValue.longValue(); + long upper = unscaledValue.shiftRight(64).longValue(); + return appendDecimal(lower, upper); + } + default: + throw new SQLException(createErrMsg("invalid decimal internal type: '" + col.decimalInternalType + "'")); + } + } + + // append arrays + + public DuckDBAppender append(boolean[] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(boolean[] values, boolean[] nullMask) throws SQLException { + Column col = currentArrayInnerColumn(DUCKDB_TYPE_BOOLEAN); + if (values == null) { + return appendNull(); + } + + byte[] bytes = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + bytes[i] = (byte) (values[i] ? 1 : 0); + } + + checkArrayLength(col, values.length); + setArrayNullMask(col, nullMask); + + int pos = (int) (rowIdx * col.arraySize); + col.data.position(pos); + col.data.put(bytes); + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(boolean[][] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(boolean[][] values, boolean[][] nullMask) throws SQLException { + Column arrayCol = currentArrayInnerColumn(DUCKDB_TYPE_ARRAY); + if (values == null) { + return appendNull(); + } + checkArrayLength(arrayCol, values.length); + + Column col = currentNestedArrayInnerColumn(DUCKDB_TYPE_BOOLEAN); + byte[] buf = new byte[(int) col.arraySize]; + + for (int i = 0; i < values.length; i++) { + boolean[] childValues = values[i]; + + if (childValues == null) { + arrayCol.setNull(rowIdx, i); + continue; + } + checkArrayLength(col, childValues.length); + if (nullMask != null) { + setArrayNullMask(col, nullMask[i], i); + } + + for (int j = 0; j < childValues.length; j++) { + buf[j] = (byte) (childValues[j] ? 1 : 0); + } + + int pos = (int) ((rowIdx * arrayCol.arraySize + i) * col.arraySize); + col.data.position(pos); + col.data.put(buf); + } + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendByteArray(byte[] values) throws SQLException { + return appendByteArray(values, null); + } + + public DuckDBAppender appendByteArray(byte[] values, boolean[] nullMask) throws SQLException { + Column col = currentArrayInnerColumn(int8Types); + if (values == null) { + return appendNull(); + } + + checkArrayLength(col, values.length); + setArrayNullMask(col, nullMask); + + int pos = (int) (rowIdx * col.arraySize); + col.data.position(pos); + col.data.put(values); + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendByteArray(byte[][] values) throws SQLException { + return appendByteArray(values, null); + } + + public DuckDBAppender appendByteArray(byte[][] values, boolean[][] nullMask) throws SQLException { + Column arrayCol = currentArrayInnerColumn(DUCKDB_TYPE_ARRAY); + if (values == null) { + return appendNull(); + } + checkArrayLength(arrayCol, values.length); + + Column col = currentNestedArrayInnerColumn(int8Types); + + for (int i = 0; i < values.length; i++) { + byte[] childValues = values[i]; + + if (childValues == null) { + arrayCol.setNull(rowIdx, i); + continue; + } + checkArrayLength(col, childValues.length); + if (nullMask != null) { + setArrayNullMask(col, nullMask[i], i); + } + + int pos = (int) ((rowIdx * arrayCol.arraySize + i) * col.arraySize); + col.data.position(pos); + col.data.put(childValues); + } + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(byte[] values) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_BLOB); + if (values == null) { + return appendNull(); + } + return appendStringOrBlobInternal(DUCKDB_TYPE_BLOB, values); + } + + public DuckDBAppender append(char[] characters) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_VARCHAR); + if (characters == null) { + return appendNull(); + } + String str = String.valueOf(characters); + return append(str); + } + + public DuckDBAppender append(short[] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(short[] values, boolean[] nullMask) throws SQLException { + Column col = currentArrayInnerColumn(int16Types); + if (values == null) { + return appendNull(); + } + + checkArrayLength(col, values.length); + setArrayNullMask(col, nullMask); + + ShortBuffer shortData = col.data.asShortBuffer(); + int pos = (int) (rowIdx * col.arraySize); + shortData.position(pos); + shortData.put(values); + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(short[][] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(short[][] values, boolean[][] nullMask) throws SQLException { + Column arrayCol = currentArrayInnerColumn(DUCKDB_TYPE_ARRAY); + if (values == null) { + return appendNull(); + } + checkArrayLength(arrayCol, values.length); + + Column col = currentNestedArrayInnerColumn(int16Types); + + for (int i = 0; i < values.length; i++) { + short[] childValues = values[i]; + + if (childValues == null) { + arrayCol.setNull(rowIdx, i); + continue; + } + checkArrayLength(col, childValues.length); + if (nullMask != null) { + setArrayNullMask(col, nullMask[i], i); + } + + ShortBuffer shortBuffer = col.data.asShortBuffer(); + int pos = (int) ((rowIdx * arrayCol.arraySize + i) * col.arraySize); + shortBuffer.position(pos); + shortBuffer.put(childValues); + } + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(int[] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(int[] values, boolean[] nullMask) throws SQLException { + Column col = currentArrayInnerColumn(int32Types); + if (values == null) { + return appendNull(); + } + + checkArrayLength(col, values.length); + setArrayNullMask(col, nullMask); + + IntBuffer intData = col.data.asIntBuffer(); + int pos = (int) (rowIdx * col.arraySize); + intData.position(pos); + intData.put(values); + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(int[][] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(int[][] values, boolean[][] nullMask) throws SQLException { + Column arrayCol = currentArrayInnerColumn(DUCKDB_TYPE_ARRAY); + if (values == null) { + return appendNull(); + } + checkArrayLength(arrayCol, values.length); + + Column col = currentNestedArrayInnerColumn(int32Types); + + for (int i = 0; i < values.length; i++) { + int[] childValues = values[i]; + + if (childValues == null) { + arrayCol.setNull(rowIdx, i); + continue; + } + checkArrayLength(col, childValues.length); + if (nullMask != null) { + setArrayNullMask(col, nullMask[i], i); + } + + IntBuffer intData = col.data.asIntBuffer(); + int pos = (int) ((rowIdx * arrayCol.arraySize + i) * col.arraySize); + intData.position(pos); + intData.put(childValues); + } + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(long[] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(long[] values, boolean[] nullMask) throws SQLException { + Column col = currentArrayInnerColumn(int64Types); + if (values == null) { + return appendNull(); + } + + checkArrayLength(col, values.length); + setArrayNullMask(col, nullMask); + + LongBuffer longData = col.data.asLongBuffer(); + int pos = (int) (rowIdx * col.arraySize); + longData.position(pos); + longData.put(values); + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(long[][] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(long[][] values, boolean[][] nullMask) throws SQLException { + Column arrayCol = currentArrayInnerColumn(DUCKDB_TYPE_ARRAY); + if (values == null) { + return appendNull(); + } + checkArrayLength(arrayCol, values.length); + + Column col = currentNestedArrayInnerColumn(int64Types); + + for (int i = 0; i < values.length; i++) { + long[] childValues = values[i]; + + if (childValues == null) { + arrayCol.setNull(rowIdx, i); + continue; + } + checkArrayLength(col, childValues.length); + if (nullMask != null) { + setArrayNullMask(col, nullMask[i], i); + } + + LongBuffer longData = col.data.asLongBuffer(); + int pos = (int) ((rowIdx * arrayCol.arraySize + i) * col.arraySize); + longData.position(pos); + longData.put(childValues); + } + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(float[] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(float[] values, boolean[] nullMask) throws SQLException { + Column col = currentArrayInnerColumn(DUCKDB_TYPE_FLOAT); + if (values == null) { + return appendNull(); + } + + checkArrayLength(col, values.length); + setArrayNullMask(col, nullMask); + + FloatBuffer floatData = col.data.asFloatBuffer(); + int pos = (int) (rowIdx * col.arraySize); + floatData.position(pos); + floatData.put(values); + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(float[][] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(float[][] values, boolean[][] nullMask) throws SQLException { + Column arrayCol = currentArrayInnerColumn(DUCKDB_TYPE_ARRAY); + if (values == null) { + return appendNull(); + } + checkArrayLength(arrayCol, values.length); + + Column col = currentNestedArrayInnerColumn(DUCKDB_TYPE_FLOAT); + + for (int i = 0; i < values.length; i++) { + float[] childValues = values[i]; + + if (childValues == null) { + arrayCol.setNull(rowIdx, i); + continue; + } + checkArrayLength(col, childValues.length); + if (nullMask != null) { + setArrayNullMask(col, nullMask[i], i); + } + + FloatBuffer floatData = col.data.asFloatBuffer(); + int pos = (int) ((rowIdx * arrayCol.arraySize + i) * col.arraySize); + floatData.position(pos); + floatData.put(childValues); + } + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(double[] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(double[] values, boolean[] nullMask) throws SQLException { + Column col = currentArrayInnerColumn(DUCKDB_TYPE_DOUBLE); + if (values == null) { + return appendNull(); + } + + checkArrayLength(col, values.length); + setArrayNullMask(col, nullMask); + + DoubleBuffer doubleData = col.data.asDoubleBuffer(); + int pos = (int) (rowIdx * col.arraySize); + doubleData.position(pos); + doubleData.put(values); + + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(double[][] values) throws SQLException { + return append(values, null); + } + + public DuckDBAppender append(double[][] values, boolean[][] nullMask) throws SQLException { + Column arrayCol = currentArrayInnerColumn(DUCKDB_TYPE_ARRAY); + if (values == null) { + return appendNull(); + } + checkArrayLength(arrayCol, values.length); + + Column col = currentNestedArrayInnerColumn(DUCKDB_TYPE_DOUBLE); + + for (int i = 0; i < values.length; i++) { + double[] childValues = values[i]; + + if (childValues == null) { + arrayCol.setNull(rowIdx, i); + continue; + } + checkArrayLength(col, childValues.length); + if (nullMask != null) { + setArrayNullMask(col, nullMask[i], i); + } + + DoubleBuffer doubleBuffer = col.data.asDoubleBuffer(); + int pos = (int) ((rowIdx * arrayCol.arraySize + i) * col.arraySize); + doubleBuffer.position(pos); + doubleBuffer.put(childValues); + } + + incrementColOrStructFieldIdx(); + return this; + } + + // append objects + + public DuckDBAppender append(String value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_VARCHAR); + if (value == null) { + return appendNull(); + } + + byte[] bytes = value.getBytes(UTF_8); + return appendStringOrBlobInternal(DUCKDB_TYPE_VARCHAR, bytes); + } + + public DuckDBAppender appendEpochDays(int days) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_DATE); + col.data.putInt(days); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(LocalDate value) throws SQLException { + if (value == null) { + return appendNull(); + } + long days = value.toEpochDay(); + if (days < Integer.MIN_VALUE || days > Integer.MAX_VALUE) { + throw new SQLException(createErrMsg("unsupported number of days: " + days + ", must fit into 'int32_t'")); + } + return appendEpochDays((int) days); + } + + public DuckDBAppender appendDayMicros(long micros) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_TIME); + col.data.putLong(micros); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(LocalTime value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_TIME); + if (value == null) { + return appendNull(); + } + long micros = value.toNanoOfDay() / 1000; + return appendDayMicros(micros); + } + + public DuckDBAppender appendDayMicros(long micros, int offset) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_TIME_TZ); + long packed = ((micros & 0xFFFFFFFFFFL) << 24) | (long) (offset & 0xFFFFFF); + col.data.putLong(packed); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(OffsetTime value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_TIME_TZ); + if (value == null) { + return appendNull(); + } + int offset = value.getOffset().getTotalSeconds(); + long micros = value.toLocalTime().toNanoOfDay() / 1000; + return appendDayMicros(micros, offset); + } + + public DuckDBAppender appendEpochSeconds(long seconds) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_TIMESTAMP_S); + col.data.putLong(seconds); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendEpochMillis(long millis) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_TIMESTAMP_MS); + col.data.putLong(millis); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendEpochMicros(long micros) throws SQLException { + Column col = currentColumnWithRowPos(timestampMicrosTypes); + col.data.putLong(micros); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendEpochNanos(long nanos) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_TIMESTAMP_NS); + col.data.putLong(nanos); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender append(LocalDateTime value) throws SQLException { + Column col = currentColumn(); + checkCurrentColumnType(timestampLocalTypes); + if (value == null) { + return appendNull(); + } + switch (col.colType) { + case DUCKDB_TYPE_TIMESTAMP_S: + long seconds = EPOCH_DATE_TIME.until(value, SECONDS); + return appendEpochSeconds(seconds); + case DUCKDB_TYPE_TIMESTAMP_MS: { + long millis = EPOCH_DATE_TIME.until(value, MILLIS); + return appendEpochMillis(millis); + } + case DUCKDB_TYPE_TIMESTAMP: { + long micros = EPOCH_DATE_TIME.until(value, MICROS); + return appendEpochMicros(micros); + } + case DUCKDB_TYPE_TIMESTAMP_NS: { + long nanos = EPOCH_DATE_TIME.until(value, NANOS); + return appendEpochNanos(nanos); + } + default: + throw new SQLException(createErrMsg("invalid column type: " + col.colType)); + } + } + + public DuckDBAppender append(java.util.Date value) throws SQLException { + Column col = currentColumn(); + checkCurrentColumnType(timestampLocalTypes); + if (value == null) { + return appendNull(); + } + switch (col.colType) { + case DUCKDB_TYPE_TIMESTAMP_S: + long seconds = value.getTime() / 1000; + return appendEpochSeconds(seconds); + case DUCKDB_TYPE_TIMESTAMP_MS: { + long millis = value.getTime(); + return appendEpochMillis(millis); + } + case DUCKDB_TYPE_TIMESTAMP: { + long micros = Math.multiplyExact(value.getTime(), 1000); + return appendEpochMicros(micros); + } + case DUCKDB_TYPE_TIMESTAMP_NS: { + long nanos = Math.multiplyExact(value.getTime(), 1000000); + return appendEpochNanos(nanos); + } + default: + throw new SQLException(createErrMsg("invalid column type: " + col.colType)); } } - public void append(byte[] value) throws SQLException { + public DuckDBAppender append(OffsetDateTime value) throws SQLException { + checkCurrentColumnType(DUCKDB_TYPE_TIMESTAMP_TZ); if (value == null) { - DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); + return appendNull(); + } + ZonedDateTime zdt = value.atZoneSameInstant(ZoneOffset.UTC); + LocalDateTime ldt = zdt.toLocalDateTime(); + long micros = EPOCH_DATE_TIME.until(ldt, MICROS); + return appendEpochMicros(micros); + } + + // append special + + public DuckDBAppender appendNull() throws SQLException { + Column col = currentColumn(); + col.setNull(rowIdx); + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender appendDefault() throws SQLException { + currentColumn(); + appenderRefLock.lock(); + try { + checkOpen(); + duckdb_append_default_to_chunk(appenderRef, chunkRef, colIdx, rowIdx); + } finally { + appenderRefLock.unlock(); + } + incrementColOrStructFieldIdx(); + return this; + } + + // options + + public boolean getWriteInlinedStrings() { + return writeInlinedStrings; + } + + public void setWriteInlinedStrings(boolean writeInlinedStrings) { + this.writeInlinedStrings = writeInlinedStrings; + } + + private String createErrMsg(String error) { + return "Appender error" + + ", catalog: '" + catalog + "'" + + ", schema: '" + schema + "'" + + ", table: '" + table + "'" + + ", message: " + (null != error ? error : "N/A"); + } + + private void checkOpen() throws SQLException { + if (isClosed()) { + throw new SQLException(createErrMsg("appender was closed")); + } + } + + private void checkAppendingRow(boolean expected) throws SQLException { + if (appendingRow != expected) { + throw new SQLException(createErrMsg("'beginRow' and 'endRow' calls must be paired")); + } + } + + private void checkAppendingStruct(boolean expected) throws SQLException { + if (appendingStruct != expected) { + throw new SQLException( + createErrMsg("'beginStruct' and 'endStruct' calls cannot be interleaved with 'beginRow' and 'endRow'")); + } + } + + private void incrementColOrStructFieldIdx() throws SQLException { + if (appendingStruct) { + structFieldIdx++; + return; + } + if (appendingRow) { + colIdx++; + return; + } + throw new SQLException(createErrMsg("'beginRow' must be called before calling `append`")); + } + + private Column currentColumn() throws SQLException { + checkOpen(); + + if (colIdx >= columns.length) { + throw new SQLException( + createErrMsg("invalid columns count, expected: " + columns.length + ", actual: " + (colIdx + 1))); + } + + Column col = columns[colIdx]; + + if (!appendingStruct || col.colType != DUCKDB_TYPE_STRUCT) { + return col; + } + + if (structFieldIdx >= col.children.size()) { + throw new SQLException(createErrMsg("invalid struct fields count, expected: " + columns.length + + ", actual: " + (structFieldIdx + 1))); + } + + return col.children.get(structFieldIdx); + } + + private Column currentArrayInnerColumn(CAPIType ctype) throws SQLException { + return currentArrayInnerColumn(ctype.typeArray); + } + + private Column currentArrayInnerColumn(CAPIType[] ctypes) throws SQLException { + Column parentCol = currentColumn(); + if (parentCol.colType != DUCKDB_TYPE_ARRAY) { + throw new SQLException(createErrMsg("invalid array column type: '" + parentCol.colType + "'")); + } + + Column col = parentCol.children.get(0); + for (CAPIType ct : ctypes) { + if (col.colType == ct) { + return col; + } + } + throw new SQLException(createErrMsg("invalid array inner column type, expected one of: '" + + Arrays.toString(ctypes) + "', actual: '" + col.colType + "'")); + } + + private Column currentNestedArrayInnerColumn(CAPIType ctype) throws SQLException { + return currentNestedArrayInnerColumn(ctype.typeArray); + } + + private Column currentNestedArrayInnerColumn(CAPIType[] ctypes) throws SQLException { + Column parentCol = currentColumn(); + if (parentCol.colType != DUCKDB_TYPE_ARRAY) { + throw new SQLException(createErrMsg("invalid array column type: '" + parentCol.colType + "'")); + } + + Column arrayCol = parentCol.children.get(0); + if (arrayCol.colType != DUCKDB_TYPE_ARRAY) { + throw new SQLException(createErrMsg("invalid nested array column type: '" + arrayCol.colType + "'")); + } + + Column col = arrayCol.children.get(0); + for (CAPIType ct : ctypes) { + if (col.colType == ct) { + return col; + } + } + throw new SQLException(createErrMsg("invalid nested array inner column type, expected one of: '" + + Arrays.toString(ctypes) + "', actual: '" + col.colType + "'")); + } + + private void checkCurrentColumnType(CAPIType ctype) throws SQLException { + checkCurrentColumnType(ctype.typeArray); + } + + private void checkCurrentColumnType(CAPIType[] ctypes) throws SQLException { + Column col = currentColumn(); + checkColumnType(col, ctypes); + } + + private void checkColumnType(Column col, CAPIType ctype) throws SQLException { + checkColumnType(col, ctype.typeArray); + } + + private void checkColumnType(Column col, CAPIType[] ctypes) throws SQLException { + for (CAPIType ct : ctypes) { + if (col.colType == ct) { + return; + } + } + throw new SQLException(createErrMsg("invalid column type, expected one of: '" + Arrays.toString(ctypes) + + "', actual: '" + col.colType + "'")); + } + + private void checkArrayLength(Column col, int length) throws SQLException { + if (col.arraySize != length) { + throw new SQLException( + createErrMsg("invalid array size, expected: " + col.arraySize + ", actual: " + length)); + } + } + + private void setArrayNullMask(Column col, boolean[] nullMask) throws SQLException { + setArrayNullMask(col, nullMask, 0); + } + + private void setArrayNullMask(Column col, boolean[] nullMask, int parentArrayIdx) throws SQLException { + if (null == nullMask) { + return; + } + // if (nullMask.length != col.arraySize) { + // throw new SQLException(createErrMsg("invalid null mask size, expected: " + col.arraySize + + // ", actual: " + nullMask.length)); + // } + for (int i = 0; i < nullMask.length; i++) { + if (nullMask[i]) { + col.setNull(rowIdx, (int) (i + col.arraySize * parentArrayIdx)); + } + } + } + + private Column currentDecimalColumnWithRowPos(CAPIType decimalInternalType) throws SQLException { + Column col = currentColumnWithRowPos(DUCKDB_TYPE_DECIMAL); + if (col.decimalInternalType != decimalInternalType) { + throw new SQLException(createErrMsg("invalid decimal internal type, expected: '" + col.decimalInternalType + + "', actual: '" + decimalInternalType + "'")); + } + setRowPos(col, col.decimalInternalType.widthBytes); + return col; + } + + private Column currentColumnWithRowPos(CAPIType ctype) throws SQLException { + return currentColumnWithRowPos(ctype.typeArray); + } + + private void setRowPos(Column col, long widthBytes) throws SQLException { + long pos = rowIdx * widthBytes; + if (pos >= col.data.capacity()) { + throw new SQLException( + createErrMsg("invalid calculated position: " + pos + ", type: '" + col.colType + "'")); + } + col.data.position((int) (pos)); + } + + private Column currentColumnWithRowPos(CAPIType[] ctypes) throws SQLException { + Column col = currentColumn(); + + boolean typeMatches = false; + for (CAPIType ct : ctypes) { + if (col.colType.typeId == ct.typeId) { + typeMatches = true; + } + if (col.colType.widthBytes != ct.widthBytes) { + throw new SQLException( + createErrMsg("invalid columns type width, expected: '" + ct + "', actual: '" + col.colType + "'")); + } + } + if (!typeMatches) { + String[] typeStrs = new String[ctypes.length]; + for (int i = 0; i < ctypes.length; i++) { + typeStrs[i] = String.valueOf(ctypes[i]); + } + throw new SQLException(createErrMsg("invalid columns type, expected one of: '" + Arrays.toString(typeStrs) + + "', actual: '" + col.colType + "'")); + } + + if (col.colType.widthBytes > 0) { + setRowPos(col, col.colType.widthBytes); + } + + return col; + } + + private void checkDecimalPrecision(BigDecimal value, CAPIType decimalInternalType, int maxPrecision) + throws SQLException { + if (value.precision() > maxPrecision) { + throw new SQLException(createErrMsg("invalid decimal precision, value: " + value.precision() + + ", max value: " + maxPrecision + + ", decimal internal type: " + decimalInternalType)); + } + } + + private DuckDBAppender appendStringOrBlobInternal(CAPIType ctype, byte[] bytes) throws SQLException { + if (writeInlinedStrings && bytes.length < STRING_MAX_INLINE_BYTES) { + Column col = currentColumnWithRowPos(ctype); + col.data.putInt(bytes.length); + if (bytes.length > 0) { + col.data.put(bytes); + } } else { - DuckDBNative.duckdb_jdbc_appender_append_bytes(appender_ref, value); + Column col = currentColumn(); + checkColumnType(col, ctype); + appenderRefLock.lock(); + try { + checkOpen(); + duckdb_vector_assign_string_element_len(col.vectorRef, rowIdx, bytes); + } finally { + appenderRefLock.unlock(); + } + } + + incrementColOrStructFieldIdx(); + return this; + } + + private static byte[] utf8(String str) { + if (null == str) { + return null; + } + return str.getBytes(UTF_8); + } + + private static String strFromUTF8(byte[] utf8) { + if (null == utf8) { + return null; + } + return new String(utf8, UTF_8); + } + + private static ByteBuffer createAppender(DuckDBConnection conn, String catalog, String schema, String table) + throws SQLException { + conn.checkOpen(); + Lock connRefLock = conn.connRefLock; + connRefLock.lock(); + try { + ByteBuffer[] out = new ByteBuffer[1]; + int state = duckdb_appender_create_ext(conn.connRef, utf8(catalog), utf8(schema), utf8(table), out); + if (0 != state) { + throw new SQLException("duckdb_appender_create_ext error"); + } + return out[0]; + } finally { + connRefLock.unlock(); + } + } + + private static ByteBuffer[] readTableTypes(ByteBuffer appenderRef) throws SQLException { + long colCountLong = duckdb_appender_column_count(appenderRef); + if (colCountLong > Integer.MAX_VALUE || colCountLong < 0) { + throw new SQLException("invalid columns count: " + colCountLong); + } + int colCount = (int) colCountLong; + + ByteBuffer[] res = new ByteBuffer[colCount]; + + for (int i = 0; i < colCount; i++) { + ByteBuffer colType = duckdb_appender_column_type(appenderRef, i); + if (null == colType) { + throw new SQLException("cannot get logical type for column: " + i); + } + int typeId = duckdb_get_type_id(colType); + if (!supportedTypes.contains(typeId)) { + for (ByteBuffer lt : res) { + if (null != lt) { + duckdb_destroy_logical_type(lt); + } + } + throw new SQLException("unsupported C API type: " + typeId); + } + res[i] = colType; + } + + return res; + } + + private static ByteBuffer createChunk(ByteBuffer[] colTypes) throws SQLException { + ByteBuffer chunkRef = duckdb_create_data_chunk(colTypes); + if (null == chunkRef) { + throw new SQLException("cannot create data chunk"); + } + return chunkRef; + } + + private static void initVecChildren(Column parent) throws SQLException { + List children = new ArrayList<>(); + + switch (parent.colType) { + case DUCKDB_TYPE_LIST: + case DUCKDB_TYPE_MAP: { + ByteBuffer vec = duckdb_list_vector_get_child(parent.vectorRef); + children.add(vec); + break; + } + case DUCKDB_TYPE_STRUCT: + case DUCKDB_TYPE_UNION: { + long count = duckdb_struct_type_child_count(parent.colTypeRef); + for (int i = 0; i < count; i++) { + ByteBuffer vec = duckdb_struct_vector_get_child(parent.vectorRef, i); + children.add(vec); + } + break; + } + case DUCKDB_TYPE_ARRAY: { + ByteBuffer vec = duckdb_array_vector_get_child(parent.vectorRef); + children.add(vec); + break; + } + } + + for (ByteBuffer child : children) { + if (null == child) { + throw new SQLException("cannot initialize data chunk child list vector"); + } + ByteBuffer lt = duckdb_vector_get_column_type(child); + if (null == lt) { + throw new SQLException("cannot initialize data chunk child list vector type"); + } + Column cvec = new Column(parent, lt, child); + parent.children.add(cvec); } } - protected void finalize() throws Throwable { - close(); + private static Column[] createVectors(ByteBuffer chunkRef, ByteBuffer[] colTypes) throws SQLException { + Column[] vectors = new Column[colTypes.length]; + try { + for (int i = 0; i < colTypes.length; i++) { + ByteBuffer vector = duckdb_data_chunk_get_vector(chunkRef, i); + vectors[i] = new Column(null, colTypes[i], vector); + colTypes[i] = null; + } + } catch (Exception e) { + for (Column col : vectors) { + if (null != col) { + col.destroy(); + } + } + throw e; + } + return vectors; } - public synchronized void close() throws SQLException { - if (appender_ref != null) { - DuckDBNative.duckdb_jdbc_appender_close(appender_ref); - appender_ref = null; + private static class Column { + private final Column parent; + private ByteBuffer colTypeRef; + private final CAPIType colType; + private final CAPIType decimalInternalType; + private final int decimalPrecision; + private final int decimalScale; + private final long arraySize; + private final ByteBuffer vectorRef; + private ByteBuffer data; + private ByteBuffer validity; + private final List children = new ArrayList<>(); + + private Column(Column parent, ByteBuffer colTypeRef, ByteBuffer vector) throws SQLException { + this.parent = parent; + this.colTypeRef = colTypeRef; + int colTypeId = duckdb_get_type_id(colTypeRef); + this.colType = capiTypeFromTypeId(colTypeId); + + if (colType == DUCKDB_TYPE_DECIMAL) { + int decimalInternalTypeId = duckdb_decimal_internal_type(colTypeRef); + this.decimalInternalType = capiTypeFromTypeId(decimalInternalTypeId); + this.decimalPrecision = duckdb_decimal_width(colTypeRef); + this.decimalScale = duckdb_decimal_scale(colTypeRef); + } else { + this.decimalInternalType = DUCKDB_TYPE_INVALID; + this.decimalPrecision = -1; + this.decimalScale = -1; + } + + if (null == parent || parent.colType != DUCKDB_TYPE_ARRAY) { + this.arraySize = 1; + } else { + this.arraySize = duckdb_array_type_array_size(parent.colTypeRef); + } + + this.vectorRef = vector; + if (null == this.vectorRef) { + throw new SQLException("cannot initialize data chunk vector"); + } + + if (colType.widthBytes > 0 || colType == DUCKDB_TYPE_DECIMAL) { + this.data = duckdb_vector_get_data(vectorRef, widthBytes() * arraySize * parentArraySize()); + if (null == this.data) { + throw new SQLException("cannot initialize data chunk vector data"); + } + } else { + this.data = null; + } + + duckdb_vector_ensure_validity_writable(vectorRef); + this.validity = duckdb_vector_get_validity(vectorRef, arraySize * parentArraySize()); + if (null == this.validity) { + throw new SQLException("cannot initialize data chunk vector validity"); + } + + // last call in constructor + initVecChildren(this); + } + + void reset() throws SQLException { + if (null != this.data) { + this.data = duckdb_vector_get_data(vectorRef, widthBytes() * arraySize * parentArraySize()); + if (null == this.data) { + throw new SQLException("cannot reset data chunk vector data"); + } + } + + duckdb_vector_ensure_validity_writable(vectorRef); + this.validity = duckdb_vector_get_validity(vectorRef, arraySize * parentArraySize()); + if (null == this.validity) { + throw new SQLException("cannot reset data chunk vector validity"); + } + + for (Column col : children) { + col.reset(); + } + } + + void destroy() { + for (Column cvec : children) { + cvec.destroy(); + } + children.clear(); + if (null != colTypeRef) { + duckdb_destroy_logical_type(colTypeRef); + colTypeRef = null; + } + } + + void setNull(long rowIdx) throws SQLException { + if (1 != arraySize) { + throw new SQLException("Invalid API usage for array, size: " + arraySize); + } + setNull(rowIdx, 0); + for (Column col : children) { + for (int i = 0; i < col.arraySize; i++) { + col.setNull(rowIdx, i); + } + } + } + + void setNull(long rowIdx, int arrayIdx) { + LongBuffer entries = this.validity.asLongBuffer(); + + long vectorPos = rowIdx * arraySize * parentArraySize() + arrayIdx; + long validityPos = vectorPos / 64; + entries.position((int) validityPos); + long mask = entries.get(); + + long idxInEntry = vectorPos % 64; + mask &= ~(1L << idxInEntry); + entries.position((int) validityPos); + entries.put(mask); + } + + long widthBytes() { + if (colType == DUCKDB_TYPE_DECIMAL) { + return decimalInternalType.widthBytes; + } else { + return colType.widthBytes; + } + } + + long parentArraySize() { + if (null == parent) { + return 1; + } + return parent.arraySize; } } } diff --git a/src/main/java/org/duckdb/DuckDBArrayResultSet.java b/src/main/java/org/duckdb/DuckDBArrayResultSet.java index 2f427a410..8185730eb 100644 --- a/src/main/java/org/duckdb/DuckDBArrayResultSet.java +++ b/src/main/java/org/duckdb/DuckDBArrayResultSet.java @@ -119,6 +119,7 @@ public double getDouble(int columnIndex) throws SQLException { } @Override + @SuppressWarnings("deprecation") public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { if (columnIndex == 1) { return BigDecimal.valueOf(getIndexColumnValue()); @@ -152,6 +153,7 @@ public InputStream getAsciiStream(int columnIndex) throws SQLException { } @Override + @SuppressWarnings("deprecation") public InputStream getUnicodeStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("getUnicodeStream"); } @@ -202,6 +204,7 @@ public double getDouble(String columnLabel) throws SQLException { } @Override + @SuppressWarnings("deprecation") public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { return getBigDecimal(findColumn(columnLabel), scale); } @@ -232,6 +235,7 @@ public InputStream getAsciiStream(String columnLabel) throws SQLException { } @Override + @SuppressWarnings("deprecation") public InputStream getUnicodeStream(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("getUnicodeStream"); } diff --git a/src/main/java/org/duckdb/DuckDBBindings.java b/src/main/java/org/duckdb/DuckDBBindings.java new file mode 100644 index 000000000..81fe25a85 --- /dev/null +++ b/src/main/java/org/duckdb/DuckDBBindings.java @@ -0,0 +1,217 @@ +package org.duckdb; + +import java.nio.ByteBuffer; +import java.sql.SQLException; + +public class DuckDBBindings { + + static { + try { + Class.forName(DuckDBNative.class.getName()); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + // common + + static native long duckdb_vector_size(); + + // logical type + + static native ByteBuffer duckdb_create_logical_type(int duckdb_type); + + static native int duckdb_get_type_id(ByteBuffer logical_type); + + static native int duckdb_decimal_width(ByteBuffer logical_type); + + static native int duckdb_decimal_scale(ByteBuffer logical_type); + + static native int duckdb_decimal_internal_type(ByteBuffer logical_type); + + static native ByteBuffer duckdb_create_list_type(ByteBuffer logical_type); + + static native ByteBuffer duckdb_create_array_type(ByteBuffer logical_type, long array_size); + + static native ByteBuffer duckdb_create_struct_type(ByteBuffer[] member_types, byte[][] member_names); + + static native long duckdb_struct_type_child_count(ByteBuffer logical_type); + + static native long duckdb_array_type_array_size(ByteBuffer logical_type); + + static native void duckdb_destroy_logical_type(ByteBuffer logical_type); + + // vector + + static native ByteBuffer duckdb_create_vector(ByteBuffer logical_type); + + static native void duckdb_destroy_vector(ByteBuffer vector); + + static native ByteBuffer duckdb_vector_get_column_type(ByteBuffer vector); + + static native ByteBuffer duckdb_vector_get_data(ByteBuffer vector, long col_width_bytes); + + static native ByteBuffer duckdb_vector_get_validity(ByteBuffer vector, long array_size); + + static native void duckdb_vector_ensure_validity_writable(ByteBuffer vector); + + static native void duckdb_vector_assign_string_element_len(ByteBuffer vector, long index, byte[] str); + + static native ByteBuffer duckdb_list_vector_get_child(ByteBuffer vector); + + static native long duckdb_list_vector_get_size(ByteBuffer vector); + + static native int duckdb_list_vector_set_size(ByteBuffer vector, long size); + + static native int duckdb_list_vector_reserve(ByteBuffer vector, long capacity); + + static native ByteBuffer duckdb_struct_vector_get_child(ByteBuffer vector, long index); + + static native ByteBuffer duckdb_array_vector_get_child(ByteBuffer vector); + + // validity + + static native boolean duckdb_validity_row_is_valid(ByteBuffer validity, long row); + + static native void duckdb_validity_set_row_validity(ByteBuffer validity, long row, boolean valid); + + // data chunk + + static native ByteBuffer duckdb_create_data_chunk(ByteBuffer[] logical_types); + + static native void duckdb_destroy_data_chunk(ByteBuffer chunk); + + static native void duckdb_data_chunk_reset(ByteBuffer chunk); + + static native long duckdb_data_chunk_get_column_count(ByteBuffer chunk); + + static native ByteBuffer duckdb_data_chunk_get_vector(ByteBuffer chunk, long col_idx); + + static native long duckdb_data_chunk_get_size(ByteBuffer chunk); + + static native void duckdb_data_chunk_set_size(ByteBuffer chunk, long size); + + // appender + + static native int duckdb_appender_create_ext(ByteBuffer connection, byte[] catalog, byte[] schema, byte[] table, + ByteBuffer[] out_appender); + + static native byte[] duckdb_appender_error(ByteBuffer appender); + + static native int duckdb_appender_flush(ByteBuffer appender); + + static native int duckdb_appender_close(ByteBuffer appender); + + static native int duckdb_appender_destroy(ByteBuffer appender); + + static native long duckdb_appender_column_count(ByteBuffer appender); + + static native ByteBuffer duckdb_appender_column_type(ByteBuffer appender, long col_idx); + + static native int duckdb_append_data_chunk(ByteBuffer appender, ByteBuffer chunk); + + static native int duckdb_append_default_to_chunk(ByteBuffer appender, ByteBuffer chunk, long col, long row); + + enum CAPIType { + DUCKDB_TYPE_INVALID(0, 0), + // bool + DUCKDB_TYPE_BOOLEAN(1, 1), + // int8_t + DUCKDB_TYPE_TINYINT(2, 1), + // int16_t + DUCKDB_TYPE_SMALLINT(3, 2), + // int32_t + DUCKDB_TYPE_INTEGER(4, 4), + // int64_t + DUCKDB_TYPE_BIGINT(5, 8), + // uint8_t + DUCKDB_TYPE_UTINYINT(6, 1), + // uint16_t + DUCKDB_TYPE_USMALLINT(7, 2), + // uint32_t + DUCKDB_TYPE_UINTEGER(8, 4), + // uint64_t + DUCKDB_TYPE_UBIGINT(9, 8), + // float + DUCKDB_TYPE_FLOAT(10, 4), + // double + DUCKDB_TYPE_DOUBLE(11, 8), + // duckdb_timestamp (microseconds) + DUCKDB_TYPE_TIMESTAMP(12, 8), + // duckdb_date + DUCKDB_TYPE_DATE(13, 4), + // duckdb_time + DUCKDB_TYPE_TIME(14, 8), + // duckdb_interval + DUCKDB_TYPE_INTERVAL(15), + // duckdb_hugeint + DUCKDB_TYPE_HUGEINT(16, 16), + // duckdb_uhugeint + DUCKDB_TYPE_UHUGEINT(32, 16), + // const char* + DUCKDB_TYPE_VARCHAR(17, 16), + // duckdb_blob + DUCKDB_TYPE_BLOB(18, 16), + // duckdb_decimal + DUCKDB_TYPE_DECIMAL(19, 0), + // duckdb_timestamp_s (seconds) + DUCKDB_TYPE_TIMESTAMP_S(20, 8), + // duckdb_timestamp_ms (milliseconds) + DUCKDB_TYPE_TIMESTAMP_MS(21, 8), + // duckdb_timestamp_ns (nanoseconds) + DUCKDB_TYPE_TIMESTAMP_NS(22, 8), + // enum type, only useful as logical type + DUCKDB_TYPE_ENUM(23), + // list type, only useful as logical type + DUCKDB_TYPE_LIST(24), + // struct type, only useful as logical type + DUCKDB_TYPE_STRUCT(25, 0), + // map type, only useful as logical type + DUCKDB_TYPE_MAP(26), + // duckdb_array, only useful as logical type + DUCKDB_TYPE_ARRAY(33, 0), + // duckdb_hugeint + DUCKDB_TYPE_UUID(27), + // union type, only useful as logical type + DUCKDB_TYPE_UNION(28), + // duckdb_bit + DUCKDB_TYPE_BIT(29), + // duckdb_time_tz + DUCKDB_TYPE_TIME_TZ(30, 8), + // duckdb_timestamp (microseconds) + DUCKDB_TYPE_TIMESTAMP_TZ(31, 8), + // enum type, only useful as logical type + DUCKDB_TYPE_ANY(34), + // duckdb_varint + DUCKDB_TYPE_VARINT(35), + // enum type, only useful as logical type + DUCKDB_TYPE_SQLNULL(36), + // enum type, only useful as logical type + DUCKDB_TYPE_STRING_LITERAL(37), + // enum type, only useful as logical type + DUCKDB_TYPE_INTEGER_LITERAL(38); + + final int typeId; + final long widthBytes; + final CAPIType[] typeArray; + + CAPIType(int typeId) { + this(typeId, 0); + } + + CAPIType(int typeId, long widthBytes) { + this.typeId = typeId; + this.widthBytes = widthBytes; + this.typeArray = new CAPIType[] {this}; + } + + static CAPIType capiTypeFromTypeId(int typeId) throws SQLException { + for (CAPIType ct : CAPIType.values()) { + if (ct.typeId == typeId) { + return ct; + } + } + throw new SQLException("Invalid unknown ID not found: " + typeId); + } + } +} diff --git a/src/main/java/org/duckdb/DuckDBConnection.java b/src/main/java/org/duckdb/DuckDBConnection.java index ae3acefa1..617f23213 100644 --- a/src/main/java/org/duckdb/DuckDBConnection.java +++ b/src/main/java/org/duckdb/DuckDBConnection.java @@ -25,7 +25,6 @@ import java.sql.Struct; import java.util.*; import java.util.concurrent.Executor; -import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import org.duckdb.user.DuckDBMap; import org.duckdb.user.DuckDBUserArray; @@ -39,6 +38,7 @@ public final class DuckDBConnection implements java.sql.Connection { ByteBuffer connRef; final ReentrantLock connRefLock = new ReentrantLock(); final LinkedHashSet preparedStatements = new LinkedHashSet<>(); + final LinkedHashSet appenders = new LinkedHashSet<>(); volatile boolean closing; volatile boolean autoCommit; @@ -122,6 +122,8 @@ public void rollback() throws SQLException { } } + @Override + @SuppressWarnings("deprecation") protected void finalize() throws Throwable { close(); } @@ -157,6 +159,14 @@ public void close() throws SQLException { } preparedStatements.clear(); + // Last appender created is first deleted + List appList = new ArrayList<>(appenders); + Collections.reverse(appList); + for (DuckDBAppender app : appList) { + app.close(); + } + appenders.clear(); + DuckDBNative.duckdb_jdbc_disconnect(connRef); connRef = null; } finally { @@ -436,8 +446,24 @@ public int getNetworkTimeout() throws SQLException { throw new SQLFeatureNotSupportedException("getNetworkTimeout"); } + @SuppressWarnings("deprecation") + public DuckDBSingleValueAppender createSingleValueAppender(String schemaName, String tableName) + throws SQLException { + return new DuckDBSingleValueAppender(this, schemaName, tableName); + } + + public DuckDBAppender createAppender(String tableName) throws SQLException { + return createAppender(null, null, tableName); + } + public DuckDBAppender createAppender(String schemaName, String tableName) throws SQLException { - return new DuckDBAppender(this, schemaName, tableName); + return createAppender(null, schemaName, tableName); + } + + public DuckDBAppender createAppender(String catalogName, String schemaName, String tableName) throws SQLException { + DuckDBAppender appender = new DuckDBAppender(this, catalogName, schemaName, tableName); + this.appenders.add(appender); + return appender; } private static long getArrowStreamAddress(Object arrow_array_stream) { diff --git a/src/main/java/org/duckdb/DuckDBHugeInt.java b/src/main/java/org/duckdb/DuckDBHugeInt.java index 31339172a..5912e0fa2 100644 --- a/src/main/java/org/duckdb/DuckDBHugeInt.java +++ b/src/main/java/org/duckdb/DuckDBHugeInt.java @@ -4,8 +4,8 @@ import java.sql.SQLException; class DuckDBHugeInt { - private static final BigInteger HUGE_INT_MIN = BigInteger.ONE.shiftLeft(127).negate(); - private static final BigInteger HUGE_INT_MAX = BigInteger.ONE.shiftLeft(127).subtract(BigInteger.ONE); + static final BigInteger HUGE_INT_MIN = BigInteger.ONE.shiftLeft(127).negate(); + static final BigInteger HUGE_INT_MAX = BigInteger.ONE.shiftLeft(127).subtract(BigInteger.ONE); private final long lower; private final long upper; diff --git a/src/main/java/org/duckdb/DuckDBPreparedStatement.java b/src/main/java/org/duckdb/DuckDBPreparedStatement.java index 95b3e4b8f..2368c7149 100644 --- a/src/main/java/org/duckdb/DuckDBPreparedStatement.java +++ b/src/main/java/org/duckdb/DuckDBPreparedStatement.java @@ -404,6 +404,7 @@ public boolean isClosed() throws SQLException { } @Override + @SuppressWarnings("deprecation") protected void finalize() throws Throwable { close(); } @@ -795,6 +796,7 @@ public boolean execute(String sql, String[] columnNames) throws SQLException { } @Override + @SuppressWarnings("deprecation") public int getResultSetHoldability() throws SQLException { checkOpen(); return ResultSet.HOLD_CURSORS_OVER_COMMIT; @@ -859,6 +861,7 @@ public void setAsciiStream(int parameterIndex, InputStream x, int length) throws } @Override + @SuppressWarnings("deprecation") public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { setCharacterStreamInternal(parameterIndex, x, length, UTF_8); } @@ -923,7 +926,7 @@ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQ } else if (x instanceof String) { setObject(parameterIndex, Integer.parseInt((String) x)); } else if (x instanceof Boolean) { - setObject(parameterIndex, (int) (((Boolean) x) ? 1 : 0)); + setObject(parameterIndex, ((Boolean) x) ? 1 : 0); } else { throw new SQLException("Can't convert value to int " + x.getClass().toString()); } diff --git a/src/main/java/org/duckdb/DuckDBResultSet.java b/src/main/java/org/duckdb/DuckDBResultSet.java index b7f81a15a..bc5d00497 100644 --- a/src/main/java/org/duckdb/DuckDBResultSet.java +++ b/src/main/java/org/duckdb/DuckDBResultSet.java @@ -113,6 +113,8 @@ public void close() throws SQLException { } } + @Override + @SuppressWarnings("deprecation") protected void finalize() throws Throwable { close(); } @@ -356,6 +358,7 @@ public Object getObject(String columnLabel) throws SQLException { return getObject(findColumn(columnLabel)); } + @SuppressWarnings("deprecation") public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { throw new SQLFeatureNotSupportedException("getBigDecimal"); } @@ -528,6 +531,7 @@ public InputStream getAsciiStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("getAsciiStream"); } + @SuppressWarnings("deprecation") public InputStream getUnicodeStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("getUnicodeStream"); } @@ -536,6 +540,7 @@ public InputStream getBinaryStream(int columnIndex) throws SQLException { throw new SQLFeatureNotSupportedException("getBinaryStream"); } + @SuppressWarnings("deprecation") public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { throw new SQLFeatureNotSupportedException("getBigDecimal"); } @@ -560,6 +565,7 @@ public InputStream getAsciiStream(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("getAsciiStream"); } + @SuppressWarnings("deprecation") public InputStream getUnicodeStream(String columnLabel) throws SQLException { throw new SQLFeatureNotSupportedException("getUnicodeStream"); } @@ -1167,7 +1173,9 @@ public void updateNClob(String columnLabel, Reader reader) throws SQLException { } private boolean isTimestamp(DuckDBColumnType sqlType) { - return (sqlType == DuckDBColumnType.TIMESTAMP || sqlType == DuckDBColumnType.TIMESTAMP_WITH_TIME_ZONE); + return (sqlType == DuckDBColumnType.TIMESTAMP || sqlType == DuckDBColumnType.TIMESTAMP_WITH_TIME_ZONE || + sqlType == DuckDBColumnType.TIMESTAMP_S || sqlType == DuckDBColumnType.TIMESTAMP_MS || + sqlType == DuckDBColumnType.TIMESTAMP_NS); } public T getObject(int columnIndex, Class type) throws SQLException { @@ -1292,7 +1300,7 @@ public T getObject(int columnIndex, Class type) throws SQLException { throw new SQLException("Can't convert value to Date, Java type: " + type + ", SQL type: " + sqlType); } } else if (type == Time.class) { - if (sqlType == DuckDBColumnType.TIME) { + if (sqlType == DuckDBColumnType.TIME || sqlType == DuckDBColumnType.TIME_WITH_TIME_ZONE) { return type.cast(getTime(columnIndex)); } else { throw new SQLException("Can't convert value to Time, Java type: " + type + ", SQL type: " + sqlType); diff --git a/src/main/java/org/duckdb/DuckDBSingleValueAppender.java b/src/main/java/org/duckdb/DuckDBSingleValueAppender.java new file mode 100644 index 000000000..4cbf64b20 --- /dev/null +++ b/src/main/java/org/duckdb/DuckDBSingleValueAppender.java @@ -0,0 +1,107 @@ +package org.duckdb; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.sql.SQLException; +import java.time.LocalDateTime; + +@Deprecated +public class DuckDBSingleValueAppender implements AutoCloseable { + + protected ByteBuffer appender_ref = null; + + public DuckDBSingleValueAppender(DuckDBConnection con, String schemaName, String tableName) throws SQLException { + if (con == null) { + throw new SQLException("Invalid connection"); + } + appender_ref = DuckDBNative.duckdb_jdbc_create_appender( + con.connRef, schemaName.getBytes(StandardCharsets.UTF_8), tableName.getBytes(StandardCharsets.UTF_8)); + } + + public void beginRow() throws SQLException { + DuckDBNative.duckdb_jdbc_appender_begin_row(appender_ref); + } + + public void endRow() throws SQLException { + DuckDBNative.duckdb_jdbc_appender_end_row(appender_ref); + } + + public void flush() throws SQLException { + DuckDBNative.duckdb_jdbc_appender_flush(appender_ref); + } + + public void append(boolean value) throws SQLException { + DuckDBNative.duckdb_jdbc_appender_append_boolean(appender_ref, value); + } + + public void append(byte value) throws SQLException { + DuckDBNative.duckdb_jdbc_appender_append_byte(appender_ref, value); + } + + public void append(short value) throws SQLException { + DuckDBNative.duckdb_jdbc_appender_append_short(appender_ref, value); + } + + public void append(int value) throws SQLException { + DuckDBNative.duckdb_jdbc_appender_append_int(appender_ref, value); + } + + public void append(long value) throws SQLException { + DuckDBNative.duckdb_jdbc_appender_append_long(appender_ref, value); + } + + // New naming schema for object params to keep compatibility with calling "append(null)" + public void appendLocalDateTime(LocalDateTime value) throws SQLException { + if (value == null) { + DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); + } else { + long timeInMicros = DuckDBTimestamp.localDateTime2Micros(value); + DuckDBNative.duckdb_jdbc_appender_append_timestamp(appender_ref, timeInMicros); + } + } + + public void appendBigDecimal(BigDecimal value) throws SQLException { + if (value == null) { + DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); + } else { + DuckDBNative.duckdb_jdbc_appender_append_decimal(appender_ref, value); + } + } + + public void append(float value) throws SQLException { + DuckDBNative.duckdb_jdbc_appender_append_float(appender_ref, value); + } + + public void append(double value) throws SQLException { + DuckDBNative.duckdb_jdbc_appender_append_double(appender_ref, value); + } + + public void append(String value) throws SQLException { + if (value == null) { + DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); + } else { + DuckDBNative.duckdb_jdbc_appender_append_string(appender_ref, value.getBytes(StandardCharsets.UTF_8)); + } + } + + public void append(byte[] value) throws SQLException { + if (value == null) { + DuckDBNative.duckdb_jdbc_appender_append_null(appender_ref); + } else { + DuckDBNative.duckdb_jdbc_appender_append_bytes(appender_ref, value); + } + } + + @SuppressWarnings("deprecation") + protected void finalize() throws Throwable { + close(); + } + + public synchronized void close() throws SQLException { + if (appender_ref != null) { + DuckDBNative.duckdb_jdbc_appender_close(appender_ref); + appender_ref = null; + } + } +} diff --git a/src/main/java/org/duckdb/user/DuckDBMap.java b/src/main/java/org/duckdb/user/DuckDBMap.java index b294f8177..dc8179a8b 100644 --- a/src/main/java/org/duckdb/user/DuckDBMap.java +++ b/src/main/java/org/duckdb/user/DuckDBMap.java @@ -4,6 +4,7 @@ import java.util.Map; public class DuckDBMap extends HashMap { + private static final long serialVersionUID = 0L; private final String typeName; public DuckDBMap(String typeName, Map map) { diff --git a/src/test/java/org/duckdb/TestAppender.java b/src/test/java/org/duckdb/TestAppender.java new file mode 100644 index 000000000..188e4e846 --- /dev/null +++ b/src/test/java/org/duckdb/TestAppender.java @@ -0,0 +1,1716 @@ +package org.duckdb; + +import static java.time.ZoneOffset.UTC; +import static org.duckdb.DuckDBHugeInt.HUGE_INT_MAX; +import static org.duckdb.DuckDBHugeInt.HUGE_INT_MIN; +import static org.duckdb.TestDuckDBJDBC.JDBC_URL; +import static org.duckdb.test.Assertions.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.sql.*; +import java.time.*; +import java.util.*; + +public class TestAppender { + + public static void test_appender_basic() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE tab1(col1 INTEGER, col2 VARCHAR)"); + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(Integer.MAX_VALUE).append("foo").endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), Integer.MAX_VALUE); + assertEquals(rs.getString(2), "foo"); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_null_basic() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE tab1(col1 INTEGER, col2 VARCHAR)"); + try (DuckDBAppender appender = conn.createAppender("tab1")) { + String str = null; + appender.beginRow().append(41).append(str).endRow(); + appender.beginRow().append(42).appendNull().endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getObject(1), 41); + assertEquals(rs.getString(2), null); + assertTrue(rs.next()); + assertEquals(rs.getObject(1), 42); + assertEquals(rs.getString(2), null); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_default() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE tab1(col1 INTEGER DEFAULT 42, col2 VARCHAR DEFAULT 'foo')"); + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().appendDefault().appendDefault().endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 42); + assertEquals(rs.getString(2), "foo"); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_boolean() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 BOOLEAN)"); + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(41).append(true).endRow(); + appender.beginRow().append(42).append(false).endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getBoolean(2), true); + assertTrue(rs.next()); + assertEquals(rs.getBoolean(2), false); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_unsigned() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 UTINYINT, col3 USMALLINT, col4 UINTEGER, col5 UBIGINT)"); + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow() + .append(41) + .append((byte) -1) + .append((short) -1) + .append(-1) + .append((long) -1) + .endRow(); + appender.beginRow() + .append(42) + .append((byte) -2) + .append((short) -2) + .append(-2) + .append((long) -2) + .endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getShort(2), (short) ((1 << 8) - 1)); + assertEquals(rs.getInt(3), (1 << 16) - 1); + assertEquals(rs.getLong(4), (1L << 32) - 1); + assertEquals(rs.getObject(5, BigInteger.class), + BigInteger.ONE.shiftLeft(64).subtract(BigInteger.valueOf(1))); + assertTrue(rs.next()); + assertEquals(rs.getShort(2), (short) ((1 << 8) - 2)); + assertEquals(rs.getInt(3), (1 << 16) - 2); + assertEquals(rs.getLong(4), (1L << 32) - 2); + assertEquals(rs.getObject(5, BigInteger.class), + BigInteger.ONE.shiftLeft(64).subtract(BigInteger.valueOf(2))); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_long_string() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 VARCHAR)"); + + String inlineStr = "foobar"; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1 << 10; i++) { + sb.append(i); + } + String longStr = sb.toString(); + String emptyStr = ""; + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(41).append(inlineStr).endRow(); + appender.beginRow().append(42).append(longStr).endRow(); + appender.beginRow().append(43).append(emptyStr).endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getString(2), inlineStr); + assertTrue(rs.next()); + assertEquals(rs.getString(2), longStr); + assertTrue(rs.next()); + assertEquals(rs.getString(2), emptyStr); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_huge_integer() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 HUGEINT, col3 UHUGEINT)"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(41).append(HUGE_INT_MIN).append(BigInteger.ZERO).endRow(); + appender.beginRow().append(42).append(HUGE_INT_MIN.add(BigInteger.ONE)).append(BigInteger.ONE).endRow(); + appender.beginRow().append(43).append(HUGE_INT_MAX).append(HUGE_INT_MAX).endRow(); + appender.beginRow() + .append(44) + .append(HUGE_INT_MAX.subtract(BigInteger.ONE)) + .append(HUGE_INT_MAX.subtract(BigInteger.ONE)) + .endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getObject(2, BigInteger.class), HUGE_INT_MIN); + assertEquals(rs.getObject(3, BigInteger.class), BigInteger.ZERO); + assertTrue(rs.next()); + assertEquals(rs.getObject(2, BigInteger.class), HUGE_INT_MIN.add(BigInteger.ONE)); + assertEquals(rs.getObject(3, BigInteger.class), BigInteger.ONE); + assertTrue(rs.next()); + assertEquals(rs.getObject(2, BigInteger.class), HUGE_INT_MAX); + assertEquals(rs.getObject(3, BigInteger.class), HUGE_INT_MAX); + assertTrue(rs.next()); + assertEquals(rs.getObject(2, BigInteger.class), HUGE_INT_MAX.subtract(BigInteger.ONE)); + assertEquals(rs.getObject(3, BigInteger.class), HUGE_INT_MAX.subtract(BigInteger.ONE)); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_timestamp_local() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute( + "CREATE TABLE tab1(col1 INT, col2 TIMESTAMP_S, col3 TIMESTAMP_MS, col4 TIMESTAMP, col5 TIMESTAMP_NS)"); + + LocalDateTime ldt = LocalDateTime.of(2025, 6, 25, 12, 34, 45, 678901234); + java.util.Date dt = java.util.Date.from(ldt.toInstant(UTC)); + + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO tab1 VALUES(?, ?, ?, ?, ?)")) { + ps.setInt(1, 41); + ps.setObject(2, ldt); + ps.setObject(3, ldt); + ps.setObject(4, ldt); + ps.setObject(5, ldt); + ps.execute(); + } + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(42).append(ldt).append(ldt).append(ldt).append(ldt).endRow(); + appender.beginRow().append(43).append(dt).append(dt).append(dt).append(dt).endRow(); + appender.flush(); + } + + // todo: check rounding rules + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + assertEquals(rs.getObject(2, LocalDateTime.class).toString(), "2025-06-25T12:34:46"); + assertEquals(rs.getObject(3, LocalDateTime.class).toString(), "2025-06-25T12:34:45.679"); + assertEquals(rs.getObject(4, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678901"); + assertEquals(rs.getObject(5, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678901"); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 42); + assertEquals(rs.getObject(2, LocalDateTime.class).toString(), "2025-06-25T12:34:45"); + assertEquals(rs.getObject(3, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678"); + assertEquals(rs.getObject(4, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678901"); + assertEquals(rs.getObject(5, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678901234"); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 43); + assertEquals(rs.getObject(2, LocalDateTime.class).toString(), "2025-06-25T12:34:45"); + assertEquals(rs.getObject(3, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678"); + assertEquals(rs.getObject(4, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678"); + assertEquals(rs.getObject(5, LocalDateTime.class).toString(), "2025-06-25T12:34:45.678"); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_timestamp_tz() throws Exception { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone activeTimeZone = TimeZone.getTimeZone("Europe/Sofia"); + TimeZone.setDefault(activeTimeZone); + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 TIMESTAMP WITH TIME ZONE)"); + + OffsetDateTime odt = OffsetDateTime.of(2025, 6, 25, 12, 34, 45, 678901234, ZoneOffset.ofHours(8)); + + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO tab1 VALUES(?, ?)")) { + ps.setInt(1, 41); + ps.setObject(2, odt); + ps.execute(); + } + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(42).append(odt).endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + assertEquals(rs.getObject(2, LocalDateTime.class).toString(), "2025-06-25T07:34:45.678901"); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 42); + assertEquals(rs.getObject(2, LocalDateTime.class).toString(), "2025-06-25T07:34:45.678901"); + assertFalse(rs.next()); + } + } finally { + TimeZone.setDefault(defaultTimeZone); + } + } + + public static void test_appender_time_local() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 TIME)"); + + LocalTime lt = LocalTime.of(12, 34, 45, 678901234); + + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO tab1 VALUES(?, ?)")) { + ps.setInt(1, 41); + ps.setObject(2, lt); + ps.execute(); + } + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(42).append(lt).endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + assertEquals(rs.getObject(2, Time.class).toString(), "12:34:45"); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 42); + assertEquals(rs.getObject(2, Time.class).toString(), "12:34:45"); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_time_tz() throws Exception { + TimeZone defaultTimeZone = TimeZone.getDefault(); + TimeZone activeTimeZone = TimeZone.getTimeZone("Europe/Sofia"); + TimeZone.setDefault(activeTimeZone); + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 TIME WITH TIME ZONE)"); + + ZoneId zoneId = ZoneId.of("Australia/Melbourne"); + LocalDateTime ldt = LocalDateTime.of(2025, 6, 25, 12, 34, 45, 678901234); + ZoneOffset zoneOffset = zoneId.getRules().getOffset(ldt.toInstant(UTC)); + OffsetTime ot = ldt.toLocalTime().atOffset(zoneOffset); + Instant instant = ldt.toInstant(zoneOffset); + Calendar cal = new GregorianCalendar(); + cal.setTimeZone(TimeZone.getTimeZone(zoneId)); + cal.setTime(java.util.Date.from(instant)); + + try (PreparedStatement ps = conn.prepareStatement("INSERT INTO tab1 VALUES(?, ?)")) { + ps.setInt(1, 41); + ps.setTime(2, Time.valueOf(ldt.toLocalTime()), cal); + ps.execute(); + } + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(42).append(ot).endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + assertEquals(rs.getObject(2, Time.class).toString(), "20:34:45"); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 42); + assertEquals(rs.getObject(2, Time.class).toString(), "12:34:45"); + assertFalse(rs.next()); + } + } finally { + TimeZone.setDefault(defaultTimeZone); + } + } + + public static void test_appender_basic_auto_flush() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 12; // two flushes + int tail = 16; // flushed on close + + stmt.execute("CREATE TABLE tab1(col1 INTEGER, col2 VARCHAR)"); + try (DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "tab1")) { + for (int i = 0; i < count + tail; i++) { + appender.beginRow().append(Integer.MAX_VALUE - i).append("foo" + i).endRow(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1 DESC")) { + for (int i = 0; i < count; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), Integer.MAX_VALUE - i); + assertEquals(rs.getString(2), "foo" + i); + } + assertFalse(rs.next()); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT count(*) FROM tab1")) { + rs.next(); + assertEquals(rs.getInt(1), count + tail); + } + } + } + + public static void test_appender_numbers() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + // int8, int4, int2, int1, float8, float4 + stmt.execute("CREATE TABLE numbers (a BIGINT, b INTEGER, c SMALLINT, d TINYINT, e DOUBLE, f FLOAT)"); + try (DuckDBAppender appender = conn.createAppender("numbers")) { + for (int i = 0; i < 50; i++) { + appender.beginRow() + .append(Long.MAX_VALUE - i) + .append(Integer.MAX_VALUE - i) + .append((short) (Short.MAX_VALUE - i)) + .append((byte) (Byte.MAX_VALUE - i)) + .append((double) i) + .append((float) i) + .endRow(); + } + appender.flush(); + } + + try (ResultSet rs = + stmt.executeQuery("SELECT max(a), max(b), max(c), max(d), max(e), max(f) FROM numbers")) { + assertFalse(rs.isClosed()); + assertTrue(rs.next()); + + long resA = rs.getLong(1); + assertEquals(resA, Long.MAX_VALUE); + + int resB = rs.getInt(2); + assertEquals(resB, Integer.MAX_VALUE); + + short resC = rs.getShort(3); + assertEquals(resC, Short.MAX_VALUE); + + byte resD = rs.getByte(4); + assertEquals(resD, Byte.MAX_VALUE); + + double resE = rs.getDouble(5); + assertEquals(resE, 49.0d); + + float resF = rs.getFloat(6); + assertEquals(resF, 49.0f); + } + } + } + + public static void test_appender_date() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + LocalDate ld1 = LocalDate.now(); + // TODO: https://github.com/duckdb/duckdb-java/issues/200 + LocalDate ld2 = LocalDate.of(/*-*/ 23434, 3, 5); + LocalDate ld3 = LocalDate.of(1970, 1, 1); + LocalDate ld4 = LocalDate.of(11111, 12, 31); + LocalDate ld5 = LocalDate.of(999999999, 12, 31); + + stmt.execute("CREATE TABLE date_only (id INT4, a DATE)"); + try (DuckDBAppender appender = conn.createAppender("date_only")) { + appender.beginRow() + .append(1) + .appendEpochDays((int) ld1.toEpochDay()) + .endRow() + .beginRow() + .append(2) + .append(ld2) + .endRow() + .beginRow() + .append(3) + .append(ld3) + .endRow() + .beginRow() + .append(4) + .append(ld4) + .endRow() + .beginRow() + .append(5); + assertThrows(() -> { appender.append(ld5); }, SQLException.class); + appender.append(ld4).endRow().flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT a FROM date_only ORDER BY id")) { + assertFalse(rs.isClosed()); + + assertTrue(rs.next()); + LocalDate res1 = rs.getObject(1, LocalDate.class); + assertEquals(res1, ld1); + + assertTrue(rs.next()); + LocalDate res2 = rs.getObject(1, LocalDate.class); + assertEquals(res2, ld2); + + assertTrue(rs.next()); + LocalDate res3 = rs.getObject(1, LocalDate.class); + assertEquals(res3, ld3); + + assertTrue(rs.next()); + LocalDate res4 = rs.getObject(1, LocalDate.class); + assertEquals(res4, ld4); + + assertTrue(rs.next()); + LocalDate res5 = rs.getObject(1, LocalDate.class); + assertEquals(res5, ld4); + + assertFalse(rs.next()); + } + } + } + + public static void test_appender_string_with_emoji() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE data (id INTEGER, str_value VARCHAR(10))"); + String expectedValue = "\u4B54\uD86D\uDF7C\uD83D\uDD25\uD83D\uDE1C"; + char cjk1 = '\u4b54'; + char cjk2 = '\u5b57'; + + try (DuckDBAppender appender = conn.createAppender("data")) { + appender.beginRow().append(1).append(expectedValue).endRow(); + // append char + appender.beginRow().append(2).append(cjk1).endRow(); + // append char array + appender.beginRow().append(3).append(new char[] {cjk1, cjk2}).endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT str_value FROM data ORDER BY id")) { + assertTrue(rs.next()); + String row1 = rs.getString(1); + assertEquals(row1, expectedValue); + + assertTrue(rs.next()); + String row2 = rs.getString(1); + assertEquals(row2, String.valueOf(cjk1)); + + assertTrue(rs.next()); + String row3 = rs.getString(1); + assertEquals(row3, String.valueOf(new char[] {cjk1, cjk2})); + + assertFalse(rs.next()); + } + } + } + + public static void test_appender_table_does_not_exist() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class)) { + assertThrows(() -> { conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); }, SQLException.class); + } + } + + public static void test_appender_table_deleted() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + try (DuckDBAppender appender = + conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data").beginRow().append(1).endRow()) { + stmt.execute("DROP TABLE data"); + appender.beginRow().append(2).endRow(); + assertThrows(appender::flush, SQLException.class); + } + } + } + + public static void test_appender_append_too_many_columns() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + + try (DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + assertThrows(() -> { appender.beginRow().append(1).append(2).flush(); }, SQLException.class); + } + } + } + + public static void test_appender_append_too_few_columns() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER, b INTEGER)"); + try (DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + assertThrows(() -> { appender.beginRow().append(1).endRow(); }, SQLException.class); + } + } + } + + public static void test_appender_type_mismatch() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + try (DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + assertThrows(() -> { appender.beginRow().append("str"); }, SQLException.class); + } + } + } + + public static void test_appender_null_integer() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + + try (DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + appender.beginRow().appendNull().endRow().flush(); + } + + try (ResultSet results = stmt.executeQuery("SELECT * FROM data")) { + assertTrue(results.next()); + // java.sql.ResultSet.getInt(int) returns 0 if the value is NULL + assertEquals(0, results.getInt(1)); + assertTrue(results.wasNull()); + } + } + } + + public static void test_appender_decimal() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + BigDecimal bigdec16 = new BigDecimal("12.34").setScale(2); + BigDecimal bigdec32 = new BigDecimal("1234.5678").setScale(4); + BigDecimal bigdec64 = new BigDecimal("123456789012.345678").setScale(6); + BigDecimal bigdec128 = new BigDecimal("123456789012345678.90123456789012345678").setScale(20); + BigDecimal negbigdec16 = new BigDecimal("-12.34").setScale(2); + BigDecimal negbigdec32 = new BigDecimal("-1234.5678").setScale(4); + BigDecimal negbigdec64 = new BigDecimal("-123456789012.345678").setScale(6); + BigDecimal negbigdec128 = new BigDecimal("-123456789012345678.90123456789012345678").setScale(20); + BigDecimal smallbigdec16 = new BigDecimal("-1.34").setScale(2); + BigDecimal smallbigdec32 = new BigDecimal("-123.5678").setScale(4); + BigDecimal smallbigdec64 = new BigDecimal("-12345678901.345678").setScale(6); + BigDecimal smallbigdec128 = new BigDecimal("-12345678901234567.90123456789012345678").setScale(20); + BigDecimal intbigdec16 = new BigDecimal("-1").setScale(2); + BigDecimal intbigdec32 = new BigDecimal("-123").setScale(4); + BigDecimal intbigdec64 = new BigDecimal("-12345678901").setScale(6); + BigDecimal intbigdec128 = new BigDecimal("-12345678901234567").setScale(20); + BigDecimal onebigdec16 = new BigDecimal("1").setScale(2); + BigDecimal onebigdec32 = new BigDecimal("1").setScale(4); + BigDecimal onebigdec64 = new BigDecimal("1").setScale(6); + BigDecimal onebigdec128 = new BigDecimal("1").setScale(20); + + stmt.execute( + "CREATE TABLE decimals (id INT4, a DECIMAL(4,2), b DECIMAL(8,4), c DECIMAL(18,6), d DECIMAL(38,20))"); + + try (DuckDBAppender appender = conn.createAppender("decimals")) { + appender.beginRow() + .append(1) + .append(bigdec16) + .append(bigdec32) + .append(bigdec64) + .append(bigdec128) + .endRow() + .beginRow() + .append(2) + .append(negbigdec16) + .append(negbigdec32) + .append(negbigdec64) + .append(negbigdec128) + .endRow() + .beginRow() + .append(3) + .append(smallbigdec16) + .append(smallbigdec32) + .append(smallbigdec64) + .append(smallbigdec128) + .endRow() + .beginRow() + .append(4) + .append(intbigdec16) + .append(intbigdec32) + .append(intbigdec64) + .append(intbigdec128) + .endRow() + .beginRow() + .append(5) + .append(onebigdec16) + .append(onebigdec32) + .append(onebigdec64) + .append(onebigdec128) + .endRow() + .flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT a,b,c,d FROM decimals ORDER BY id")) { + assertFalse(rs.isClosed()); + assertTrue(rs.next()); + + BigDecimal rs1 = rs.getObject(1, BigDecimal.class); + BigDecimal rs2 = rs.getObject(2, BigDecimal.class); + BigDecimal rs3 = rs.getObject(3, BigDecimal.class); + BigDecimal rs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(rs1, bigdec16); + assertEquals(rs2, bigdec32); + assertEquals(rs3, bigdec64); + assertEquals(rs4, bigdec128); + assertTrue(rs.next()); + + BigDecimal nrs1 = rs.getObject(1, BigDecimal.class); + BigDecimal nrs2 = rs.getObject(2, BigDecimal.class); + BigDecimal nrs3 = rs.getObject(3, BigDecimal.class); + BigDecimal nrs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(nrs1, negbigdec16); + assertEquals(nrs2, negbigdec32); + assertEquals(nrs3, negbigdec64); + assertEquals(nrs4, negbigdec128); + assertTrue(rs.next()); + + BigDecimal srs1 = rs.getObject(1, BigDecimal.class); + BigDecimal srs2 = rs.getObject(2, BigDecimal.class); + BigDecimal srs3 = rs.getObject(3, BigDecimal.class); + BigDecimal srs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(srs1, smallbigdec16); + assertEquals(srs2, smallbigdec32); + assertEquals(srs3, smallbigdec64); + assertEquals(srs4, smallbigdec128); + assertTrue(rs.next()); + + BigDecimal irs1 = rs.getObject(1, BigDecimal.class); + BigDecimal irs2 = rs.getObject(2, BigDecimal.class); + BigDecimal irs3 = rs.getObject(3, BigDecimal.class); + BigDecimal irs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(irs1, intbigdec16); + assertEquals(irs2, intbigdec32); + assertEquals(irs3, intbigdec64); + assertEquals(irs4, intbigdec128); + assertTrue(rs.next()); + + BigDecimal oners1 = rs.getObject(1, BigDecimal.class); + BigDecimal oners2 = rs.getObject(2, BigDecimal.class); + BigDecimal oners3 = rs.getObject(3, BigDecimal.class); + BigDecimal oners4 = rs.getObject(4, BigDecimal.class); + + assertEquals(oners1, onebigdec16); + assertEquals(oners2, onebigdec32); + assertEquals(oners3, onebigdec64); + assertEquals(oners4, onebigdec128); + } + } + } + + public static void test_appender_decimal_wrong_scale() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute( + "CREATE TABLE decimals (id INT4, a DECIMAL(4,2), b DECIMAL(8,4), c DECIMAL(18,6), d DECIMAL(38,20))"); + + assertThrows(() -> { + try (DuckDBAppender appender = conn.createAppender("decimals")) { + appender.append(1).beginRow().append(new BigDecimal("121.14").setScale(2)); + } + }, SQLException.class); + + assertThrows(() -> { + try (DuckDBAppender appender = conn.createAppender("decimals")) { + appender.beginRow() + .append(2) + .append(new BigDecimal("21.1").setScale(2)) + .append(new BigDecimal("12111.1411").setScale(4)); + } + }, SQLException.class); + + assertThrows(() -> { + try (DuckDBAppender appender = conn.createAppender("decimals")) { + appender.beginRow() + .append(3) + .append(new BigDecimal("21.1").setScale(2)) + .append(new BigDecimal("21.1").setScale(4)) + .append(new BigDecimal("1234567890123.123456").setScale(6)); + } + }, SQLException.class); + } + } + + public static void test_appender_array_basic() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 INTEGER[3])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(41).append(new int[] {41, 42, 43}).endRow(); + appender.beginRow() + .append(42) + .append(new int[] {44, 45, 46}, new boolean[] {false, true, false}) + .endRow(); + appender.beginRow().append(43).appendNull().endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = 41")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 42); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 43); + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = 42")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 44); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 0); + assertTrue(rs.wasNull()); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 46); + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT col2 FROM tab1 WHERE col1 = 43")) { + assertTrue(rs.next()); + assertNull(rs.getObject(1)); + assertTrue(rs.wasNull()); + assertFalse(rs.next()); + } + } + } + + public static void test_appender_array_bool() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 16; // flushed on close + int arrayLen = (1 << 6) + 6; // increase this for stress tests + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 BOOLEAN[" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + boolean[] arr = new boolean[arrayLen]; + for (byte j = 0; j < arrayLen; j++) { + arr[j] = true; + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = + stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getBoolean(2), true); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = " + row)) { + for (byte j = 0; j < arrayLen; j++) { + assertTrue(rs2.next()); + assertEquals(rs2.getBoolean(1), true); + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_nested_array_bool() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 17; // flushed on close + int arrayLen = (1 << 6) + 5; // increase this for stress tests + int childLen = (1 << 6) + 7; + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 BOOLEAN[" + childLen + "][" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + boolean[][] arr = new boolean[arrayLen][childLen]; + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + arr[j][k] = 0 == (i + j + k) % 2; + } + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "][" + (childLen - 1) + + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getBoolean(2), 0 == (i + arrayLen + childLen - 4) % 2); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery( + "SELECT unnest(ucol2) FROM (SELECT unnest(col2) as ucol2 FROM tab1 WHERE col1 = " + row + ")")) { + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + assertTrue(rs2.next()); + assertEquals(rs2.getBoolean(1), 0 == (row + j + k) % 2); + } + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_array_tinyint() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 16; // flushed on close + int arrayLen = (1 << 6) + 6; // increase this for stress tests + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 TINYINT[" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + byte[] arr = new byte[arrayLen]; + for (byte j = 0; j < arrayLen; j++) { + arr[j] = (byte) ((i % 8) + j); + } + appender.beginRow().append(i).appendByteArray(arr).endRow(); + } + } + + try (ResultSet rs = + stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getInt(2), (i % 8) + arrayLen - 2); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = " + row)) { + for (byte j = 0; j < arrayLen; j++) { + assertTrue(rs2.next()); + assertEquals(rs2.getByte(1), (byte) ((row % 8) + j)); + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_nested_array_tinyint() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 17; // flushed on close + int arrayLen = (1 << 6) + 5; // increase this for stress tests + int childLen = (1 << 6) + 7; + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 TINYINT[" + childLen + "][" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + byte[][] arr = new byte[arrayLen][childLen]; + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + arr[j][k] = (byte) (i + j + k); + } + } + appender.beginRow().append(i).appendByteArray(arr).endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "][" + (childLen - 1) + + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getByte(2), (byte) (i + arrayLen + childLen - 4)); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery( + "SELECT unnest(ucol2) FROM (SELECT unnest(col2) as ucol2 FROM tab1 WHERE col1 = " + row + ")")) { + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + assertTrue(rs2.next()); + assertEquals(rs2.getByte(1), (byte) (row + j + k)); + } + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_array_smallint() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 16; // flushed on close + int arrayLen = (1 << 12) + 6; // increase this for stress tests + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 SMALLINT[" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + short[] arr = new short[arrayLen]; + for (int j = 0; j < arrayLen; j++) { + arr[j] = (short) (i + j); + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = + stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getShort(2), (short) (i + arrayLen - 2)); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = " + row)) { + for (int j = 0; j < arrayLen; j++) { + assertTrue(rs2.next()); + assertEquals(rs2.getShort(1), (short) (row + j)); + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_nested_array_smallint() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 17; // flushed on close + int arrayLen = (1 << 6) + 5; // increase this for stress tests + int childLen = (1 << 6) + 7; + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 SMALLINT[" + childLen + "][" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + short[][] arr = new short[arrayLen][childLen]; + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + arr[j][k] = (short) (i + j + k); + } + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "][" + (childLen - 1) + + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getShort(2), (short) (i + arrayLen + childLen - 4)); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery( + "SELECT unnest(ucol2) FROM (SELECT unnest(col2) as ucol2 FROM tab1 WHERE col1 = " + row + ")")) { + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + assertTrue(rs2.next()); + assertEquals(rs2.getShort(1), (short) (row + j + k)); + } + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_array_integer() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 16; // flushed on close + int arrayLen = (1 << 12) + 6; // increase this for stress tests + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 INTEGER[" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + int[] arr = new int[arrayLen]; + for (int j = 0; j < arrayLen; j++) { + arr[j] = i + j; + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = + stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getInt(2), i + arrayLen - 2); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = " + row)) { + for (int j = 0; j < arrayLen; j++) { + assertTrue(rs2.next()); + assertEquals(rs2.getInt(1), row + j); + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_nested_array_basic_integer() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 INTEGER[2][3])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow().append(41).append(new int[][] {{42, 43}, {44, 45}, {46, 47}}).endRow(); + appender.beginRow().append(48).append(new int[][] {{49, 50}, null, {53, 54}}).endRow(); + appender.beginRow() + .append(55) + .append(new int[][] {{56, 57}, {58, 59}, {60, 61}}, + new boolean[][] {{false, true}, {false, false}, {true, false}}) + .endRow(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, unnest(col2) FROM tab1 WHERE col1 = 41")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + Object[] array1 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array1.length, 2); + assertEquals(array1[0], 42); + assertEquals(array1[1], 43); + + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + Object[] array2 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array2.length, 2); + assertEquals(array2[0], 44); + assertEquals(array2[1], 45); + + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 41); + Object[] array3 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array3.length, 2); + assertEquals(array3[0], 46); + assertEquals(array3[1], 47); + + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, unnest(col2) FROM tab1 WHERE col1 = 48")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 48); + Object[] array1 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array1.length, 2); + assertEquals(array1[0], 49); + assertEquals(array1[1], 50); + + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 48); + assertNull(rs.getObject(2)); + + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 48); + Object[] array3 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array3.length, 2); + assertEquals(array3[0], 53); + assertEquals(array3[1], 54); + + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, unnest(col2) FROM tab1 WHERE col1 = 55")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 55); + Object[] array1 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array1.length, 2); + assertEquals(array1[0], 56); + assertNull(array1[1]); + + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 55); + Object[] array2 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array2.length, 2); + assertEquals(array2[0], 58); + assertEquals(array2[1], 59); + + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 55); + Object[] array3 = (Object[]) rs.getArray(2).getArray(); + assertEquals(array3.length, 2); + assertNull(array3[0]); + assertEquals(array3[1], 61); + + assertFalse(rs.next()); + } + } + } + + public static void test_appender_nested_array_integer() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 12; // auto flush twice + int tail = 17; // flushed on close + int arrayLen = (1 << 6) + 5; // increase this for stress tests + int childLen = (1 << 6) + 7; + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 INTEGER[" + childLen + "][" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + int[][] arr = new int[arrayLen][childLen]; + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + arr[j][k] = (i + 1) * (j + 1) * (k + 1); + } + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "][" + (childLen - 1) + + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getInt(2), (i + 1) * (arrayLen - 1) * (childLen - 1)); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery( + "SELECT unnest(ucol2) FROM (SELECT unnest(col2) as ucol2 FROM tab1 WHERE col1 = " + row + ")")) { + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + assertTrue(rs2.next()); + assertEquals(rs2.getInt(1), (row + 1) * (j + 1) * (k + 1)); + } + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_array_bigint() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 16; // flushed on close + int arrayLen = (1 << 12) + 6; // increase this for stress tests + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 BIGINT[" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + long[] arr = new long[arrayLen]; + for (int j = 0; j < arrayLen; j++) { + arr[j] = i + j; + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = + stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getLong(2), (long) (i + arrayLen - 2)); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = " + row)) { + for (int j = 0; j < arrayLen; j++) { + assertTrue(rs2.next()); + assertEquals(rs2.getLong(1), (long) (row + j)); + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_nested_array_bigint() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 17; // flushed on close + int arrayLen = (1 << 6) + 5; // increase this for stress tests + int childLen = (1 << 6) + 7; + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 BIGINT[" + childLen + "][" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + long[][] arr = new long[arrayLen][childLen]; + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + arr[j][k] = (long) (i + 1) * (j + 1) * (k + 1); + } + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "][" + (childLen - 1) + + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getLong(2), (long) ((i + 1) * (arrayLen - 1) * (childLen - 1))); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery( + "SELECT unnest(ucol2) FROM (SELECT unnest(col2) as ucol2 FROM tab1 WHERE col1 = " + row + ")")) { + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + assertTrue(rs2.next()); + assertEquals(rs2.getLong(1), (long) (row + 1) * (j + 1) * (k + 1)); + } + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_array_float() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 16; // flushed on close + int arrayLen = (1 << 12) + 6; // increase this for stress tests + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 FLOAT[" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + float[] arr = new float[arrayLen]; + for (int j = 0; j < arrayLen; j++) { + arr[j] = (float) (i + j + 0.001); + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = + stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getFloat(2), (float) (i + arrayLen - 2 + 0.001)); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = " + row)) { + for (int j = 0; j < arrayLen; j++) { + assertTrue(rs2.next()); + assertEquals(rs2.getFloat(1), (float) (row + j + 0.001)); + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_nested_array_float() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 12; // auto flush twice + int tail = 17; // flushed on close + int arrayLen = (1 << 6) + 5; // increase this for stress tests + int childLen = (1 << 6) + 7; + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 FLOAT[" + childLen + "][" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + float[][] arr = new float[arrayLen][childLen]; + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + arr[j][k] = (i + 1) * (j + 1) * (k + 1) + 0.001f; + } + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "][" + (childLen - 1) + + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getFloat(2), (i + 1) * (arrayLen - 1) * (childLen - 1) + 0.001f); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery( + "SELECT unnest(ucol2) FROM (SELECT unnest(col2) as ucol2 FROM tab1 WHERE col1 = " + row + ")")) { + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + assertTrue(rs2.next()); + assertEquals(rs2.getFloat(1), (row + 1) * (j + 1) * (k + 1) + 0.001f); + } + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_array_double() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 16; // flushed on close + int arrayLen = (1 << 12) + 6; // increase this for stress tests + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 DOUBLE[" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + double[] arr = new double[arrayLen]; + for (int j = 0; j < arrayLen; j++) { + arr[j] = (i + j + 0.001); + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = + stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getDouble(2), (i + arrayLen - 2 + 0.001)); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery("SELECT unnest(col2) FROM tab1 WHERE col1 = " + row)) { + for (int j = 0; j < arrayLen; j++) { + assertTrue(rs2.next()); + assertEquals(rs2.getDouble(1), (row + j + 0.001)); + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_nested_array_double() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 11; // auto flush + int tail = 17; // flushed on close + int arrayLen = (1 << 6) + 5; // increase this for stress tests + int childLen = (1 << 6) + 7; + + stmt.execute("CREATE TABLE tab1(col1 INT, col2 DOUBLE[" + childLen + "][" + arrayLen + "])"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + double[][] arr = new double[arrayLen][childLen]; + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + arr[j][k] = (i + 1) * (j + 1) * (k + 1) + 0.001; + } + } + appender.beginRow().append(i).append(arr).endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT col1, col2[" + (arrayLen - 1) + "][" + (childLen - 1) + + "] FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + assertEquals(rs.getDouble(2), (i + 1) * (arrayLen - 1) * (childLen - 1) + 0.001); + } + assertFalse(rs.next()); + } + + int row = count - 2; + try (Statement stmt2 = conn.createStatement(); + ResultSet rs2 = stmt2.executeQuery( + "SELECT unnest(ucol2) FROM (SELECT unnest(col2) as ucol2 FROM tab1 WHERE col1 = " + row + ")")) { + for (int j = 0; j < arrayLen; j++) { + for (int k = 0; k < childLen; k++) { + assertTrue(rs2.next()); + assertEquals(rs2.getDouble(1), (row + 1) * (j + 1) * (k + 1) + 0.001); + } + } + assertFalse(rs2.next()); + } + } + } + + public static void test_appender_roundtrip_blob() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + SecureRandom random = SecureRandom.getInstanceStrong(); + byte[] data = new byte[512]; + random.nextBytes(data); + + stmt.execute("CREATE TABLE data (a BLOB)"); + + try (DuckDBAppender appender = conn.createAppender("data")) { + appender.beginRow().append(data).endRow().flush(); + } + + try (ResultSet results = stmt.executeQuery("SELECT * FROM data")) { + assertTrue(results.next()); + + Blob resultBlob = results.getBlob(1); + byte[] resultBytes = resultBlob.getBytes(1, (int) resultBlob.length()); + assertTrue(Arrays.equals(resultBytes, data), "byte[] data is round tripped untouched"); + } + } + } + + public static void test_appender_struct_basic() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1 (col1 INTEGER, col2 STRUCT(s1 INTEGER, s2 VARCHAR))"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow() + .append(42) + .beginStruct() + .append(43) + .append("foo") + .endStruct() + .endRow() + + .beginRow() + .append(44) + .beginStruct() + .append(45) + .appendNull() + .endStruct() + .endRow() + + .beginRow() + .append(46) + .appendNull() + .endRow() + .flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 WHERE col1 = 42")) { + assertTrue(rs.next()); + + assertEquals(rs.getInt(1), 42); + DuckDBStruct struct = (DuckDBStruct) rs.getObject(2); + Map map = struct.getMap(); + assertEquals(map.get("s1"), 43); + assertEquals(map.get("s2"), "foo"); + + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 WHERE col1 = 44")) { + assertTrue(rs.next()); + + assertEquals(rs.getInt(1), 44); + DuckDBStruct struct = (DuckDBStruct) rs.getObject(2); + Map map = struct.getMap(); + assertEquals(map.get("s1"), 45); + assertNull(map.get("s2")); + + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 WHERE col1 = 46")) { + assertTrue(rs.next()); + + assertEquals(rs.getInt(1), 46); + assertNull(rs.getObject(2)); + + assertFalse(rs.next()); + } + } + } + + public static void test_appender_struct_with_array() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1 (col1 INTEGER, col2 STRUCT(s1 INTEGER, s2 INTEGER[2]))"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + appender.beginRow() + .append(42) + .beginStruct() + .append(43) + .append(new int[] {44, 45}) + .endStruct() + .endRow() + .flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1")) { + assertTrue(rs.next()); + + assertEquals(rs.getInt(1), 42); + DuckDBStruct struct = (DuckDBStruct) rs.getObject(2); + Map map = struct.getMap(); + assertEquals(map.get("s1"), 43); + DuckDBArray arrayWrapper = (DuckDBArray) map.get("s2"); + Object[] array = (Object[]) arrayWrapper.getArray(); + assertEquals(array.length, 2); + assertEquals(array[0], 44); + assertEquals(array[1], 45); + + assertFalse(rs.next()); + } + } + } + + public static void test_appender_struct_flush() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + int count = 1 << 12; // auto flush twice + int tail = 17; // flushed on close + + stmt.execute("CREATE TABLE tab1 (col1 INTEGER, col2 STRUCT(s1 INTEGER, s2 INTEGER[2], s3 VARCHAR))"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + appender.beginRow().append(i); + appender.beginStruct().append(i + 1); + if (0 == i % 7) { + appender.append(new int[] {i + 2, i + 3}, new boolean[] {false, true}); + } else { + appender.append(new int[] {i + 2, i + 3}); + } + if (0 == i % 13) { + appender.appendNull(); + } else { + appender.append("foo" + i); + } + appender.endStruct(); + appender.endRow(); + } + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + for (int i = 0; i < count + tail; i++) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), i); + DuckDBStruct struct = (DuckDBStruct) rs.getObject(2); + Map map = struct.getMap(); + assertEquals(map.get("s1"), i + 1); + DuckDBArray arrayWrapper = (DuckDBArray) map.get("s2"); + Object[] array = (Object[]) arrayWrapper.getArray(); + assertEquals(array.length, 2); + assertEquals(array[0], i + 2); + if (0 == i % 7) { + assertNull(array[1]); + } else { + assertEquals(array[1], i + 3); + } + if (0 == i % 13) { + assertNull(map.get("s3")); + } else { + assertEquals(map.get("s3"), "foo" + i); + } + } + + assertFalse(rs.next()); + } + } + } +} diff --git a/src/test/java/org/duckdb/TestBindings.java b/src/test/java/org/duckdb/TestBindings.java new file mode 100644 index 000000000..de0316227 --- /dev/null +++ b/src/test/java/org/duckdb/TestBindings.java @@ -0,0 +1,338 @@ +package org.duckdb; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.duckdb.DuckDBBindings.*; +import static org.duckdb.DuckDBBindings.CAPIType.*; +import static org.duckdb.TestDuckDBJDBC.JDBC_URL; +import static org.duckdb.test.Assertions.*; + +import java.nio.ByteBuffer; +import java.sql.*; + +public class TestBindings { + + static final int STRING_T_SIZE_BYTES = 16; + + public static void test_bindings_vector_size() throws Exception { + long size = duckdb_vector_size(); + assertTrue(size > 0); + } + + public static void test_bindings_logical_type() throws Exception { + ByteBuffer lt = duckdb_create_logical_type(DUCKDB_TYPE_INTEGER.typeId); + assertNotNull(lt); + assertEquals(DUCKDB_TYPE_INTEGER.typeId, duckdb_get_type_id(lt)); + + ByteBuffer listType = duckdb_create_list_type(lt); + assertTrue(duckdb_get_type_id(listType) != DUCKDB_TYPE_INVALID.typeId); + duckdb_destroy_logical_type(listType); + + ByteBuffer arrayType = duckdb_create_array_type(lt, 42); + assertTrue(duckdb_get_type_id(arrayType) != DUCKDB_TYPE_INVALID.typeId); + duckdb_destroy_logical_type(arrayType); + + duckdb_destroy_logical_type(lt); + + assertEquals(duckdb_get_type_id(duckdb_create_logical_type(-1)), DUCKDB_TYPE_INVALID.typeId); + assertEquals(duckdb_get_type_id(duckdb_create_logical_type(DUCKDB_TYPE_INVALID.typeId)), + DUCKDB_TYPE_INVALID.typeId); + + assertThrows(() -> { duckdb_destroy_logical_type(null); }, SQLException.class); + } + + public static void test_bindings_vector_create() throws Exception { + ByteBuffer lt = duckdb_create_logical_type(DUCKDB_TYPE_INTEGER.typeId); + ByteBuffer vec = duckdb_create_vector(lt); + assertNotNull(vec); + + ByteBuffer data = duckdb_vector_get_data(vec, 4); + assertNotNull(data); + assertEquals(data.capacity(), (int) duckdb_vector_size() * 4); + + ByteBuffer vecLt = duckdb_vector_get_column_type(vec); + assertNotNull(vecLt); + assertEquals(duckdb_get_type_id(vecLt), DUCKDB_TYPE_INTEGER.typeId); + duckdb_destroy_logical_type(vecLt); + + assertThrows(() -> { duckdb_create_vector(null); }, SQLException.class); + assertThrows(() -> { duckdb_vector_get_data(null, 0); }, SQLException.class); + + duckdb_destroy_vector(vec); + duckdb_destroy_logical_type(lt); + } + + private static void checkVectorInsertString(ByteBuffer vec) throws Exception { + String str = "foo"; + int idx = 7; + + byte[] bytes = str.getBytes(UTF_8); + duckdb_vector_assign_string_element_len(vec, idx, bytes); + ByteBuffer data = duckdb_vector_get_data(vec, 16); + assertEquals(data.remaining(), (int) duckdb_vector_size() * 16); + + int lengthPos = STRING_T_SIZE_BYTES * idx; + int length = data.getInt(lengthPos); + assertEquals(length, str.length()); + + int dataPos = lengthPos + 4; + byte[] buf = new byte[length]; + data.position(dataPos); + data.get(buf); + assertEquals(new String(buf, UTF_8), str); + } + + public static void test_bindings_vector_strings() throws Exception { + ByteBuffer lt = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR.typeId); + ByteBuffer vec = duckdb_create_vector(lt); + + checkVectorInsertString(vec); + + String str = "bar"; + int idx = 9; + duckdb_vector_assign_string_element_len(vec, idx, str.getBytes(UTF_8)); + ByteBuffer data = duckdb_vector_get_data(vec, 16); + + int lengthPos = STRING_T_SIZE_BYTES * idx; + int length = data.getInt(lengthPos); + assertEquals(length, str.length()); + + int dataPos = lengthPos + 4; + byte[] buf = new byte[length]; + data.position(dataPos); + data.get(buf); + assertEquals(new String(buf, UTF_8), str); + + duckdb_destroy_vector(vec); + duckdb_destroy_logical_type(lt); + } + + public static void test_bindings_vector_validity() throws Exception { + ByteBuffer lt = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR.typeId); + ByteBuffer vec = duckdb_create_vector(lt); + + ByteBuffer emptyValidity = duckdb_vector_get_validity(vec, 1); + assertNull(emptyValidity); + + duckdb_vector_ensure_validity_writable(vec); + ByteBuffer validity = duckdb_vector_get_validity(vec, 1); + assertNotNull(validity); + assertEquals(validity.capacity(), (int) duckdb_vector_size() / 8); + + duckdb_destroy_vector(vec); + duckdb_destroy_logical_type(lt); + } + + public static void test_bindings_list_vector() throws Exception { + ByteBuffer lt = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR.typeId); + ByteBuffer listType = duckdb_create_list_type(lt); + assertTrue(duckdb_get_type_id(listType) != DUCKDB_TYPE_INVALID.typeId); + ByteBuffer vec = duckdb_create_vector(listType); + + ByteBuffer childVec = duckdb_list_vector_get_child(vec); + assertNotNull(childVec); + ByteBuffer data = duckdb_vector_get_data(childVec, 16); + assertNotNull(data); + assertEquals(data.capacity(), (int) duckdb_vector_size() * 16); + checkVectorInsertString(childVec); + + assertEquals(duckdb_list_vector_get_size(vec), 0L); + assertEquals(duckdb_list_vector_reserve(vec, 42), 0); + assertEquals(duckdb_list_vector_set_size(vec, 42), 0); + assertEquals(duckdb_list_vector_get_size(vec), 42L); + + duckdb_destroy_vector(vec); + duckdb_destroy_logical_type(listType); + duckdb_destroy_logical_type(lt); + } + + public static void test_bindings_array_vector() throws Exception { + ByteBuffer lt = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR.typeId); + ByteBuffer arrayType = duckdb_create_array_type(lt, 42); + assertTrue(duckdb_get_type_id(arrayType) != DUCKDB_TYPE_INVALID.typeId); + assertEquals(duckdb_array_type_array_size(arrayType), 42L); + ByteBuffer vec = duckdb_create_vector(arrayType); + + ByteBuffer childVec = duckdb_array_vector_get_child(vec); + assertNotNull(childVec); + ByteBuffer data = duckdb_vector_get_data(childVec, 16); + assertNotNull(data); + assertEquals(data.capacity(), (int) duckdb_vector_size() * 16); + checkVectorInsertString(childVec); + + duckdb_destroy_vector(vec); + duckdb_destroy_logical_type(arrayType); + duckdb_destroy_logical_type(lt); + } + + public static void test_bindings_struct_vector() throws Exception { + ByteBuffer intType = duckdb_create_logical_type(DUCKDB_TYPE_INTEGER.typeId); + ByteBuffer varcharType = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR.typeId); + ByteBuffer structType = duckdb_create_struct_type(new ByteBuffer[] {intType, varcharType}, + new byte[][] {"foo".getBytes(UTF_8), "bar".getBytes(UTF_8)}); + assertTrue(duckdb_get_type_id(structType) != DUCKDB_TYPE_INVALID.typeId); + assertEquals(duckdb_struct_type_child_count(structType), 2L); + + ByteBuffer vec = duckdb_create_vector(structType); + + ByteBuffer childVec = duckdb_struct_vector_get_child(vec, 1); + assertNotNull(childVec); + ByteBuffer data = duckdb_vector_get_data(childVec, 16); + assertNotNull(data); + assertEquals(data.capacity(), (int) duckdb_vector_size() * 16); + checkVectorInsertString(childVec); + + duckdb_destroy_vector(vec); + duckdb_destroy_logical_type(structType); + duckdb_destroy_logical_type(varcharType); + duckdb_destroy_logical_type(intType); + } + + public static void test_bindings_validity() throws Exception { + ByteBuffer lt = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR.typeId); + ByteBuffer vec = duckdb_create_vector(lt); + duckdb_vector_ensure_validity_writable(vec); + ByteBuffer validity = duckdb_vector_get_validity(vec, 1); + + long row = 7; + assertTrue(duckdb_validity_row_is_valid(validity, row)); + duckdb_validity_set_row_validity(validity, row, false); + assertFalse(duckdb_validity_row_is_valid(validity, row)); + duckdb_validity_set_row_validity(validity, row, true); + assertTrue(duckdb_validity_row_is_valid(validity, row)); + + duckdb_destroy_vector(vec); + duckdb_destroy_logical_type(lt); + } + + public static void test_bindings_data_chunk() throws Exception { + ByteBuffer intType = duckdb_create_logical_type(DUCKDB_TYPE_INTEGER.typeId); + ByteBuffer varcharType = duckdb_create_logical_type(DUCKDB_TYPE_VARCHAR.typeId); + + ByteBuffer chunk = duckdb_create_data_chunk(new ByteBuffer[] {intType, varcharType}); + assertNotNull(chunk); + + assertEquals(duckdb_data_chunk_get_column_count(chunk), 2L); + + assertEquals(duckdb_data_chunk_get_size(chunk), 0L); + duckdb_data_chunk_set_size(chunk, 42L); + assertEquals(duckdb_data_chunk_get_size(chunk), 42L); + + ByteBuffer vec = duckdb_data_chunk_get_vector(chunk, 1); + assertNotNull(vec); + ByteBuffer data = duckdb_vector_get_data(vec, 16); + assertNotNull(data); + assertEquals(data.capacity(), (int) duckdb_vector_size() * 16); + checkVectorInsertString(vec); + + duckdb_vector_ensure_validity_writable(vec); + assertNotNull(duckdb_vector_get_validity(vec, 1)); + duckdb_data_chunk_reset(chunk); + assertNull(duckdb_vector_get_validity(vec, 1)); + + duckdb_destroy_data_chunk(chunk); + duckdb_destroy_logical_type(varcharType); + duckdb_destroy_logical_type(intType); + } + + public static void test_bindings_appender() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 VARCHAR)"); + + ByteBuffer[] out = new ByteBuffer[1]; + int state = + duckdb_appender_create_ext(conn.connRef, "memory".getBytes(UTF_8), null, "tab1".getBytes(UTF_8), out); + + assertEquals(state, 0); + ByteBuffer appender = out[0]; + assertNotNull(appender); + + assertEquals(duckdb_appender_column_count(appender), 2L); + ByteBuffer col1Type = duckdb_appender_column_type(appender, 0); + assertEquals(duckdb_get_type_id(col1Type), DUCKDB_TYPE_INTEGER.typeId); + ByteBuffer col2Type = duckdb_appender_column_type(appender, 1); + assertEquals(duckdb_get_type_id(col2Type), DUCKDB_TYPE_VARCHAR.typeId); + + ByteBuffer chunk = duckdb_create_data_chunk(new ByteBuffer[] {col1Type, col2Type}); + + ByteBuffer col1Vec = duckdb_data_chunk_get_vector(chunk, 0); + duckdb_vector_ensure_validity_writable(col1Vec); + ByteBuffer col1Data = duckdb_vector_get_data(col1Vec, 4); + col1Data.putInt(42); + col1Data.putInt(43); + duckdb_append_default_to_chunk(appender, chunk, 0, 2); + + ByteBuffer col2Vec = duckdb_data_chunk_get_vector(chunk, 1); + duckdb_vector_ensure_validity_writable(col2Vec); + duckdb_vector_assign_string_element_len(col2Vec, 0, "foo".getBytes(UTF_8)); + duckdb_vector_assign_string_element_len(col2Vec, 1, "bar".getBytes(UTF_8)); + duckdb_append_default_to_chunk(appender, chunk, 1, 2); + + duckdb_data_chunk_set_size(chunk, 3); + assertEquals(duckdb_append_data_chunk(appender, chunk), 0); + assertEquals(duckdb_appender_flush(appender), 0); + assertNull(duckdb_appender_error(appender)); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 ORDER BY col1")) { + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 42); + assertEquals(rs.getString(2), "foo"); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 43); + assertEquals(rs.getString(2), "bar"); + assertTrue(rs.next()); + assertEquals(rs.getInt(1), 0); + assertEquals(rs.getString(2), null); + assertFalse(rs.next()); + } + + assertEquals(duckdb_appender_close(appender), 0); + assertEquals(duckdb_appender_destroy(appender), 0); + duckdb_destroy_data_chunk(chunk); + duckdb_destroy_logical_type(col1Type); + duckdb_destroy_logical_type(col2Type); + } + } + + public static void test_bindings_decimal_type() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute( + "CREATE TABLE tab1(dec2 DECIMAL(3,1), dec4 DECIMAL(7,3), dec8 DECIMAL(15,5), dec16 DECIMAL(30,10))"); + + ByteBuffer[] out = new ByteBuffer[1]; + int state = + duckdb_appender_create_ext(conn.connRef, "memory".getBytes(UTF_8), null, "tab1".getBytes(UTF_8), out); + assertEquals(state, 0); + ByteBuffer appender = out[0]; + assertNotNull(appender); + + ByteBuffer dec2Type = duckdb_appender_column_type(appender, 0); + assertEquals(duckdb_decimal_width(dec2Type), 3); + assertEquals(duckdb_decimal_scale(dec2Type), 1); + assertEquals(duckdb_decimal_internal_type(dec2Type), DUCKDB_TYPE_SMALLINT.typeId); + + ByteBuffer dec4Type = duckdb_appender_column_type(appender, 1); + assertEquals(duckdb_decimal_width(dec4Type), 7); + assertEquals(duckdb_decimal_scale(dec4Type), 3); + assertEquals(duckdb_decimal_internal_type(dec4Type), DUCKDB_TYPE_INTEGER.typeId); + + ByteBuffer dec8Type = duckdb_appender_column_type(appender, 2); + assertEquals(duckdb_decimal_width(dec8Type), 15); + assertEquals(duckdb_decimal_scale(dec8Type), 5); + assertEquals(duckdb_decimal_internal_type(dec8Type), DUCKDB_TYPE_BIGINT.typeId); + + ByteBuffer dec16Type = duckdb_appender_column_type(appender, 3); + assertEquals(duckdb_decimal_width(dec16Type), 30); + assertEquals(duckdb_decimal_scale(dec16Type), 10); + assertEquals(duckdb_decimal_internal_type(dec16Type), DUCKDB_TYPE_HUGEINT.typeId); + + duckdb_destroy_logical_type(dec16Type); + duckdb_destroy_logical_type(dec8Type); + duckdb_destroy_logical_type(dec4Type); + duckdb_destroy_logical_type(dec2Type); + assertEquals(duckdb_appender_close(appender), 0); + assertEquals(duckdb_appender_destroy(appender), 0); + } + } +} diff --git a/src/test/java/org/duckdb/TestClosure.java b/src/test/java/org/duckdb/TestClosure.java index 558f81ce1..fa4322397 100644 --- a/src/test/java/org/duckdb/TestClosure.java +++ b/src/test/java/org/duckdb/TestClosure.java @@ -52,6 +52,17 @@ public static void test_statements_auto_closed_on_conn_close() throws Exception assertTrue(stmt2.isClosed()); } + public static void test_appender_auto_closed_on_conn_close() throws Exception { + DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + try (Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE tab1(col1 INT, col2 VARCHAR)"); + } + DuckDBAppender appender = conn.createAppender("tab1"); + appender.beginRow().append(42).append("foo").endRow().flush(); + conn.close(); + assertTrue(appender.isClosed()); + } + public static void test_results_auto_closed_on_conn_close() throws Exception { Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement(); @@ -220,6 +231,7 @@ public static void test_results_close_prepared_stmt_no_crash() throws Exception } } + @SuppressWarnings("try") public static void test_results_fetch_no_hang() throws Exception { ExecutorService executor = Executors.newSingleThreadExecutor(); Properties config = new Properties(); diff --git a/src/test/java/org/duckdb/TestDuckDBJDBC.java b/src/test/java/org/duckdb/TestDuckDBJDBC.java index 02fecee2a..169bc8397 100644 --- a/src/test/java/org/duckdb/TestDuckDBJDBC.java +++ b/src/test/java/org/duckdb/TestDuckDBJDBC.java @@ -23,7 +23,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.security.SecureRandom; import java.sql.*; import java.time.*; import java.time.format.DateTimeFormatter; @@ -604,15 +603,8 @@ public static void test_read_only() throws Exception { conn_rw.close(); - try (Statement ignored = conn_rw.createStatement()) { - fail("Connection was already closed; shouldn't be able to create a statement"); - } catch (SQLException e) { - } - - try (Connection ignored = conn_rw.unwrap(DuckDBConnection.class).duplicate()) { - fail("Connection was already closed; shouldn't be able to duplicate"); - } catch (SQLException e) { - } + assertThrows(conn_rw::createStatement, SQLException.class); + assertThrows(() -> { conn_rw.unwrap(DuckDBConnection.class).duplicate(); }, SQLException.class); // // we can create two parallel read only connections and query them, too try (Connection conn_ro1 = DriverManager.getConnection(jdbc_url, ro_prop); @@ -1179,519 +1171,6 @@ public static void test_explain_bug958() throws Exception { conn.close(); } - public static void test_appender_numbers() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - // int8, int4, int2, int1, float8, float4 - stmt.execute("CREATE TABLE numbers (a BIGINT, b INTEGER, c SMALLINT, d TINYINT, e DOUBLE, f FLOAT)"); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "numbers"); - - for (int i = 0; i < 50; i++) { - appender.beginRow(); - appender.append(Long.MAX_VALUE - i); - appender.append(Integer.MAX_VALUE - i); - appender.append(Short.MAX_VALUE - i); - appender.append(Byte.MAX_VALUE - i); - appender.append(i); - appender.append(i); - appender.endRow(); - } - appender.close(); - - ResultSet rs = stmt.executeQuery("SELECT max(a), max(b), max(c), max(d), max(e), max(f) FROM numbers"); - assertFalse(rs.isClosed()); - assertTrue(rs.next()); - - long resA = rs.getLong(1); - assertEquals(resA, Long.MAX_VALUE); - - int resB = rs.getInt(2); - assertEquals(resB, Integer.MAX_VALUE); - - short resC = rs.getShort(3); - assertEquals(resC, Short.MAX_VALUE); - - byte resD = rs.getByte(4); - assertEquals(resD, Byte.MAX_VALUE); - - double resE = rs.getDouble(5); - assertEquals(resE, 49.0d); - - float resF = rs.getFloat(6); - assertEquals(resF, 49.0f); - - rs.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_date_and_time() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE date_and_time (id INT4, a TIMESTAMP)"); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "date_and_time"); - - LocalDateTime ldt1 = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); - LocalDateTime ldt2 = LocalDateTime.of(-23434, 3, 5, 23, 2); - LocalDateTime ldt3 = LocalDateTime.of(1970, 1, 1, 0, 0); - LocalDateTime ldt4 = LocalDateTime.of(11111, 12, 31, 23, 59, 59, 999999000); - - appender.beginRow(); - appender.append(1); - appender.appendLocalDateTime(ldt1); - appender.endRow(); - appender.beginRow(); - appender.append(2); - appender.appendLocalDateTime(ldt2); - appender.endRow(); - appender.beginRow(); - appender.append(3); - appender.appendLocalDateTime(ldt3); - appender.endRow(); - appender.beginRow(); - appender.append(4); - appender.appendLocalDateTime(ldt4); - appender.endRow(); - appender.close(); - - ResultSet rs = stmt.executeQuery("SELECT a FROM date_and_time ORDER BY id"); - assertFalse(rs.isClosed()); - assertTrue(rs.next()); - - LocalDateTime res1 = (LocalDateTime) rs.getObject(1, LocalDateTime.class); - assertEquals(res1, ldt1); - assertTrue(rs.next()); - - LocalDateTime res2 = (LocalDateTime) rs.getObject(1, LocalDateTime.class); - assertEquals(res2, ldt2); - assertTrue(rs.next()); - - LocalDateTime res3 = (LocalDateTime) rs.getObject(1, LocalDateTime.class); - assertEquals(res3, ldt3); - assertTrue(rs.next()); - - LocalDateTime res4 = (LocalDateTime) rs.getObject(1, LocalDateTime.class); - assertEquals(res4, ldt4); - - rs.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_decimal() throws Exception { - DuckDBConnection conn = DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute( - "CREATE TABLE decimals (id INT4, a DECIMAL(4,2), b DECIMAL(8,4), c DECIMAL(18,6), d DECIMAL(38,20))"); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals"); - - BigDecimal bigdec16 = new BigDecimal("12.34").setScale(2); - BigDecimal bigdec32 = new BigDecimal("1234.5678").setScale(4); - BigDecimal bigdec64 = new BigDecimal("123456789012.345678").setScale(6); - BigDecimal bigdec128 = new BigDecimal("123456789012345678.90123456789012345678").setScale(20); - BigDecimal negbigdec16 = new BigDecimal("-12.34").setScale(2); - BigDecimal negbigdec32 = new BigDecimal("-1234.5678").setScale(4); - BigDecimal negbigdec64 = new BigDecimal("-123456789012.345678").setScale(6); - BigDecimal negbigdec128 = new BigDecimal("-123456789012345678.90123456789012345678").setScale(20); - BigDecimal smallbigdec16 = new BigDecimal("-1.34").setScale(2); - BigDecimal smallbigdec32 = new BigDecimal("-123.5678").setScale(4); - BigDecimal smallbigdec64 = new BigDecimal("-12345678901.345678").setScale(6); - BigDecimal smallbigdec128 = new BigDecimal("-12345678901234567.90123456789012345678").setScale(20); - BigDecimal intbigdec16 = new BigDecimal("-1").setScale(2); - BigDecimal intbigdec32 = new BigDecimal("-123").setScale(4); - BigDecimal intbigdec64 = new BigDecimal("-12345678901").setScale(6); - BigDecimal intbigdec128 = new BigDecimal("-12345678901234567").setScale(20); - BigDecimal onebigdec16 = new BigDecimal("1").setScale(2); - BigDecimal onebigdec32 = new BigDecimal("1").setScale(4); - BigDecimal onebigdec64 = new BigDecimal("1").setScale(6); - BigDecimal onebigdec128 = new BigDecimal("1").setScale(20); - - appender.beginRow(); - appender.append(1); - appender.appendBigDecimal(bigdec16); - appender.appendBigDecimal(bigdec32); - appender.appendBigDecimal(bigdec64); - appender.appendBigDecimal(bigdec128); - appender.endRow(); - appender.beginRow(); - appender.append(2); - appender.appendBigDecimal(negbigdec16); - appender.appendBigDecimal(negbigdec32); - appender.appendBigDecimal(negbigdec64); - appender.appendBigDecimal(negbigdec128); - appender.endRow(); - appender.beginRow(); - appender.append(3); - appender.appendBigDecimal(smallbigdec16); - appender.appendBigDecimal(smallbigdec32); - appender.appendBigDecimal(smallbigdec64); - appender.appendBigDecimal(smallbigdec128); - appender.endRow(); - appender.beginRow(); - appender.append(4); - appender.appendBigDecimal(intbigdec16); - appender.appendBigDecimal(intbigdec32); - appender.appendBigDecimal(intbigdec64); - appender.appendBigDecimal(intbigdec128); - appender.endRow(); - appender.beginRow(); - appender.append(5); - appender.appendBigDecimal(onebigdec16); - appender.appendBigDecimal(onebigdec32); - appender.appendBigDecimal(onebigdec64); - appender.appendBigDecimal(onebigdec128); - appender.endRow(); - appender.close(); - - ResultSet rs = stmt.executeQuery("SELECT a,b,c,d FROM decimals ORDER BY id"); - assertFalse(rs.isClosed()); - assertTrue(rs.next()); - - BigDecimal rs1 = (BigDecimal) rs.getObject(1, BigDecimal.class); - BigDecimal rs2 = (BigDecimal) rs.getObject(2, BigDecimal.class); - BigDecimal rs3 = (BigDecimal) rs.getObject(3, BigDecimal.class); - BigDecimal rs4 = (BigDecimal) rs.getObject(4, BigDecimal.class); - - assertEquals(rs1, bigdec16); - assertEquals(rs2, bigdec32); - assertEquals(rs3, bigdec64); - assertEquals(rs4, bigdec128); - assertTrue(rs.next()); - - BigDecimal nrs1 = (BigDecimal) rs.getObject(1, BigDecimal.class); - BigDecimal nrs2 = (BigDecimal) rs.getObject(2, BigDecimal.class); - BigDecimal nrs3 = (BigDecimal) rs.getObject(3, BigDecimal.class); - BigDecimal nrs4 = (BigDecimal) rs.getObject(4, BigDecimal.class); - - assertEquals(nrs1, negbigdec16); - assertEquals(nrs2, negbigdec32); - assertEquals(nrs3, negbigdec64); - assertEquals(nrs4, negbigdec128); - assertTrue(rs.next()); - - BigDecimal srs1 = (BigDecimal) rs.getObject(1, BigDecimal.class); - BigDecimal srs2 = (BigDecimal) rs.getObject(2, BigDecimal.class); - BigDecimal srs3 = (BigDecimal) rs.getObject(3, BigDecimal.class); - BigDecimal srs4 = (BigDecimal) rs.getObject(4, BigDecimal.class); - - assertEquals(srs1, smallbigdec16); - assertEquals(srs2, smallbigdec32); - assertEquals(srs3, smallbigdec64); - assertEquals(srs4, smallbigdec128); - assertTrue(rs.next()); - - BigDecimal irs1 = (BigDecimal) rs.getObject(1, BigDecimal.class); - BigDecimal irs2 = (BigDecimal) rs.getObject(2, BigDecimal.class); - BigDecimal irs3 = (BigDecimal) rs.getObject(3, BigDecimal.class); - BigDecimal irs4 = (BigDecimal) rs.getObject(4, BigDecimal.class); - - assertEquals(irs1, intbigdec16); - assertEquals(irs2, intbigdec32); - assertEquals(irs3, intbigdec64); - assertEquals(irs4, intbigdec128); - assertTrue(rs.next()); - - BigDecimal oners1 = (BigDecimal) rs.getObject(1, BigDecimal.class); - BigDecimal oners2 = (BigDecimal) rs.getObject(2, BigDecimal.class); - BigDecimal oners3 = (BigDecimal) rs.getObject(3, BigDecimal.class); - BigDecimal oners4 = (BigDecimal) rs.getObject(4, BigDecimal.class); - - assertEquals(oners1, onebigdec16); - assertEquals(oners2, onebigdec32); - assertEquals(oners3, onebigdec64); - assertEquals(oners4, onebigdec128); - - rs.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_decimal_wrong_scale() throws Exception { - DuckDBConnection conn = DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute( - "CREATE TABLE decimals (id INT4, a DECIMAL(4,2), b DECIMAL(8,4), c DECIMAL(18,6), d DECIMAL(38,20))"); - - assertThrows(() -> { - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals"); - appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals"); - appender.append(1); - appender.beginRow(); - appender.appendBigDecimal(new BigDecimal("121.14").setScale(2)); - }, SQLException.class); - - assertThrows(() -> { - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals"); - appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals"); - appender.beginRow(); - appender.append(2); - appender.appendBigDecimal(new BigDecimal("21.1").setScale(2)); - appender.appendBigDecimal(new BigDecimal("12111.1411").setScale(4)); - }, SQLException.class); - - assertThrows(() -> { - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals"); - appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals"); - appender.beginRow(); - appender.append(3); - appender.appendBigDecimal(new BigDecimal("21.1").setScale(2)); - appender.appendBigDecimal(new BigDecimal("21.1").setScale(4)); - appender.appendBigDecimal(new BigDecimal("1234567890123.123456").setScale(6)); - }, SQLException.class); - - stmt.close(); - conn.close(); - } - - public static void test_appender_int_string() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a INTEGER, s VARCHAR)"); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - for (int i = 0; i < 1000; i++) { - appender.beginRow(); - appender.append(i); - appender.append("str " + i); - appender.endRow(); - } - appender.close(); - - ResultSet rs = stmt.executeQuery("SELECT max(a), min(s) FROM data"); - assertFalse(rs.isClosed()); - - assertTrue(rs.next()); - int resA = rs.getInt(1); - assertEquals(resA, 999); - String resB = rs.getString(2); - assertEquals(resB, "str 0"); - - rs.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_string_with_emoji() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (str_value VARCHAR(10))"); - String expectedValue = "δ­”\uD86D\uDF7CπŸ”₯\uD83D\uDE1C"; - try (DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { - appender.beginRow(); - appender.append(expectedValue); - appender.endRow(); - } - - ResultSet rs = stmt.executeQuery("SELECT str_value FROM data"); - assertFalse(rs.isClosed()); - assertTrue(rs.next()); - - String appendedValue = rs.getString(1); - assertEquals(appendedValue, expectedValue); - - rs.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_table_does_not_exist() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - assertThrows(() -> { - @SuppressWarnings("unused") - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - }, SQLException.class); - - stmt.close(); - conn.close(); - } - - public static void test_appender_table_deleted() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a INTEGER)"); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - appender.beginRow(); - appender.append(1); - appender.endRow(); - - stmt.execute("DROP TABLE data"); - - appender.beginRow(); - appender.append(2); - appender.endRow(); - - assertThrows(appender::close, SQLException.class); - - stmt.close(); - conn.close(); - } - - public static void test_appender_append_too_many_columns() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a INTEGER)"); - stmt.close(); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - assertThrows(() -> { - appender.beginRow(); - appender.append(1); - appender.append(2); - }, SQLException.class); - - conn.close(); - } - - public static void test_appender_append_too_few_columns() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a INTEGER, b INTEGER)"); - stmt.close(); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - assertThrows(() -> { - appender.beginRow(); - appender.append(1); - appender.endRow(); - }, SQLException.class); - - conn.close(); - } - - public static void test_appender_type_mismatch() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a INTEGER)"); - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - assertThrows(() -> { - appender.beginRow(); - appender.append("str"); - }, SQLException.class); - - stmt.close(); - conn.close(); - } - - public static void test_appender_null_integer() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a INTEGER)"); - - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - appender.beginRow(); - - // int foo = null won't compile - // Integer foo = null will compile, but NPE on cast to int - // So, use the String appender to pass an arbitrary null value - appender.append((String) null); - appender.endRow(); - appender.flush(); - appender.close(); - - ResultSet results = stmt.executeQuery("SELECT * FROM data"); - assertTrue(results.next()); - // java.sql.ResultSet.getInt(int) returns 0 if the value is NULL - assertEquals(0, results.getInt(1)); - assertTrue(results.wasNull()); - - results.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_null_varchar() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a VARCHAR)"); - - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - appender.beginRow(); - appender.append((String) null); - appender.endRow(); - appender.flush(); - appender.close(); - - ResultSet results = stmt.executeQuery("SELECT * FROM data"); - assertTrue(results.next()); - assertNull(results.getString(1)); - assertTrue(results.wasNull()); - - results.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_null_blob() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a BLOB)"); - - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - - appender.beginRow(); - appender.append((byte[]) null); - appender.endRow(); - appender.flush(); - appender.close(); - - ResultSet results = stmt.executeQuery("SELECT * FROM data"); - assertTrue(results.next()); - assertNull(results.getString(1)); - assertTrue(results.wasNull()); - - results.close(); - stmt.close(); - conn.close(); - } - - public static void test_appender_roundtrip_blob() throws Exception { - DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); - Statement stmt = conn.createStatement(); - - stmt.execute("CREATE TABLE data (a BLOB)"); - - DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); - SecureRandom random = SecureRandom.getInstanceStrong(); - byte[] data = new byte[512]; - random.nextBytes(data); - - appender.beginRow(); - appender.append(data); - appender.endRow(); - appender.flush(); - appender.close(); - - ResultSet results = stmt.executeQuery("SELECT * FROM data"); - assertTrue(results.next()); - - Blob resultBlob = results.getBlob(1); - byte[] resultBytes = resultBlob.getBytes(1, (int) resultBlob.length()); - assertTrue(Arrays.equals(resultBytes, data), "byte[] data is round tripped untouched"); - - results.close(); - stmt.close(); - conn.close(); - } - public static void test_get_catalog() throws Exception { Connection conn = DriverManager.getConnection(JDBC_URL); ResultSet rs = conn.getMetaData().getCatalogs(); @@ -2522,6 +2001,7 @@ public static void test_list() throws Exception { } } + @SuppressWarnings("try") public static void test_array_resultset() throws Exception { try (Connection connection = DriverManager.getConnection(JDBC_URL); Statement statement = connection.createStatement()) { @@ -2655,10 +2135,12 @@ public static void test_array_resultset() throws Exception { } } + @SuppressWarnings("unchecked") private static List arrayToList(Array array) throws SQLException { return arrayToList((T[]) array.getArray()); } + @SuppressWarnings("unchecked") private static List arrayToList(T[] array) throws SQLException { List out = new ArrayList<>(); for (Object t : array) { @@ -2667,6 +2149,7 @@ private static List arrayToList(T[] array) throws SQLException { return out; } + @SuppressWarnings("unchecked") private static T toJavaObject(Object t) { try { if (t instanceof Array) { @@ -2754,6 +2237,7 @@ static DuckDBResultSet.DuckDBBlobResult blobOf(String source) { .toFormatter() .withResolverStyle(ResolverStyle.LENIENT); + @SuppressWarnings("unchecked") static Map mapOf(Object... pairs) { Map result = new HashMap<>(pairs.length / 2); for (int i = 0; i < pairs.length - 1; i += 2) { @@ -2902,6 +2386,7 @@ private static OffsetDateTime localDateTimeToOffset(LocalDateTime ldt) { TimeZone.setDefault(defaultTimeZone); } + @SuppressWarnings("unchecked") public static void test_all_types() throws Exception { TimeZone defaultTimeZone = TimeZone.getDefault(); TimeZone.setDefault(ALL_TYPES_TIME_ZONE); @@ -3431,6 +2916,7 @@ public static void test_get_profiling_information() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL); Statement stmt = conn.createStatement()) { stmt.execute("SET enable_profiling = 'no_output';"); try (ResultSet rs = stmt.executeQuery("SELECT 1+1")) { + assertNotNull(rs); String profile = ((DuckDBConnection) conn).getProfilingInformation(ProfilerPrintFormat.JSON); assertTrue(profile.contains("\"query_name\": \"SELECT 1+1\",")); } @@ -3670,9 +3156,10 @@ public static void main(String[] args) throws Exception { Class clazz = Class.forName("org.duckdb." + arg1); statusCode = runTests(new String[0], clazz); } else { - statusCode = runTests(args, TestDuckDBJDBC.class, TestBatch.class, TestClosure.class, - TestExtensionTypes.class, TestSpatial.class, TestParameterMetadata.class, - TestPrepare.class, TestResults.class, TestSessionInit.class, TestTimestamp.class); + statusCode = runTests(args, TestDuckDBJDBC.class, TestAppender.class, TestSingleValueAppender.class, + TestBatch.class, TestBindings.class, TestClosure.class, TestExtensionTypes.class, + TestSpatial.class, TestParameterMetadata.class, TestPrepare.class, TestResults.class, + TestSessionInit.class, TestTimestamp.class); } System.exit(statusCode); } diff --git a/src/test/java/org/duckdb/TestNumeric.java b/src/test/java/org/duckdb/TestNumeric.java index add1d13e2..6cd95a4c3 100644 --- a/src/test/java/org/duckdb/TestNumeric.java +++ b/src/test/java/org/duckdb/TestNumeric.java @@ -97,7 +97,7 @@ public static void test_bigdecimal() throws Exception { assertEquals(rs2.getBigDecimal(5), new BigDecimal("-18446744073709551616.0000000000")); // Metadata tests - assertEquals(Types.DECIMAL, meta.type_to_int(DuckDBColumnType.DECIMAL)); + assertEquals(Types.DECIMAL, DuckDBResultSetMetaData.type_to_int(DuckDBColumnType.DECIMAL)); assertTrue(BigDecimal.class.getName().equals(meta.getColumnClassName(1))); assertTrue(BigDecimal.class.getName().equals(meta.getColumnClassName(2))); assertTrue(BigDecimal.class.getName().equals(meta.getColumnClassName(3))); @@ -125,7 +125,6 @@ public static void test_lots_of_decimals() throws Exception { // Create the table stmt.execute( "CREATE TABLE q (id DECIMAL(4,0),dec32 DECIMAL(9,4),dec64 DECIMAL(18,7),dec128 DECIMAL(38,10))"); - stmt.close(); } // Create the INSERT prepared statement we will use @@ -228,16 +227,16 @@ public static void test_unsigned_integers() throws Exception { "SELECT 201::utinyint uint8, 40001::usmallint uint16, 4000000001::uinteger uint32, 18446744073709551615::ubigint uint64")) { assertTrue(rs.next()); - assertEquals(rs.getShort("uint8"), Short.valueOf((short) 201)); - assertEquals(rs.getObject("uint8"), Short.valueOf((short) 201)); - assertEquals(rs.getInt("uint8"), Integer.valueOf((int) 201)); + assertEquals(rs.getShort("uint8"), (short) 201); + assertEquals(rs.getObject("uint8"), (short) 201); + assertEquals(rs.getInt("uint8"), 201); - assertEquals(rs.getInt("uint16"), Integer.valueOf((int) 40001)); - assertEquals(rs.getObject("uint16"), Integer.valueOf((int) 40001)); - assertEquals(rs.getLong("uint16"), Long.valueOf((long) 40001)); + assertEquals(rs.getInt("uint16"), 40001); + assertEquals(rs.getObject("uint16"), 40001); + assertEquals(rs.getLong("uint16"), 40001L); - assertEquals(rs.getLong("uint32"), Long.valueOf((long) 4000000001L)); - assertEquals(rs.getObject("uint32"), Long.valueOf((long) 4000000001L)); + assertEquals(rs.getLong("uint32"), 4000000001L); + assertEquals(rs.getObject("uint32"), 4000000001L); assertEquals(rs.getObject("uint64"), new BigInteger("18446744073709551615")); } @@ -282,8 +281,8 @@ private static void check_get_object_with_class(ResultSet rs, long value, Cl public static void test_duckdb_get_object_tinyint() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL)) { try (PreparedStatement ps = conn.prepareStatement("SELECT ?::TINYINT")) { - Class[] classes = new Class[] {Byte.class, Short.class, Integer.class, - Long.class, BigInteger.class, BigDecimal.class}; + Class[] classes = new Class[] {Byte.class, Short.class, Integer.class, + Long.class, BigInteger.class, BigDecimal.class}; ps.setByte(1, Byte.MIN_VALUE); try (ResultSet rs = ps.executeQuery()) { rs.next(); @@ -302,7 +301,7 @@ public static void test_duckdb_get_object_tinyint() throws Exception { try (PreparedStatement ps = conn.prepareStatement("SELECT ?::UTINYINT")) { Class[] classes = - new Class[] {Short.class, Integer.class, Long.class, BigInteger.class, BigDecimal.class}; + new Class[] {Short.class, Integer.class, Long.class, BigInteger.class, BigDecimal.class}; short MAX_UTINYINT = (1 << 8) - 1; ps.setShort(1, MAX_UTINYINT); try (ResultSet rs = ps.executeQuery()) { @@ -320,7 +319,7 @@ public static void test_duckdb_get_object_smallint() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL)) { try (PreparedStatement ps = conn.prepareStatement("SELECT ?::SMALLINT")) { Class[] classes = - new Class[] {Short.class, Integer.class, Long.class, BigInteger.class, BigDecimal.class}; + new Class[] {Short.class, Integer.class, Long.class, BigInteger.class, BigDecimal.class}; ps.setShort(1, Short.MIN_VALUE); try (ResultSet rs = ps.executeQuery()) { rs.next(); @@ -337,7 +336,7 @@ public static void test_duckdb_get_object_smallint() throws Exception { } } try (PreparedStatement ps = conn.prepareStatement("SELECT ?::USMALLINT")) { - Class[] classes = new Class[] {Integer.class, Long.class, BigInteger.class, BigDecimal.class}; + Class[] classes = new Class[] {Integer.class, Long.class, BigInteger.class, BigDecimal.class}; int MAX_USMALLINT = (1 << 16) - 1; ps.setInt(1, MAX_USMALLINT); try (ResultSet rs = ps.executeQuery()) { @@ -355,7 +354,7 @@ public static void test_duckdb_get_object_smallint() throws Exception { public static void test_duckdb_get_object_integer() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL)) { try (PreparedStatement ps = conn.prepareStatement("SELECT ?::INTEGER")) { - Class[] classes = new Class[] {Integer.class, Long.class, BigInteger.class, BigDecimal.class}; + Class[] classes = new Class[] {Integer.class, Long.class, BigInteger.class, BigDecimal.class}; ps.setInt(1, Integer.MIN_VALUE); try (ResultSet rs = ps.executeQuery()) { rs.next(); @@ -372,7 +371,7 @@ public static void test_duckdb_get_object_integer() throws Exception { } } try (PreparedStatement ps = conn.prepareStatement("SELECT ?::UINTEGER")) { - Class[] classes = new Class[] {Long.class, BigInteger.class, BigDecimal.class}; + Class[] classes = new Class[] {Long.class, BigInteger.class, BigDecimal.class}; long MAX_UINTEGER = (1L << 32) - 1; ps.setLong(1, MAX_UINTEGER); try (ResultSet rs = ps.executeQuery()) { @@ -391,7 +390,7 @@ public static void test_duckdb_get_object_integer() throws Exception { public static void test_duckdb_get_object_bigint() throws Exception { try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class)) { try (PreparedStatement ps = conn.prepareStatement("SELECT ?::BIGINT")) { - Class[] classes = new Class[] {Long.class, BigInteger.class, BigDecimal.class}; + Class[] classes = new Class[] {Long.class, BigInteger.class, BigDecimal.class}; ps.setLong(1, Long.MIN_VALUE); try (ResultSet rs = ps.executeQuery()) { rs.next(); diff --git a/src/test/java/org/duckdb/TestPrepare.java b/src/test/java/org/duckdb/TestPrepare.java index d2e31eb64..0fc5984d8 100644 --- a/src/test/java/org/duckdb/TestPrepare.java +++ b/src/test/java/org/duckdb/TestPrepare.java @@ -23,9 +23,9 @@ public static void test_prepare_types() throws Exception { ps.setByte(2, (byte) 42); ps.setShort(3, (short) 43); ps.setInt(4, 44); - ps.setLong(5, (long) 45); + ps.setLong(5, 45); ps.setFloat(6, (float) 4.6); - ps.setDouble(7, (double) 4.7); + ps.setDouble(7, 4.7); ps.setString(8, "four eight"); try (ResultSet rs = ps.executeQuery()) { @@ -46,7 +46,7 @@ public static void test_prepare_types() throws Exception { ps.setInt(4, 84); ps.setLong(5, (long) 85); ps.setFloat(6, (float) 8.6); - ps.setDouble(7, (double) 8.7); + ps.setDouble(7, 8.7); ps.setString(8, "eight eight\n\t"); try (ResultSet rs = ps.executeQuery()) { @@ -67,7 +67,7 @@ public static void test_prepare_types() throws Exception { ps.setObject(4, 84); ps.setObject(5, (long) 85); ps.setObject(6, (float) 8.6); - ps.setObject(7, (double) 8.7); + ps.setObject(7, 8.7); ps.setObject(8, "𫝼πŸ”₯πŸ˜œδ­”πŸŸ’"); try (ResultSet rs = ps.executeQuery()) { diff --git a/src/test/java/org/duckdb/TestResults.java b/src/test/java/org/duckdb/TestResults.java index 61b4c9923..f1e87c615 100644 --- a/src/test/java/org/duckdb/TestResults.java +++ b/src/test/java/org/duckdb/TestResults.java @@ -175,13 +175,13 @@ public static void test_lots_of_big_data() throws Exception { for (int col = 1; col <= 6; col++) { assertEquals(rs.getShort(col), (short) count); assertFalse(rs.wasNull()); - assertEquals(rs.getInt(col), (int) count); + assertEquals(rs.getInt(col), count); assertFalse(rs.wasNull()); assertEquals(rs.getLong(col), (long) count); assertFalse(rs.wasNull()); assertEquals(rs.getFloat(col), (float) count, 0.001); assertFalse(rs.wasNull()); - assertEquals(rs.getDouble(col), (double) count, 0.001); + assertEquals(rs.getDouble(col), count, 0.001); assertFalse(rs.wasNull()); assertEquals(Double.parseDouble(rs.getString(col)), (double) count, 0.001); assertFalse(rs.wasNull()); @@ -250,7 +250,8 @@ public static void test_stream_multiple_open_results() throws Exception { try (Connection conn = DriverManager.getConnection(JDBC_URL, props); Statement stmt1 = conn.createStatement(); Statement stmt2 = conn.createStatement()) { - try (ResultSet rs1 = stmt1.executeQuery(QUERY); ResultSet ignored = stmt2.executeQuery(QUERY)) { + try (ResultSet rs1 = stmt1.executeQuery(QUERY); ResultSet rs2 = stmt2.executeQuery(QUERY)) { + assertNotNull(rs2); assertThrows(rs1::next, SQLException.class); } } diff --git a/src/test/java/org/duckdb/TestSessionInit.java b/src/test/java/org/duckdb/TestSessionInit.java index 961435883..c07177fac 100644 --- a/src/test/java/org/duckdb/TestSessionInit.java +++ b/src/test/java/org/duckdb/TestSessionInit.java @@ -37,6 +37,7 @@ public static void test_session_init_db_and_connection() throws Exception { try (Connection conn1 = DriverManager.getConnection(url); Connection conn2 = DriverManager.getConnection(url); Statement stmt = conn2.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM tab1")) { + assertNotNull(conn1); rs.next(); assertEquals(rs.getInt(1), 42); rs.next(); diff --git a/src/test/java/org/duckdb/TestSingleValueAppender.java b/src/test/java/org/duckdb/TestSingleValueAppender.java new file mode 100644 index 000000000..e9e2549d6 --- /dev/null +++ b/src/test/java/org/duckdb/TestSingleValueAppender.java @@ -0,0 +1,557 @@ +package org.duckdb; + +import static org.duckdb.TestDuckDBJDBC.JDBC_URL; +import static org.duckdb.test.Assertions.*; +import static org.duckdb.test.Assertions.assertTrue; + +import java.math.BigDecimal; +import java.security.SecureRandom; +import java.sql.*; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; + +@SuppressWarnings("deprecation") +public class TestSingleValueAppender { + + public static void test_sv_appender_numbers() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + // int8, int4, int2, int1, float8, float4 + stmt.execute("CREATE TABLE numbers (a BIGINT, b INTEGER, c SMALLINT, d TINYINT, e DOUBLE, f FLOAT)"); + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "numbers")) { + for (int i = 0; i < 50; i++) { + appender.beginRow(); + appender.append(Long.MAX_VALUE - i); + appender.append(Integer.MAX_VALUE - i); + appender.append(Short.MAX_VALUE - i); + appender.append(Byte.MAX_VALUE - i); + appender.append(i); + appender.append(i); + appender.endRow(); + } + appender.flush(); + } + + try (ResultSet rs = + stmt.executeQuery("SELECT max(a), max(b), max(c), max(d), max(e), max(f) FROM numbers")) { + assertFalse(rs.isClosed()); + assertTrue(rs.next()); + + long resA = rs.getLong(1); + assertEquals(resA, Long.MAX_VALUE); + + int resB = rs.getInt(2); + assertEquals(resB, Integer.MAX_VALUE); + + short resC = rs.getShort(3); + assertEquals(resC, Short.MAX_VALUE); + + byte resD = rs.getByte(4); + assertEquals(resD, Byte.MAX_VALUE); + + double resE = rs.getDouble(5); + assertEquals(resE, 49.0d); + + float resF = rs.getFloat(6); + assertEquals(resF, 49.0f); + } + } + } + + public static void test_sv_appender_date_and_time() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + LocalDateTime ldt1 = LocalDateTime.now().truncatedTo(ChronoUnit.MICROS); + LocalDateTime ldt2 = LocalDateTime.of(-23434, 3, 5, 23, 2); + LocalDateTime ldt3 = LocalDateTime.of(1970, 1, 1, 0, 0); + LocalDateTime ldt4 = LocalDateTime.of(11111, 12, 31, 23, 59, 59, 999999000); + + stmt.execute("CREATE TABLE date_and_time (id INT4, a TIMESTAMP)"); + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "date_and_time")) { + appender.beginRow(); + appender.append(1); + appender.appendLocalDateTime(ldt1); + appender.endRow(); + appender.beginRow(); + appender.append(2); + appender.appendLocalDateTime(ldt2); + appender.endRow(); + appender.beginRow(); + appender.append(3); + appender.appendLocalDateTime(ldt3); + appender.endRow(); + appender.beginRow(); + appender.append(4); + appender.appendLocalDateTime(ldt4); + appender.endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT a FROM date_and_time ORDER BY id")) { + assertFalse(rs.isClosed()); + assertTrue(rs.next()); + + LocalDateTime res1 = rs.getObject(1, LocalDateTime.class); + assertEquals(res1, ldt1); + assertTrue(rs.next()); + + LocalDateTime res2 = rs.getObject(1, LocalDateTime.class); + assertEquals(res2, ldt2); + assertTrue(rs.next()); + + LocalDateTime res3 = rs.getObject(1, LocalDateTime.class); + assertEquals(res3, ldt3); + assertTrue(rs.next()); + + LocalDateTime res4 = rs.getObject(1, LocalDateTime.class); + assertEquals(res4, ldt4); + } + } + } + + public static void test_sv_appender_decimal() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + BigDecimal bigdec16 = new BigDecimal("12.34").setScale(2); + BigDecimal bigdec32 = new BigDecimal("1234.5678").setScale(4); + BigDecimal bigdec64 = new BigDecimal("123456789012.345678").setScale(6); + BigDecimal bigdec128 = new BigDecimal("123456789012345678.90123456789012345678").setScale(20); + BigDecimal negbigdec16 = new BigDecimal("-12.34").setScale(2); + BigDecimal negbigdec32 = new BigDecimal("-1234.5678").setScale(4); + BigDecimal negbigdec64 = new BigDecimal("-123456789012.345678").setScale(6); + BigDecimal negbigdec128 = new BigDecimal("-123456789012345678.90123456789012345678").setScale(20); + BigDecimal smallbigdec16 = new BigDecimal("-1.34").setScale(2); + BigDecimal smallbigdec32 = new BigDecimal("-123.5678").setScale(4); + BigDecimal smallbigdec64 = new BigDecimal("-12345678901.345678").setScale(6); + BigDecimal smallbigdec128 = new BigDecimal("-12345678901234567.90123456789012345678").setScale(20); + BigDecimal intbigdec16 = new BigDecimal("-1").setScale(2); + BigDecimal intbigdec32 = new BigDecimal("-123").setScale(4); + BigDecimal intbigdec64 = new BigDecimal("-12345678901").setScale(6); + BigDecimal intbigdec128 = new BigDecimal("-12345678901234567").setScale(20); + BigDecimal onebigdec16 = new BigDecimal("1").setScale(2); + BigDecimal onebigdec32 = new BigDecimal("1").setScale(4); + BigDecimal onebigdec64 = new BigDecimal("1").setScale(6); + BigDecimal onebigdec128 = new BigDecimal("1").setScale(20); + + stmt.execute( + "CREATE TABLE decimals (id INT4, a DECIMAL(4,2), b DECIMAL(8,4), c DECIMAL(18,6), d DECIMAL(38,20))"); + + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals")) { + appender.beginRow(); + appender.append(1); + appender.appendBigDecimal(bigdec16); + appender.appendBigDecimal(bigdec32); + appender.appendBigDecimal(bigdec64); + appender.appendBigDecimal(bigdec128); + appender.endRow(); + appender.beginRow(); + appender.append(2); + appender.appendBigDecimal(negbigdec16); + appender.appendBigDecimal(negbigdec32); + appender.appendBigDecimal(negbigdec64); + appender.appendBigDecimal(negbigdec128); + appender.endRow(); + appender.beginRow(); + appender.append(3); + appender.appendBigDecimal(smallbigdec16); + appender.appendBigDecimal(smallbigdec32); + appender.appendBigDecimal(smallbigdec64); + appender.appendBigDecimal(smallbigdec128); + appender.endRow(); + appender.beginRow(); + appender.append(4); + appender.appendBigDecimal(intbigdec16); + appender.appendBigDecimal(intbigdec32); + appender.appendBigDecimal(intbigdec64); + appender.appendBigDecimal(intbigdec128); + appender.endRow(); + appender.beginRow(); + appender.append(5); + appender.appendBigDecimal(onebigdec16); + appender.appendBigDecimal(onebigdec32); + appender.appendBigDecimal(onebigdec64); + appender.appendBigDecimal(onebigdec128); + appender.endRow(); + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT a,b,c,d FROM decimals ORDER BY id")) { + assertFalse(rs.isClosed()); + assertTrue(rs.next()); + + BigDecimal rs1 = rs.getObject(1, BigDecimal.class); + BigDecimal rs2 = rs.getObject(2, BigDecimal.class); + BigDecimal rs3 = rs.getObject(3, BigDecimal.class); + BigDecimal rs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(rs1, bigdec16); + assertEquals(rs2, bigdec32); + assertEquals(rs3, bigdec64); + assertEquals(rs4, bigdec128); + assertTrue(rs.next()); + + BigDecimal nrs1 = rs.getObject(1, BigDecimal.class); + BigDecimal nrs2 = rs.getObject(2, BigDecimal.class); + BigDecimal nrs3 = rs.getObject(3, BigDecimal.class); + BigDecimal nrs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(nrs1, negbigdec16); + assertEquals(nrs2, negbigdec32); + assertEquals(nrs3, negbigdec64); + assertEquals(nrs4, negbigdec128); + assertTrue(rs.next()); + + BigDecimal srs1 = rs.getObject(1, BigDecimal.class); + BigDecimal srs2 = rs.getObject(2, BigDecimal.class); + BigDecimal srs3 = rs.getObject(3, BigDecimal.class); + BigDecimal srs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(srs1, smallbigdec16); + assertEquals(srs2, smallbigdec32); + assertEquals(srs3, smallbigdec64); + assertEquals(srs4, smallbigdec128); + assertTrue(rs.next()); + + BigDecimal irs1 = rs.getObject(1, BigDecimal.class); + BigDecimal irs2 = rs.getObject(2, BigDecimal.class); + BigDecimal irs3 = rs.getObject(3, BigDecimal.class); + BigDecimal irs4 = rs.getObject(4, BigDecimal.class); + + assertEquals(irs1, intbigdec16); + assertEquals(irs2, intbigdec32); + assertEquals(irs3, intbigdec64); + assertEquals(irs4, intbigdec128); + assertTrue(rs.next()); + + BigDecimal oners1 = rs.getObject(1, BigDecimal.class); + BigDecimal oners2 = rs.getObject(2, BigDecimal.class); + BigDecimal oners3 = rs.getObject(3, BigDecimal.class); + BigDecimal oners4 = rs.getObject(4, BigDecimal.class); + + assertEquals(oners1, onebigdec16); + assertEquals(oners2, onebigdec32); + assertEquals(oners3, onebigdec64); + assertEquals(oners4, onebigdec128); + } + } + } + + public static void test_sv_appender_decimal_wrong_scale() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection("jdbc:duckdb:").unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute( + "CREATE TABLE decimals (id INT4, a DECIMAL(4,2), b DECIMAL(8,4), c DECIMAL(18,6), d DECIMAL(38,20))"); + + assertThrows(() -> { + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals")) { + appender.append(1); + appender.beginRow(); + appender.appendBigDecimal(new BigDecimal("121.14").setScale(2)); + } + }, SQLException.class); + + assertThrows(() -> { + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals")) { + appender.beginRow(); + appender.append(2); + appender.appendBigDecimal(new BigDecimal("21.1").setScale(2)); + appender.appendBigDecimal(new BigDecimal("12111.1411").setScale(4)); + } + }, SQLException.class); + + assertThrows(() -> { + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "decimals")) { + appender.beginRow(); + appender.append(3); + appender.appendBigDecimal(new BigDecimal("21.1").setScale(2)); + appender.appendBigDecimal(new BigDecimal("21.1").setScale(4)); + appender.appendBigDecimal(new BigDecimal("1234567890123.123456").setScale(6)); + } + }, SQLException.class); + } + } + + public static void test_sv_appender_int_string() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER, s VARCHAR)"); + + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + for (int i = 0; i < 1000; i++) { + appender.beginRow(); + appender.append(i); + appender.append("str " + i); + appender.endRow(); + } + appender.flush(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT max(a), min(s) FROM data")) { + assertFalse(rs.isClosed()); + + assertTrue(rs.next()); + int resA = rs.getInt(1); + assertEquals(resA, 999); + String resB = rs.getString(2); + assertEquals(resB, "str 0"); + } + } + } + + public static void test_sv_appender_string_with_emoji() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + stmt.execute("CREATE TABLE data (id INTEGER, str_value VARCHAR(10))"); + String expectedValue = "δ­”\uD86D\uDF7CπŸ”₯\uD83D\uDE1C"; + + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + appender.beginRow(); + appender.append(1); + appender.append(expectedValue); + appender.endRow(); + } + + try (ResultSet rs = stmt.executeQuery("SELECT str_value FROM data ORDER BY id")) { + assertFalse(rs.isClosed()); + + assertTrue(rs.next()); + String row1 = rs.getString(1); + assertEquals(row1, expectedValue); + + assertFalse(rs.next()); + } + } + } + + public static void test_sv_appender_table_does_not_exist() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class)) { + assertThrows( + () -> { conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); }, SQLException.class); + } + } + + public static void test_sv_appender_table_deleted() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); + appender.beginRow(); + appender.append(1); + appender.endRow(); + + stmt.execute("DROP TABLE data"); + + appender.beginRow(); + appender.append(2); + appender.endRow(); + + assertThrows(appender::flush, SQLException.class); + } + } + + public static void test_sv_appender_append_too_many_columns() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + + assertThrows(() -> { + DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); + appender.beginRow(); + appender.append(1); + appender.append(2); + appender.flush(); + }, SQLException.class); + } + } + + public static void test_sv_appender_append_too_few_columns() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER, b INTEGER)"); + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + assertThrows(() -> { + appender.beginRow(); + appender.append(1); + appender.endRow(); + }, SQLException.class); + } + } + } + + public static void test_sv_appender_type_mismatch() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + assertThrows(() -> { + appender.beginRow(); + appender.append("str"); + }, SQLException.class); + } + } + } + + public static void test_sv_appender_null_integer() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a INTEGER)"); + + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + appender.beginRow(); + appender.append((String) null); + appender.endRow(); + appender.flush(); + } + + try (ResultSet results = stmt.executeQuery("SELECT * FROM data")) { + assertTrue(results.next()); + // java.sql.ResultSet.getInt(int) returns 0 if the value is NULL + assertEquals(0, results.getInt(1)); + assertTrue(results.wasNull()); + } + } + } + + public static void test_sv_appender_number_wrappers() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + // int8, int4, int2, int1, float8, float4 + stmt.execute("CREATE TABLE numbers (a BIGINT, b INTEGER, c SMALLINT, d TINYINT, e DOUBLE, f FLOAT)"); + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "numbers")) { + for (int i = 0; i < 50; i++) { + appender.beginRow(); + appender.append(Long.valueOf(Long.MAX_VALUE - i)); + appender.append(Integer.valueOf(Integer.MAX_VALUE - i)); + appender.append(Short.valueOf((short) (Short.MAX_VALUE - i))); + appender.append(Byte.valueOf((byte) (Byte.MAX_VALUE - i))); + appender.append(Double.valueOf(i)); + appender.append(Float.valueOf(i)); + appender.endRow(); + } + appender.flush(); + } + + try (ResultSet rs = + stmt.executeQuery("SELECT max(a), max(b), max(c), max(d), max(e), max(f) FROM numbers")) { + assertFalse(rs.isClosed()); + assertTrue(rs.next()); + + long resA = rs.getLong(1); + assertEquals(resA, Long.MAX_VALUE); + + int resB = rs.getInt(2); + assertEquals(resB, Integer.MAX_VALUE); + + short resC = rs.getShort(3); + assertEquals(resC, Short.MAX_VALUE); + + byte resD = rs.getByte(4); + assertEquals(resD, Byte.MAX_VALUE); + + double resE = rs.getDouble(5); + assertEquals(resE, 49.0d); + + float resF = rs.getFloat(6); + assertEquals(resF, 49.0f); + } + } + } + + public static void test_sv_appender_append_null_varchar() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a VARCHAR)"); + + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + appender.beginRow(); + appender.append((String) null); + appender.endRow(); + appender.flush(); + } + + try (ResultSet results = stmt.executeQuery("SELECT * FROM data")) { + assertTrue(results.next()); + assertNull(results.getString(1)); + assertTrue(results.wasNull()); + } + } + } + + public static void test_sv_appender_append_null_blob() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + stmt.execute("CREATE TABLE data (a BLOB)"); + + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + appender.beginRow(); + appender.append((String) null); + appender.endRow(); + appender.flush(); + } + + try (ResultSet results = stmt.executeQuery("SELECT * FROM data")) { + assertTrue(results.next()); + assertNull(results.getString(1)); + assertTrue(results.wasNull()); + } + } + } + + public static void test_sv_appender_roundtrip_blob() throws Exception { + try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class); + Statement stmt = conn.createStatement()) { + + SecureRandom random = SecureRandom.getInstanceStrong(); + byte[] data = new byte[512]; + random.nextBytes(data); + + stmt.execute("CREATE TABLE data (a BLOB)"); + + try (DuckDBSingleValueAppender appender = + conn.createSingleValueAppender(DuckDBConnection.DEFAULT_SCHEMA, "data")) { + appender.beginRow(); + appender.append(data); + appender.endRow(); + appender.flush(); + } + + try (ResultSet results = stmt.executeQuery("SELECT * FROM data")) { + assertTrue(results.next()); + + Blob resultBlob = results.getBlob(1); + byte[] resultBytes = resultBlob.getBytes(1, (int) resultBlob.length()); + assertTrue(Arrays.equals(resultBytes, data), "byte[] data is round tripped untouched"); + } + } + } +} diff --git a/src/test/java/org/duckdb/TestTimestamp.java b/src/test/java/org/duckdb/TestTimestamp.java index cbcc7fcff..80f14c3be 100644 --- a/src/test/java/org/duckdb/TestTimestamp.java +++ b/src/test/java/org/duckdb/TestTimestamp.java @@ -99,8 +99,7 @@ public static void test_timestamp_tz() throws Exception { // Metadata tests assertEquals(Types.TIMESTAMP_WITH_TIMEZONE, - (meta.unwrap(DuckDBResultSetMetaData.class) - .type_to_int(DuckDBColumnType.TIMESTAMP_WITH_TIME_ZONE))); + DuckDBResultSetMetaData.type_to_int(DuckDBColumnType.TIMESTAMP_WITH_TIME_ZONE)); assertTrue(OffsetDateTime.class.getName().equals(meta.getColumnClassName(2))); } } diff --git a/src/test/java/org/duckdb/test/Assertions.java b/src/test/java/org/duckdb/test/Assertions.java index 9a036e709..7363e3293 100644 --- a/src/test/java/org/duckdb/test/Assertions.java +++ b/src/test/java/org/duckdb/test/Assertions.java @@ -5,7 +5,6 @@ import java.util.ListIterator; import java.util.Objects; import java.util.function.Function; -import org.duckdb.test.Thrower; public class Assertions { public static void assertTrue(boolean val) throws Exception {