Skip to content

Commit 8ac5e6b

Browse files
committed
feat: add option to return metadata and stats
Adds an option to return the full ResultSetMetadata and ResultSetStats as separate result sets. This gives the caller access to more details than can be provided through the standard database/sql interfaces.
1 parent 3aa5ca2 commit 8ac5e6b

File tree

10 files changed

+419
-3
lines changed

10 files changed

+419
-3
lines changed

checksum_row_iterator.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ func init() {
4141
gob.Register(structpb.Value_StructValue{})
4242
}
4343

44+
var _ rowIterator = &checksumRowIterator{}
45+
4446
// checksumRowIterator implements rowIterator and keeps track of a running
4547
// checksum for all results that have been seen during the iteration of the
4648
// results. This checksum can be used to verify whether a retry returned the
@@ -249,3 +251,12 @@ func (it *checksumRowIterator) Stop() {
249251
func (it *checksumRowIterator) Metadata() (*sppb.ResultSetMetadata, error) {
250252
return it.metadata, nil
251253
}
254+
255+
func (it *checksumRowIterator) ResultSetStats() *sppb.ResultSetStats {
256+
// TODO: The Spanner client library should offer an option to get the full
257+
// ResultSetStats, instead of only the RowCount and QueryPlan.
258+
return &sppb.ResultSetStats{
259+
RowCount: &sppb.ResultSetStats_RowCountExact{RowCountExact: it.RowIterator.RowCount},
260+
QueryPlan: it.RowIterator.QueryPlan,
261+
}
262+
}

client_side_statement.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@ func createSingleValueIterator(column string, value interface{}, code spannerpb.
414414
}, nil
415415
}
416416

417+
var _ rowIterator = &clientSideIterator{}
418+
417419
// clientSideIterator implements the rowIterator interface for client side
418420
// statements. All values are created and kept in memory, and this struct
419421
// should only be used for small result sets.
@@ -442,3 +444,7 @@ func (t *clientSideIterator) Stop() {
442444
func (t *clientSideIterator) Metadata() (*spannerpb.ResultSetMetadata, error) {
443445
return t.metadata, nil
444446
}
447+
448+
func (t *clientSideIterator) ResultSetStats() *spannerpb.ResultSetStats {
449+
return &spannerpb.ResultSetStats{}
450+
}

conn.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -793,7 +793,13 @@ func (c *conn) queryContext(ctx context.Context, query string, execOptions ExecO
793793
return nil, err
794794
}
795795
}
796-
return &rows{it: iter, decodeOption: execOptions.DecodeOption, decodeToNativeArrays: execOptions.DecodeToNativeArrays}, nil
796+
return &rows{
797+
it: iter,
798+
decodeOption: execOptions.DecodeOption,
799+
decodeToNativeArrays: execOptions.DecodeToNativeArrays,
800+
returnResultSetMetadata: execOptions.ReturnResultSetMetadata,
801+
returnResultSetStats: execOptions.ReturnResultSetStats,
802+
}, nil
797803
}
798804

799805
func (c *conn) ExecContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Result, error) {

driver.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,28 @@ type ExecOptions struct {
163163
// AutoCommitDMLMode determines the type of transaction that DML statements
164164
// that are executed outside explicit transactions use.
165165
AutocommitDMLMode AutocommitDMLMode
166+
167+
// ReturnResultSetMetadata instructs the driver to return an additional result
168+
// set with the full spannerpb.ResultSetMetadata of the query. This result set
169+
// contains one row and one column, and the value in that cell is the
170+
// spannerpb.ResultSetMetadata that was returned by Spanner when executing the
171+
// query. This result set will be the first result set in the sql.Rows object
172+
// that is returned.
173+
//
174+
// You have to call [sql.Rows.NextResultSet] to move to the result set that
175+
// contains the actual query data.
176+
ReturnResultSetMetadata bool
177+
178+
// ReturnResultSetStats instructs the driver to return an additional result
179+
// set with the full spannerpb.ResultSetStats of the query. This result set
180+
// contains one row and one column, and the value in that cell is the
181+
// spannerpb.ResultSetStats that was returned by Spanner when executing the
182+
// query. This result set will be the last result set in the sql.Rows object
183+
// that is returned.
184+
//
185+
// You have to call [sql.Rows.NextResultSet] after fetching all query data in
186+
// order to move to the result set that contains the spannerpb.ResultSetStats.
187+
ReturnResultSetStats bool
166188
}
167189

168190
type DecodeOption int

driver_with_mockserver_test.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4747,6 +4747,258 @@ func TestPostgreSQLDialect(t *testing.T) {
47474747
}
47484748
}
47494749

4750+
func TestReturnResultSetMetadata(t *testing.T) {
4751+
t.Parallel()
4752+
4753+
db, _, teardown := setupTestDBConnection(t)
4754+
defer teardown()
4755+
rows, err := db.QueryContext(context.Background(), testutil.SelectFooFromBar, ExecOptions{ReturnResultSetMetadata: true})
4756+
if err != nil {
4757+
t.Fatal(err)
4758+
}
4759+
defer func() { _ = rows.Close() }()
4760+
4761+
// Verify that the first result set contains the ResultSetMetadata.
4762+
if !rows.Next() {
4763+
t.Fatal("no rows")
4764+
}
4765+
var meta *sppb.ResultSetMetadata
4766+
if err := rows.Scan(&meta); err != nil {
4767+
t.Fatalf("failed to scan metadata: %v", err)
4768+
}
4769+
if g, w := len(meta.RowType.Fields), 1; g != w {
4770+
t.Fatalf("cols count mismatch\n Got: %v\nWant: %v", g, w)
4771+
}
4772+
if rows.Next() {
4773+
t.Fatal("more rows than expected")
4774+
}
4775+
4776+
// Move to the next result set, which should contain the data.
4777+
if !rows.NextResultSet() {
4778+
t.Fatal("no more result sets found")
4779+
}
4780+
4781+
for want := int64(1); rows.Next(); want++ {
4782+
cols, err := rows.Columns()
4783+
if err != nil {
4784+
t.Fatal(err)
4785+
}
4786+
if !cmp.Equal(cols, []string{"FOO"}) {
4787+
t.Fatalf("cols mismatch\nGot: %v\nWant: %v", cols, []string{"FOO"})
4788+
}
4789+
var got int64
4790+
err = rows.Scan(&got)
4791+
if err != nil {
4792+
t.Fatal(err)
4793+
}
4794+
if got != want {
4795+
t.Fatalf("value mismatch\nGot: %v\nWant: %v", got, want)
4796+
}
4797+
}
4798+
if rows.Err() != nil {
4799+
t.Fatal(rows.Err())
4800+
}
4801+
4802+
// There should be no more result sets.
4803+
if rows.NextResultSet() {
4804+
t.Fatal("more result sets than expected")
4805+
}
4806+
}
4807+
4808+
func TestReturnResultSetMetadataError(t *testing.T) {
4809+
t.Parallel()
4810+
4811+
db, server, teardown := setupTestDBConnection(t)
4812+
defer teardown()
4813+
query := "select * from non_existing_table"
4814+
_ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{
4815+
Type: testutil.StatementResultError,
4816+
Err: gstatus.Error(codes.NotFound, "Table not found"),
4817+
})
4818+
rows, err := db.QueryContext(context.Background(), query, ExecOptions{ReturnResultSetMetadata: true})
4819+
if err != nil {
4820+
t.Fatal(err)
4821+
}
4822+
defer func() { _ = rows.Close() }()
4823+
4824+
if rows.Next() {
4825+
t.Fatal("Next should fail")
4826+
}
4827+
var meta *sppb.ResultSetMetadata
4828+
if err := rows.Scan(&meta); err == nil {
4829+
t.Fatal("missing error when scanning metadata")
4830+
} else {
4831+
if g, w := spanner.ErrCode(err), codes.NotFound; g != w {
4832+
t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
4833+
}
4834+
}
4835+
4836+
// Moving to the next result set fails because the query failed.
4837+
if rows.NextResultSet() {
4838+
t.Fatal("got unexpected next result set")
4839+
}
4840+
}
4841+
4842+
func TestReturnResultSetStats(t *testing.T) {
4843+
t.Parallel()
4844+
4845+
db, server, teardown := setupTestDBConnection(t)
4846+
defer teardown()
4847+
query := "insert into singers (name) values ('test') then return id"
4848+
resultSet := testutil.CreateSingleColumnInt64ResultSet([]int64{42598}, "id")
4849+
_ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{
4850+
Type: testutil.StatementResultResultSet,
4851+
ResultSet: resultSet,
4852+
UpdateCount: 1,
4853+
})
4854+
4855+
rows, err := db.QueryContext(context.Background(), query, ExecOptions{ReturnResultSetStats: true})
4856+
if err != nil {
4857+
t.Fatal(err)
4858+
}
4859+
defer func() { _ = rows.Close() }()
4860+
4861+
// The first result set should contain the data.
4862+
for want := int64(42598); rows.Next(); want++ {
4863+
cols, err := rows.Columns()
4864+
if err != nil {
4865+
t.Fatal(err)
4866+
}
4867+
if !cmp.Equal(cols, []string{"id"}) {
4868+
t.Fatalf("cols mismatch\nGot: %v\nWant: %v", cols, []string{"id"})
4869+
}
4870+
var got int64
4871+
err = rows.Scan(&got)
4872+
if err != nil {
4873+
t.Fatal(err)
4874+
}
4875+
if got != want {
4876+
t.Fatalf("value mismatch\nGot: %v\nWant: %v", got, want)
4877+
}
4878+
}
4879+
if rows.Err() != nil {
4880+
t.Fatal(rows.Err())
4881+
}
4882+
4883+
// The next result set should contain the stats.
4884+
if !rows.NextResultSet() {
4885+
t.Fatal("missing stats result set")
4886+
}
4887+
4888+
// Get the stats.
4889+
if !rows.Next() {
4890+
t.Fatal("no stats rows")
4891+
}
4892+
var stats *sppb.ResultSetStats
4893+
if err := rows.Scan(&stats); err != nil {
4894+
t.Fatalf("failed to scan stats: %v", err)
4895+
}
4896+
if g, w := stats.GetRowCountExact(), int64(1); g != w {
4897+
t.Fatalf("row count mismatch\n Got: %v\nWant: %v", g, w)
4898+
}
4899+
if rows.Next() {
4900+
t.Fatal("more rows than expected")
4901+
}
4902+
4903+
// There should be no more result sets.
4904+
if rows.NextResultSet() {
4905+
t.Fatal("more result sets than expected")
4906+
}
4907+
}
4908+
4909+
func TestReturnResultSetMetadataAndStats(t *testing.T) {
4910+
t.Parallel()
4911+
4912+
db, server, teardown := setupTestDBConnection(t)
4913+
defer teardown()
4914+
4915+
query := "insert into singers (name) values ('test') then return id"
4916+
resultSet := testutil.CreateSingleColumnInt64ResultSet([]int64{42598}, "id")
4917+
_ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{
4918+
Type: testutil.StatementResultResultSet,
4919+
ResultSet: resultSet,
4920+
UpdateCount: 1,
4921+
})
4922+
4923+
rows, err := db.QueryContext(context.Background(), query, ExecOptions{
4924+
ReturnResultSetMetadata: true,
4925+
ReturnResultSetStats: true,
4926+
})
4927+
if err != nil {
4928+
t.Fatal(err)
4929+
}
4930+
defer func() { _ = rows.Close() }()
4931+
4932+
// Verify that the first result set contains the ResultSetMetadata.
4933+
if !rows.Next() {
4934+
t.Fatal("no rows")
4935+
}
4936+
var meta *sppb.ResultSetMetadata
4937+
if err := rows.Scan(&meta); err != nil {
4938+
t.Fatalf("failed to scan metadata: %v", err)
4939+
}
4940+
if g, w := len(meta.RowType.Fields), 1; g != w {
4941+
t.Fatalf("cols count mismatch\n Got: %v\nWant: %v", g, w)
4942+
}
4943+
if g, w := meta.RowType.Fields[0].Name, "id"; g != w {
4944+
t.Fatalf("column name mismatch\n Got: %v\nWant: %v", g, w)
4945+
}
4946+
if rows.Next() {
4947+
t.Fatal("more rows than expected")
4948+
}
4949+
4950+
// Move to the next result set, which should contain the data.
4951+
if !rows.NextResultSet() {
4952+
t.Fatal("no more result sets found")
4953+
}
4954+
4955+
for want := int64(42598); rows.Next(); want++ {
4956+
cols, err := rows.Columns()
4957+
if err != nil {
4958+
t.Fatal(err)
4959+
}
4960+
if !cmp.Equal(cols, []string{"id"}) {
4961+
t.Fatalf("cols mismatch\nGot: %v\nWant: %v", cols, []string{"id"})
4962+
}
4963+
var got int64
4964+
err = rows.Scan(&got)
4965+
if err != nil {
4966+
t.Fatal(err)
4967+
}
4968+
if got != want {
4969+
t.Fatalf("value mismatch\n Got: %v\nWant: %v", got, want)
4970+
}
4971+
}
4972+
if rows.Err() != nil {
4973+
t.Fatal(rows.Err())
4974+
}
4975+
4976+
// The next result set should contain the stats.
4977+
if !rows.NextResultSet() {
4978+
t.Fatal("missing stats result set")
4979+
}
4980+
4981+
// Get the stats.
4982+
if !rows.Next() {
4983+
t.Fatal("no stats rows")
4984+
}
4985+
var stats *sppb.ResultSetStats
4986+
if err := rows.Scan(&stats); err != nil {
4987+
t.Fatalf("failed to scan stats: %v", err)
4988+
}
4989+
if g, w := stats.GetRowCountExact(), int64(1); g != w {
4990+
t.Fatalf("row count mismatch\n Got: %v\nWant: %v", g, w)
4991+
}
4992+
if rows.Next() {
4993+
t.Fatal("more rows than expected")
4994+
}
4995+
4996+
// There should be no more result sets.
4997+
if rows.NextResultSet() {
4998+
t.Fatal("more result sets than expected")
4999+
}
5000+
}
5001+
47505002
func numeric(v string) big.Rat {
47515003
res, _ := big.NewRat(1, 1).SetString(v)
47525004
return *res

merged_row_iterator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,3 +263,7 @@ func (m *mergedRowIterator) Metadata() (*sppb.ResultSetMetadata, error) {
263263
}
264264
return m.metadata, nil
265265
}
266+
267+
func (m *mergedRowIterator) ResultSetStats() *sppb.ResultSetStats {
268+
return &sppb.ResultSetStats{}
269+
}

0 commit comments

Comments
 (0)