diff --git a/duckdb_java.def b/duckdb_java.def index 952581f22..611b35978 100644 --- a/duckdb_java.def +++ b/duckdb_java.def @@ -58,6 +58,7 @@ 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_1struct_1type_1child_1name Java_org_duckdb_DuckDBBindings_duckdb_1array_1type_1array_1size Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1logical_1type Java_org_duckdb_DuckDBBindings_duckdb_1create_1vector diff --git a/duckdb_java.exp b/duckdb_java.exp index ea29c7dd3..85e06db35 100644 --- a/duckdb_java.exp +++ b/duckdb_java.exp @@ -55,6 +55,7 @@ _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_1struct_1type_1child_1name _Java_org_duckdb_DuckDBBindings_duckdb_1array_1type_1array_1size _Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1logical_1type _Java_org_duckdb_DuckDBBindings_duckdb_1create_1vector diff --git a/duckdb_java.map b/duckdb_java.map index 4a68adba9..24efd089a 100644 --- a/duckdb_java.map +++ b/duckdb_java.map @@ -57,6 +57,7 @@ DUCKDB_JAVA { 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_1struct_1type_1child_1name; Java_org_duckdb_DuckDBBindings_duckdb_1array_1type_1array_1size; Java_org_duckdb_DuckDBBindings_duckdb_1destroy_1logical_1type; Java_org_duckdb_DuckDBBindings_duckdb_1create_1vector; diff --git a/src/jni/bindings_logical_type.cpp b/src/jni/bindings_logical_type.cpp index bc45b2937..fc45640c7 100644 --- a/src/jni/bindings_logical_type.cpp +++ b/src/jni/bindings_logical_type.cpp @@ -2,6 +2,7 @@ #include "refs.hpp" #include "util.hpp" +#include #include duckdb_logical_type logical_type_buf_to_logical_type(JNIEnv *env, jobject logical_type_buf) { @@ -209,8 +210,9 @@ JNIEXPORT jobject JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1create_1struct_ if (env->ExceptionCheck()) { return nullptr; } - names_cstr_vec.emplace_back(str.c_str()); names_vec.emplace_back(std::move(str)); + std::string &str_ref = names_vec.back(); + names_cstr_vec.emplace_back(str_ref.c_str()); } duckdb_logical_type struct_type = @@ -237,6 +239,40 @@ JNIEXPORT jlong JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1struct_1type_1chi return static_cast(count); } +/* + * Class: org_duckdb_DuckDBBindings + * Method: duckdb_struct_type_child_name + * Signature: (Ljava/nio/ByteBuffer;J)[B + */ +JNIEXPORT jbyteArray JNICALL Java_org_duckdb_DuckDBBindings_duckdb_1struct_1type_1child_1name(JNIEnv *env, jclass, + jobject logical_type, + jlong index) { + + duckdb_logical_type lt = logical_type_buf_to_logical_type(env, logical_type); + if (env->ExceptionCheck()) { + return nullptr; + } + idx_t index_idx = jlong_to_idx(env, index); + if (env->ExceptionCheck()) { + return nullptr; + } + + idx_t count = duckdb_struct_type_child_count(lt); + if (index_idx >= count) { + env->ThrowNew(J_SQLException, "Invalid struct field index specified"); + return nullptr; + } + + auto name_ptr = varchar_ptr(duckdb_struct_type_child_name(lt, index_idx), varchar_deleter); + if (name_ptr.get() == nullptr) { + return nullptr; + } + + idx_t len = static_cast(std::strlen(name_ptr.get())); + + return make_jbyteArray(env, name_ptr.get(), len); +} + /* * Class: org_duckdb_DuckDBBindings * Method: duckdb_array_type_array_size diff --git a/src/jni/util.hpp b/src/jni/util.hpp index 2c2d3c897..83d0ddd1f 100644 --- a/src/jni/util.hpp +++ b/src/jni/util.hpp @@ -9,10 +9,14 @@ extern "C" { #include #include -using jstring_ptr = std::unique_ptr>; - using jbyteArray_ptr = std::unique_ptr>; +using varchar_ptr = std::unique_ptr; + +inline void varchar_deleter(char *val) { + duckdb_free(val); +} + void check_java_exception_and_rethrow(JNIEnv *env); std::string jbyteArray_to_string(JNIEnv *env, jbyteArray ba_j); diff --git a/src/main/java/org/duckdb/DuckDBAppender.java b/src/main/java/org/duckdb/DuckDBAppender.java index edff98fe8..96560b562 100644 --- a/src/main/java/org/duckdb/DuckDBAppender.java +++ b/src/main/java/org/duckdb/DuckDBAppender.java @@ -50,9 +50,11 @@ public class DuckDBAppender implements AutoCloseable { supportedTypes.add(DUCKDB_TYPE_TIMESTAMP_TZ.typeId); supportedTypes.add(DUCKDB_TYPE_TIMESTAMP_NS.typeId); + supportedTypes.add(DUCKDB_TYPE_UUID.typeId); + supportedTypes.add(DUCKDB_TYPE_ARRAY.typeId); supportedTypes.add(DUCKDB_TYPE_STRUCT.typeId); - supportedTypes.add(DUCKDB_TYPE_UUID.typeId); + supportedTypes.add(DUCKDB_TYPE_UNION.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}; @@ -85,6 +87,7 @@ public class DuckDBAppender implements AutoCloseable { private long rowIdx = 0; private int colIdx = 0; private int structFieldIdx = 0; + private int unionFieldIdx = 0; private boolean appendingRow = false; private boolean appendingStruct = false; @@ -147,7 +150,8 @@ public DuckDBAppender endRow() throws SQLException { checkAppendingStruct(false); if (columns.length != colIdx) { - throw new SQLException(createErrMsg("'endRow' can be called only after adding all columns")); + throw new SQLException(createErrMsg("'endRow' can be called only after adding all columns, expected: " + + columns.length + ", actual: " + colIdx)); } rowIdx++; @@ -166,17 +170,66 @@ public DuckDBAppender endRow() throws SQLException { return this; } - public DuckDBAppender beginStruct() throws Exception { + public DuckDBAppender beginStruct() throws SQLException { checkOpen(); + checkCurrentColumnType(DUCKDB_TYPE_STRUCT); checkAppendingStruct(false); this.appendingStruct = true; return this; } - public DuckDBAppender endStruct() throws Exception { + public DuckDBAppender endStruct() throws SQLException { checkOpen(); checkAppendingStruct(true); + Column structCol = currentTopLevelColumn(); + if (structCol.children.size() != structFieldIdx) { + throw new SQLException( + createErrMsg("'endStruct' can be called only after adding all struct fields, expected: " + + structCol.children.size() + ", actual: " + structFieldIdx)); + } + this.structFieldIdx = 0; + this.appendingStruct = false; + incrementColOrStructFieldIdx(); + return this; + } + + public DuckDBAppender beginUnion(String tag) throws SQLException { + checkOpen(); + checkCurrentColumnType(DUCKDB_TYPE_UNION); + checkAppendingUnion(false); + + Column structCol = currentTopLevelColumn(); + int fieldWithTag = 0; + for (int i = 1; i < structCol.children.size(); i++) { + Column childCol = structCol.children.get(i); + if (childCol.structFieldName.equals(tag)) { + fieldWithTag = i; + } + } + if (0 == fieldWithTag) { + throw new SQLException(createErrMsg("specified union field not found, value: '" + tag + "'")); + } + + this.appendingStruct = true; + // set tag + append((byte) (fieldWithTag - 1)); + // set other fields to NULL + for (int i = 1; i < structCol.children.size(); i++) { + if (i == fieldWithTag) { + continue; + } + Column childCol = structCol.children.get(i); + childCol.setNull(rowIdx); + } + this.unionFieldIdx = fieldWithTag; + return this; + } + + public DuckDBAppender endUnion() throws SQLException { + checkOpen(); + checkAppendingUnion(true); this.structFieldIdx = 0; + this.unionFieldIdx = 0; this.appendingStruct = false; incrementColOrStructFieldIdx(); return this; @@ -200,14 +253,14 @@ public long flush() throws SQLException { int appendState = duckdb_append_data_chunk(appenderRef, chunkRef); if (0 != appendState) { byte[] errorUTF8 = duckdb_appender_error(appenderRef); - String error = null != errorUTF8 ? strFromUTF8(errorUTF8) : ""; + String error = 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) : ""; + String error = strFromUTF8(errorUTF8); throw new SQLException(createErrMsg(error)); } @@ -1144,8 +1197,21 @@ private void checkAppendingRow(boolean expected) throws SQLException { 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'")); + throw new SQLException(createErrMsg( + "'beginStruct' and 'endStruct' calls must be paired and cannot be interleaved with 'beginRow' and 'endRow'")); + } + } + + private void checkAppendingUnion(boolean expected) throws SQLException { + if (appendingStruct != expected) { + throw new SQLException(createErrMsg( + "'beginUnion' and 'endUnion' calls must be paired and cannot be interleaved with 'beginRow' and 'endRow'")); + } + if (appendingStruct && unionFieldIdx == 0) { + throw new SQLException(createErrMsg("invalid zero union field index")); + } + if (!appendingStruct && unionFieldIdx != 0) { + throw new SQLException(createErrMsg("invalid non-zero union field index")); } } @@ -1161,7 +1227,7 @@ private void incrementColOrStructFieldIdx() throws SQLException { throw new SQLException(createErrMsg("'beginRow' must be called before calling `append`")); } - private Column currentColumn() throws SQLException { + private Column currentTopLevelColumn() throws SQLException { checkOpen(); if (colIdx >= columns.length) { @@ -1169,12 +1235,24 @@ private Column currentColumn() throws SQLException { createErrMsg("invalid columns count, expected: " + columns.length + ", actual: " + (colIdx + 1))); } - Column col = columns[colIdx]; + return columns[colIdx]; + } + + private Column currentColumn() throws SQLException { + Column col = currentTopLevelColumn(); - if (!appendingStruct || col.colType != DUCKDB_TYPE_STRUCT) { + if (!appendingStruct || (col.colType != DUCKDB_TYPE_STRUCT && col.colType != DUCKDB_TYPE_UNION)) { return col; } + if (unionFieldIdx > 0) { + if (unionFieldIdx > col.children.size()) { + throw new SQLException(createErrMsg("invalid union fields count, expected: " + columns.length + + ", actual: " + (structFieldIdx + 1))); + } + return col.children.get(unionFieldIdx); + } + if (structFieldIdx >= col.children.size()) { throw new SQLException(createErrMsg("invalid struct fields count, expected: " + columns.length + ", actual: " + (structFieldIdx + 1))); @@ -1370,7 +1448,7 @@ private static byte[] utf8(String str) { private static String strFromUTF8(byte[] utf8) { if (null == utf8) { - return null; + return ""; } return new String(utf8, UTF_8); } @@ -1430,13 +1508,12 @@ private static ByteBuffer createChunk(ByteBuffer[] colTypes) throws SQLException } 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); + Column col = new Column(parent, null, vec); + parent.children.add(col); break; } case DUCKDB_TYPE_STRUCT: @@ -1444,28 +1521,18 @@ private static void initVecChildren(Column parent) throws SQLException { 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); + Column col = new Column(parent, null, vec, i); + parent.children.add(col); } break; } case DUCKDB_TYPE_ARRAY: { ByteBuffer vec = duckdb_array_vector_get_child(parent.vectorRef); - children.add(vec); + Column col = new Column(parent, null, vec); + parent.children.add(col); 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); - } } private static Column[] createVectors(ByteBuffer chunkRef, ByteBuffer[] colTypes) throws SQLException { @@ -1495,22 +1562,42 @@ private static class Column { private final int decimalPrecision; private final int decimalScale; private final long arraySize; + private final String structFieldName; + 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, colTypeRef, vector, -1); + } + + private Column(Column parent, ByteBuffer colTypeRef, ByteBuffer vector, int structFieldIdx) + throws SQLException { this.parent = parent; - this.colTypeRef = colTypeRef; - int colTypeId = duckdb_get_type_id(colTypeRef); + + if (null == vector) { + throw new SQLException("cannot initialize data chunk vector"); + } + + if (null == colTypeRef) { + this.colTypeRef = duckdb_vector_get_column_type(vector); + if (null == this.colTypeRef) { + throw new SQLException("cannot initialize data chunk vector type"); + } + } else { + this.colTypeRef = colTypeRef; + } + + int colTypeId = duckdb_get_type_id(this.colTypeRef); this.colType = capiTypeFromTypeId(colTypeId); if (colType == DUCKDB_TYPE_DECIMAL) { - int decimalInternalTypeId = duckdb_decimal_internal_type(colTypeRef); + int decimalInternalTypeId = duckdb_decimal_internal_type(this.colTypeRef); this.decimalInternalType = capiTypeFromTypeId(decimalInternalTypeId); - this.decimalPrecision = duckdb_decimal_width(colTypeRef); - this.decimalScale = duckdb_decimal_scale(colTypeRef); + this.decimalPrecision = duckdb_decimal_width(this.colTypeRef); + this.decimalScale = duckdb_decimal_scale(this.colTypeRef); } else { this.decimalInternalType = DUCKDB_TYPE_INVALID; this.decimalPrecision = -1; @@ -1523,11 +1610,15 @@ private Column(Column parent, ByteBuffer colTypeRef, ByteBuffer vector) throws S 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 (structFieldIdx >= 0) { + byte[] nameUTF8 = duckdb_struct_type_child_name(parent.colTypeRef, structFieldIdx); + this.structFieldName = strFromUTF8(nameUTF8); + } else { + this.structFieldName = null; } + this.vectorRef = vector; + if (colType.widthBytes > 0 || colType == DUCKDB_TYPE_DECIMAL) { this.data = duckdb_vector_get_data(vectorRef, widthBytes() * arraySize * parentArraySize()); if (null == this.data) { diff --git a/src/main/java/org/duckdb/DuckDBBindings.java b/src/main/java/org/duckdb/DuckDBBindings.java index 22ae2c1a0..b79099fe0 100644 --- a/src/main/java/org/duckdb/DuckDBBindings.java +++ b/src/main/java/org/duckdb/DuckDBBindings.java @@ -37,6 +37,8 @@ public class DuckDBBindings { static native long duckdb_struct_type_child_count(ByteBuffer logical_type); + static native byte[] duckdb_struct_type_child_name(ByteBuffer logical_type, long index); + static native long duckdb_array_type_array_size(ByteBuffer logical_type); static native void duckdb_destroy_logical_type(ByteBuffer logical_type); diff --git a/src/test/java/org/duckdb/TestAppender.java b/src/test/java/org/duckdb/TestAppender.java index 54c674526..94a6b1654 100644 --- a/src/test/java/org/duckdb/TestAppender.java +++ b/src/test/java/org/duckdb/TestAppender.java @@ -1746,4 +1746,179 @@ public static void test_appender_struct_flush() throws Exception { } } } + + public static void test_appender_struct_incomplete() 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")) { + // underflow + assertThrows(() -> { + appender.beginRow() + .append(42) + .beginStruct() + .append(43) + // .append("foo") + .endStruct(); + }, SQLException.class); + } + try (DuckDBAppender appender = conn.createAppender("tab1")) { + // overflow + assertThrows(() -> { + appender.beginRow() + .append(42) + .beginStruct() + .append(43) + .append("foo") + .append(44) // extra field + .endStruct(); + }, SQLException.class); + } + } + } + + public static void test_appender_begin_misuse() 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")) { + assertThrows(() -> { appender.beginRow().beginRow(); }, SQLException.class); + } + try (DuckDBAppender appender = conn.createAppender("tab1")) { + assertThrows(() -> { appender.beginStruct().beginStruct(); }, SQLException.class); + } + } + } + + public static void test_appender_incomplete_flush() 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")) { + assertThrows(() -> { appender.beginRow().append(42).flush(); }, SQLException.class); + } + } + } + + public static void test_appender_union_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 UNION(u1 INTEGER, u2 VARCHAR))"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + long count = appender.beginRow() + .append(42) + .beginUnion("u1") + .append(43) + .endUnion() + .endRow() + + .beginRow() + .append(44) + .beginUnion("u1") + .appendNull() + .endUnion() + .endRow() + + .beginRow() + .append(45) + .beginUnion("u2") + .append("foo") + .endUnion() + .endRow() + + .flush(); + assertEquals(count, 3L); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 WHERE col1 = 42")) { + assertTrue(rs.next()); + + assertEquals(rs.getInt(1), 42); + Object obj = rs.getObject(2); + assertTrue(obj instanceof Integer); + assertEquals(obj, 43); + + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 WHERE col1 = 44")) { + assertTrue(rs.next()); + + assertEquals(rs.getInt(1), 44); + Object obj = rs.getObject(2); + assertNull(obj); + // assertTrue(rs.wasNull()); + + assertFalse(rs.next()); + } + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM tab1 WHERE col1 = 45")) { + assertTrue(rs.next()); + + assertEquals(rs.getInt(1), 45); + Object obj = rs.getObject(2); + assertTrue(obj instanceof String); + assertEquals(obj, "foo"); + + assertFalse(rs.next()); + } + } + } + + public static void test_appender_union_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 UNION(u1 INTEGER, u2 INTEGER[2], u3 VARCHAR))"); + + try (DuckDBAppender appender = conn.createAppender("tab1")) { + for (int i = 0; i < count + tail; i++) { + appender.beginRow().append(i); + switch (i % 3) { + case 0: + appender.beginUnion("u1").append(i + 1); + break; + case 1: + appender.beginUnion("u2").append(new int[] {i + 2, i + 3}); + break; + default: + appender.beginUnion("u3").append("foo" + i); + } + appender.endUnion(); + 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); + Object obj = rs.getObject(2); + switch (i % 3) { + case 0: + assertTrue(obj instanceof Integer); + assertEquals(obj, i + 1); + break; + case 1: + assertTrue(obj instanceof DuckDBArray); + DuckDBArray arrayWrapper = (DuckDBArray) obj; + Object[] array = (Object[]) arrayWrapper.getArray(); + assertEquals(array.length, 2); + assertEquals(array[0], i + 2); + assertEquals(array[1], i + 3); + break; + default: + assertTrue(obj instanceof String); + assertEquals(obj, "foo" + i); + } + } + + assertFalse(rs.next()); + } + } + } } diff --git a/src/test/java/org/duckdb/TestBindings.java b/src/test/java/org/duckdb/TestBindings.java index de0316227..d78353b7b 100644 --- a/src/test/java/org/duckdb/TestBindings.java +++ b/src/test/java/org/duckdb/TestBindings.java @@ -7,7 +7,9 @@ import static org.duckdb.test.Assertions.*; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.sql.*; +import java.util.Arrays; public class TestBindings { @@ -172,6 +174,11 @@ public static void test_bindings_struct_vector() throws Exception { assertTrue(duckdb_get_type_id(structType) != DUCKDB_TYPE_INVALID.typeId); assertEquals(duckdb_struct_type_child_count(structType), 2L); + assertEquals(duckdb_struct_type_child_name(structType, 0), "foo".getBytes(UTF_8)); + assertEquals(duckdb_struct_type_child_name(structType, 1), "bar".getBytes(UTF_8)); + assertThrows(() -> { duckdb_struct_type_child_name(structType, -1); }, SQLException.class); + assertThrows(() -> { duckdb_struct_type_child_name(structType, 2); }, SQLException.class); + ByteBuffer vec = duckdb_create_vector(structType); ByteBuffer childVec = duckdb_struct_vector_get_child(vec, 1); diff --git a/src/test/java/org/duckdb/test/Assertions.java b/src/test/java/org/duckdb/test/Assertions.java index 7363e3293..c6c29a70d 100644 --- a/src/test/java/org/duckdb/test/Assertions.java +++ b/src/test/java/org/duckdb/test/Assertions.java @@ -39,6 +39,10 @@ public static void assertEquals(Object actual, Object expected, String label) th assertTrue(Objects.equals(actual, expected), message); } + public static void assertEquals(byte[] actual, byte[] expected) throws Exception { + assertEquals(actual, expected, ""); + } + public static void assertEquals(byte[] actual, byte[] expected, String message) throws Exception { assertTrue(Arrays.equals(actual, expected), message); }