Skip to content

Commit e001cd1

Browse files
authored
feat: add option to return metadata and stats (#456)
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 e2620e6 commit e001cd1

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
@@ -798,7 +798,13 @@ func (c *conn) queryContext(ctx context.Context, query string, execOptions ExecO
798798
return nil, err
799799
}
800800
}
801-
res := &rows{it: iter, decodeOption: execOptions.DecodeOption, decodeToNativeArrays: execOptions.DecodeToNativeArrays}
801+
res := &rows{
802+
it: iter,
803+
decodeOption: execOptions.DecodeOption,
804+
decodeToNativeArrays: execOptions.DecodeToNativeArrays,
805+
returnResultSetMetadata: execOptions.ReturnResultSetMetadata,
806+
returnResultSetStats: execOptions.ReturnResultSetStats,
807+
}
802808
if execOptions.DirectExecuteQuery {
803809
// This call to res.getColumns() triggers the execution of the statement, as it needs to fetch the metadata.
804810
res.getColumns()

driver.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,28 @@ type ExecOptions struct {
176176
// that are executed outside explicit transactions use.
177177
AutocommitDMLMode AutocommitDMLMode
178178

179+
// ReturnResultSetMetadata instructs the driver to return an additional result
180+
// set with the full spannerpb.ResultSetMetadata of the query. This result set
181+
// contains one row and one column, and the value in that cell is the
182+
// spannerpb.ResultSetMetadata that was returned by Spanner when executing the
183+
// query. This result set will be the first result set in the sql.Rows object
184+
// that is returned.
185+
//
186+
// You have to call [sql.Rows.NextResultSet] to move to the result set that
187+
// contains the actual query data.
188+
ReturnResultSetMetadata bool
189+
190+
// ReturnResultSetStats instructs the driver to return an additional result
191+
// set with the full spannerpb.ResultSetStats of the query. This result set
192+
// contains one row and one column, and the value in that cell is the
193+
// spannerpb.ResultSetStats that was returned by Spanner when executing the
194+
// query. This result set will be the last result set in the sql.Rows object
195+
// that is returned.
196+
//
197+
// You have to call [sql.Rows.NextResultSet] after fetching all query data in
198+
// order to move to the result set that contains the spannerpb.ResultSetStats.
199+
ReturnResultSetStats bool
200+
179201
// DirectExecute determines whether a query is executed directly when the
180202
// [sql.DB.QueryContext] method is called, or whether the actual query execution
181203
// is delayed until the first call to [sql.Rows.Next]. The default is to delay

driver_with_mockserver_test.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4995,6 +4995,258 @@ func TestPostgreSQLDialect(t *testing.T) {
49954995
}
49964996
}
49974997

4998+
func TestReturnResultSetMetadata(t *testing.T) {
4999+
t.Parallel()
5000+
5001+
db, _, teardown := setupTestDBConnection(t)
5002+
defer teardown()
5003+
rows, err := db.QueryContext(context.Background(), testutil.SelectFooFromBar, ExecOptions{ReturnResultSetMetadata: true})
5004+
if err != nil {
5005+
t.Fatal(err)
5006+
}
5007+
defer func() { _ = rows.Close() }()
5008+
5009+
// Verify that the first result set contains the ResultSetMetadata.
5010+
if !rows.Next() {
5011+
t.Fatal("no rows")
5012+
}
5013+
var meta *sppb.ResultSetMetadata
5014+
if err := rows.Scan(&meta); err != nil {
5015+
t.Fatalf("failed to scan metadata: %v", err)
5016+
}
5017+
if g, w := len(meta.RowType.Fields), 1; g != w {
5018+
t.Fatalf("cols count mismatch\n Got: %v\nWant: %v", g, w)
5019+
}
5020+
if rows.Next() {
5021+
t.Fatal("more rows than expected")
5022+
}
5023+
5024+
// Move to the next result set, which should contain the data.
5025+
if !rows.NextResultSet() {
5026+
t.Fatal("no more result sets found")
5027+
}
5028+
5029+
for want := int64(1); rows.Next(); want++ {
5030+
cols, err := rows.Columns()
5031+
if err != nil {
5032+
t.Fatal(err)
5033+
}
5034+
if !cmp.Equal(cols, []string{"FOO"}) {
5035+
t.Fatalf("cols mismatch\nGot: %v\nWant: %v", cols, []string{"FOO"})
5036+
}
5037+
var got int64
5038+
err = rows.Scan(&got)
5039+
if err != nil {
5040+
t.Fatal(err)
5041+
}
5042+
if got != want {
5043+
t.Fatalf("value mismatch\nGot: %v\nWant: %v", got, want)
5044+
}
5045+
}
5046+
if rows.Err() != nil {
5047+
t.Fatal(rows.Err())
5048+
}
5049+
5050+
// There should be no more result sets.
5051+
if rows.NextResultSet() {
5052+
t.Fatal("more result sets than expected")
5053+
}
5054+
}
5055+
5056+
func TestReturnResultSetMetadataError(t *testing.T) {
5057+
t.Parallel()
5058+
5059+
db, server, teardown := setupTestDBConnection(t)
5060+
defer teardown()
5061+
query := "select * from non_existing_table"
5062+
_ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{
5063+
Type: testutil.StatementResultError,
5064+
Err: gstatus.Error(codes.NotFound, "Table not found"),
5065+
})
5066+
rows, err := db.QueryContext(context.Background(), query, ExecOptions{ReturnResultSetMetadata: true})
5067+
if err != nil {
5068+
t.Fatal(err)
5069+
}
5070+
defer func() { _ = rows.Close() }()
5071+
5072+
if rows.Next() {
5073+
t.Fatal("Next should fail")
5074+
}
5075+
var meta *sppb.ResultSetMetadata
5076+
if err := rows.Scan(&meta); err == nil {
5077+
t.Fatal("missing error when scanning metadata")
5078+
} else {
5079+
if g, w := spanner.ErrCode(err), codes.NotFound; g != w {
5080+
t.Fatalf("error code mismatch\n Got: %v\nWant: %v", g, w)
5081+
}
5082+
}
5083+
5084+
// Moving to the next result set fails because the query failed.
5085+
if rows.NextResultSet() {
5086+
t.Fatal("got unexpected next result set")
5087+
}
5088+
}
5089+
5090+
func TestReturnResultSetStats(t *testing.T) {
5091+
t.Parallel()
5092+
5093+
db, server, teardown := setupTestDBConnection(t)
5094+
defer teardown()
5095+
query := "insert into singers (name) values ('test') then return id"
5096+
resultSet := testutil.CreateSingleColumnInt64ResultSet([]int64{42598}, "id")
5097+
_ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{
5098+
Type: testutil.StatementResultResultSet,
5099+
ResultSet: resultSet,
5100+
UpdateCount: 1,
5101+
})
5102+
5103+
rows, err := db.QueryContext(context.Background(), query, ExecOptions{ReturnResultSetStats: true})
5104+
if err != nil {
5105+
t.Fatal(err)
5106+
}
5107+
defer func() { _ = rows.Close() }()
5108+
5109+
// The first result set should contain the data.
5110+
for want := int64(42598); rows.Next(); want++ {
5111+
cols, err := rows.Columns()
5112+
if err != nil {
5113+
t.Fatal(err)
5114+
}
5115+
if !cmp.Equal(cols, []string{"id"}) {
5116+
t.Fatalf("cols mismatch\nGot: %v\nWant: %v", cols, []string{"id"})
5117+
}
5118+
var got int64
5119+
err = rows.Scan(&got)
5120+
if err != nil {
5121+
t.Fatal(err)
5122+
}
5123+
if got != want {
5124+
t.Fatalf("value mismatch\nGot: %v\nWant: %v", got, want)
5125+
}
5126+
}
5127+
if rows.Err() != nil {
5128+
t.Fatal(rows.Err())
5129+
}
5130+
5131+
// The next result set should contain the stats.
5132+
if !rows.NextResultSet() {
5133+
t.Fatal("missing stats result set")
5134+
}
5135+
5136+
// Get the stats.
5137+
if !rows.Next() {
5138+
t.Fatal("no stats rows")
5139+
}
5140+
var stats *sppb.ResultSetStats
5141+
if err := rows.Scan(&stats); err != nil {
5142+
t.Fatalf("failed to scan stats: %v", err)
5143+
}
5144+
if g, w := stats.GetRowCountExact(), int64(1); g != w {
5145+
t.Fatalf("row count mismatch\n Got: %v\nWant: %v", g, w)
5146+
}
5147+
if rows.Next() {
5148+
t.Fatal("more rows than expected")
5149+
}
5150+
5151+
// There should be no more result sets.
5152+
if rows.NextResultSet() {
5153+
t.Fatal("more result sets than expected")
5154+
}
5155+
}
5156+
5157+
func TestReturnResultSetMetadataAndStats(t *testing.T) {
5158+
t.Parallel()
5159+
5160+
db, server, teardown := setupTestDBConnection(t)
5161+
defer teardown()
5162+
5163+
query := "insert into singers (name) values ('test') then return id"
5164+
resultSet := testutil.CreateSingleColumnInt64ResultSet([]int64{42598}, "id")
5165+
_ = server.TestSpanner.PutStatementResult(query, &testutil.StatementResult{
5166+
Type: testutil.StatementResultResultSet,
5167+
ResultSet: resultSet,
5168+
UpdateCount: 1,
5169+
})
5170+
5171+
rows, err := db.QueryContext(context.Background(), query, ExecOptions{
5172+
ReturnResultSetMetadata: true,
5173+
ReturnResultSetStats: true,
5174+
})
5175+
if err != nil {
5176+
t.Fatal(err)
5177+
}
5178+
defer func() { _ = rows.Close() }()
5179+
5180+
// Verify that the first result set contains the ResultSetMetadata.
5181+
if !rows.Next() {
5182+
t.Fatal("no rows")
5183+
}
5184+
var meta *sppb.ResultSetMetadata
5185+
if err := rows.Scan(&meta); err != nil {
5186+
t.Fatalf("failed to scan metadata: %v", err)
5187+
}
5188+
if g, w := len(meta.RowType.Fields), 1; g != w {
5189+
t.Fatalf("cols count mismatch\n Got: %v\nWant: %v", g, w)
5190+
}
5191+
if g, w := meta.RowType.Fields[0].Name, "id"; g != w {
5192+
t.Fatalf("column name mismatch\n Got: %v\nWant: %v", g, w)
5193+
}
5194+
if rows.Next() {
5195+
t.Fatal("more rows than expected")
5196+
}
5197+
5198+
// Move to the next result set, which should contain the data.
5199+
if !rows.NextResultSet() {
5200+
t.Fatal("no more result sets found")
5201+
}
5202+
5203+
for want := int64(42598); rows.Next(); want++ {
5204+
cols, err := rows.Columns()
5205+
if err != nil {
5206+
t.Fatal(err)
5207+
}
5208+
if !cmp.Equal(cols, []string{"id"}) {
5209+
t.Fatalf("cols mismatch\nGot: %v\nWant: %v", cols, []string{"id"})
5210+
}
5211+
var got int64
5212+
err = rows.Scan(&got)
5213+
if err != nil {
5214+
t.Fatal(err)
5215+
}
5216+
if got != want {
5217+
t.Fatalf("value mismatch\n Got: %v\nWant: %v", got, want)
5218+
}
5219+
}
5220+
if rows.Err() != nil {
5221+
t.Fatal(rows.Err())
5222+
}
5223+
5224+
// The next result set should contain the stats.
5225+
if !rows.NextResultSet() {
5226+
t.Fatal("missing stats result set")
5227+
}
5228+
5229+
// Get the stats.
5230+
if !rows.Next() {
5231+
t.Fatal("no stats rows")
5232+
}
5233+
var stats *sppb.ResultSetStats
5234+
if err := rows.Scan(&stats); err != nil {
5235+
t.Fatalf("failed to scan stats: %v", err)
5236+
}
5237+
if g, w := stats.GetRowCountExact(), int64(1); g != w {
5238+
t.Fatalf("row count mismatch\n Got: %v\nWant: %v", g, w)
5239+
}
5240+
if rows.Next() {
5241+
t.Fatal("more rows than expected")
5242+
}
5243+
5244+
// There should be no more result sets.
5245+
if rows.NextResultSet() {
5246+
t.Fatal("more result sets than expected")
5247+
}
5248+
}
5249+
49985250
func numeric(v string) big.Rat {
49995251
res, _ := big.NewRat(1, 1).SetString(v)
50005252
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)