Skip to content

Commit 73f1bc3

Browse files
authored
Add typename in header and add DESCRIBE statement (#191)
* Add typename into header * Implements DESCRIBE statement * Fix column name in DESCRIBE * Update tests * Unify ExplainDmlStatement into ExplainStatement * Add IsMutation in ExplainStatement * Fix AffectedRows * Improve EXPLAIN in Cloud Spanner Emulator * Add comment * Rename field name * Fix PROTO typename * Fix integration_test.go * Update README.md * Separate ExplainStatement and DescribeStatement * Implement formatTypeSimple * Refactor printResult * Support typename in header of DML * Use raw []*pb.StructType_Field * Implement extractColumnNames() to extract column names * Revert signature of parseQueryResult() * Fix AffectedRows
1 parent e44e60f commit 73f1bc3

10 files changed

Lines changed: 192 additions & 86 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ and `{}` for a mutually exclusive keyword.
231231
| Show DML Execution Plan | `EXPLAIN {INSERT\|UPDATE\|DELETE} ...;` | |
232232
| Show Query Execution Plan with Stats | `EXPLAIN ANALYZE SELECT ...;` | |
233233
| Show DML Execution Plan with Stats | `EXPLAIN ANALYZE {INSERT\|UPDATE\|DELETE} ...;` | |
234+
| Show Query Result Shape | `DESCRIBE SELECT ...;` | |
235+
| Show DML Result Shape | `DESCRIBE {INSERT\|UPDATE\|DELETE} ... THEN RETURN ...;` | |
234236
| Start a new query optimizer statistics package construction | `ANALYZE;` | |
235237
| Start Read-Write Transaction | `BEGIN [RW] [PRIORITY {HIGH\|MEDIUM\|LOW}] [TAG <tag>];` | See [Request Priority](#request-priority) for details on the priority. The tag you set is used as both transaction tag and request tag. See also [Transaction Tags and Request Tags](#transaction-tags-and-request-tags).|
236238
| Commit Read-Write Transaction | `COMMIT;` | |

cli.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,11 +362,25 @@ func printResult(out io.Writer, result *Result, mode DisplayMode, interactive, v
362362
table.SetAlignment(tablewriter.ALIGN_LEFT)
363363
table.SetAutoWrapText(false)
364364

365+
var forceTableRender bool
366+
// This condition is true if statement is SelectStatement or DmlStatement
367+
if verbose && len(result.ColumnTypes) > 0 {
368+
forceTableRender = true
369+
var headers []string
370+
for _, field := range result.ColumnTypes {
371+
typename := formatTypeSimple(field.GetType())
372+
headers = append(headers, field.GetName()+"\n"+typename)
373+
}
374+
table.SetHeader(headers)
375+
} else {
376+
table.SetHeader(result.ColumnNames)
377+
}
378+
365379
for _, row := range result.Rows {
366380
table.Append(row.Columns)
367381
}
368-
table.SetHeader(result.ColumnNames)
369-
if len(result.Rows) > 0 {
382+
383+
if forceTableRender || len(result.Rows) > 0 {
370384
table.Render()
371385
}
372386
} else if mode == DisplayModeVertical {

decoder.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,41 @@ func nullJSONToString(v spanner.NullJSON) string {
323323
return "NULL"
324324
}
325325
}
326+
327+
func formatTypeSimple(typ *sppb.Type) string {
328+
switch code := typ.GetCode(); code {
329+
case sppb.TypeCode_ARRAY:
330+
return fmt.Sprintf("ARRAY<%v>", formatTypeSimple(typ.GetArrayElementType()))
331+
default:
332+
if name, ok := sppb.TypeCode_name[int32(code)]; ok {
333+
return name
334+
} else {
335+
return "UNKNOWN"
336+
}
337+
}
338+
}
339+
340+
func formatTypeVerbose(typ *sppb.Type) string {
341+
switch code := typ.GetCode(); code {
342+
case sppb.TypeCode_ARRAY:
343+
return fmt.Sprintf("ARRAY<%v>", formatTypeVerbose(typ.GetArrayElementType()))
344+
case sppb.TypeCode_ENUM, sppb.TypeCode_PROTO:
345+
return typ.GetProtoTypeFqn()
346+
case sppb.TypeCode_STRUCT:
347+
var structTypeStrs []string
348+
for _, v := range typ.GetStructType().GetFields() {
349+
if v.GetName() != "" {
350+
structTypeStrs = append(structTypeStrs, fmt.Sprintf("%v %v", v.GetName(), formatTypeVerbose(v.GetType())))
351+
} else {
352+
structTypeStrs = append(structTypeStrs, fmt.Sprintf("%v", formatTypeVerbose(v.GetType())))
353+
}
354+
}
355+
return fmt.Sprintf("STRUCT<%v>", strings.Join(structTypeStrs, ", "))
356+
default:
357+
if name, ok := sppb.TypeCode_name[int32(code)]; ok {
358+
return name
359+
} else {
360+
return "UNKNOWN"
361+
}
362+
}
363+
}

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ require (
77
cloud.google.com/go/spanner v1.62.0
88
github.com/apstndb/gsqlsep v0.0.0-20230324124551-0e8335710080
99
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
10+
github.com/davecgh/go-spew v1.1.1
1011
github.com/google/go-cmp v0.6.0
1112
github.com/jessevdk/go-flags v1.4.0
12-
github.com/olekukonko/tablewriter v0.0.4
13+
github.com/olekukonko/tablewriter v0.0.5
1314
github.com/xlab/treeprint v1.0.1-0.20200715141336-10e0bc383e01
1415
google.golang.org/api v0.180.0
1516
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda
@@ -38,7 +39,7 @@ require (
3839
github.com/google/s2a-go v0.1.7 // indirect
3940
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
4041
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
41-
github.com/mattn/go-runewidth v0.0.8 // indirect
42+
github.com/mattn/go-runewidth v0.0.9 // indirect
4243
go.opencensus.io v0.24.0 // indirect
4344
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
4445
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect

go.sum

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -849,14 +849,13 @@ github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuz
849849
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
850850
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
851851
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
852-
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
853-
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
854-
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
852+
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
853+
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
855854
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
856855
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
857856
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
858-
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
859-
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
857+
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
858+
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
860859
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
861860
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
862861
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=

integration_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package main
1919
import (
2020
"context"
2121
"fmt"
22+
"google.golang.org/protobuf/testing/protocmp"
2223
"os"
2324
"strings"
2425
"sync/atomic"
@@ -142,11 +143,13 @@ func setup(t *testing.T, ctx context.Context, dmls []string) (*Session, string,
142143
}
143144

144145
func compareResult(t *testing.T, got *Result, expected *Result) {
146+
t.Helper()
145147
opts := []cmp.Option{
146148
cmpopts.IgnoreFields(Result{}, "Stats"),
147149
cmpopts.IgnoreFields(Result{}, "Timestamp"),
148150
// Commit Stats is only provided by real instances
149151
cmpopts.IgnoreFields(Result{}, "CommitStats"),
152+
protocmp.Transform(),
150153
}
151154
if !cmp.Equal(got, expected, opts...) {
152155
t.Errorf("diff: %s", cmp.Diff(got, expected, opts...))
@@ -183,7 +186,11 @@ func TestSelect(t *testing.T) {
183186
Row{[]string{"2", "false"}},
184187
},
185188
AffectedRows: 2,
186-
IsMutation: false,
189+
ColumnTypes: []*pb.StructType_Field{
190+
{Name: "id", Type: &pb.Type{Code: pb.TypeCode_INT64}},
191+
{Name: "active", Type: &pb.Type{Code: pb.TypeCode_BOOL}},
192+
},
193+
IsMutation: false,
187194
})
188195
}
189196

@@ -481,6 +488,11 @@ func TestReadOnlyTransaction(t *testing.T) {
481488
Row{[]string{"1", "true"}},
482489
Row{[]string{"2", "false"}},
483490
},
491+
492+
ColumnTypes: []*pb.StructType_Field{
493+
{Name: "id", Type: &pb.Type{Code: pb.TypeCode_INT64}},
494+
{Name: "active", Type: &pb.Type{Code: pb.TypeCode_BOOL}},
495+
},
484496
AffectedRows: 2,
485497
IsMutation: false,
486498
})
@@ -552,6 +564,10 @@ func TestReadOnlyTransaction(t *testing.T) {
552564
Row{[]string{"1", "true"}},
553565
Row{[]string{"2", "false"}},
554566
},
567+
ColumnTypes: []*pb.StructType_Field{
568+
{Name: "id", Type: &pb.Type{Code: pb.TypeCode_INT64}},
569+
{Name: "active", Type: &pb.Type{Code: pb.TypeCode_BOOL}},
570+
},
555571
AffectedRows: 2,
556572
IsMutation: false,
557573
})

session.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func (s *Session) RunQuery(ctx context.Context, stmt spanner.Statement) (*spanne
242242
}
243243

244244
// RunAnalyzeQuery analyzes a statement either on the running transaction or on the temporal read-only transaction.
245-
func (s *Session) RunAnalyzeQuery(ctx context.Context, stmt spanner.Statement) (*pb.QueryPlan, error) {
245+
func (s *Session) RunAnalyzeQuery(ctx context.Context, stmt spanner.Statement) (*pb.QueryPlan, *pb.ResultSetMetadata, error) {
246246
mode := pb.ExecuteSqlRequest_PLAN
247247
opts := spanner.QueryOptions{
248248
Mode: &mode,
@@ -251,13 +251,13 @@ func (s *Session) RunAnalyzeQuery(ctx context.Context, stmt spanner.Statement) (
251251
iter, _ := s.runQueryWithOptions(ctx, stmt, opts)
252252

253253
// Need to read rows from iterator to get the query plan.
254-
iter.Do(func(r *spanner.Row) error {
254+
err := iter.Do(func(r *spanner.Row) error {
255255
return nil
256256
})
257-
if iter.QueryPlan == nil {
258-
return nil, errors.New("query plan unavailable")
257+
if err != nil {
258+
return nil, nil, err
259259
}
260-
return iter.QueryPlan, nil
260+
return iter.QueryPlan, iter.Metadata, nil
261261
}
262262

263263
func (s *Session) runQueryWithOptions(ctx context.Context, stmt spanner.Statement, opts spanner.QueryOptions) (*spanner.RowIterator, *spanner.ReadOnlyTransaction) {
@@ -282,9 +282,9 @@ func (s *Session) runQueryWithOptions(ctx context.Context, stmt spanner.Statemen
282282
// RunUpdate executes a DML statement on the running read-write transaction.
283283
// It returns error if there is no running read-write transaction.
284284
// useUpdate flag enforce to use Update function internally and disable `THEN RETURN` result printing.
285-
func (s *Session) RunUpdate(ctx context.Context, stmt spanner.Statement, useUpdate bool) ([]Row, []string, int64, error) {
285+
func (s *Session) RunUpdate(ctx context.Context, stmt spanner.Statement, useUpdate bool) ([]Row, []string, int64, *pb.ResultSetMetadata, error) {
286286
if !s.InReadWriteTransaction() {
287-
return nil, nil, 0, errors.New("read-write transaction is not running")
287+
return nil, nil, 0, nil, errors.New("read-write transaction is not running")
288288
}
289289

290290
opts := spanner.QueryOptions{
@@ -298,14 +298,14 @@ func (s *Session) RunUpdate(ctx context.Context, stmt spanner.Statement, useUpda
298298
if useUpdate {
299299
rowCount, err := s.tc.rwTxn.UpdateWithOptions(ctx, stmt, opts)
300300
s.tc.sendHeartbeat = true
301-
return nil, nil, rowCount, err
301+
return nil, nil, rowCount, nil, err
302302
}
303303

304304
rowIter := s.tc.rwTxn.QueryWithOptions(ctx, stmt, opts)
305305
defer rowIter.Stop()
306306
result, columnNames, err := parseQueryResult(rowIter)
307307
s.tc.sendHeartbeat = true
308-
return result, columnNames, rowIter.RowCount, err
308+
return result, columnNames, rowIter.RowCount, rowIter.Metadata, err
309309
}
310310

311311
func (s *Session) Close() {

session_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestRequestPriority(t *testing.T) {
8181
}); err != nil {
8282
t.Fatalf("failed to run query: %v", err)
8383
}
84-
if _, _, _, err := session.RunUpdate(ctx, spanner.NewStatement("DELETE FROM t1 WHERE Id = 1"), true); err != nil {
84+
if _, _, _, _, err := session.RunUpdate(ctx, spanner.NewStatement("DELETE FROM t1 WHERE Id = 1"), true); err != nil {
8585
t.Fatalf("failed to run update: %v", err)
8686
}
8787
if _, err := session.CommitReadWriteTransaction(ctx); err != nil {

0 commit comments

Comments
 (0)