Skip to content

Commit 5c4cb7d

Browse files
committed
address pr comments
1 parent 759844d commit 5c4cb7d

5 files changed

Lines changed: 142 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Both support TCP and HTTP transport. When in doubt, use the native interface.
2424
* Supports both native ClickHouse TCP and HTTP client-server protocols
2525
* Compatibility with [`database/sql`](#std-databasesql-interface) ([slower](#benchmark) than [native interface](#native-interface)!)
2626
* [`database/sql`](#std-databasesql-interface) supports both native TCP and HTTP protocols for transport.
27-
* Marshal rows into structs ([ScanStruct](examples/clickhouse_api/scan_struct.go), [Select](examples/clickhouse_api/select_struct.go))
27+
* Marshal rows into structs ([ScanStruct](examples/clickhouse_api/scan_struct.go), [Select](examples/clickhouse_api/select_struct.go), [StructIter](examples/clickhouse_api/iterators.go) for native `driver.Rows`)
2828
* Unmarshal struct to row ([AppendStruct](benchmark/v2/write-native-struct/main.go))
2929
* Connection pool (for both TCP-Native and HTTP)
3030
* Failover and load balancing

examples/clickhouse_api/iterators.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,25 @@ import (
77
chdriver "github.com/ClickHouse/clickhouse-go/v2/lib/driver"
88
)
99

10+
<<<<<<< Updated upstream
1011
func Iterators() error {
12+
=======
13+
func Iterators() (err error) {
14+
>>>>>>> Stashed changes
1115
conn, err := GetNativeConnection(nil, nil, nil)
1216
if err != nil {
1317
return err
1418
}
1519

1620
ctx := context.Background()
1721
defer func() {
22+
<<<<<<< Updated upstream
1823
conn.Exec(ctx, "DROP TABLE example_iterators")
24+
=======
25+
if dropErr := conn.Exec(ctx, "DROP TABLE example_iterators"); dropErr != nil && err == nil {
26+
err = fmt.Errorf("drop example_iterators: %w", dropErr)
27+
}
28+
>>>>>>> Stashed changes
1929
}()
2030

2131
if err := conn.Exec(ctx, `DROP TABLE IF EXISTS example_iterators`); err != nil {

lib/driver/iter.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,43 @@
11
package driver
22

3-
import "iter"
3+
import (
4+
"errors"
5+
"iter"
6+
)
47

8+
// StructIter returns an iterator that scans each row into T with ScanStruct.
9+
// It works with native Rows, not database/sql.Rows.
510
func StructIter[T any](rows Rows) iter.Seq2[T, error] {
611
return func(yield func(T, error) bool) {
7-
defer rows.Close()
8-
912
for rows.Next() {
1013
var value T
1114
if err := rows.ScanStruct(&value); err != nil {
1215
var zero T
16+
if closeErr := rows.Close(); closeErr != nil {
17+
err = errors.Join(err, closeErr)
18+
}
1319
_ = yield(zero, err)
1420
return
1521
}
1622
if !yield(value, nil) {
23+
// The caller stopped iteration, so the protocol forbids yielding a close error.
24+
if closeErr := rows.Close(); closeErr != nil {
25+
return
26+
}
1727
return
1828
}
1929
}
2030

2131
if err := rows.Err(); err != nil {
32+
var zero T
33+
if closeErr := rows.Close(); closeErr != nil {
34+
err = errors.Join(err, closeErr)
35+
}
36+
_ = yield(zero, err)
37+
return
38+
}
39+
40+
if err := rows.Close(); err != nil {
2241
var zero T
2342
_ = yield(zero, err)
2443
}

lib/driver/iter_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type testRows struct {
1212
index int
1313
closeCalls int
1414
err error
15+
closeErr error
1516
scanStructErrAt int
1617
}
1718

@@ -49,7 +50,7 @@ func (r *testRows) Columns() []string { return nil }
4950

5051
func (r *testRows) Close() error {
5152
r.closeCalls++
52-
return nil
53+
return r.closeErr
5354
}
5455

5556
func (r *testRows) Err() error { return r.err }
@@ -128,3 +129,55 @@ func TestStructIterTerminalRowsError(t *testing.T) {
128129
t.Fatalf("unexpected values before terminal error: %#v", got)
129130
}
130131
}
132+
133+
func TestStructIterCloseError(t *testing.T) {
134+
type item struct {
135+
Value int
136+
}
137+
138+
rows := &testRows{values: []int{1}, closeErr: io.ErrClosedPipe}
139+
140+
var got []item
141+
var gotErr error
142+
for value, err := range StructIter[item](rows) {
143+
if err != nil {
144+
gotErr = err
145+
break
146+
}
147+
got = append(got, value)
148+
}
149+
150+
if !errors.Is(gotErr, io.ErrClosedPipe) {
151+
t.Fatalf("unexpected close error: %v", gotErr)
152+
}
153+
if !reflect.DeepEqual(got, []item{{Value: 1}}) {
154+
t.Fatalf("unexpected values before close error: %#v", got)
155+
}
156+
if rows.closeCalls != 1 {
157+
t.Fatalf("unexpected close calls: %d", rows.closeCalls)
158+
}
159+
}
160+
161+
func TestStructIterStopsAfterCallerBreak(t *testing.T) {
162+
type item struct {
163+
Value int
164+
}
165+
166+
rows := &testRows{values: []int{1, 2, 3}}
167+
168+
var got []item
169+
for value, err := range StructIter[item](rows) {
170+
if err != nil {
171+
t.Fatalf("unexpected iter error: %v", err)
172+
}
173+
got = append(got, value)
174+
break
175+
}
176+
177+
if !reflect.DeepEqual(got, []item{{Value: 1}}) {
178+
t.Fatalf("unexpected values before caller break: %#v", got)
179+
}
180+
if rows.closeCalls != 1 {
181+
t.Fatalf("unexpected close calls: %d", rows.closeCalls)
182+
}
183+
}

tests/iterator_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package tests
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/ClickHouse/clickhouse-go/v2"
10+
chdriver "github.com/ClickHouse/clickhouse-go/v2/lib/driver"
11+
)
12+
13+
func TestStructIterProtocols(t *testing.T) {
14+
TestProtocols(t, func(t *testing.T, protocol clickhouse.Protocol) {
15+
conn, err := GetNativeConnection(t, protocol, nil, nil, nil)
16+
require.NoError(t, err)
17+
18+
ctx := context.Background()
19+
const table = "test_struct_iter"
20+
require.NoError(t, conn.Exec(ctx, "DROP TABLE IF EXISTS "+table))
21+
t.Cleanup(func() {
22+
require.NoError(t, conn.Exec(ctx, "DROP TABLE IF EXISTS "+table))
23+
})
24+
require.NoError(t, conn.Exec(ctx, `
25+
CREATE TABLE test_struct_iter (
26+
Col1 UInt8,
27+
Col2 String
28+
) ENGINE = Memory
29+
`))
30+
31+
batch, err := conn.PrepareBatch(ctx, "INSERT INTO test_struct_iter")
32+
require.NoError(t, err)
33+
require.NoError(t, batch.Append(uint8(1), "one"))
34+
require.NoError(t, batch.Append(uint8(2), "two"))
35+
require.NoError(t, batch.Send())
36+
37+
rows, err := conn.Query(ctx, "SELECT Col1, Col2 FROM test_struct_iter ORDER BY Col1")
38+
require.NoError(t, err)
39+
40+
type result struct {
41+
Col1 uint8
42+
Col2 string
43+
}
44+
var got []result
45+
for value, err := range chdriver.StructIter[result](rows) {
46+
require.NoError(t, err)
47+
got = append(got, value)
48+
}
49+
50+
require.Equal(t, []result{
51+
{Col1: 1, Col2: "one"},
52+
{Col1: 2, Col2: "two"},
53+
}, got)
54+
})
55+
}

0 commit comments

Comments
 (0)