diff --git a/appender.go b/appender.go index f7e8d184..96f1d268 100644 --- a/appender.go +++ b/appender.go @@ -74,7 +74,7 @@ func NewAppenderFromConn(driverConn driver.Conn, schema, table string) (*Appende // Ensure that we only create an appender for supported column types. duckdbType := C.duckdb_get_type_id(a.types[i]) - name, found := unsupportedAppenderTypeMap[duckdbType] + name, found := unsupportedTypeMap[duckdbType] if found { err := columnError(unsupportedTypeError(name), i+1) destroyTypeSlice(a.ptr, a.types) diff --git a/appender_test.go b/appender_test.go index 01ae8367..16a33fd7 100644 --- a/appender_test.go +++ b/appender_test.go @@ -6,6 +6,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "math/big" "math/rand" "testing" "time" @@ -46,6 +47,9 @@ type mixedStruct struct { B []struct { L []int32 } + C struct { + L Map + } } type nestedDataRow struct { @@ -106,23 +110,13 @@ func randInt(lo int64, hi int64) int64 { return rand.Int63n(hi-lo+1) + lo } -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") - -func randString(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = letters[rand.Intn(len(letters))] - } - return string(b) -} - -func cleanupAppender(t *testing.T, c *Connector, con driver.Conn, a *Appender) { +func cleanupAppender[T require.TestingT](t T, c *Connector, con driver.Conn, a *Appender) { require.NoError(t, a.Close()) require.NoError(t, con.Close()) require.NoError(t, c.Close()) } -func prepareAppender(t *testing.T, createTbl string) (*Connector, driver.Conn, *Appender) { +func prepareAppender[T require.TestingT](t T, createTbl string) (*Connector, driver.Conn, *Appender) { c, err := NewConnector("", nil) require.NoError(t, err) @@ -139,115 +133,31 @@ func prepareAppender(t *testing.T, createTbl string) (*Connector, driver.Conn, * } func TestAppenderClose(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, `CREATE TABLE test (i INTEGER)`) require.NoError(t, a.AppendRow(int32(42))) cleanupAppender(t, c, con, a) } -func TestAppenderPrimitive(t *testing.T) { +func TestAppendChunks(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( id BIGINT, - uint8 UTINYINT, - int8 TINYINT, - uint16 USMALLINT, - int16 SMALLINT, - uint32 UINTEGER, - int32 INTEGER, - uint64 UBIGINT, - int64 BIGINT, - timestamp TIMESTAMP, - timestampS TIMESTAMP_S, - timestampMS TIMESTAMP_MS, - timestampNS TIMESTAMP_NS, - timestampTZ TIMESTAMPTZ, - float REAL, - double DOUBLE, - string VARCHAR, - bool BOOLEAN + uint8 UTINYINT )`) // Test appending a few data chunks. rowCount := GetDataChunkCapacity() * 5 type row struct { - ID int64 - UInt8 uint8 - Int8 int8 - UInt16 uint16 - Int16 int16 - UInt32 uint32 - Int32 int32 - UInt64 uint64 - Int64 int64 - Timestamp time.Time - TimestampS time.Time - TimestampMS time.Time - TimestampNS time.Time - TimestampTZ time.Time - Float float32 - Double float64 - String string - Bool bool + ID int64 + UInt8 uint8 } - // Get the timestamp for all TS columns. - IST, err := time.LoadLocation("Asia/Kolkata") - require.NoError(t, err) - - const longForm = "2006-01-02 15:04:05 MST" - ts, err := time.ParseInLocation(longForm, "2016-01-17 20:04:05 IST", IST) - require.NoError(t, err) - rowsToAppend := make([]row, rowCount) for i := 0; i < rowCount; i++ { - - u64 := rand.Uint64() - // Go SQL does not support uint64 values with their high bit set (see for example https://github.com/lib/pq/issues/72). - if u64 > 9223372036854775807 { - u64 = 9223372036854775807 - } - - rowsToAppend[i] = row{ - ID: int64(i), - UInt8: uint8(randInt(0, 255)), - Int8: int8(randInt(-128, 127)), - UInt16: uint16(randInt(0, 65535)), - Int16: int16(randInt(-32768, 32767)), - UInt32: uint32(randInt(0, 4294967295)), - Int32: int32(randInt(-2147483648, 2147483647)), - UInt64: u64, - Int64: rand.Int63(), - Timestamp: ts, - TimestampS: ts, - TimestampMS: ts, - TimestampNS: ts, - TimestampTZ: ts, - Float: rand.Float32(), - Double: rand.Float64(), - String: randString(int(randInt(0, 128))), - Bool: rand.Int()%2 == 0, - } - - require.NoError(t, a.AppendRow( - rowsToAppend[i].ID, - rowsToAppend[i].UInt8, - rowsToAppend[i].Int8, - rowsToAppend[i].UInt16, - rowsToAppend[i].Int16, - rowsToAppend[i].UInt32, - rowsToAppend[i].Int32, - rowsToAppend[i].UInt64, - rowsToAppend[i].Int64, - rowsToAppend[i].Timestamp, - rowsToAppend[i].TimestampS, - rowsToAppend[i].TimestampMS, - rowsToAppend[i].TimestampNS, - rowsToAppend[i].TimestampTZ, - rowsToAppend[i].Float, - rowsToAppend[i].Double, - rowsToAppend[i].String, - rowsToAppend[i].Bool, - )) + rowsToAppend[i] = row{ID: int64(i), UInt8: uint8(randInt(0, 255))} + require.NoError(t, a.AppendRow(rowsToAppend[i].ID, rowsToAppend[i].UInt8)) } require.NoError(t, a.Flush()) @@ -258,31 +168,7 @@ func TestAppenderPrimitive(t *testing.T) { i := 0 for res.Next() { r := row{} - require.NoError(t, res.Scan( - &r.ID, - &r.UInt8, - &r.Int8, - &r.UInt16, - &r.Int16, - &r.UInt32, - &r.Int32, - &r.UInt64, - &r.Int64, - &r.Timestamp, - &r.TimestampS, - &r.TimestampMS, - &r.TimestampNS, - &r.TimestampTZ, - &r.Float, - &r.Double, - &r.String, - &r.Bool, - )) - rowsToAppend[i].Timestamp = rowsToAppend[i].Timestamp.UTC() - rowsToAppend[i].TimestampS = rowsToAppend[i].TimestampS.UTC() - rowsToAppend[i].TimestampMS = rowsToAppend[i].TimestampMS.UTC() - rowsToAppend[i].TimestampNS = rowsToAppend[i].TimestampNS.UTC() - rowsToAppend[i].TimestampTZ = rowsToAppend[i].TimestampTZ.UTC() + require.NoError(t, res.Scan(&r.ID, &r.UInt8)) require.Equal(t, rowsToAppend[i], r) i++ } @@ -293,6 +179,7 @@ func TestAppenderPrimitive(t *testing.T) { } func TestAppenderList(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( string_list VARCHAR[], @@ -329,6 +216,7 @@ func TestAppenderList(t *testing.T) { } func TestAppenderNested(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( id BIGINT, @@ -352,11 +240,13 @@ func TestAppenderNested(t *testing.T) { struct_with_list STRUCT(L INT[]), mix STRUCT( A STRUCT(L VARCHAR[]), - B STRUCT(L INT[])[] + B STRUCT(L INT[])[], + C STRUCT(L MAP(VARCHAR, INT)) ), mix_list STRUCT( A STRUCT(L VARCHAR[]), - B STRUCT(L INT[])[] + B STRUCT(L INT[])[], + C STRUCT(L MAP(VARCHAR, INT)) )[] ) `) @@ -372,6 +262,9 @@ func TestAppenderNested(t *testing.T) { }{ {[]int32{1, 2, 3}}, }, + C: struct { + L Map + }{L: Map{"foo": int32(1), "bar": int32(2)}}, } rowsToAppend := make([]nestedDataRow, 10) for i := 0; i < 10; i++ { @@ -464,6 +357,7 @@ func TestAppenderNested(t *testing.T) { } func TestAppenderNullList(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, `CREATE TABLE test (int_slice VARCHAR[][][])`) require.NoError(t, a.AppendRow([][][]string{{{}}})) @@ -506,6 +400,7 @@ func TestAppenderNullList(t *testing.T) { } func TestAppenderNullStruct(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( simple_struct STRUCT(A INT, B VARCHAR) @@ -537,6 +432,7 @@ func TestAppenderNullStruct(t *testing.T) { } func TestAppenderNestedNullStruct(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( double_wrapped_struct STRUCT( @@ -589,6 +485,7 @@ func TestAppenderNestedNullStruct(t *testing.T) { } func TestAppenderNullIntAndString(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, `CREATE TABLE test (id BIGINT, str VARCHAR)`) require.NoError(t, a.AppendRow(int64(32), "hello")) @@ -629,6 +526,7 @@ func TestAppenderNullIntAndString(t *testing.T) { } func TestAppenderUUID(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, `CREATE TABLE test (id UUID)`) id := UUID(uuid.New()) @@ -644,10 +542,11 @@ func TestAppenderUUID(t *testing.T) { cleanupAppender(t, c, con, a) } -func TestAppenderTime(t *testing.T) { - c, con, a := prepareAppender(t, `CREATE TABLE test (timestamp TIMESTAMP)`) +func TestAppenderTsNs(t *testing.T) { + t.Parallel() + c, con, a := prepareAppender(t, `CREATE TABLE test (timestamp TIMESTAMP_NS)`) - ts := time.Date(2022, time.January, 1, 12, 0, 0, 0, time.UTC) + ts := time.Date(2022, time.January, 1, 12, 0, 33, 242, time.UTC) require.NoError(t, a.AppendRow(ts)) require.NoError(t, a.Flush()) @@ -661,9 +560,10 @@ func TestAppenderTime(t *testing.T) { } func TestAppenderDate(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, `CREATE TABLE test (date DATE)`) - ts := time.Date(1996, time.July, 23, 11, 42, 23, 0, time.UTC) + ts := time.Date(1996, time.July, 23, 11, 42, 23, 123, time.UTC) require.NoError(t, a.AppendRow(ts)) require.NoError(t, a.Flush()) @@ -678,7 +578,25 @@ func TestAppenderDate(t *testing.T) { cleanupAppender(t, c, con, a) } +func TestAppenderTime(t *testing.T) { + t.Parallel() + c, con, a := prepareAppender(t, `CREATE TABLE test (time TIME)`) + + ts := time.Date(1996, time.July, 23, 11, 42, 23, 123, time.UTC) + require.NoError(t, a.AppendRow(ts)) + require.NoError(t, a.Flush()) + + // Verify results. + row := sql.OpenDB(c).QueryRowContext(context.Background(), `SELECT time FROM test`) + + var res time.Time + require.NoError(t, row.Scan(&res)) + require.Equal(t, ts.UnixMicro(), res.UnixMicro()) + cleanupAppender(t, c, con, a) +} + func TestAppenderBlob(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, `CREATE TABLE test (data BLOB)`) data := []byte{0x01, 0x02, 0x00, 0x03, 0x04} @@ -707,6 +625,7 @@ func TestAppenderBlob(t *testing.T) { } func TestAppenderBlobTinyInt(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( data UTINYINT[] @@ -745,6 +664,7 @@ func TestAppenderBlobTinyInt(t *testing.T) { } func TestAppenderUint8SliceTinyInt(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( data UTINYINT[] @@ -778,6 +698,41 @@ func TestAppenderUint8SliceTinyInt(t *testing.T) { cleanupAppender(t, c, con, a) } +func TestAppenderDecimal(t *testing.T) { + t.Parallel() + c, con, a := prepareAppender(t, ` + CREATE TABLE test ( + data DECIMAL(4,3) + )`) + + require.NoError(t, a.AppendRow(nil)) + require.NoError(t, a.AppendRow(Decimal{Width: uint8(4), Value: big.NewInt(1), Scale: 3})) + require.NoError(t, a.AppendRow(Decimal{Width: uint8(4), Value: big.NewInt(2), Scale: 3})) + require.NoError(t, a.Flush()) + + // Verify results. + res, err := sql.OpenDB(c).QueryContext(context.Background(), `SELECT CASE WHEN data IS NULL THEN 'NULL' ELSE data::VARCHAR END FROM test`) + require.NoError(t, err) + + expected := []string{ + "NULL", + "0.001", + "0.002", + } + + i := 0 + for res.Next() { + var str string + require.NoError(t, res.Scan(&str)) + require.Equal(t, expected[i], str) + i++ + } + + require.Equal(t, 3, i) + require.NoError(t, res.Close()) + cleanupAppender(t, c, con, a) +} + var jsonInputs = [][]byte{ []byte(`{"c1": 42, "l1": [1, 2, 3], "s1": {"a": 101, "b": ["hello", "world"]}, "l2": [{"a": [{"a": [4.2, 7.9]}]}]}`), []byte(`{"c1": null, "l1": [null, 2, null], "s1": {"a": null, "b": ["hello", null]}, "l2": [{"a": [{"a": [null, 7.9]}]}]}`), @@ -799,6 +754,7 @@ var jsonResults = [][]string{ } func TestAppenderWithJSON(t *testing.T) { + t.Parallel() c, con, a := prepareAppender(t, ` CREATE TABLE test ( c1 UBIGINT, diff --git a/data_chunk.go b/data_chunk.go index e372c329..8b692496 100644 --- a/data_chunk.go +++ b/data_chunk.go @@ -33,6 +33,7 @@ func (chunk *DataChunk) SetSize(size int) error { } // SetValue writes a single value to a column in a data chunk. Note that this requires casting the type for each invocation. +// NOTE: Custom ENUM types must be passed as string. func (chunk *DataChunk) SetValue(colIdx int, rowIdx int, val any) error { if colIdx >= len(chunk.columns) { return getError(errAPI, columnCountError(colIdx, len(chunk.columns))) @@ -42,6 +43,8 @@ func (chunk *DataChunk) SetValue(colIdx int, rowIdx int, val any) error { // Ensure that the types match before attempting to set anything. // This is done to prevent failures 'halfway through' writing column values, // potentially corrupting data in that column. + // FIXME: Can we improve efficiency here? We are casting back-and-forth to any A LOT. + // FIXME: Maybe we can make columnar insertions unsafe, i.e., we always assume a correct type. v, err := column.tryCast(val) if err != nil { return columnError(err, colIdx) diff --git a/errors.go b/errors.go index 978b555b..53ea9439 100644 --- a/errors.go +++ b/errors.go @@ -56,8 +56,6 @@ const ( ) var ( - errDriver = errors.New("internal driver error: please file a bug report") - errAPI = errors.New("API error") errVectorSize = errors.New("data chunks cannot exceed duckdb's internal vector size") diff --git a/errors_test.go b/errors_test.go index bf210982..2e23933b 100644 --- a/errors_test.go +++ b/errors_test.go @@ -56,6 +56,8 @@ func TestErrNestedMap(t *testing.T) { } func TestErrAppender(t *testing.T) { + t.Parallel() + t.Run(errAppenderInvalidCon.Error(), func(t *testing.T) { var con driver.Conn _, err := NewAppenderFromConn(con, "", "test") @@ -110,7 +112,7 @@ func TestErrAppender(t *testing.T) { c, err := NewConnector("", nil) require.NoError(t, err) - _, err = sql.OpenDB(c).Exec(`CREATE TABLE test AS SELECT MAP() AS m`) + _, err = sql.OpenDB(c).Exec(`CREATE TABLE test (int_array INTEGER[2])`) require.NoError(t, err) con, err := c.Connect(context.Background()) @@ -160,6 +162,13 @@ func TestErrAppender(t *testing.T) { require.NoError(t, con.Close()) require.NoError(t, c.Close()) }) + + t.Run(errUnsupportedMapKeyType.Error(), func(t *testing.T) { + c, con, a := prepareAppender(t, `CREATE TABLE test (m MAP(INT[], STRUCT(v INT)))`) + err := a.AppendRow(nil) + testError(t, err, errAppenderAppendRow.Error(), errUnsupportedMapKeyType.Error()) + cleanupAppender(t, c, con, a) + }) } func TestErrAppend(t *testing.T) { @@ -173,6 +182,24 @@ func TestErrAppend(t *testing.T) { cleanupAppender(t, c, con, a) } +func TestErrAppendDecimal(t *testing.T) { + c, con, a := prepareAppender(t, `CREATE TABLE test (d DECIMAL(8, 2))`) + + err := a.AppendRow(Decimal{Width: 9, Scale: 2}) + testError(t, err, errAppenderAppendRow.Error(), castErrMsg) + err = a.AppendRow(Decimal{Width: 8, Scale: 3}) + testError(t, err, errAppenderAppendRow.Error(), castErrMsg) + + cleanupAppender(t, c, con, a) +} + +func TestErrAppendEnum(t *testing.T) { + c, con, a := prepareAppender(t, testTypesEnumSQL+";"+`CREATE TABLE test (e my_enum)`) + err := a.AppendRow("3") + testError(t, err, errAppenderAppendRow.Error(), castErrMsg) + cleanupAppender(t, c, con, a) +} + func TestErrAppendSimpleStruct(t *testing.T) { c, con, a := prepareAppender(t, ` CREATE TABLE test ( diff --git a/rows.go b/rows.go index 2cefdf73..8c986170 100644 --- a/rows.go +++ b/rows.go @@ -95,12 +95,6 @@ func scanValue(vector C.duckdb_vector, rowIdx C.idx_t) (any, error) { } func scan(vector C.duckdb_vector, rowIdx C.idx_t) (any, error) { - // FIXME: implement support for these types: - // DUCKDB_TYPE_UHUGEINT - // DUCKDB_TYPE_UNION - // DUCKDB_TYPE_BIT - // DUCKDB_TYPE_TIME_TZ - validity := C.duckdb_vector_get_validity(vector) if !C.duckdb_validity_row_is_valid(validity, rowIdx) { return nil, nil @@ -389,7 +383,7 @@ func scanDecimal(ty C.duckdb_logical_type, vector C.duckdb_vector, rowIdx C.idx_ case C.DUCKDB_TYPE_INTEGER: nativeValue = big.NewInt(int64(get[int32](vector, rowIdx))) case C.DUCKDB_TYPE_BIGINT: - nativeValue = big.NewInt(int64(get[int64](vector, rowIdx))) + nativeValue = big.NewInt(get[int64](vector, rowIdx)) case C.DUCKDB_TYPE_HUGEINT: i := get[C.duckdb_hugeint](vector, rowIdx) nativeValue = hugeIntToNative(C.duckdb_hugeint{ diff --git a/types.go b/types.go index bd2997b6..b698d350 100644 --- a/types.go +++ b/types.go @@ -13,15 +13,11 @@ import ( "github.com/mitchellh/mapstructure" ) -var unsupportedAppenderTypeMap = map[C.duckdb_type]string{ +// FIXME: Implement support for these types. +var unsupportedTypeMap = map[C.duckdb_type]string{ C.DUCKDB_TYPE_INVALID: "INVALID", - C.DUCKDB_TYPE_TIME: "TIME", - C.DUCKDB_TYPE_INTERVAL: "INTERVAL", - C.DUCKDB_TYPE_HUGEINT: "HUGEINT", C.DUCKDB_TYPE_UHUGEINT: "UHUGEINT", - C.DUCKDB_TYPE_DECIMAL: "DECIMAL", - C.DUCKDB_TYPE_ENUM: "ENUM", - C.DUCKDB_TYPE_MAP: "MAP", + C.DUCKDB_TYPE_ARRAY: "ARRAY", C.DUCKDB_TYPE_UNION: "UNION", C.DUCKDB_TYPE_BIT: "BIT", C.DUCKDB_TYPE_TIME_TZ: "TIME_TZ", @@ -139,6 +135,14 @@ func (m *Map) Scan(v any) error { return nil } +func mapKeysField() string { + return "key" +} + +func mapValuesField() string { + return "value" +} + type Interval struct { Days int32 `json:"days"` Months int32 `json:"months"` @@ -172,3 +176,7 @@ func (d *Decimal) Float64() float64 { f, _ := value.Float64() return f } + +func (d *Decimal) toString() string { + return fmt.Sprintf("DECIMAL(%d,%d)", d.Width, d.Scale) +} diff --git a/types_test.go b/types_test.go index 46d501e7..e0219c0e 100644 --- a/types_test.go +++ b/types_test.go @@ -1,6 +1,7 @@ package duckdb import ( + "context" "database/sql" "fmt" "math/big" @@ -12,6 +13,9 @@ import ( "github.com/stretchr/testify/require" ) +// First, this test inserts all types (except UUID and DECIMAL) with the Appender. +// Then, it tests scanning these types. + type testTypesEnum string const testTypesEnumSQL = `CREATE TYPE my_enum AS ENUM ('0', '1', '2')` @@ -21,7 +25,6 @@ type testTypesStruct struct { B string } -// NOTE: Includes all supported types except UUID and DECIMAL. type testTypesRow struct { Boolean_col bool Tinyint_col int8 @@ -38,7 +41,7 @@ type testTypesRow struct { Date_col time.Time Time_col time.Time Interval_col Interval - Hugeint_col Composite[big.Int] + Hugeint_col *big.Int Varchar_col string Blob_col []byte Timestamp_s_col time.Time @@ -51,7 +54,7 @@ type testTypesRow struct { Timestamp_tz_col time.Time } -const testTypesTableSQL = `CREATE TABLE types_tbl ( +const testTypesTableSQL = `CREATE TABLE test ( Boolean_col BOOLEAN, Tinyint_col TINYINT, Smallint_col SMALLINT, @@ -80,44 +83,27 @@ const testTypesTableSQL = `CREATE TABLE types_tbl ( Timestamp_tz_col TIMESTAMPTZ )` -// We could perform the insertions via the testTypesRow struct by iterating over the rows, -// but that significantly decreases performance. -const testTypesInsertSQL = `INSERT INTO types_tbl SELECT - r % 2 AS Boolean_col, - r % 127 AS Tinyint_col, - r % 32767 AS Smallint_col, - 2147483647 - r AS Integer_col, - 9223372036854775807 - r AS Bigint_col, - r % 256 AS Utinyint_col, - r % 65535 AS Usmallint_col, - 2147483647 - r AS Uinteger_col, - 9223372036854775807 - r AS Ubigint_col, - r AS Float_col, - r AS Double_col, - '1992-09-20 11:30:00'::TIMESTAMP AS Timestamp_col, - '1992-09-20'::DATE AS Date_col, - '11:42:07'::TIME AS Time_col, - INTERVAL (r) MONTH AS Interval_col, - r AS Hugeint_col, - repeat('hello!', r) AS Varchar_col, - 'AB' AS Blob_col, - '1992-09-20 11:30:00'::TIMESTAMP_S AS Timestamp_s_col, - '1992-09-20 11:30:00'::TIMESTAMP_MS AS Timestamp_ms_col, - '1992-09-20 11:30:00'::TIMESTAMP_NS AS Timestamp_ns_col, - (r % 3)::VARCHAR AS Enum_col, - list_value(r) AS List_col, - ROW(r, 'a' || r) AS Struct_col, - MAP{r: 'other_longer_val'} AS Map_col, - '1992-09-20 11:30:00'::TIMESTAMPTZ AS Timestamp_tz_col -FROM range(?) tbl(r)` - -func testTypesGenerateRow(i int) testTypesRow { - ts := time.Date(1992, 9, 20, 11, 30, 0, 0, time.UTC) - date := time.Date(1992, 9, 20, 0, 0, 0, 0, time.UTC) - t := time.Date(1970, 1, 1, 11, 42, 7, 0, time.UTC) - hugeintCol := Composite[big.Int]{ - *big.NewInt(int64(i)), - } +func (r *testTypesRow) toUTC() { + r.Timestamp_col = r.Timestamp_col.UTC() + r.Timestamp_s_col = r.Timestamp_s_col.UTC() + r.Timestamp_ms_col = r.Timestamp_ms_col.UTC() + r.Timestamp_ns_col = r.Timestamp_ns_col.UTC() + r.Timestamp_tz_col = r.Timestamp_tz_col.UTC() +} + +func testTypesGenerateRow[T require.TestingT](t T, i int) testTypesRow { + // Get the timestamp for all TS columns. + IST, err := time.LoadLocation("Asia/Kolkata") + require.NoError(t, err) + + const longForm = "2006-01-02 15:04:05 MST" + ts, err := time.ParseInLocation(longForm, "2016-01-17 20:04:05 IST", IST) + require.NoError(t, err) + + // Get the DATE and TIME column values. + dateUTC := time.Date(1992, 9, 20, 0, 0, 0, 0, time.UTC) + timeUTC := time.Date(1970, 1, 1, 11, 42, 7, 0, time.UTC) + varcharCol := "" for j := 0; j < i; j++ { varcharCol += "hello!" @@ -145,10 +131,10 @@ func testTypesGenerateRow(i int) testTypesRow { float32(i), float64(i), ts, - date, - t, + dateUTC, + timeUTC, Interval{Days: 0, Months: int32(i), Micros: 0}, - hugeintCol, + big.NewInt(int64(i)), varcharCol, []byte{'A', 'B'}, ts, @@ -162,13 +148,60 @@ func testTypesGenerateRow(i int) testTypesRow { } } -func testTypesQuery(db *sql.DB) ([]testTypesRow, error) { - res, err := db.Query(`SELECT * FROM types_tbl ORDER BY Smallint_col`) - if err != nil { - return nil, err +func testTypesGenerateRows[T require.TestingT](t T, rowCount int) []testTypesRow { + var expectedRows []testTypesRow + for i := 0; i < rowCount; i++ { + r := testTypesGenerateRow(t, i) + expectedRows = append(expectedRows, r) } + return expectedRows +} + +func testTypesReset[T require.TestingT](t T, c *Connector) { + _, err := sql.OpenDB(c).ExecContext(context.Background(), `DELETE FROM test`) + require.NoError(t, err) +} + +func testTypes[T require.TestingT](t T, c *Connector, a *Appender, expectedRows []testTypesRow) []testTypesRow { + // Append the rows. We cannot append Composite types. + for i := 0; i < len(expectedRows); i++ { + r := &expectedRows[i] + err := a.AppendRow( + r.Boolean_col, + r.Tinyint_col, + r.Smallint_col, + r.Integer_col, + r.Bigint_col, + r.Utinyint_col, + r.Usmallint_col, + r.Uinteger_col, + r.Ubigint_col, + r.Float_col, + r.Double_col, + r.Timestamp_col, + r.Date_col, + r.Time_col, + r.Interval_col, + r.Hugeint_col, + r.Varchar_col, + r.Blob_col, + r.Timestamp_s_col, + r.Timestamp_ms_col, + r.Timestamp_ns_col, + string(r.Enum_col), + r.List_col.Get(), + r.Struct_col.Get(), + r.Map_col, + r.Timestamp_tz_col) + require.NoError(t, err) + } + require.NoError(t, a.Flush()) + + res, err := sql.OpenDB(c).QueryContext(context.Background(), `SELECT * FROM test ORDER BY Smallint_col`) + require.NoError(t, err) - var slice []testTypesRow + // Scan the rows. + var actualRows []testTypesRow for res.Next() { var r testTypesRow err = res.Scan( @@ -198,36 +231,48 @@ func testTypesQuery(db *sql.DB) ([]testTypesRow, error) { &r.Struct_col, &r.Map_col, &r.Timestamp_tz_col) - if err != nil { - return slice, err - } - slice = append(slice, r) + require.NoError(t, err) + actualRows = append(actualRows, r) } - return slice, res.Close() + + require.NoError(t, err) + require.Equal(t, len(expectedRows), len(actualRows)) + return actualRows } func TestTypes(t *testing.T) { t.Parallel() - db := openDB(t) - rowCount := 3 + expectedRows := testTypesGenerateRows(t, 3) + c, con, a := prepareAppender(t, testTypesEnumSQL+";"+testTypesTableSQL) + actualRows := testTypes(t, c, a, expectedRows) - _, err := db.Exec(testTypesEnumSQL) - require.NoError(t, err) - createTable(db, t, testTypesTableSQL) + for i := range actualRows { + expectedRows[i].toUTC() + require.Equal(t, expectedRows[i], actualRows[i]) + } - _, err = db.Exec(testTypesInsertSQL, rowCount) - require.NoError(t, err) + require.Equal(t, len(expectedRows), len(actualRows)) + cleanupAppender(t, c, con, a) +} - slice, err := testTypesQuery(db) - require.NoError(t, err) - require.Equal(t, rowCount, len(slice)) +// NOTE: go-duckdb only contains very few benchmarks. The purpose of those benchmarks is to avoid regressions +// of its main functionalities. I.e., functions related to implementing the database/sql interface. +var benchmarkTypesResult []testTypesRow - for i, r := range slice { - expected := testTypesGenerateRow(i) - require.Equal(t, expected, r) +func BenchmarkTypes(b *testing.B) { + expectedRows := testTypesGenerateRows(b, GetDataChunkCapacity()*3+10) + c, con, a := prepareAppender(b, testTypesEnumSQL+";"+testTypesTableSQL) + + var r []testTypesRow + b.ResetTimer() + for n := 0; n < b.N; n++ { + r = testTypes(b, c, a, expectedRows) + testTypesReset(b, c) } - require.NoError(t, db.Close()) + // Ensure that the compiler does not eliminate the line by storing the result. + benchmarkTypesResult = r + cleanupAppender(b, c, con, a) } func compareDecimal(t *testing.T, want Decimal, got Decimal) { @@ -595,26 +640,3 @@ func TestInterval(t *testing.T) { require.NoError(t, db.Close()) } - -// NOTE: go-duckdb only contains very few benchmarks. The purpose of those benchmarks is to avoid regressions -// of its main functionalities. I.e., functions related to implementing the database/sql interface. - -func BenchmarkTypes(b *testing.B) { - db, err := sql.Open("duckdb", "") - require.NoError(b, err) - rowCount := 10000 - - _, err = db.Exec(testTypesEnumSQL) - require.NoError(b, err) - _, err = db.Exec(testTypesTableSQL) - require.NoError(b, err) - _, err = db.Exec(testTypesInsertSQL, rowCount) - require.NoError(b, err) - - for n := 0; n < b.N; n++ { - _, err = testTypesQuery(db) - require.NoError(b, err) - } - - require.NoError(b, db.Close()) -} diff --git a/vector.go b/vector.go index ee2f2c0b..e7e1d57c 100644 --- a/vector.go +++ b/vector.go @@ -7,6 +7,7 @@ package duckdb import "C" import ( + "math/big" "reflect" "strconv" "time" @@ -21,10 +22,17 @@ type vector struct { setFn fnSetVectorValue // The data type of the vector. duckdbType C.duckdb_type - // The child names of STRUCT vectors. - childNames []string // The child vectors of nested data types. childVectors []vector + + // The child names of STRUCT vectors. + childNames []string + // The dictionary for ENUM types. + dict map[string]uint32 + // The width of DECIMAL types. + width uint8 + // The scale of DECIMAL types. + scale uint8 } func (vec *vector) tryCast(val any) (any, error) { @@ -33,44 +41,67 @@ func (vec *vector) tryCast(val any) (any, error) { } switch vec.duckdbType { - case C.DUCKDB_TYPE_UTINYINT: - return tryNumericCast[uint8](val, reflect.Uint8.String()) + case C.DUCKDB_TYPE_INVALID: + return nil, unsupportedTypeError(duckdbTypeMap[vec.duckdbType]) + case C.DUCKDB_TYPE_BOOLEAN: + return tryPrimitiveCast[bool](val, reflect.Bool.String()) case C.DUCKDB_TYPE_TINYINT: return tryNumericCast[int8](val, reflect.Int8.String()) - case C.DUCKDB_TYPE_USMALLINT: - return tryNumericCast[uint16](val, reflect.Uint16.String()) case C.DUCKDB_TYPE_SMALLINT: return tryNumericCast[int16](val, reflect.Int16.String()) - case C.DUCKDB_TYPE_UINTEGER: - return tryNumericCast[uint32](val, reflect.Uint32.String()) case C.DUCKDB_TYPE_INTEGER: return tryNumericCast[int32](val, reflect.Int32.String()) - case C.DUCKDB_TYPE_UBIGINT: - return tryNumericCast[uint64](val, reflect.Uint64.String()) case C.DUCKDB_TYPE_BIGINT: return tryNumericCast[int64](val, reflect.Int64.String()) + case C.DUCKDB_TYPE_UTINYINT: + return tryNumericCast[uint8](val, reflect.Uint8.String()) + case C.DUCKDB_TYPE_USMALLINT: + return tryNumericCast[uint16](val, reflect.Uint16.String()) + case C.DUCKDB_TYPE_UINTEGER: + return tryNumericCast[uint32](val, reflect.Uint32.String()) + case C.DUCKDB_TYPE_UBIGINT: + return tryNumericCast[uint64](val, reflect.Uint64.String()) case C.DUCKDB_TYPE_FLOAT: return tryNumericCast[float32](val, reflect.Float32.String()) case C.DUCKDB_TYPE_DOUBLE: return tryNumericCast[float64](val, reflect.Float64.String()) - case C.DUCKDB_TYPE_BOOLEAN: - return tryPrimitiveCast[bool](val, reflect.Bool.String()) + case C.DUCKDB_TYPE_TIMESTAMP, C.DUCKDB_TYPE_TIMESTAMP_S, C.DUCKDB_TYPE_TIMESTAMP_MS, + C.DUCKDB_TYPE_TIMESTAMP_NS, C.DUCKDB_TYPE_TIMESTAMP_TZ, C.DUCKDB_TYPE_DATE, C.DUCKDB_TYPE_TIME: + return tryPrimitiveCast[time.Time](val, reflect.TypeOf(time.Time{}).String()) + case C.DUCKDB_TYPE_INTERVAL: + return tryPrimitiveCast[Interval](val, reflect.TypeOf(Interval{}).String()) + case C.DUCKDB_TYPE_HUGEINT: + // Note that this expects *big.Int. + return tryPrimitiveCast[*big.Int](val, reflect.TypeOf(big.Int{}).String()) + case C.DUCKDB_TYPE_UHUGEINT: + return nil, unsupportedTypeError(duckdbTypeMap[vec.duckdbType]) case C.DUCKDB_TYPE_VARCHAR: return tryPrimitiveCast[string](val, reflect.String.String()) case C.DUCKDB_TYPE_BLOB: return tryPrimitiveCast[[]byte](val, reflect.TypeOf([]byte{}).String()) - case C.DUCKDB_TYPE_TIMESTAMP, C.DUCKDB_TYPE_TIMESTAMP_S, C.DUCKDB_TYPE_TIMESTAMP_MS, - C.DUCKDB_TYPE_TIMESTAMP_NS, C.DUCKDB_TYPE_TIMESTAMP_TZ, C.DUCKDB_TYPE_DATE: - return tryPrimitiveCast[time.Time](val, reflect.TypeOf(time.Time{}).String()) - case C.DUCKDB_TYPE_UUID: - return tryPrimitiveCast[UUID](val, reflect.TypeOf(UUID{}).String()) + case C.DUCKDB_TYPE_DECIMAL: + return vec.tryCastDecimal(val) + case C.DUCKDB_TYPE_ENUM: + return vec.tryCastEnum(val) case C.DUCKDB_TYPE_LIST: return vec.tryCastList(val) case C.DUCKDB_TYPE_STRUCT: return vec.tryCastStruct(val) + case C.DUCKDB_TYPE_MAP: + return tryPrimitiveCast[Map](val, reflect.TypeOf(Map{}).String()) + case C.DUCKDB_TYPE_ARRAY: + return nil, unsupportedTypeError(duckdbTypeMap[vec.duckdbType]) + case C.DUCKDB_TYPE_UUID: + return tryPrimitiveCast[UUID](val, reflect.TypeOf(UUID{}).String()) + case C.DUCKDB_TYPE_UNION: + return nil, unsupportedTypeError(duckdbTypeMap[vec.duckdbType]) + case C.DUCKDB_TYPE_BIT: + return nil, unsupportedTypeError(duckdbTypeMap[vec.duckdbType]) + case C.DUCKDB_TYPE_TIME_TZ: + return nil, unsupportedTypeError(duckdbTypeMap[vec.duckdbType]) + default: + return nil, unsupportedTypeError("unknown type") } - - return nil, getError(errDriver, nil) } func (*vector) canNil(val reflect.Value) bool { @@ -109,6 +140,34 @@ func tryNumericCast[T numericType](val any, expected string) (T, error) { return v, castError(goType.String(), expected) } +func (vec *vector) tryCastDecimal(val any) (Decimal, error) { + v, ok := val.(Decimal) + if !ok { + goType := reflect.TypeOf(val) + return v, castError(goType.String(), reflect.TypeOf(Decimal{}).String()) + } + + if v.Width != vec.width || v.Scale != vec.scale { + d := Decimal{Width: vec.width, Scale: vec.scale} + return v, castError(d.toString(), v.toString()) + } + return v, nil +} + +func (vec *vector) tryCastEnum(val any) (string, error) { + v, ok := val.(string) + if !ok { + goType := reflect.TypeOf(val) + return v, castError(goType.String(), reflect.String.String()) + } + + _, ok = vec.dict[v] + if !ok { + return v, castError(v, "ENUM value") + } + return v, nil +} + func (vec *vector) tryCastList(val any) ([]any, error) { goType := reflect.TypeOf(val) if goType.Kind() != reflect.Slice { @@ -215,25 +274,25 @@ func (vec *vector) init(logicalType C.duckdb_logical_type, colIdx int) error { case C.DUCKDB_TYPE_DATE: vec.initDate() case C.DUCKDB_TYPE_TIME: - return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) + vec.initTime() case C.DUCKDB_TYPE_INTERVAL: - return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) + vec.initInterval() case C.DUCKDB_TYPE_HUGEINT: - return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) + vec.initHugeint() case C.DUCKDB_TYPE_UHUGEINT: return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) case C.DUCKDB_TYPE_VARCHAR, C.DUCKDB_TYPE_BLOB: vec.initCString(duckdbType) case C.DUCKDB_TYPE_DECIMAL: - return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) + return vec.initDecimal(logicalType, colIdx) case C.DUCKDB_TYPE_ENUM: - return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) + return vec.initEnum(logicalType, colIdx) case C.DUCKDB_TYPE_LIST: return vec.initList(logicalType, colIdx) case C.DUCKDB_TYPE_STRUCT: return vec.initStruct(logicalType, colIdx) case C.DUCKDB_TYPE_MAP: - return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) + return vec.initMap(logicalType, colIdx) case C.DUCKDB_TYPE_ARRAY: return columnError(unsupportedTypeError(duckdbTypeMap[duckdbType]), colIdx) case C.DUCKDB_TYPE_UUID: @@ -253,7 +312,7 @@ func (vec *vector) init(logicalType C.duckdb_logical_type, colIdx int) error { func (vec *vector) getChildVectors(vector C.duckdb_vector) { switch vec.duckdbType { - case C.DUCKDB_TYPE_LIST: + case C.DUCKDB_TYPE_LIST, C.DUCKDB_TYPE_MAP: child := C.duckdb_list_vector_get_child(vector) vec.childVectors[0].duckdbVector = child vec.childVectors[0].getChildVectors(child) @@ -273,7 +332,7 @@ func initPrimitive[T any](vec *vector, duckdbType C.duckdb_type) { vec.setNull(rowIdx) return } - setPrimitive[T](vec, rowIdx, val.(T)) + setPrimitive(vec, rowIdx, val.(T)) } vec.duckdbType = duckdbType } @@ -300,6 +359,39 @@ func (vec *vector) initDate() { vec.duckdbType = C.DUCKDB_TYPE_DATE } +func (vec *vector) initTime() { + vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { + if val == nil { + vec.setNull(rowIdx) + return + } + vec.setTime(rowIdx, val) + } + vec.duckdbType = C.DUCKDB_TYPE_TIME +} + +func (vec *vector) initInterval() { + vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { + if val == nil { + vec.setNull(rowIdx) + return + } + vec.setInterval(rowIdx, val) + } + vec.duckdbType = C.DUCKDB_TYPE_INTERVAL +} + +func (vec *vector) initHugeint() { + vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { + if val == nil { + vec.setNull(rowIdx) + return + } + vec.setHugeint(rowIdx, val) + } + vec.duckdbType = C.DUCKDB_TYPE_HUGEINT +} + func (vec *vector) initCString(duckdbType C.duckdb_type) { vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { if val == nil { @@ -311,6 +403,57 @@ func (vec *vector) initCString(duckdbType C.duckdb_type) { vec.duckdbType = duckdbType } +func (vec *vector) initDecimal(logicalType C.duckdb_logical_type, colIdx int) error { + vec.width = uint8(C.duckdb_decimal_width(logicalType)) + vec.scale = uint8(C.duckdb_decimal_scale(logicalType)) + + internalType := C.duckdb_decimal_internal_type(logicalType) + switch internalType { + case C.DUCKDB_TYPE_SMALLINT, C.DUCKDB_TYPE_INTEGER, C.DUCKDB_TYPE_BIGINT, C.DUCKDB_TYPE_HUGEINT: + vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { + if val == nil { + vec.setNull(rowIdx) + return + } + vec.setDecimal(internalType, rowIdx, val) + } + default: + return columnError(unsupportedTypeError(duckdbTypeMap[internalType]), colIdx) + } + + vec.duckdbType = C.DUCKDB_TYPE_DECIMAL + return nil +} + +func (vec *vector) initEnum(logicalType C.duckdb_logical_type, colIdx int) error { + // Initialize the dictionary. + dictSize := uint32(C.duckdb_enum_dictionary_size(logicalType)) + vec.dict = make(map[string]uint32) + for i := uint32(0); i < dictSize; i++ { + cStr := C.duckdb_enum_dictionary_value(logicalType, C.idx_t(i)) + str := C.GoString(cStr) + vec.dict[str] = i + C.duckdb_free(unsafe.Pointer(cStr)) + } + + internalType := C.duckdb_enum_internal_type(logicalType) + switch internalType { + case C.DUCKDB_TYPE_UTINYINT, C.DUCKDB_TYPE_USMALLINT, C.DUCKDB_TYPE_UINTEGER, C.DUCKDB_TYPE_UBIGINT: + vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { + if val == nil { + vec.setNull(rowIdx) + return + } + vec.setEnum(internalType, rowIdx, val) + } + default: + return columnError(unsupportedTypeError(duckdbTypeMap[internalType]), colIdx) + } + + vec.duckdbType = C.DUCKDB_TYPE_ENUM + return nil +} + func (vec *vector) initList(logicalType C.duckdb_logical_type, colIdx int) error { // Get the child vector type. childType := C.duckdb_list_type_child_type(logicalType) @@ -368,13 +511,49 @@ func (vec *vector) initStruct(logicalType C.duckdb_logical_type, colIdx int) err return nil } +func (vec *vector) initMap(logicalType C.duckdb_logical_type, colIdx int) error { + // A MAP is a LIST of STRUCT values. Each STRUCT holds two children: a key and a value. + + // Get the child vector type. + childType := C.duckdb_list_type_child_type(logicalType) + defer C.duckdb_destroy_logical_type(&childType) + + // Recurse into the child. + vec.childVectors = make([]vector, 1) + err := vec.childVectors[0].init(childType, colIdx) + if err != nil { + return err + } + + // DuckDB supports more MAP key types than Go, which only supports comparable types. + // We ensure that the key type itself is comparable. + keyType := C.duckdb_map_type_key_type(logicalType) + defer C.duckdb_destroy_logical_type(&keyType) + + duckdbKeyType := C.duckdb_get_type_id(keyType) + switch duckdbKeyType { + case C.DUCKDB_TYPE_LIST, C.DUCKDB_TYPE_STRUCT, C.DUCKDB_TYPE_MAP, C.DUCKDB_TYPE_ARRAY: + return columnError(errUnsupportedMapKeyType, colIdx) + } + + vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { + if val == nil { + vec.setNull(rowIdx) + return + } + vec.setMap(rowIdx, val) + } + vec.duckdbType = C.DUCKDB_TYPE_MAP + return nil +} + func (vec *vector) initUUID() { vec.setFn = func(vec *vector, rowIdx C.idx_t, val any) { if val == nil { vec.setNull(rowIdx) return } - setPrimitive[C.duckdb_hugeint](vec, rowIdx, uuidToHugeInt(val.(UUID))) + setPrimitive(vec, rowIdx, uuidToHugeInt(val.(UUID))) } vec.duckdbType = C.DUCKDB_TYPE_UUID } diff --git a/vector_setters.go b/vector_setters.go index 70da14cf..440c1bd5 100644 --- a/vector_setters.go +++ b/vector_setters.go @@ -7,6 +7,7 @@ package duckdb import "C" import ( + "math/big" "time" "unsafe" ) @@ -53,7 +54,7 @@ func (vec *vector) setTS(duckdbType C.duckdb_type, rowIdx C.idx_t, val any) { var ts C.duckdb_timestamp ts.micros = C.int64_t(ticks) - setPrimitive[C.duckdb_timestamp](vec, rowIdx, ts) + setPrimitive(vec, rowIdx, ts) } func (vec *vector) setDate(rowIdx C.idx_t, val any) { @@ -63,7 +64,31 @@ func (vec *vector) setDate(rowIdx C.idx_t, val any) { var date C.duckdb_date date.days = C.int32_t(days) - setPrimitive[C.duckdb_date](vec, rowIdx, date) + setPrimitive(vec, rowIdx, date) +} + +func (vec *vector) setTime(rowIdx C.idx_t, val any) { + v := val.(time.Time) + ticks := v.UTC().UnixMicro() + + var t C.duckdb_time + t.micros = C.int64_t(ticks) + setPrimitive(vec, rowIdx, t) +} + +func (vec *vector) setInterval(rowIdx C.idx_t, val any) { + v := val.(Interval) + var interval C.duckdb_interval + interval.days = C.int32_t(v.Days) + interval.months = C.int32_t(v.Months) + interval.micros = C.int64_t(v.Micros) + setPrimitive(vec, rowIdx, interval) +} + +func (vec *vector) setHugeint(rowIdx C.idx_t, val any) { + v := val.(*big.Int) + hugeInt, _ := hugeIntFromNative(v) + setPrimitive(vec, rowIdx, hugeInt) } func (vec *vector) setCString(rowIdx C.idx_t, val any) { @@ -80,6 +105,37 @@ func (vec *vector) setCString(rowIdx C.idx_t, val any) { C.free(unsafe.Pointer(cStr)) } +func (vec *vector) setDecimal(internalType C.duckdb_type, rowIdx C.idx_t, val any) { + v := val.(Decimal) + + switch internalType { + case C.DUCKDB_TYPE_SMALLINT: + setPrimitive(vec, rowIdx, int16(v.Value.Int64())) + case C.DUCKDB_TYPE_INTEGER: + setPrimitive(vec, rowIdx, int32(v.Value.Int64())) + case C.DUCKDB_TYPE_BIGINT: + setPrimitive(vec, rowIdx, v.Value.Int64()) + case C.DUCKDB_TYPE_HUGEINT: + value, _ := hugeIntFromNative(v.Value) + setPrimitive(vec, rowIdx, value) + } +} + +func (vec *vector) setEnum(internalType C.duckdb_type, rowIdx C.idx_t, val any) { + v := vec.dict[val.(string)] + + switch internalType { + case C.DUCKDB_TYPE_UTINYINT: + setPrimitive(vec, rowIdx, uint8(v)) + case C.DUCKDB_TYPE_USMALLINT: + setPrimitive(vec, rowIdx, uint16(v)) + case C.DUCKDB_TYPE_UINTEGER: + setPrimitive(vec, rowIdx, v) + case C.DUCKDB_TYPE_UBIGINT: + setPrimitive(vec, rowIdx, uint64(v)) + } +} + func (vec *vector) setList(rowIdx C.idx_t, val any) { list := val.([]any) childVectorSize := C.duckdb_list_vector_get_size(vec.duckdbVector) @@ -89,7 +145,7 @@ func (vec *vector) setList(rowIdx C.idx_t, val any) { offset: C.idx_t(childVectorSize), length: C.idx_t(len(list)), } - setPrimitive[C.duckdb_list_entry](vec, rowIdx, listEntry) + setPrimitive(vec, rowIdx, listEntry) newLength := C.idx_t(len(list)) + childVectorSize C.duckdb_list_vector_set_size(vec.duckdbVector, newLength) @@ -111,3 +167,17 @@ func (vec *vector) setStruct(rowIdx C.idx_t, val any) { childVector.setFn(childVector, rowIdx, m[childName]) } } + +func (vec *vector) setMap(rowIdx C.idx_t, val any) { + m := val.(Map) + + // Create a LIST of STRUCT values. + i := 0 + list := make([]any, len(m)) + for key, value := range m { + list[i] = map[string]any{mapKeysField(): key, mapValuesField(): value} + i++ + } + + vec.setList(rowIdx, list) +}