From 9a2289c3a38492bc2e84e0f4000c68a8718f5c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Knut=20Olav=20L=C3=B8ite?= Date: Tue, 20 Oct 2020 12:13:51 +0200 Subject: [PATCH] feat(spanner): add metadata to RowIterator (#3050) Adds ResultSetMetaData to the RowIterator struct. The metadata are available after the first call to RowIterator.Next() as long as that call did not return any other error than iterator.Done. Fixes #1805 --- spanner/client_test.go | 45 +++++++++++++++++++ spanner/integration_test.go | 5 +++ .../internal/testutil/inmem_spanner_server.go | 4 ++ spanner/read.go | 21 ++++++--- spanner/read_test.go | 2 +- 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/spanner/client_test.go b/spanner/client_test.go index c30f39e8139..8750541c7f9 100644 --- a/spanner/client_test.go +++ b/spanner/client_test.go @@ -1880,6 +1880,51 @@ func TestClient_QueryWithCallOptions(t *testing.T) { } } +func TestClient_ShouldReceiveMetadataForEmptyResultSet(t *testing.T) { + t.Parallel() + + server, client, teardown := setupMockedTestServer(t) + // This creates an empty result set. + res := server.CreateSingleRowSingersResult(SelectSingerIDAlbumIDAlbumTitleFromAlbumsRowCount) + sql := "SELECT SingerId, AlbumId, AlbumTitle FROM Albums WHERE 1=2" + server.TestSpanner.PutStatementResult(sql, res) + defer teardown() + ctx := context.Background() + iter := client.Single().Query(ctx, NewStatement(sql)) + defer iter.Stop() + row, err := iter.Next() + if err != iterator.Done { + t.Errorf("Query result mismatch:\nGot: %v\nWant: ", row) + } + metadata := iter.Metadata + if metadata == nil { + t.Fatalf("Missing ResultSet Metadata") + } + if metadata.RowType == nil { + t.Fatalf("Missing ResultSet RowType") + } + if metadata.RowType.Fields == nil { + t.Fatalf("Missing ResultSet Fields") + } + if g, w := len(metadata.RowType.Fields), 3; g != w { + t.Fatalf("Field count mismatch\nGot: %v\nWant: %v", g, w) + } + wantFieldNames := []string{"SingerId", "AlbumId", "AlbumTitle"} + for i, w := range wantFieldNames { + g := metadata.RowType.Fields[i].Name + if g != w { + t.Fatalf("Field[%v] name mismatch\nGot: %v\nWant: %v", i, g, w) + } + } + wantFieldTypes := []sppb.TypeCode{sppb.TypeCode_INT64, sppb.TypeCode_INT64, sppb.TypeCode_STRING} + for i, w := range wantFieldTypes { + g := metadata.RowType.Fields[i].Type.Code + if g != w { + t.Fatalf("Field[%v] type mismatch\nGot: %v\nWant: %v", i, g, w) + } + } +} + func TestClient_EncodeCustomFieldType(t *testing.T) { t.Parallel() diff --git a/spanner/integration_test.go b/spanner/integration_test.go index dd0a09ca536..47b266d7e75 100644 --- a/spanner/integration_test.go +++ b/spanner/integration_test.go @@ -3156,6 +3156,11 @@ func readAllTestTable(iter *RowIterator) ([]testTableRow, error) { for { row, err := iter.Next() if err == iterator.Done { + if iter.Metadata == nil { + // All queries should always return metadata, regardless whether + // they return any rows or not. + return nil, errors.New("missing metadata from query") + } return vals, nil } if err != nil { diff --git a/spanner/internal/testutil/inmem_spanner_server.go b/spanner/internal/testutil/inmem_spanner_server.go index 0735e095ee7..fd39d747868 100644 --- a/spanner/internal/testutil/inmem_spanner_server.go +++ b/spanner/internal/testutil/inmem_spanner_server.go @@ -148,6 +148,10 @@ func (s *StatementResult) ToPartialResultSets(resumeToken []byte) (result []*spa break } } + } else { + result = append(result, &spannerpb.PartialResultSet{ + Metadata: s.ResultSet.Metadata, + }) } return result, nil } diff --git a/spanner/read.go b/spanner/read.go index 268cf10b6fc..5ede3a3c2d0 100644 --- a/spanner/read.go +++ b/spanner/read.go @@ -102,6 +102,11 @@ type RowIterator struct { // iterator.Done. RowCount int64 + // The metadata of the results of the query. The metadata are available + // after the first call to RowIterator.Next(), unless the first call to + // RowIterator.Next() returned an error that is not equal to iterator.Done. + Metadata *sppb.ResultSetMetadata + streamd *resumableStreamDecoder rowd *partialResultSetDecoder setTimestamp func(time.Time) @@ -133,7 +138,11 @@ func (r *RowIterator) Next() (*Row, error) { r.RowCount = rc } } - r.rows, r.err = r.rowd.add(prs) + var metadata *sppb.ResultSetMetadata + r.rows, metadata, r.err = r.rowd.add(prs) + if metadata != nil { + r.Metadata = metadata + } if r.err != nil { return nil, r.err } @@ -648,7 +657,7 @@ func errChunkedEmptyRow() error { // add tries to merge a new PartialResultSet into buffered Row. It returns any // rows that have been completed as a result. -func (p *partialResultSetDecoder) add(r *sppb.PartialResultSet) ([]*Row, error) { +func (p *partialResultSetDecoder) add(r *sppb.PartialResultSet) ([]*Row, *sppb.ResultSetMetadata, error) { var rows []*Row if r.Metadata != nil { // Metadata should only be returned in the first result. @@ -663,20 +672,20 @@ func (p *partialResultSetDecoder) add(r *sppb.PartialResultSet) ([]*Row, error) } } if len(r.Values) == 0 { - return nil, nil + return nil, r.Metadata, nil } if p.chunked { p.chunked = false // Try to merge first value in r.Values into uncompleted row. last := len(p.row.vals) - 1 if last < 0 { // confidence check - return nil, errChunkedEmptyRow() + return nil, nil, errChunkedEmptyRow() } var err error // If p is chunked, then we should always try to merge p.last with // r.first. if p.row.vals[last], err = p.merge(p.row.vals[last], r.Values[0]); err != nil { - return nil, err + return nil, r.Metadata, err } r.Values = r.Values[1:] // Merge is done, try to yield a complete Row. @@ -698,7 +707,7 @@ func (p *partialResultSetDecoder) add(r *sppb.PartialResultSet) ([]*Row, error) // also chunked. p.chunked = true } - return rows, nil + return rows, r.Metadata, nil } // isMergeable returns if a protobuf Value can be potentially merged with other diff --git a/spanner/read_test.go b/spanner/read_test.go index 2b1ea48413a..87f658960c2 100644 --- a/spanner/read_test.go +++ b/spanner/read_test.go @@ -584,7 +584,7 @@ nextTest: var rows []*Row p := &partialResultSetDecoder{} for j, v := range test.input { - rs, err := p.add(v) + rs, _, err := p.add(v) if err != nil { t.Errorf("test %d.%d: partialResultSetDecoder.add(%v) = %v; want nil", i, j, v, err) continue nextTest