Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
221 lines
5.9 KiB
Go
221 lines
5.9 KiB
Go
package spanner
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"cloud.google.com/go/spanner/apiv1/spannerpb"
|
|
)
|
|
|
|
// The query plan is the one thing that says where a result column comes
|
|
// from and what a parameter stands for. It is a list of nodes: relational
|
|
// ones, such as a Scan of a table or the Serialize Result that produces the
|
|
// rows, and scalar ones, such as a Reference to a column or a variable, a
|
|
// Parameter, a Function. A relational node defines variables through its
|
|
// child links: a Scan of a table defines one per column it reads, named
|
|
// after the column, and any node may define one as another scalar, which
|
|
// a Reference names with a $. The children of Serialize Result after the
|
|
// relation it serializes are the result columns, in order; a DML plan
|
|
// lists its THEN RETURN columns first and the values it writes after
|
|
// them: for an INSERT the columns it inserts, in the statement's order,
|
|
// and for an UPDATE or DELETE the key columns of the table, then the
|
|
// columns an UPDATE sets. A comparison is a Function whose description
|
|
// reads ($col = @param).
|
|
|
|
// origin is what a scalar of the plan resolves to: a table column, a
|
|
// parameter, or nothing.
|
|
type origin struct {
|
|
table, column string
|
|
param string
|
|
}
|
|
|
|
// plan is a parsed query plan.
|
|
type plan struct {
|
|
nodes []*spannerpb.PlanNode
|
|
// vars maps a variable to the node that defines it and the scalar it
|
|
// is defined as.
|
|
vars map[string]definition
|
|
}
|
|
|
|
type definition struct {
|
|
owner *spannerpb.PlanNode
|
|
child *spannerpb.PlanNode
|
|
}
|
|
|
|
func newPlan(qp *spannerpb.QueryPlan) *plan {
|
|
p := &plan{vars: map[string]definition{}}
|
|
if qp == nil {
|
|
return p
|
|
}
|
|
p.nodes = qp.PlanNodes
|
|
for _, n := range p.nodes {
|
|
for _, l := range n.ChildLinks {
|
|
if l.Variable != "" {
|
|
p.vars[l.Variable] = definition{owner: n, child: p.node(l.ChildIndex)}
|
|
}
|
|
}
|
|
}
|
|
return p
|
|
}
|
|
|
|
func (p *plan) node(index int32) *spannerpb.PlanNode {
|
|
for _, n := range p.nodes {
|
|
if n.Index == index {
|
|
return n
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *plan) root() *spannerpb.PlanNode {
|
|
return p.node(0)
|
|
}
|
|
|
|
func meta(n *spannerpb.PlanNode, key string) string {
|
|
if n == nil || n.Metadata == nil {
|
|
return ""
|
|
}
|
|
if v, ok := n.Metadata.Fields[key]; ok {
|
|
return v.GetStringValue()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func description(n *spannerpb.PlanNode) string {
|
|
if n == nil || n.ShortRepresentation == nil {
|
|
return ""
|
|
}
|
|
return n.ShortRepresentation.Description
|
|
}
|
|
|
|
// serializeResult finds the node that produces the rows, the first
|
|
// Serialize Result reached from the root.
|
|
func (p *plan) serializeResult() *spannerpb.PlanNode {
|
|
var found *spannerpb.PlanNode
|
|
seen := map[int32]bool{}
|
|
var walk func(n *spannerpb.PlanNode)
|
|
walk = func(n *spannerpb.PlanNode) {
|
|
if n == nil || found != nil || seen[n.Index] {
|
|
return
|
|
}
|
|
seen[n.Index] = true
|
|
if n.DisplayName == "Serialize Result" {
|
|
found = n
|
|
return
|
|
}
|
|
for _, l := range n.ChildLinks {
|
|
walk(p.node(l.ChildIndex))
|
|
}
|
|
}
|
|
walk(p.root())
|
|
return found
|
|
}
|
|
|
|
// outputs lists the scalars Serialize Result produces, in order: every
|
|
// child after the relation it serializes.
|
|
func (p *plan) outputs() []*spannerpb.PlanNode {
|
|
sr := p.serializeResult()
|
|
if sr == nil {
|
|
return nil
|
|
}
|
|
var out []*spannerpb.PlanNode
|
|
for _, l := range sr.ChildLinks {
|
|
if c := p.node(l.ChildIndex); c != nil && c.Kind == spannerpb.PlanNode_SCALAR {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// mutation names the table a DML plan writes, or "" for a query.
|
|
func (p *plan) mutation() (table, operation string) {
|
|
for _, n := range p.nodes {
|
|
if n.DisplayName == "Apply Mutations" {
|
|
return meta(n, "table"), meta(n, "operation_type")
|
|
}
|
|
}
|
|
return "", ""
|
|
}
|
|
|
|
// resolve follows a scalar to what it stands for.
|
|
func (p *plan) resolve(n *spannerpb.PlanNode, seen map[int32]bool) origin {
|
|
if n == nil || seen[n.Index] {
|
|
return origin{}
|
|
}
|
|
seen[n.Index] = true
|
|
switch n.DisplayName {
|
|
case "Parameter":
|
|
return origin{param: meta(n, "name")}
|
|
case "Reference":
|
|
desc := description(n)
|
|
if name, ok := strings.CutPrefix(desc, "$"); ok {
|
|
return p.resolveVar(name, seen)
|
|
}
|
|
// A bare name is a column of the scan it is read by, or an input
|
|
// of a union, which names it in the link's type.
|
|
for _, m := range p.nodes {
|
|
for _, l := range m.ChildLinks {
|
|
if l.ChildIndex != n.Index {
|
|
continue
|
|
}
|
|
switch {
|
|
case m.DisplayName == "Scan" && meta(m, "scan_type") == "TableScan":
|
|
return origin{table: meta(m, "scan_target"), column: desc}
|
|
case m.DisplayName == "Scan":
|
|
// A batch scan reads the batch variable of that name.
|
|
target := strings.TrimPrefix(meta(m, "scan_target"), "$")
|
|
return p.resolveVar(target+".Batch."+desc, seen)
|
|
}
|
|
}
|
|
}
|
|
for _, m := range p.nodes {
|
|
if m.DisplayName != "Union Input" {
|
|
continue
|
|
}
|
|
for _, l := range m.ChildLinks {
|
|
if l.Type == desc {
|
|
return p.resolve(p.node(l.ChildIndex), seen)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return origin{}
|
|
}
|
|
|
|
// resolveVar follows a variable to what it is defined as.
|
|
func (p *plan) resolveVar(name string, seen map[int32]bool) origin {
|
|
d, ok := p.vars[name]
|
|
if !ok {
|
|
return origin{}
|
|
}
|
|
return p.resolve(d.child, seen)
|
|
}
|
|
|
|
var compareRe = regexp.MustCompile(`^\((.+) (=|!=|<>|<|<=|>|>=) (.+)\)$`)
|
|
|
|
// partners finds the table column each parameter is compared with, keyed
|
|
// by parameter name: the other operand of a comparison Function the
|
|
// parameter is an operand of. The first found wins.
|
|
func (p *plan) partners() map[string]origin {
|
|
out := map[string]origin{}
|
|
for _, n := range p.nodes {
|
|
if n.DisplayName != "Function" || len(n.ChildLinks) != 2 || !compareRe.MatchString(description(n)) {
|
|
continue
|
|
}
|
|
sides := []*spannerpb.PlanNode{p.node(n.ChildLinks[0].ChildIndex), p.node(n.ChildLinks[1].ChildIndex)}
|
|
for i, side := range sides {
|
|
o := p.resolve(side, map[int32]bool{})
|
|
if o.param == "" {
|
|
continue
|
|
}
|
|
other := p.resolve(sides[1-i], map[int32]bool{})
|
|
if other.table != "" {
|
|
if _, ok := out[o.param]; !ok {
|
|
out[o.param] = other
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|