diff --git a/src/jni/duckdb_java.cpp b/src/jni/duckdb_java.cpp index 4cc3ea82..ef051abe 100644 --- a/src/jni/duckdb_java.cpp +++ b/src/jni/duckdb_java.cpp @@ -1174,6 +1174,16 @@ void _duckdb_jdbc_appender_append_string(JNIEnv *env, jclass, jobject appender_r get_appender(env, appender_ref_buf)->Append(string_value.c_str()); } +void _duckdb_jdbc_appender_append_bytes(JNIEnv *env, jclass, jobject appender_ref_buf, jbyteArray value) { + if (env->IsSameObject(value, NULL)) { + get_appender(env, appender_ref_buf)->Append(nullptr); + return; + } + + auto string_value = byte_array_to_string(env, value); + get_appender(env, appender_ref_buf)->Append(Value::BLOB_RAW(string_value)); +} + void _duckdb_jdbc_appender_append_null(JNIEnv *env, jclass, jobject appender_ref_buf) { get_appender(env, appender_ref_buf)->Append(nullptr); } diff --git a/src/jni/functions.cpp b/src/jni/functions.cpp index 6aacd949..e4f0826b 100644 --- a/src/jni/functions.cpp +++ b/src/jni/functions.cpp @@ -347,6 +347,16 @@ JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1appender_1appe } } +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1appender_1append_1bytes(JNIEnv * env, jclass param0, jobject param1, jbyteArray param2) { + try { + return _duckdb_jdbc_appender_append_bytes(env, param0, param1, param2); + } catch (const std::exception &e) { + duckdb::ErrorData error(e); + ThrowJNI(env, error.Message().c_str()); + + } +} + JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1appender_1append_1timestamp(JNIEnv * env, jclass param0, jobject param1, jlong param2) { try { return _duckdb_jdbc_appender_append_timestamp(env, param0, param1, param2); diff --git a/src/jni/functions.hpp b/src/jni/functions.hpp index 4160a1ac..1267b3f7 100644 --- a/src/jni/functions.hpp +++ b/src/jni/functions.hpp @@ -141,6 +141,10 @@ void _duckdb_jdbc_appender_append_string(JNIEnv * env, jclass param0, jobject pa JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1appender_1append_1string(JNIEnv * env, jclass param0, jobject param1, jbyteArray param2); +void _duckdb_jdbc_appender_append_bytes(JNIEnv * env, jclass param0, jobject param1, jbyteArray param2); + +JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1appender_1append_1bytes(JNIEnv * env, jclass param0, jobject param1, jbyteArray param2); + void _duckdb_jdbc_appender_append_timestamp(JNIEnv * env, jclass param0, jobject param1, jlong param2); JNIEXPORT void JNICALL Java_org_duckdb_DuckDBNative_duckdb_1jdbc_1appender_1append_1timestamp(JNIEnv * env, jclass param0, jobject param1, jlong param2); diff --git a/src/main/java/org/duckdb/DuckDBAppender.java b/src/main/java/org/duckdb/DuckDBAppender.java index 44fd44c4..08eb5ef3 100644 --- a/src/main/java/org/duckdb/DuckDBAppender.java +++ b/src/main/java/org/duckdb/DuckDBAppender.java @@ -85,6 +85,14 @@ public void append(String value) throws SQLException { } } + 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); + } + } + protected void finalize() throws Throwable { close(); } diff --git a/src/main/java/org/duckdb/DuckDBNative.java b/src/main/java/org/duckdb/DuckDBNative.java index 7e8adf44..d03eff58 100644 --- a/src/main/java/org/duckdb/DuckDBNative.java +++ b/src/main/java/org/duckdb/DuckDBNative.java @@ -154,6 +154,9 @@ protected static native void duckdb_jdbc_appender_append_double(ByteBuffer appen protected static native void duckdb_jdbc_appender_append_string(ByteBuffer appender_ref, byte[] value) throws SQLException; + protected static native void duckdb_jdbc_appender_append_bytes(ByteBuffer appender_ref, byte[] value) + throws SQLException; + protected static native void duckdb_jdbc_appender_append_timestamp(ByteBuffer appender_ref, long value) throws SQLException; diff --git a/src/test/java/org/duckdb/TestDuckDBJDBC.java b/src/test/java/org/duckdb/TestDuckDBJDBC.java index bc36741b..1908d89e 100644 --- a/src/test/java/org/duckdb/TestDuckDBJDBC.java +++ b/src/test/java/org/duckdb/TestDuckDBJDBC.java @@ -26,6 +26,7 @@ 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.Instant; import java.time.LocalDate; @@ -2692,7 +2693,11 @@ public static void test_appender_null_integer() throws Exception { DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); appender.beginRow(); - appender.append(null); + + // 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(); @@ -2717,7 +2722,31 @@ public static void test_appender_null_varchar() throws Exception { DuckDBAppender appender = conn.createAppender(DuckDBConnection.DEFAULT_SCHEMA, "data"); appender.beginRow(); - appender.append(null); + 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(); @@ -2732,6 +2761,35 @@ public static void test_appender_null_varchar() throws Exception { 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();