diff --git a/lib/column/tuple.go b/lib/column/tuple.go index 95e00db04b..95c4e24209 100644 --- a/lib/column/tuple.go +++ b/lib/column/tuple.go @@ -447,7 +447,7 @@ func (col *Tuple) scan(targetType reflect.Type, row int) (reflect.Value, error) //tuples can be scanned into slices - specifically default for unnamed tuples rSlice, err := col.scanSlice(targetType, row) if err != nil { - return reflect.Value{}, nil + return reflect.Value{}, err } return rSlice, nil case reflect.Interface: diff --git a/tests/issues/1245_test.go b/tests/issues/1245_test.go new file mode 100644 index 0000000000..18565566c2 --- /dev/null +++ b/tests/issues/1245_test.go @@ -0,0 +1,60 @@ +package issues + +import ( + "context" + "testing" + + clickhouse_tests "github.com/ClickHouse/clickhouse-go/v2/tests" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test1245Native(t *testing.T) { + testEnv, err := clickhouse_tests.GetTestEnvironment("issues") + require.NoError(t, err) + conn, err := clickhouse_tests.TestClientWithDefaultSettings(testEnv) + require.NoError(t, err) + ctx := context.Background() + const ddl = "CREATE TABLE IF NOT EXISTS test_1245 (`id` Int32, `segment` Tuple(Tuple(UInt16, UInt16), Tuple(UInt16, UInt16))) Engine = Memory" + require.NoError(t, conn.Exec(ctx, ddl)) + + defer func() { + require.NoError(t, conn.Exec(ctx, "DROP TABLE IF EXISTS test_1245")) + }() + + require.NoError(t, conn.Exec(ctx, "INSERT INTO test_1245 VALUES (1, ((1,3),(8,9)))")) + + rows, err := conn.Query(ctx, "SELECT id, segment FROM test_1245") + require.NoError(t, err) + defer rows.Close() + assert.True(t, rows.Next()) + var id int32 + var segment []any + assert.Errorf(t, rows.Scan(&id, &segment), "cannot use interface for unnamed tuples, use slice") +} + +func Test1245DatabaseSQLDriver(t *testing.T) { + testEnv, err := clickhouse_tests.GetTestEnvironment("issues") + require.NoError(t, err) + conn, err := clickhouse_tests.TestDatabaseSQLClientWithDefaultSettings(testEnv) + require.NoError(t, err) + const ddl = "CREATE TABLE IF NOT EXISTS test_1245 (`id` Int32, `segment` Tuple(Tuple(UInt16, UInt16), Tuple(UInt16, UInt16))) Engine = Memory" + _, err = conn.Exec(ddl) + require.NoError(t, err) + + defer func() { + _, err = conn.Exec("DROP TABLE IF EXISTS test_1245") + require.NoError(t, err) + }() + + _, err = conn.Exec("INSERT INTO test_1245 VALUES (1, ((1,3),(8,9)))") + require.NoError(t, err) + + rows, err := conn.Query("SELECT id, segment FROM test_1245") + require.NoError(t, err) + defer rows.Close() + assert.True(t, rows.Next()) + var id int32 + var segment []any + assert.Errorf(t, rows.Scan(&id, &segment), "cannot use interface for unnamed tuples, use slice") +} diff --git a/tests/utils.go b/tests/utils.go index e07a46d488..ed707f6fb2 100644 --- a/tests/utils.go +++ b/tests/utils.go @@ -20,21 +20,13 @@ package tests import ( "context" "crypto/tls" + "database/sql" "encoding/json" "errors" "fmt" - "github.com/ClickHouse/clickhouse-go/v2" - "github.com/ClickHouse/clickhouse-go/v2/lib/driver" - "github.com/ClickHouse/clickhouse-go/v2/lib/proto" - "github.com/docker/docker/api/types/container" - "github.com/docker/go-connections/nat" - "github.com/docker/go-units" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go" - "github.com/testcontainers/testcontainers-go/wait" "math/rand" "net" + "net/url" "os" "path" "path/filepath" @@ -43,6 +35,17 @@ import ( "strings" "testing" "time" + + "github.com/ClickHouse/clickhouse-go/v2" + "github.com/ClickHouse/clickhouse-go/v2/lib/driver" + "github.com/ClickHouse/clickhouse-go/v2/lib/proto" + "github.com/docker/docker/api/types/container" + "github.com/docker/go-connections/nat" + "github.com/docker/go-units" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" ) var testUUID = uuid.NewString()[0:12] @@ -291,7 +294,7 @@ func testClientWithDefaultOptions(env ClickHouseTestEnvironment, settings clickh return clickhouse.Open(&opts) } -func TestClientWithDefaultSettings(env ClickHouseTestEnvironment) (driver.Conn, error) { +func TestClientDefaultSettings(env ClickHouseTestEnvironment) clickhouse.Settings { settings := clickhouse.Settings{} if proto.CheckMinVersion(proto.Version{ @@ -305,7 +308,20 @@ func TestClientWithDefaultSettings(env ClickHouseTestEnvironment) (driver.Conn, settings["insert_quorum_parallel"] = 0 settings["select_sequential_consistency"] = 1 - return testClientWithDefaultOptions(env, settings) + return settings +} + +func TestClientWithDefaultSettings(env ClickHouseTestEnvironment) (driver.Conn, error) { + return testClientWithDefaultOptions(env, TestClientDefaultSettings(env)) +} + +func TestDatabaseSQLClientWithDefaultOptions(env ClickHouseTestEnvironment, settings clickhouse.Settings) (*sql.DB, error) { + opts := ClientOptionsFromEnv(env, settings) + return sql.Open("clickhouse", optionsToDSN(&opts)) +} + +func TestDatabaseSQLClientWithDefaultSettings(env ClickHouseTestEnvironment) (*sql.DB, error) { + return TestDatabaseSQLClientWithDefaultOptions(env, TestClientDefaultSettings(env)) } func GetConnection(testSet string, settings clickhouse.Settings, tlsConfig *tls.Config, compression *clickhouse.Compression) (driver.Conn, error) { @@ -627,3 +643,109 @@ func CreateTinyProxyTestEnvironment(t *testing.T) (TinyProxyTestEnvironment, err Container: container, }, nil } + +func optionsToDSN(o *clickhouse.Options) string { + var u url.URL + + if o.Protocol == clickhouse.Native { + u.Scheme = "clickhouse" + } else { + if o.TLS != nil { + u.Scheme = "https" + } else { + u.Scheme = "http" + } + } + + u.Host = strings.Join(o.Addr, ",") + u.User = url.UserPassword(o.Auth.Username, o.Auth.Password) + u.Path = fmt.Sprintf("/%s", o.Auth.Database) + + params := u.Query() + + if o.TLS != nil { + params.Set("secure", "true") + } + + if o.TLS != nil && o.TLS.InsecureSkipVerify { + params.Set("skip_verify", "true") + } + + if o.Debug { + params.Set("debug", "true") + } + + if o.Compression != nil { + params.Set("compress", o.Compression.Method.String()) + if o.Compression.Level > 0 { + params.Set("compress_level", strconv.Itoa(o.Compression.Level)) + } + } + + if o.MaxCompressionBuffer > 0 { + params.Set("max_compression_buffer", strconv.Itoa(o.MaxCompressionBuffer)) + } + + if o.DialTimeout > 0 { + params.Set("dial_timeout", o.DialTimeout.String()) + } + + if o.BlockBufferSize > 0 { + params.Set("block_buffer_size", strconv.Itoa(int(o.BlockBufferSize))) + } + + if o.ReadTimeout > 0 { + params.Set("read_timeout", o.ReadTimeout.String()) + } + + if o.ConnOpenStrategy != 0 { + var strategy string + switch o.ConnOpenStrategy { + case clickhouse.ConnOpenInOrder: + strategy = "in_order" + case clickhouse.ConnOpenRoundRobin: + strategy = "round_robin" + } + + params.Set("connection_open_strategy", strategy) + } + + if o.MaxOpenConns > 0 { + params.Set("max_open_conns", strconv.Itoa(o.MaxOpenConns)) + } + + if o.MaxIdleConns > 0 { + params.Set("max_idle_conns", strconv.Itoa(o.MaxIdleConns)) + } + + if o.ConnMaxLifetime > 0 { + params.Set("conn_max_lifetime", o.ConnMaxLifetime.String()) + } + + if o.ClientInfo.Products != nil { + var products []string + for _, product := range o.ClientInfo.Products { + products = append(products, fmt.Sprintf("%s/%s", product.Name, product.Version)) + } + params.Set("client_info_product", strings.Join(products, ",")) + } + + for k, v := range o.Settings { + switch v := v.(type) { + case bool: + if v { + params.Set(k, "true") + } else { + params.Set(k, "false") + } + case int: + params.Set(k, strconv.Itoa(v)) + case string: + params.Set(k, v) + } + } + + u.RawQuery = params.Encode() + + return u.String() +}