diff --git a/go.mod b/go.mod index 24bbbe9..45eea28 100644 --- a/go.mod +++ b/go.mod @@ -14,4 +14,5 @@ require ( google.golang.org/api v0.21.0 google.golang.org/genproto v0.0.0-20200416231807-8751e049a2a0 google.golang.org/grpc v1.28.1 + google.golang.org/protobuf v1.21.0 ) diff --git a/query_plan.go b/query_plan.go index 2f322fd..22f29d0 100644 --- a/query_plan.go +++ b/query_plan.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "sort" "strings" "github.com/xlab/treeprint" @@ -88,32 +89,56 @@ func (n *Node) IsVisible() bool { } func (n *Node) String() string { - operator := n.PlanNode.DisplayName - metadata := getAllMetadataString(n) - return operator + " " + metadata -} - -func getMetadataString(node *Node, key string) (string, bool) { - if node.PlanNode.Metadata == nil { - return "", false - } - if v, ok := node.PlanNode.Metadata.Fields[key]; ok { - return v.GetStringValue(), true - } else { - return "", false + metadataFields := n.PlanNode.GetMetadata().GetFields() + + var operator string + { + var components []string + for _, s := range []string{ + metadataFields["call_type"].GetStringValue(), + metadataFields["iterator_type"].GetStringValue(), + strings.TrimSuffix(metadataFields["scan_type"].GetStringValue(), "Scan"), + n.PlanNode.GetDisplayName(), + } { + if s != "" { + components = append(components, s) + } + } + operator = strings.Join(components, " ") } -} -func getAllMetadataString(node *Node) string { - if node.PlanNode.Metadata == nil { - return "" + + var metadata string + { + fields := make([]string, 0) + for k, v := range metadataFields { + switch k { + case "call_type", "iterator_type": // Skip because it is displayed in node title + continue + case "scan_target": // Skip because it is combined with scan_type + continue + case "subquery_cluster_node": // Skip because it is useless without displaying node id + continue + case "scan_type": + fields = append(fields, fmt.Sprintf("%s: %s", + strings.TrimSuffix(v.GetStringValue(), "Scan"), + metadataFields["scan_target"].GetStringValue())) + default: + fields = append(fields, fmt.Sprintf("%s: %s", k, v.GetStringValue())) + } + } + + sort.Strings(fields) + + if len(fields) != 0 { + metadata = fmt.Sprintf(`(%s)`, strings.Join(fields, ", ")) + } } - fields := make([]string, 0) - for k, v := range node.PlanNode.Metadata.Fields { - fields = append(fields, fmt.Sprintf("%s: %s", k, v.GetStringValue())) + if metadata == "" { + return operator } - return fmt.Sprintf(`(%s)`, strings.Join(fields, ", ")) + return operator + " " + metadata } func renderTree(tree treeprint.Tree, linkType string, node *Node) { diff --git a/query_plan_test.go b/query_plan_test.go new file mode 100644 index 0000000..c065e7f --- /dev/null +++ b/query_plan_test.go @@ -0,0 +1,90 @@ +package main + +import ( + "testing" + + "google.golang.org/genproto/googleapis/spanner/v1" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestNodeString(t *testing.T) { + for _, test := range []struct { + title string + node *Node + want string + }{ + {"Distributed Union with call_type=Local", + &Node{PlanNode: &spanner.PlanNode{ + DisplayName: "Distributed Union", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "call_type": {Kind: &structpb.Value_StringValue{StringValue: "Local"}}, + "subquery_cluster_node": {Kind: &structpb.Value_StringValue{StringValue: "4"}}, + }, + }, + }}, "Local Distributed Union", + }, + {"Scan with scan_type=IndexScan and Full scan=true", + &Node{PlanNode: &spanner.PlanNode{ + DisplayName: "Scan", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "scan_type": {Kind: &structpb.Value_StringValue{StringValue: "IndexScan"}}, + "scan_target": {Kind: &structpb.Value_StringValue{StringValue: "SongsBySongName"}}, + "Full scan": {Kind: &structpb.Value_StringValue{StringValue: "true"}}, + }, + }, + }}, "Index Scan (Full scan: true, Index: SongsBySongName)"}, + { "Scan with scan_type=TableScan", + &Node{PlanNode: &spanner.PlanNode{ + DisplayName: "Scan", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "scan_type": {Kind: &structpb.Value_StringValue{StringValue: "TableScan"}}, + "scan_target": {Kind: &structpb.Value_StringValue{StringValue: "Songs"}}, + }, + }, + }}, "Table Scan (Table: Songs)"}, + {"Scan with scan_type=BatchScan", + &Node{PlanNode: &spanner.PlanNode{ + DisplayName: "Scan", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "scan_type": {Kind: &structpb.Value_StringValue{StringValue: "BatchScan"}}, + "scan_target": {Kind: &structpb.Value_StringValue{StringValue: "$v2"}}, + }, + }, + }}, "Batch Scan (Batch: $v2)"}, + {"Sort Limit with call_type=Local", + &Node{PlanNode: &spanner.PlanNode{ + DisplayName: "Sort Limit", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "call_type": {Kind: &structpb.Value_StringValue{StringValue: "Local"}}, + }, + }, + }}, "Local Sort Limit"}, + {"Sort Limit with call_type=Global", + &Node{PlanNode: &spanner.PlanNode{ + DisplayName: "Sort Limit", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "call_type": {Kind: &structpb.Value_StringValue{StringValue: "Global"}}, + }, + }, + }}, "Global Sort Limit"}, + {"Aggregate with iterator_type=Stream", + &Node{PlanNode: &spanner.PlanNode{ + DisplayName: "Aggregate", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "iterator_type": {Kind: &structpb.Value_StringValue{StringValue: "Stream"}}, + }, + }, + }}, "Stream Aggregate"}, + } { + if got := test.node.String(); got != test.want { + t.Errorf("%s: node.String() = %q but want %q", test.title, got, test.want) + } + } +}