Skip to content
Merged
188 changes: 188 additions & 0 deletions go/array_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright (c) 2025 ADBC Drivers Contributors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's now well into 2026 :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha. Removed the test completely.

//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package trino

import (
"reflect"
"testing"

sqlwrapper "github.com/adbc-drivers/driverbase-go/sqlwrapper"
"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/array"
"github.com/apache/arrow-go/v18/arrow/memory"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
trinoClient "github.com/trinodb/trino-go-client/trino"
)

func newTestTypeConverter() *trinoTypeConverter {
return &trinoTypeConverter{
DefaultTypeConverter: sqlwrapper.DefaultTypeConverter{VendorName: "Trino"},
}
}

func TestScanTypeToListType(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test effectively just hardcodes the map in a second place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed array_test.go completely. TestSelect covers the functionality end to end

tc := newTestTypeConverter()

tests := []struct {
name string
scanType reflect.Type
want arrow.DataType
}{
{"NullSliceString", reflect.TypeFor[trinoClient.NullSliceString](), arrow.ListOf(arrow.BinaryTypes.String)},
{"NullSliceInt64", reflect.TypeFor[trinoClient.NullSliceInt64](), arrow.ListOf(arrow.PrimitiveTypes.Int64)},
{"NullSliceFloat64", reflect.TypeFor[trinoClient.NullSliceFloat64](), arrow.ListOf(arrow.PrimitiveTypes.Float64)},
{"NullSliceBool", reflect.TypeFor[trinoClient.NullSliceBool](), arrow.ListOf(arrow.FixedWidthTypes.Boolean)},
{"NullSlice2String", reflect.TypeFor[trinoClient.NullSlice2String](), arrow.ListOf(arrow.ListOf(arrow.BinaryTypes.String))},
{"NullSlice3Int64", reflect.TypeFor[trinoClient.NullSlice3Int64](), arrow.ListOf(arrow.ListOf(arrow.ListOf(arrow.PrimitiveTypes.Int64)))},
{"non-array type returns nil", reflect.TypeFor[int](), nil},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tc.scanTypeToListType(tt.scanType)
if tt.want == nil {
assert.Nil(t, got)
} else {
assert.True(t, arrow.TypeEqual(tt.want, got), "want %s, got %s", tt.want, got)
}
})
}
}

func TestConvertRawColumnType_Array(t *testing.T) {
tc := newTestTypeConverter()

tests := []struct {
name string
colType sqlwrapper.ColumnType
wantType arrow.DataType
}{
{
name: "ARRAY(VARCHAR) via ScanType",
colType: sqlwrapper.ColumnType{
Name: "arr_col",
DatabaseTypeName: "ARRAY(VARCHAR(1))",
ScanType: reflect.TypeFor[trinoClient.NullSliceString](),
Nullable: true,
},
wantType: arrow.ListOf(arrow.BinaryTypes.String),
},
{
name: "ARRAY(INTEGER) via ScanType",
colType: sqlwrapper.ColumnType{
Name: "int_arr",
DatabaseTypeName: "ARRAY(INTEGER)",
ScanType: reflect.TypeFor[trinoClient.NullSliceInt64](),
Nullable: true,
},
wantType: arrow.ListOf(arrow.PrimitiveTypes.Int64),
},
{
name: "ARRAY(ARRAY(INTEGER)) via ScanType",
colType: sqlwrapper.ColumnType{
Name: "nested",
DatabaseTypeName: "ARRAY(ARRAY(INTEGER))",
ScanType: reflect.TypeFor[trinoClient.NullSlice2Int64](),
Nullable: true,
},
wantType: arrow.ListOf(arrow.ListOf(arrow.PrimitiveTypes.Int64)),
},
{
name: "VARCHAR unchanged",
colType: sqlwrapper.ColumnType{
Name: "str_col",
DatabaseTypeName: "VARCHAR",
Nullable: true,
},
wantType: arrow.BinaryTypes.String,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
arrowType, _, _, err := tc.ConvertRawColumnType(tt.colType)
require.NoError(t, err)
assert.True(t, arrow.TypeEqual(tt.wantType, arrowType), "want %s, got %s", tt.wantType, arrowType)
})
}
}

func TestListInserter_StringArray(t *testing.T) {
mem := memory.NewGoAllocator()
lb := array.NewListBuilder(mem, arrow.BinaryTypes.String)
defer lb.Release()

tc := newTestTypeConverter()
elemField := arrow.Field{Name: "item", Type: arrow.BinaryTypes.String, Nullable: true}
childIns, err := tc.CreateInserter(&elemField, lb.ValueBuilder())
require.NoError(t, err)

ins := &listInserter{builder: lb, childInserter: childIns}

require.NoError(t, ins.AppendValue([]any{"a", "b", "c"}))
require.NoError(t, ins.AppendValue(nil))
require.NoError(t, ins.AppendValue([]any{"x", nil, "z"}))

arr := lb.NewListArray()
defer arr.Release()

assert.Equal(t, 3, arr.Len())
assert.False(t, arr.IsNull(0))
assert.True(t, arr.IsNull(1))
assert.False(t, arr.IsNull(2))

offsets := arr.Offsets()
assert.Equal(t, 3, int(offsets[1]-offsets[0]))
assert.Equal(t, 3, int(offsets[3]-offsets[2]))
}

func TestListInserter_IntArray(t *testing.T) {
mem := memory.NewGoAllocator()
lb := array.NewListBuilder(mem, arrow.PrimitiveTypes.Int64)
defer lb.Release()

tc := newTestTypeConverter()
elemField := arrow.Field{Name: "item", Type: arrow.PrimitiveTypes.Int64, Nullable: true}
childIns, err := tc.CreateInserter(&elemField, lb.ValueBuilder())
require.NoError(t, err)

ins := &listInserter{builder: lb, childInserter: childIns}

require.NoError(t, ins.AppendValue([]any{float64(1), float64(2), float64(3)}))
require.NoError(t, ins.AppendValue(nil))

arr := lb.NewListArray()
defer arr.Release()

assert.Equal(t, 2, arr.Len())
assert.False(t, arr.IsNull(0))
assert.True(t, arr.IsNull(1))
}

func TestCreateInserter_ListType(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure these tests are actually useful?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

mem := memory.NewGoAllocator()
listType := arrow.ListOf(arrow.BinaryTypes.String)
field := arrow.Field{Name: "arr", Type: listType, Nullable: true}
lb := array.NewListBuilder(mem, arrow.BinaryTypes.String)
defer lb.Release()

tc := newTestTypeConverter()
ins, err := tc.CreateInserter(&field, lb)
require.NoError(t, err)

_, ok := ins.(*listInserter)
assert.True(t, ok, "expected *listInserter, got %T", ins)
}
1 change: 1 addition & 0 deletions go/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ func (c *trinoConnectionImpl) GetTableSchema(ctx context.Context, catalog *strin
Name: colType.Name(),
DatabaseTypeName: colType.DatabaseTypeName(),
Nullable: true, // Assume every column in always nullable since trino go client does not provide clean way to get nullability.
ScanType: colType.ScanType(),
}

// Add precision and scale if available
Expand Down
64 changes: 64 additions & 0 deletions go/trino.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"database/sql/driver"
"fmt"
"math/big"
"reflect"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -128,10 +129,42 @@ func (m *trinoTypeConverter) ConvertRawColumnType(colType sqlwrapper.ColumnType)
return extensions.NewUUIDType(), colType.Nullable, metadata, nil
}

if colType.ScanType != nil {
if listType := m.scanTypeToListType(colType.ScanType); listType != nil {
metadata := arrow.MetadataFrom(map[string]string{
sqlwrapper.MetaKeyDatabaseTypeName: colType.DatabaseTypeName,
sqlwrapper.MetaKeyColumnName: colType.Name,
})
return listType, colType.Nullable, metadata, nil
}
}

// For all other types, fall back to default conversion
return m.DefaultTypeConverter.ConvertRawColumnType(colType)
}

var scanTypeToListMap = map[reflect.Type]arrow.DataType{
reflect.TypeFor[trino.NullSliceString](): arrow.ListOf(arrow.BinaryTypes.String),
reflect.TypeFor[trino.NullSliceInt64](): arrow.ListOf(arrow.PrimitiveTypes.Int64),
reflect.TypeFor[trino.NullSliceFloat64](): arrow.ListOf(arrow.PrimitiveTypes.Float64),
reflect.TypeFor[trino.NullSliceBool](): arrow.ListOf(arrow.FixedWidthTypes.Boolean),
reflect.TypeFor[trino.NullSliceTime](): arrow.ListOf(&arrow.TimestampType{Unit: arrow.Microsecond, TimeZone: "UTC"}),
reflect.TypeFor[trino.NullSlice2String](): arrow.ListOf(arrow.ListOf(arrow.BinaryTypes.String)),
reflect.TypeFor[trino.NullSlice2Int64](): arrow.ListOf(arrow.ListOf(arrow.PrimitiveTypes.Int64)),
reflect.TypeFor[trino.NullSlice2Float64](): arrow.ListOf(arrow.ListOf(arrow.PrimitiveTypes.Float64)),
reflect.TypeFor[trino.NullSlice2Bool](): arrow.ListOf(arrow.ListOf(arrow.FixedWidthTypes.Boolean)),
reflect.TypeFor[trino.NullSlice2Time](): arrow.ListOf(arrow.ListOf(&arrow.TimestampType{Unit: arrow.Microsecond, TimeZone: "UTC"})),
reflect.TypeFor[trino.NullSlice3String](): arrow.ListOf(arrow.ListOf(arrow.ListOf(arrow.BinaryTypes.String))),
reflect.TypeFor[trino.NullSlice3Int64](): arrow.ListOf(arrow.ListOf(arrow.ListOf(arrow.PrimitiveTypes.Int64))),
reflect.TypeFor[trino.NullSlice3Float64](): arrow.ListOf(arrow.ListOf(arrow.ListOf(arrow.PrimitiveTypes.Float64))),
reflect.TypeFor[trino.NullSlice3Bool](): arrow.ListOf(arrow.ListOf(arrow.ListOf(arrow.FixedWidthTypes.Boolean))),
reflect.TypeFor[trino.NullSlice3Time](): arrow.ListOf(arrow.ListOf(arrow.ListOf(&arrow.TimestampType{Unit: arrow.Microsecond, TimeZone: "UTC"}))),
}

func (m *trinoTypeConverter) scanTypeToListType(t reflect.Type) arrow.DataType {
return scanTypeToListMap[t]
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? Just directly index the map.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was just to cover tests. Removed, used the map directly


// Clamps precision to maximum supported value (9 fractional digits = nanoseconds)
func convertPrecisionToTimeUnit(precision int64) arrow.TimeUnit {
if precision > 9 {
Expand Down Expand Up @@ -178,12 +211,43 @@ func (m *trinoTypeConverter) CreateInserter(field *arrow.Field, builder array.Bu
default:
return nil, fmt.Errorf("unsupported interval type: %s", dbTypeName)
}
case *arrow.ListType:
lb := builder.(*array.ListBuilder)
elemField := arrow.Field{Name: "item", Type: fieldType.Elem(), Nullable: true}
childIns, err := m.CreateInserter(&elemField, lb.ValueBuilder())
if err != nil {
return nil, fmt.Errorf("failed to create child inserter for list: %w", err)
}
return &listInserter{builder: lb, childInserter: childIns}, nil
default:
// For all other types, use default inserter
return m.DefaultTypeConverter.CreateInserter(field, builder)
}
}

type listInserter struct {
builder *array.ListBuilder
childInserter sqlwrapper.Inserter
}

func (ins *listInserter) AppendValue(sqlValue any) error {
if sqlValue == nil {
ins.builder.AppendNull()
return nil
}
slice, ok := sqlValue.([]any)
if !ok {
return fmt.Errorf("expected []interface{} for list type, got %T", sqlValue)
}
ins.builder.Append(true)
for _, elem := range slice {
if err := ins.childInserter.AppendValue(elem); err != nil {
return err
}
}
return nil
}

func unwrap(val any) (any, error) {
if v, ok := val.(driver.Valuer); ok {
return v.Value()
Expand Down
95 changes: 95 additions & 0 deletions go/trino_test.go
Comment thread
lidavidm marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,101 @@ func (s *TrinoTests) TestSelect() {
}
}

func (s *TrinoTests) TestSelectArray() {
for _, testCase := range []selectCase{
{
name: "array_varchar",
query: "SELECT ARRAY['a', 'b', 'c'] AS arr",
schema: arrow.NewSchema([]arrow.Field{
{
Name: "arr",
Type: arrow.ListOf(arrow.BinaryTypes.String),
Nullable: true,
Metadata: arrow.MetadataFrom(map[string]string{
"sql.column_name": "arr",
"sql.database_type_name": "ARRAY(VARCHAR(1))",
}),
},
}, nil),
expected: `[{"arr": ["a", "b", "c"]}]`,
},
{
name: "array_integer",
query: "SELECT ARRAY[1, 2, 3] AS int_arr",
schema: arrow.NewSchema([]arrow.Field{
{
Name: "int_arr",
Type: arrow.ListOf(arrow.PrimitiveTypes.Int64),
Nullable: true,
Metadata: arrow.MetadataFrom(map[string]string{
"sql.column_name": "int_arr",
"sql.database_type_name": "ARRAY(INTEGER)",
}),
},
}, nil),
expected: `[{"int_arr": [1, 2, 3]}]`,
},
{
name: "array_nested",
query: "SELECT ARRAY[ARRAY[1,2], ARRAY[3,4]] AS nested",
schema: arrow.NewSchema([]arrow.Field{
{
Name: "nested",
Type: arrow.ListOf(arrow.ListOf(arrow.PrimitiveTypes.Int64)),
Nullable: true,
Metadata: arrow.MetadataFrom(map[string]string{
"sql.column_name": "nested",
"sql.database_type_name": "ARRAY(ARRAY(INTEGER))",
}),
},
}, nil),
expected: `[{"nested": [[1, 2], [3, 4]]}]`,
},
{
name: "null_array",
query: "SELECT CAST(NULL AS ARRAY(VARCHAR)) AS null_arr",
schema: arrow.NewSchema([]arrow.Field{
{
Name: "null_arr",
Type: arrow.ListOf(arrow.BinaryTypes.String),
Nullable: true,
Metadata: arrow.MetadataFrom(map[string]string{
"sql.column_name": "null_arr",
"sql.database_type_name": "ARRAY(VARCHAR)",
}),
},
}, nil),
expected: `[{"null_arr": null}]`,
},
} {
s.Run(testCase.name, func() {
s.NoError(s.stmt.SetSqlQuery(s.ctx, testCase.query))

rdr, rows, err := s.stmt.ExecuteQuery(s.ctx)
s.NoError(err)
if rdr != nil {
defer rdr.Release()
}

s.Truef(testCase.schema.Equal(rdr.Schema()), "expected: %s\ngot: %s", testCase.schema, rdr.Schema())
s.Equal(int64(-1), rows)
s.Truef(rdr.Next(), "no record, error? %s", rdr.Err())

expectedRecord, _, err := array.RecordFromJSON(s.Quirks.Alloc(), testCase.schema, bytes.NewReader([]byte(testCase.expected)))
s.NoError(err)
defer expectedRecord.Release()

rec := rdr.RecordBatch()
s.NotNil(rec)

s.Truef(array.RecordEqual(expectedRecord, rec), "expected: %s\ngot: %s", expectedRecord, rec)

s.False(rdr.Next())
s.NoError(rdr.Err())
})
}
}

type TrinoTestSuite struct {
suite.Suite
dsn string
Expand Down