Skip to content

Supports scanning of Array, IPv4, IPv6, and Map types into Go values that implement the sql.Scanner interface. #1570

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/column/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
package column

import (
"database/sql"
"fmt"
"github.com/ClickHouse/ch-go/proto"
"reflect"
"strings"
"time"
)

var scanTypeAny = reflect.TypeOf((*interface{})(nil)).Elem()

type offset struct {
values UInt64
scanType reflect.Type
Expand Down Expand Up @@ -268,6 +271,13 @@ func (col *Array) WriteStatePrefix(buffer *proto.Buffer) error {
}

func (col *Array) ScanRow(dest any, row int) error {
if scanner, ok := dest.(sql.Scanner); ok {
value, err := col.scan(scanTypeAny, row)
if err != nil {
return err
}
return scanner.Scan(value.Interface())
}
elem := reflect.Indirect(reflect.ValueOf(dest))
value, err := col.scan(elem.Type(), row)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions lib/column/ipv4.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package column

import (
"database/sql"
"database/sql/driver"
"encoding/binary"
"fmt"
Expand Down Expand Up @@ -98,6 +99,8 @@ func (col *IPv4) ScanRow(dest any, row int) error {
}
*d = new(uint32)
**d = binary.BigEndian.Uint32(ipV4[:])
case sql.Scanner:
return d.Scan(col.row(row))
default:
return &ColumnConverterError{
Op: "ScanRow",
Expand Down
3 changes: 3 additions & 0 deletions lib/column/ipv6.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package column

import (
"database/sql"
"database/sql/driver"
"fmt"
"github.com/ClickHouse/ch-go/proto"
Expand Down Expand Up @@ -91,6 +92,8 @@ func (col *IPv6) ScanRow(dest any, row int) error {
case **[16]byte:
*d = new([16]byte)
**d = col.col.Row(row)
case sql.Scanner:
return d.Scan(col.row(row))
default:
return &ColumnConverterError{
Op: "ScanRow",
Expand Down
4 changes: 4 additions & 0 deletions lib/column/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package column

import (
"database/sql"
"database/sql/driver"
"fmt"
"reflect"
Expand Down Expand Up @@ -114,6 +115,9 @@ func (col *Map) Row(i int, ptr bool) any {
}

func (col *Map) ScanRow(dest any, i int) error {
if scanner, ok := dest.(sql.Scanner); ok {
return scanner.Scan(col.row(i).Interface())
}
value := reflect.Indirect(reflect.ValueOf(dest))
if value.Type() == col.scanType {
value.Set(col.row(i))
Expand Down
47 changes: 47 additions & 0 deletions tests/array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,50 @@ func TestSimpleArrayValuer(t *testing.T) {
require.NoError(t, rows.Close())
require.NoError(t, rows.Err())
}

func TestSQLScannerArray(t *testing.T) {
conn, err := GetNativeConnection(nil, nil, &clickhouse.Compression{
Method: clickhouse.CompressionLZ4,
})
ctx := context.Background()
require.NoError(t, err)
const ddl = `
CREATE TABLE test_array (
Col1 Array(String)
) Engine MergeTree() ORDER BY tuple()
`
defer func() {
conn.Exec(ctx, "DROP TABLE test_array")
}()
require.NoError(t, conn.Exec(ctx, ddl))
batch, err := conn.PrepareBatch(ctx, "INSERT INTO test_array")
require.NoError(t, err)
var (
col1Data = []string{"A", "b", "c"}
)
for i := 0; i < 10; i++ {
require.NoError(t, batch.Append(col1Data))
}
require.Equal(t, 10, batch.Rows())
require.Nil(t, batch.Send())
Copy link
Preview

Copilot AI Jun 9, 2025

Choose a reason for hiding this comment

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

[nitpick] For consistency with other tests, consider replacing require.Nil(t, batch.Send()) with require.NoError(t, batch.Send()).

Suggested change
require.Nil(t, batch.Send())
require.NoError(t, batch.Send())

Copilot uses AI. Check for mistakes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The test added in this PR follows the structure of a test that was already in the repository (TestInterfaceArray), so I would maintain consistency between the two tests. I would suggest making this change separately, if necessary, outside of this PR, in order to update it in the other test and potentially in other areas as well.

rows, err := conn.Query(ctx, "SELECT * FROM test_array")
require.NoError(t, err)
for rows.Next() {
var (
col1 = sqlScannerArray{}
)
require.NoError(t, rows.Scan(&col1))
assert.Equal(t, col1Data, col1.value)
}
require.NoError(t, rows.Close())
require.NoError(t, rows.Err())
}

type sqlScannerArray struct {
value any
}

func (s *sqlScannerArray) Scan(src any) error {
s.value = src
return nil
}
41 changes: 41 additions & 0 deletions tests/ipv4_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,44 @@ func TestIPv4Valuer(t *testing.T) {
}
require.Equal(t, 1000, i)
}

func TestSQLScannerIPv4(t *testing.T) {
conn, err := GetNativeConnection(nil, nil, &clickhouse.Compression{
Method: clickhouse.CompressionLZ4,
})
ctx := context.Background()
require.NoError(t, err)
const ddl = `
CREATE TABLE test_ipv4 (
Col1 IPv4
) Engine MergeTree() ORDER BY tuple()
`
defer func() {
conn.Exec(ctx, "DROP TABLE test_ipv4")
}()

require.NoError(t, conn.Exec(ctx, ddl))
batch, err := conn.PrepareBatch(ctx, "INSERT INTO test_ipv4")
require.NoError(t, err)

var (
col1Data = net.ParseIP("127.0.0.1")
)
require.NoError(t, batch.Append(col1Data))
require.Equal(t, 1, batch.Rows())
require.NoError(t, batch.Send())
var (
col1 sqlScannerIPv4
)
require.NoError(t, conn.QueryRow(ctx, "SELECT * FROM test_ipv4").Scan(&col1))
assert.Equal(t, col1Data.To4(), col1.value)
}

type sqlScannerIPv4 struct {
value any
}

func (s *sqlScannerIPv4) Scan(src any) error {
s.value = src
return nil
}
48 changes: 45 additions & 3 deletions tests/ipv6_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ import (
"context"
"database/sql/driver"
"fmt"
"github.com/ClickHouse/ch-go/proto"
"github.com/ClickHouse/clickhouse-go/v2/lib/column"
"github.com/stretchr/testify/require"
"net"
"net/netip"
"testing"

"github.com/ClickHouse/ch-go/proto"
"github.com/ClickHouse/clickhouse-go/v2/lib/column"
"github.com/stretchr/testify/require"

"github.com/ClickHouse/clickhouse-go/v2"
"github.com/stretchr/testify/assert"
)
Expand Down Expand Up @@ -520,3 +521,44 @@ func TestIPv6Valuer(t *testing.T) {
}
require.Equal(t, 1000, i)
}

func TestSQLScannerIPv6(t *testing.T) {
conn, err := GetNativeConnection(nil, nil, &clickhouse.Compression{
Method: clickhouse.CompressionLZ4,
})
ctx := context.Background()
require.NoError(t, err)
const ddl = `
CREATE TABLE test_ipv6 (
Col1 IPv6
) Engine MergeTree() ORDER BY tuple()
`
defer func() {
conn.Exec(ctx, "DROP TABLE test_ipv6")
}()

require.NoError(t, conn.Exec(ctx, ddl))
batch, err := conn.PrepareBatch(ctx, "INSERT INTO test_ipv6")
require.NoError(t, err)

var (
col1Data = net.ParseIP("2001:44c8:129:2632:33:0:252:2")
)
require.NoError(t, batch.Append(col1Data))
require.Equal(t, 1, batch.Rows())
require.NoError(t, batch.Send())
var (
col1 sqlScannerIPv6
)
require.NoError(t, conn.QueryRow(ctx, "SELECT * FROM test_ipv6").Scan(&col1))
assert.Equal(t, col1Data, col1.value)
}

type sqlScannerIPv6 struct {
value any
}

func (s *sqlScannerIPv6) Scan(src any) error {
s.value = src
return nil
}
46 changes: 46 additions & 0 deletions tests/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,52 @@ func (i *mapIter) Value() any {
return i.om.valuesIter[i.iterIndex]
}

func TestSQLScannerMap(t *testing.T) {
conn, err := GetNativeConnection(clickhouse.Settings{}, nil, &clickhouse.Compression{
Method: clickhouse.CompressionLZ4,
})
ctx := context.Background()
require.NoError(t, err)
if !CheckMinServerServerVersion(conn, 21, 9, 0) {
t.Skip(fmt.Errorf("unsupported clickhouse version"))
return
}
const ddl = `
CREATE TABLE test_map (
Col1 Map(String, UInt64)
) Engine MergeTree() ORDER BY tuple()
`
defer func() {
conn.Exec(ctx, "DROP TABLE IF EXISTS test_map")
}()
require.NoError(t, conn.Exec(ctx, ddl))
batch, err := conn.PrepareBatch(ctx, "INSERT INTO test_map")
require.NoError(t, err)
var (
col1Data = map[string]uint64{
"key_col_1_1": 1,
"key_col_1_2": 2,
}
)
require.NoError(t, batch.Append(col1Data))
require.Equal(t, 1, batch.Rows())
require.NoError(t, batch.Send())
var (
col1 sqlScannerMap
)
require.NoError(t, conn.QueryRow(ctx, "SELECT * FROM test_map").Scan(&col1))
assert.Equal(t, col1Data, col1.value)
}

type sqlScannerMap struct {
value any
}

func (s *sqlScannerMap) Scan(src any) error {
s.value = src
return nil
}

func BenchmarkOrderedMapUseChanGo(b *testing.B) {
m := NewOrderedMap()
for i := 0; i < 10; i++ {
Expand Down
Loading