Skip to content

Commit 401d201

Browse files
committed
Appender: support composite/nested lists and maps
This PR adds support for appending to `MAP` and composite `LIST` columns, for example `LIST` of `STRUCT`s, `LIST` of `ARRAY`s or nested `LIST`s. It should cover virtually any table schema with composite fields, with the notable exception of `ARRAY`s with more than 2 dimensions (like `INT[2][3][4]`) - only 2D `ARRAY`s are supported. Higher dimensions could be possible, but the interface for them appeared to be tough to generalize. `LIST`s of `ARRAY`s can be used instead, like `INT[2][3][]`. `LIST`s can be specified as a `Collection` or `Iterator` (with `count`) of objects. `MAP`s are specified as `java.util.Map` instances. `STRUCT` inside a `LIST` can be specified either as `java.utils.LinkedHashMap` (keys are ignored) or as a `Collection` of values for `STRUCT` fields. `UNION` inside a `LIST` can be specified as an instance of `java.util.AbstractMap.SimpleEntry`, where a `key` is used as a tag to choose which `UNION` field to append to. If some required input cases are missed - please raise an issue. Testing: new tests added to cover various combinations of nesting.
1 parent d7cb992 commit 401d201

11 files changed

Lines changed: 3432 additions & 1105 deletions

src/main/java/org/duckdb/DuckDBAppender.java

Lines changed: 1257 additions & 663 deletions
Large diffs are not rendered by default.

src/main/java/org/duckdb/DuckDBBindings.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ enum CAPIType {
169169
// struct type, only useful as logical type
170170
DUCKDB_TYPE_STRUCT(25, 0),
171171
// map type, only useful as logical type
172-
DUCKDB_TYPE_MAP(26),
172+
DUCKDB_TYPE_MAP(26, 16),
173173
// duckdb_array, only useful as logical type
174174
DUCKDB_TYPE_ARRAY(33, 0),
175175
// duckdb_hugeint

src/main/java/org/duckdb/DuckDBResultSetMetaData.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import java.time.OffsetDateTime;
1313
import java.time.OffsetTime;
1414
import java.util.ArrayList;
15-
import java.util.HashMap;
15+
import java.util.LinkedHashMap;
1616
import java.util.UUID;
1717

1818
public class DuckDBResultSetMetaData implements ResultSetMetaData {
@@ -234,7 +234,7 @@ protected static String type_to_javaString(DuckDBColumnType type) {
234234
case ARRAY:
235235
return DuckDBArray.class.getName();
236236
case MAP:
237-
return HashMap.class.getName();
237+
return LinkedHashMap.class.getName();
238238
case STRUCT:
239239
return DuckDBStruct.class.getName();
240240
default:

src/main/java/org/duckdb/DuckDBVector.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ Map<Object, Object> getMap(int idx) throws SQLException {
286286
}
287287

288288
Object[] entries = (Object[]) (((Array) varlen_data[idx]).getArray());
289-
Map<Object, Object> result = new HashMap<>();
289+
Map<Object, Object> result = new LinkedHashMap<>();
290290

291291
for (Object entry : entries) {
292292
Object[] entry_val = ((Struct) entry).getAttributes();

src/test/java/org/duckdb/TestAppender.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ public static void test_appender_uuid() throws Exception {
144144
Statement stmt = conn.createStatement()) {
145145

146146
stmt.execute("CREATE TABLE tab1(col1 INT, col2 UUID)");
147-
UUID uuid1 = UUID.randomUUID();
148-
UUID uuid2 = UUID.randomUUID();
147+
UUID uuid1 = UUID.fromString("777dfbdb-83e7-40f5-ae1b-e12215bdd798");
148+
UUID uuid2 = UUID.fromString("b8708825-3b58-45a1-9a6e-dab053c3f387");
149149
try (DuckDBAppender appender = conn.createAppender("tab1")) {
150150
appender.beginRow().append(1).append(uuid1).endRow();
151151
appender.beginRow().append(2).append(uuid2).endRow();

src/test/java/org/duckdb/TestAppenderCollection.java

Lines changed: 1076 additions & 429 deletions
Large diffs are not rendered by default.

src/test/java/org/duckdb/TestAppenderCollection2D.java

Lines changed: 875 additions & 0 deletions
Large diffs are not rendered by default.

src/test/java/org/duckdb/TestAppenderComposite.java

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package org.duckdb;
22

3+
import static java.util.Arrays.asList;
4+
import static java.util.Collections.singletonList;
35
import static org.duckdb.TestDuckDBJDBC.JDBC_URL;
46
import static org.duckdb.test.Assertions.*;
7+
import static org.duckdb.test.Helpers.createMap;
58

9+
import java.math.BigDecimal;
10+
import java.math.BigInteger;
611
import java.sql.DriverManager;
712
import java.sql.ResultSet;
813
import java.sql.SQLException;
914
import java.sql.Statement;
10-
import java.util.Map;
15+
import java.util.*;
1116

1217
public class TestAppenderComposite {
1318

@@ -474,4 +479,195 @@ public static void test_appender_union_nested() throws Exception {
474479
}
475480
}
476481
}
482+
483+
private static void assertFetchedStructEquals(Object dbs, Collection<Object> struct) throws Exception {
484+
DuckDBStruct dbStruct = (DuckDBStruct) dbs;
485+
Map<String, Object> map = dbStruct.getMap();
486+
Collection<Object> fetched = map.values();
487+
assertEquals(fetched.size(), struct.size());
488+
List<Object> structList = new ArrayList<>(struct);
489+
int i = 0;
490+
for (Object f : fetched) {
491+
assertEquals(f, structList.get(i));
492+
i++;
493+
}
494+
}
495+
496+
public static void test_appender_list_basic_struct() throws Exception {
497+
Collection<Object> struct1 = asList(42, "foo");
498+
Collection<Object> struct2 = asList(null, "bar");
499+
Collection<Object> struct3 = asList(43, null);
500+
Collection<Object> struct4 = asList(44, "baz");
501+
try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class);
502+
Statement stmt = conn.createStatement()) {
503+
stmt.execute("CREATE TABLE tab1(col1 INT, col2 STRUCT(s1 INT, s2 VARCHAR)[])");
504+
try (DuckDBAppender appender = conn.createAppender("tab1")) {
505+
appender.beginRow()
506+
.append(42)
507+
.append(asList(struct1, struct2, struct3))
508+
.endRow()
509+
.beginRow()
510+
.append(43)
511+
.append((List<Object>) null)
512+
.endRow()
513+
.beginRow()
514+
.append(44)
515+
.append(asList(null, struct4))
516+
.endRow()
517+
.flush();
518+
}
519+
520+
try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) from tab1 WHERE col1 = 42")) {
521+
assertTrue(rs.next());
522+
assertFetchedStructEquals(rs.getObject(1), struct1);
523+
assertTrue(rs.next());
524+
assertFetchedStructEquals(rs.getObject(1), struct2);
525+
assertTrue(rs.next());
526+
assertFetchedStructEquals(rs.getObject(1), struct3);
527+
assertFalse(rs.next());
528+
}
529+
try (ResultSet rs = stmt.executeQuery("SELECT col2 from tab1 WHERE col1 = 43")) {
530+
assertTrue(rs.next());
531+
assertNull(rs.getObject(1));
532+
assertTrue(rs.wasNull());
533+
assertFalse(rs.next());
534+
}
535+
try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) from tab1 WHERE col1 = 44")) {
536+
assertTrue(rs.next());
537+
assertNull(rs.getObject(1));
538+
assertTrue(rs.wasNull());
539+
assertTrue(rs.next());
540+
assertFetchedStructEquals(rs.getObject(1), struct4);
541+
assertFalse(rs.next());
542+
}
543+
}
544+
}
545+
546+
public static void test_appender_list_basic_struct_as_map() throws Exception {
547+
LinkedHashMap<Object, Object> struct1 = createMap("key1", 42, "key2", "foo");
548+
LinkedHashMap<Object, Object> struct2 = createMap("key1", null, "key2", "bar");
549+
LinkedHashMap<Object, Object> struct3 = createMap("key1", 43, "key2", null);
550+
LinkedHashMap<Object, Object> struct4 = createMap("key1", 44, "key2", "baz");
551+
try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class);
552+
Statement stmt = conn.createStatement()) {
553+
stmt.execute("CREATE TABLE tab1(col1 INT, col2 STRUCT(s1 INT, s2 VARCHAR)[])");
554+
try (DuckDBAppender appender = conn.createAppender("tab1")) {
555+
appender.beginRow()
556+
.append(42)
557+
.append(asList(struct1, struct2, struct3))
558+
.endRow()
559+
.beginRow()
560+
.append(43)
561+
.append((List<Object>) null)
562+
.endRow()
563+
.beginRow()
564+
.append(44)
565+
.append(asList(null, struct4))
566+
.endRow()
567+
.flush();
568+
}
569+
570+
try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) from tab1 WHERE col1 = 42")) {
571+
assertTrue(rs.next());
572+
assertFetchedStructEquals(rs.getObject(1), struct1.values());
573+
assertTrue(rs.next());
574+
assertFetchedStructEquals(rs.getObject(1), struct2.values());
575+
assertTrue(rs.next());
576+
assertFetchedStructEquals(rs.getObject(1), struct3.values());
577+
assertFalse(rs.next());
578+
}
579+
try (ResultSet rs = stmt.executeQuery("SELECT col2 from tab1 WHERE col1 = 43")) {
580+
assertTrue(rs.next());
581+
assertNull(rs.getObject(1));
582+
assertTrue(rs.wasNull());
583+
assertFalse(rs.next());
584+
}
585+
try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) from tab1 WHERE col1 = 44")) {
586+
assertTrue(rs.next());
587+
assertNull(rs.getObject(1));
588+
assertTrue(rs.wasNull());
589+
assertTrue(rs.next());
590+
assertFetchedStructEquals(rs.getObject(1), struct4.values());
591+
assertFalse(rs.next());
592+
}
593+
}
594+
}
595+
596+
public static void test_appender_list_basic_struct_with_primitives() throws Exception {
597+
Collection<Object> struct1 = asList(true, (byte) 42, (short) 43, 44, 45L, BigInteger.valueOf(46), 47.1F, 48.1D,
598+
BigDecimal.valueOf(49.123));
599+
try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class);
600+
Statement stmt = conn.createStatement()) {
601+
stmt.execute("CREATE TABLE tab1(col1 INT, col2 STRUCT("
602+
+ "s1 BOOL,"
603+
+ "s2 TINYINT,"
604+
+ "s3 SMALLINT,"
605+
+ "s4 INTEGER,"
606+
+ "s5 BIGINT,"
607+
+ "s6 HUGEINT,"
608+
+ "s7 FLOAT,"
609+
+ "s8 DOUBLE,"
610+
+ "s9 DECIMAL"
611+
+ ")[])");
612+
try (DuckDBAppender appender = conn.createAppender("tab1")) {
613+
appender.beginRow().append(42).append(singletonList(struct1)).endRow().flush();
614+
}
615+
616+
try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) from tab1 WHERE col1 = 42")) {
617+
assertTrue(rs.next());
618+
assertFetchedStructEquals(rs.getObject(1), struct1);
619+
assertFalse(rs.next());
620+
}
621+
}
622+
}
623+
624+
public static void test_appender_list_basic_union() throws Exception {
625+
Map.Entry<String, Object> union1 = new AbstractMap.SimpleEntry<>("u1", 42);
626+
Map.Entry<String, Object> union2 = new AbstractMap.SimpleEntry<>("u2", "foo");
627+
Map.Entry<String, Object> union3 = new AbstractMap.SimpleEntry<>("u1", null);
628+
Map.Entry<String, Object> union4 = new AbstractMap.SimpleEntry<>("u2", "bar");
629+
try (DuckDBConnection conn = DriverManager.getConnection(JDBC_URL).unwrap(DuckDBConnection.class);
630+
Statement stmt = conn.createStatement()) {
631+
stmt.execute("CREATE TABLE tab1(col1 INT, col2 UNION(u1 INT, u2 VARCHAR)[])");
632+
try (DuckDBAppender appender = conn.createAppender("tab1")) {
633+
appender.beginRow()
634+
.append(42)
635+
.append(asList(union1, union2, union3))
636+
.endRow()
637+
.beginRow()
638+
.append(43)
639+
.append((List<Object>) null)
640+
.endRow()
641+
.beginRow()
642+
.append(44)
643+
.append(asList(null, union4))
644+
.endRow()
645+
.flush();
646+
}
647+
648+
try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) from tab1 WHERE col1 = 42")) {
649+
assertTrue(rs.next());
650+
assertEquals(rs.getObject(1), union1.getValue());
651+
assertTrue(rs.next());
652+
assertEquals(rs.getObject(1), union2.getValue());
653+
assertTrue(rs.next());
654+
assertEquals(rs.getObject(1), union3.getValue());
655+
assertFalse(rs.next());
656+
}
657+
try (ResultSet rs = stmt.executeQuery("SELECT col2 from tab1 WHERE col1 = 43")) {
658+
assertTrue(rs.next());
659+
assertNull(rs.getObject(1));
660+
assertTrue(rs.wasNull());
661+
assertFalse(rs.next());
662+
}
663+
try (ResultSet rs = stmt.executeQuery("SELECT unnest(col2) from tab1 WHERE col1 = 44")) {
664+
assertTrue(rs.next());
665+
assertNull(rs.getObject(1));
666+
assertTrue(rs.wasNull());
667+
assertTrue(rs.next());
668+
assertEquals(rs.getObject(1), union4.getValue());
669+
assertFalse(rs.next());
670+
}
671+
}
672+
}
477673
}

src/test/java/org/duckdb/TestDuckDBJDBC.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3156,10 +3156,10 @@ public static void main(String[] args) throws Exception {
31563156
statusCode = runTests(new String[0], clazz);
31573157
} else {
31583158
statusCode = runTests(args, TestDuckDBJDBC.class, TestAppender.class, TestAppenderCollection.class,
3159-
TestAppenderComposite.class, TestSingleValueAppender.class, TestBatch.class,
3160-
TestBindings.class, TestClosure.class, TestExtensionTypes.class, TestSpatial.class,
3161-
TestParameterMetadata.class, TestPrepare.class, TestResults.class,
3162-
TestSessionInit.class, TestTimestamp.class);
3159+
TestAppenderCollection2D.class, TestAppenderComposite.class,
3160+
TestSingleValueAppender.class, TestBatch.class, TestBindings.class, TestClosure.class,
3161+
TestExtensionTypes.class, TestSpatial.class, TestParameterMetadata.class,
3162+
TestPrepare.class, TestResults.class, TestSessionInit.class, TestTimestamp.class);
31633163
}
31643164
System.exit(statusCode);
31653165
}

src/test/java/org/duckdb/TestParameterMetadata.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import java.math.BigDecimal;
88
import java.sql.*;
9-
import java.util.HashMap;
9+
import java.util.LinkedHashMap;
1010

1111
public class TestParameterMetadata {
1212

@@ -117,7 +117,7 @@ public static void test_parameter_metadata_map() throws Exception {
117117
try (PreparedStatement ps = conn.prepareStatement("INSERT INTO metadata_test_map_1 VALUES(?)")) {
118118
ParameterMetaData meta = ps.getParameterMetaData();
119119
assertEquals(meta.getParameterTypeName(1), "MAP(INTEGER, DOUBLE)");
120-
assertEquals(meta.getParameterClassName(1), HashMap.class.getName());
120+
assertEquals(meta.getParameterClassName(1), LinkedHashMap.class.getName());
121121
assertEquals(meta.getPrecision(1), 0);
122122
assertEquals(meta.getScale(1), 0);
123123
}

0 commit comments

Comments
 (0)