Skip to content

Commit 0226fd5

Browse files
authored
Adopt spaniter v0.1.0 row iterator adapters
Adopt `github.com/apstndb/spaniter` for Cloud Spanner row iterator lifecycle handling and update the dependency to `v0.1.0`. - Replace local row-iterator sequencing/draining paths with spaniter helpers. - Preserve metadata, query plan, query stats, and row-count assembly through `BuildResultSetFromParts`. - Add eager `PROFILE` + redacted rows coverage so stats remain available even when row values are omitted. Validation: - `go test ./...` - `make test` - `git diff --check` - GitHub checks `build` and `lint`
1 parent aa1aa67 commit 0226fd5

5 files changed

Lines changed: 61 additions & 56 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/apstndb/gsqlsep v0.0.0-20240823174243-432be37d515a
1010
github.com/apstndb/memebridge v0.6.0
1111
github.com/apstndb/spanemuboost v0.0.0-20241212215948-d4eb945c8f10
12+
github.com/apstndb/spaniter v0.1.0
1213
github.com/apstndb/spannerotel v0.0.0-20220113012943-45b1c9cc5afb
1314
github.com/apstndb/spanvalue v0.8.0
1415
github.com/cloudspannerecosystem/memefish v0.6.2

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,8 @@ github.com/apstndb/memebridge v0.6.0 h1:45uu/RFF63u5vJ+d8HT4PvOfotm1cV4eEveWFwGv
670670
github.com/apstndb/memebridge v0.6.0/go.mod h1:E/HVP4iaSgiPXMIQTPT1u1uKkjmRtRECkVKE2TrF3bc=
671671
github.com/apstndb/spanemuboost v0.0.0-20241212215948-d4eb945c8f10 h1:Ea5al1Su3ZNw2HltOc1fjLADUp16jV9ns03D9Xvpep4=
672672
github.com/apstndb/spanemuboost v0.0.0-20241212215948-d4eb945c8f10/go.mod h1:JtIpejiunpnBufWCwhEun+ayfd0JWnPp9kqbcNJkU+E=
673+
github.com/apstndb/spaniter v0.1.0 h1:OpTpePq0bN/EWS1mIXAk0lfQ98PAI4xRcBQ4pd9Ijk0=
674+
github.com/apstndb/spaniter v0.1.0/go.mod h1:vl6zVOnA2m35fAeF1WsGD7AuaWUK5aCVG6aS/35njFE=
673675
github.com/apstndb/spannerotel v0.0.0-20220113012943-45b1c9cc5afb h1:wop6M8ZQWhVLYtiBKXvkNd6TFUSpiGya86Az57KiTl0=
674676
github.com/apstndb/spannerotel v0.0.0-20220113012943-45b1c9cc5afb/go.mod h1:H9relGptAtWqSNrJy+V1jiYPeBIk8KYh6lACV3VZrqU=
675677
github.com/apstndb/spantype v0.3.11 h1:wKue4WLYGT82MH3B3TRSFn8tWIbk1Geczs+5BAEtnK4=

integration_test.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@ package main
33
import (
44
"bytes"
55
"context"
6-
"encoding/csv"
76
_ "embed"
7+
"encoding/csv"
88
"strings"
99
"testing"
1010

1111
"cloud.google.com/go/spanner"
1212
sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
1313
"github.com/apstndb/gsqlsep"
1414
"github.com/apstndb/spanemuboost"
15+
"github.com/apstndb/spaniter"
1516
"github.com/google/go-cmp/cmp"
1617
"google.golang.org/protobuf/testing/protocmp"
1718
"google.golang.org/protobuf/types/known/structpb"
@@ -84,14 +85,13 @@ func TestWithCloudSpannerEmulator(t *testing.T) {
8485
rowIter := clients.Client.Single().QueryWithOptions(ctx,
8586
spanner.Statement{SQL: "SELECT @i64, @f32, @f64, @s, @bs, @bl, @dt, @ts, @n, @a_str, @a_s, @j", Params: params},
8687
spanner.QueryOptions{Mode: sppb.ExecuteSqlRequest_PLAN.Enum()})
87-
defer rowIter.Stop()
8888

89-
err = skipRowIter(rowIter)
89+
result, err := spaniter.DrainRowIterator(rowIter)
9090
if err != nil {
9191
t.Fatal(err)
9292
}
9393

94-
got := rowIter.Metadata.GetRowType()
94+
got := result.Metadata.GetRowType()
9595
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
9696
t.Error(diff)
9797
return
@@ -258,9 +258,8 @@ func TestWithCloudSpannerEmulator(t *testing.T) {
258258
rowIter := client.Single().QueryWithOptions(ctx,
259259
spanner.Statement{SQL: "SELECT @v AS v", Params: params},
260260
spanner.QueryOptions{Mode: sppb.ExecuteSqlRequest_NORMAL.Enum()})
261-
defer rowIter.Stop()
262261

263-
rows, err := xiter.TryCollect(rowIterSeq(rowIter))
262+
rows, err := xiter.TryCollect(spaniter.RowIteratorSeq(rowIter))
264263
if err != nil {
265264
t.Error(err)
266265
return
@@ -407,6 +406,31 @@ func TestWithCloudSpannerEmulator(t *testing.T) {
407406
}
408407
})
409408

409+
t.Run("eager PROFILE redact rows preserves stats", func(t *testing.T) {
410+
rowIter := client.Single().QueryWithOptions(ctx,
411+
spanner.Statement{SQL: "SELECT SingerId FROM Singers ORDER BY SingerId LIMIT 3"},
412+
spanner.QueryOptions{Mode: sppb.ExecuteSqlRequest_PROFILE.Enum()},
413+
)
414+
415+
rs, err := consumeRowIterIntoResultSet(rowIter, true)
416+
if err != nil {
417+
t.Fatal(err)
418+
}
419+
if len(rs.GetRows()) != 0 {
420+
t.Fatalf("rows: got %d, want 0", len(rs.GetRows()))
421+
}
422+
fields := rs.GetMetadata().GetRowType().GetFields()
423+
if len(fields) != 1 || fields[0].GetName() != "SingerId" {
424+
t.Fatalf("metadata fields: got %v", fields)
425+
}
426+
if rs.GetStats() == nil {
427+
t.Fatal("stats: got nil, want PROFILE stats")
428+
}
429+
if rs.GetStats().GetQueryPlan() == nil && rs.GetStats().GetQueryStats() == nil {
430+
t.Fatal("stats: got no query plan or query stats, want PROFILE stats")
431+
}
432+
})
433+
410434
t.Run("experimental_csv writeCsvFromRowIter", func(t *testing.T) {
411435
readCSV := func(t *testing.T, out string) [][]string {
412436
t.Helper()

jqresult/protojson.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,30 +61,37 @@ func ResultSetMap(rs *sppb.ResultSet) (map[string]any, error) {
6161

6262
// BuildResultSet constructs ResultSet stats from a consumed row iterator.
6363
func BuildResultSet(rows []*structpb.ListValue, rowIter *spanner.RowIterator) (*sppb.ResultSet, error) {
64+
return BuildResultSetFromParts(rows, rowIter.Metadata, rowIter.QueryPlan, rowIter.QueryStats, rowIter.RowCount)
65+
}
66+
67+
// BuildResultSetFromParts constructs a ResultSet from materialized rows and
68+
// consumed iterator metadata. rows may be nil when row values are intentionally
69+
// redacted; metadata and stats are still preserved from the drained iterator.
70+
func BuildResultSetFromParts(rows []*structpb.ListValue, metadata *sppb.ResultSetMetadata, queryPlan *sppb.QueryPlan, queryStatsMap map[string]any, rowCount int64) (*sppb.ResultSet, error) {
6471
out := &sppb.ResultSet{
6572
Rows: rows,
66-
Metadata: rowIter.Metadata,
73+
Metadata: metadata,
6774
}
6875

6976
var queryStats *structpb.Struct
70-
if rowIter.QueryStats != nil {
71-
qs, err := structpb.NewStruct(rowIter.QueryStats)
77+
if queryStatsMap != nil {
78+
qs, err := structpb.NewStruct(queryStatsMap)
7279
if err != nil {
7380
return nil, err
7481
}
7582
queryStats = qs
7683
}
7784

78-
if rowIter.QueryPlan == nil && queryStats == nil && rowIter.RowCount == 0 {
85+
if queryPlan == nil && queryStats == nil && rowCount == 0 {
7986
return out, nil
8087
}
8188

8289
out.Stats = &sppb.ResultSetStats{
83-
QueryPlan: rowIter.QueryPlan,
90+
QueryPlan: queryPlan,
8491
QueryStats: queryStats,
8592
}
86-
if rowIter.RowCount != 0 {
87-
out.Stats.RowCount = &sppb.ResultSetStats_RowCountExact{RowCountExact: rowIter.RowCount}
93+
if rowCount != 0 {
94+
out.Stats.RowCount = &sppb.ResultSetStats_RowCountExact{RowCountExact: rowCount}
8895
}
8996
return out, nil
9097
}

main.go

Lines changed: 14 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import (
66
"errors"
77
"github.com/apstndb/execspansql/internal"
88
"github.com/apstndb/execspansql/params"
9-
"google.golang.org/api/iterator"
109
"io"
11-
"iter"
1210
"regexp"
1311
"slices"
1412
"spheric.cloud/xiter"
@@ -36,9 +34,10 @@ import (
3634

3735
"cloud.google.com/go/spanner"
3836
sppb "cloud.google.com/go/spanner/apiv1/spannerpb"
37+
"github.com/apstndb/execspansql/jqresult"
38+
"github.com/apstndb/spaniter"
3939
"github.com/apstndb/spannerotel/interceptor"
4040
svwriter "github.com/apstndb/spanvalue/writer"
41-
"github.com/apstndb/execspansql/jqresult"
4241
"github.com/jessevdk/go-flags"
4342
"github.com/wader/gojq"
4443
"google.golang.org/protobuf/types/known/structpb"
@@ -568,56 +567,28 @@ func newEncoder(writer io.Writer, format string, compactOutput bool, rawOutput b
568567
// consumeRowIterIntoResultSet construct *spannerpb.ResultSet using *spanner.RowIterator.
569568
// rowIter must be passed without calling any method, and it will be closed by this function.
570569
func consumeRowIterIntoResultSet(rowIter *spanner.RowIterator, redactRows bool) (*sppb.ResultSet, error) {
571-
defer rowIter.Stop()
572-
573-
var rows []*structpb.ListValue
574-
var err error
575-
576570
if redactRows {
577-
err = skipRowIter(rowIter)
578-
} else {
579-
rows, err = consumeRowIterIntoListValues(rowIter)
571+
result, err := spaniter.DrainRowIterator(rowIter)
572+
if err != nil {
573+
return nil, err
574+
}
575+
return jqresult.BuildResultSetFromParts(nil, result.Metadata, result.Stats.QueryPlan, result.Stats.QueryStats, result.Stats.RowCount)
580576
}
581577

578+
var result spaniter.RowIteratorResult
579+
rows, err := consumeRowIterIntoListValues(rowIter,
580+
spaniter.WithResult(&result),
581+
)
582582
if err != nil {
583583
return nil, err
584584
}
585-
586-
return convertToResultSet(rows, rowIter)
587-
}
588-
589-
// convertToResultSet convert rows and rowIter into sppb.ResultSet.
590-
// rowIter must be consumed with iterator.Done, and not Stop()-ed.
591-
func convertToResultSet(rows []*structpb.ListValue, rowIter *spanner.RowIterator) (*sppb.ResultSet, error) {
592-
return jqresult.BuildResultSet(rows, rowIter)
593-
}
594-
595-
func rowIterSeq(rowIter *spanner.RowIterator) iter.Seq2[*spanner.Row, error] {
596-
return func(yield func(*spanner.Row, error) bool) {
597-
for {
598-
r, err := rowIter.Next()
599-
if errors.Is(err, iterator.Done) {
600-
return
601-
}
602-
if err != nil {
603-
_ = yield(nil, err)
604-
return
605-
}
606-
if !yield(r, nil) {
607-
return
608-
}
609-
}
610-
}
585+
return jqresult.BuildResultSetFromParts(rows, result.Metadata, result.Stats.QueryPlan, result.Stats.QueryStats, result.Stats.RowCount)
611586
}
612587

613588
func rowToListValue(r *spanner.Row) *structpb.ListValue {
614589
return &structpb.ListValue{Values: slices.Collect(xiter.Map(xiter.Range(0, r.Size()), r.ColumnValue))}
615590
}
616591

617-
func consumeRowIterIntoListValues(rowIter *spanner.RowIterator) ([]*structpb.ListValue, error) {
618-
return xiter.TryCollect(internal.MapNonError(rowIterSeq(rowIter), rowToListValue))
619-
}
620-
621-
func skipRowIter(rowIter *spanner.RowIterator) error {
622-
return rowIter.Do(func(r *spanner.Row) error { return nil })
592+
func consumeRowIterIntoListValues(rowIter *spanner.RowIterator, opts ...spaniter.Option) ([]*structpb.ListValue, error) {
593+
return xiter.TryCollect(internal.MapNonError(spaniter.RowIteratorSeq(rowIter, opts...), rowToListValue))
623594
}

0 commit comments

Comments
 (0)