Skip to content

Commit

Permalink
Add typename in header and add DESCRIBE statement (#191)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
apstndb authored Oct 17, 2024
1 parent e44e60f commit 73f1bc3
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 86 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ and `{}` for a mutually exclusive keyword.
| Show DML Execution Plan | `EXPLAIN {INSERT\|UPDATE\|DELETE} ...;` | |
| Show Query Execution Plan with Stats | `EXPLAIN ANALYZE SELECT ...;` | |
| Show DML Execution Plan with Stats | `EXPLAIN ANALYZE {INSERT\|UPDATE\|DELETE} ...;` | |
| Show Query Result Shape | `DESCRIBE SELECT ...;` | |
| Show DML Result Shape | `DESCRIBE {INSERT\|UPDATE\|DELETE} ... THEN RETURN ...;` | |
| Start a new query optimizer statistics package construction | `ANALYZE;` | |
| 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).|
| Commit Read-Write Transaction | `COMMIT;` | |
Expand Down
18 changes: 16 additions & 2 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,25 @@ func printResult(out io.Writer, result *Result, mode DisplayMode, interactive, v
table.SetAlignment(tablewriter.ALIGN_LEFT)
table.SetAutoWrapText(false)

var forceTableRender bool
// This condition is true if statement is SelectStatement or DmlStatement
if verbose && len(result.ColumnTypes) > 0 {
forceTableRender = true
var headers []string
for _, field := range result.ColumnTypes {
typename := formatTypeSimple(field.GetType())
headers = append(headers, field.GetName()+"\n"+typename)
}
table.SetHeader(headers)
} else {
table.SetHeader(result.ColumnNames)
}

for _, row := range result.Rows {
table.Append(row.Columns)
}
table.SetHeader(result.ColumnNames)
if len(result.Rows) > 0 {

if forceTableRender || len(result.Rows) > 0 {
table.Render()
}
} else if mode == DisplayModeVertical {
Expand Down
38 changes: 38 additions & 0 deletions decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,41 @@ func nullJSONToString(v spanner.NullJSON) string {
return "NULL"
}
}

func formatTypeSimple(typ *sppb.Type) string {
switch code := typ.GetCode(); code {
case sppb.TypeCode_ARRAY:
return fmt.Sprintf("ARRAY<%v>", formatTypeSimple(typ.GetArrayElementType()))
default:
if name, ok := sppb.TypeCode_name[int32(code)]; ok {
return name
} else {
return "UNKNOWN"
}
}
}

func formatTypeVerbose(typ *sppb.Type) string {
switch code := typ.GetCode(); code {
case sppb.TypeCode_ARRAY:
return fmt.Sprintf("ARRAY<%v>", formatTypeVerbose(typ.GetArrayElementType()))
case sppb.TypeCode_ENUM, sppb.TypeCode_PROTO:
return typ.GetProtoTypeFqn()
case sppb.TypeCode_STRUCT:
var structTypeStrs []string
for _, v := range typ.GetStructType().GetFields() {
if v.GetName() != "" {
structTypeStrs = append(structTypeStrs, fmt.Sprintf("%v %v", v.GetName(), formatTypeVerbose(v.GetType())))
} else {
structTypeStrs = append(structTypeStrs, fmt.Sprintf("%v", formatTypeVerbose(v.GetType())))
}
}
return fmt.Sprintf("STRUCT<%v>", strings.Join(structTypeStrs, ", "))
default:
if name, ok := sppb.TypeCode_name[int32(code)]; ok {
return name
} else {
return "UNKNOWN"
}
}
}
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ require (
cloud.google.com/go/spanner v1.62.0
github.com/apstndb/gsqlsep v0.0.0-20230324124551-0e8335710080
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/davecgh/go-spew v1.1.1
github.com/google/go-cmp v0.6.0
github.com/jessevdk/go-flags v1.4.0
github.com/olekukonko/tablewriter v0.0.4
github.com/olekukonko/tablewriter v0.0.5
github.com/xlab/treeprint v1.0.1-0.20200715141336-10e0bc383e01
google.golang.org/api v0.180.0
google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda
Expand Down Expand Up @@ -38,7 +39,7 @@ require (
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
github.com/mattn/go-runewidth v0.0.8 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
Expand Down
9 changes: 4 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -849,14 +849,13 @@ github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuz
github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE=
github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
Expand Down
18 changes: 17 additions & 1 deletion integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"context"
"fmt"
"google.golang.org/protobuf/testing/protocmp"
"os"
"strings"
"sync/atomic"
Expand Down Expand Up @@ -142,11 +143,13 @@ func setup(t *testing.T, ctx context.Context, dmls []string) (*Session, string,
}

func compareResult(t *testing.T, got *Result, expected *Result) {
t.Helper()
opts := []cmp.Option{
cmpopts.IgnoreFields(Result{}, "Stats"),
cmpopts.IgnoreFields(Result{}, "Timestamp"),
// Commit Stats is only provided by real instances
cmpopts.IgnoreFields(Result{}, "CommitStats"),
protocmp.Transform(),
}
if !cmp.Equal(got, expected, opts...) {
t.Errorf("diff: %s", cmp.Diff(got, expected, opts...))
Expand Down Expand Up @@ -183,7 +186,11 @@ func TestSelect(t *testing.T) {
Row{[]string{"2", "false"}},
},
AffectedRows: 2,
IsMutation: false,
ColumnTypes: []*pb.StructType_Field{
{Name: "id", Type: &pb.Type{Code: pb.TypeCode_INT64}},
{Name: "active", Type: &pb.Type{Code: pb.TypeCode_BOOL}},
},
IsMutation: false,
})
}

Expand Down Expand Up @@ -481,6 +488,11 @@ func TestReadOnlyTransaction(t *testing.T) {
Row{[]string{"1", "true"}},
Row{[]string{"2", "false"}},
},

ColumnTypes: []*pb.StructType_Field{
{Name: "id", Type: &pb.Type{Code: pb.TypeCode_INT64}},
{Name: "active", Type: &pb.Type{Code: pb.TypeCode_BOOL}},
},
AffectedRows: 2,
IsMutation: false,
})
Expand Down Expand Up @@ -552,6 +564,10 @@ func TestReadOnlyTransaction(t *testing.T) {
Row{[]string{"1", "true"}},
Row{[]string{"2", "false"}},
},
ColumnTypes: []*pb.StructType_Field{
{Name: "id", Type: &pb.Type{Code: pb.TypeCode_INT64}},
{Name: "active", Type: &pb.Type{Code: pb.TypeCode_BOOL}},
},
AffectedRows: 2,
IsMutation: false,
})
Expand Down
18 changes: 9 additions & 9 deletions session.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ func (s *Session) RunQuery(ctx context.Context, stmt spanner.Statement) (*spanne
}

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

// Need to read rows from iterator to get the query plan.
iter.Do(func(r *spanner.Row) error {
err := iter.Do(func(r *spanner.Row) error {
return nil
})
if iter.QueryPlan == nil {
return nil, errors.New("query plan unavailable")
if err != nil {
return nil, nil, err
}
return iter.QueryPlan, nil
return iter.QueryPlan, iter.Metadata, nil
}

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

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

rowIter := s.tc.rwTxn.QueryWithOptions(ctx, stmt, opts)
defer rowIter.Stop()
result, columnNames, err := parseQueryResult(rowIter)
s.tc.sendHeartbeat = true
return result, columnNames, rowIter.RowCount, err
return result, columnNames, rowIter.RowCount, rowIter.Metadata, err
}

func (s *Session) Close() {
Expand Down
2 changes: 1 addition & 1 deletion session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestRequestPriority(t *testing.T) {
}); err != nil {
t.Fatalf("failed to run query: %v", err)
}
if _, _, _, err := session.RunUpdate(ctx, spanner.NewStatement("DELETE FROM t1 WHERE Id = 1"), true); err != nil {
if _, _, _, _, err := session.RunUpdate(ctx, spanner.NewStatement("DELETE FROM t1 WHERE Id = 1"), true); err != nil {
t.Fatalf("failed to run update: %v", err)
}
if _, err := session.CommitReadWriteTransaction(ctx); err != nil {
Expand Down
Loading

0 comments on commit 73f1bc3

Please sign in to comment.