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
237 lines
5.7 KiB
Go
237 lines
5.7 KiB
Go
package clickhouse
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// treeNode is one line of EXPLAIN QUERY TREE output. Lines of the form
|
|
// `KIND id: N, key: value, ...` are nodes with a kind and attributes; every
|
|
// other line (section headers such as PROJECTION or ARGUMENTS, and the
|
|
// `name Type` lines under PROJECTION COLUMNS) keeps only its text.
|
|
type treeNode struct {
|
|
kind string
|
|
id int
|
|
attrs map[string]string
|
|
text string
|
|
parent *treeNode
|
|
children []*treeNode
|
|
}
|
|
|
|
// queryTree is a parsed EXPLAIN QUERY TREE dump.
|
|
type queryTree struct {
|
|
root *treeNode
|
|
byID map[int]*treeNode
|
|
}
|
|
|
|
var nodeLineRe = regexp.MustCompile(`^([A-Z_]+) id: (\d+)(?:, (.*))?$`)
|
|
|
|
// parseQueryTree builds the tree from the dump's lines, using the two-space
|
|
// indentation to recover nesting.
|
|
func parseQueryTree(lines []string) (*queryTree, error) {
|
|
root := &treeNode{id: -1, text: "<root>"}
|
|
t := &queryTree{root: root, byID: map[int]*treeNode{}}
|
|
stack := []*treeNode{root}
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
indent := len(line) - len(strings.TrimLeft(line, " "))
|
|
if indent%2 != 0 {
|
|
return nil, fmt.Errorf("query tree: unexpected indentation in %q", line)
|
|
}
|
|
depth := indent/2 + 1
|
|
if depth > len(stack) {
|
|
return nil, fmt.Errorf("query tree: line %q is nested too deeply", line)
|
|
}
|
|
stack = stack[:depth]
|
|
parent := stack[len(stack)-1]
|
|
|
|
n := &treeNode{id: -1, text: line[indent:], parent: parent}
|
|
if m := nodeLineRe.FindStringSubmatch(n.text); m != nil {
|
|
n.kind = m[1]
|
|
n.id, _ = strconv.Atoi(m[2])
|
|
n.attrs = parseAttrs(m[3])
|
|
t.byID[n.id] = n
|
|
}
|
|
parent.children = append(parent.children, n)
|
|
stack = append(stack, n)
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
var attrKeyRe = regexp.MustCompile(`^, ([a-z_]+): `)
|
|
|
|
// parseAttrs splits `key: value, key: value` where values may themselves
|
|
// contain commas inside parentheses or quotes, as types and constants do.
|
|
func parseAttrs(s string) map[string]string {
|
|
attrs := map[string]string{}
|
|
if s == "" {
|
|
return attrs
|
|
}
|
|
// Positions at which a new `, key: ` begins at top level.
|
|
var cuts []int
|
|
depth := 0
|
|
inQuote := false
|
|
for i := 0; i < len(s); i++ {
|
|
switch c := s[i]; {
|
|
case c == '\'':
|
|
inQuote = !inQuote
|
|
case inQuote:
|
|
case c == '(':
|
|
depth++
|
|
case c == ')':
|
|
depth--
|
|
case c == ',' && depth == 0 && attrKeyRe.MatchString(s[i:]):
|
|
cuts = append(cuts, i)
|
|
}
|
|
}
|
|
cuts = append(cuts, len(s))
|
|
start := 0
|
|
for _, cut := range cuts {
|
|
pair := s[start:cut]
|
|
if key, value, ok := strings.Cut(pair, ": "); ok {
|
|
attrs[key] = value
|
|
}
|
|
start = cut + 2
|
|
}
|
|
return attrs
|
|
}
|
|
|
|
// section returns the child section header of a node, such as PROJECTION
|
|
// or JOIN TREE, or nil.
|
|
func (n *treeNode) section(name string) *treeNode {
|
|
for _, c := range n.children {
|
|
if c.kind == "" && c.text == name {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// list returns the nodes of the LIST under a section header.
|
|
func (n *treeNode) list() []*treeNode {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
for _, c := range n.children {
|
|
if c.kind == "LIST" {
|
|
return c.children
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// firstQuery returns the QUERY node describing a result set: the node itself
|
|
// or, for a UNION, its first branch, whose projection names the union's
|
|
// columns.
|
|
func firstQuery(n *treeNode) *treeNode {
|
|
for depth := 0; n != nil && depth < 32; depth++ {
|
|
switch n.kind {
|
|
case "QUERY":
|
|
return n
|
|
case "UNION":
|
|
n = firstNode(n.section("QUERIES").list())
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func firstNode(nodes []*treeNode) *treeNode {
|
|
if len(nodes) == 0 {
|
|
return nil
|
|
}
|
|
return nodes[0]
|
|
}
|
|
|
|
// projection returns the output column names of a query and the expression
|
|
// node producing each, in order.
|
|
func projection(q *treeNode) (names []string, nodes []*treeNode) {
|
|
q = firstQuery(q)
|
|
if q == nil {
|
|
return nil, nil
|
|
}
|
|
if cols := q.section("PROJECTION COLUMNS"); cols != nil {
|
|
for _, c := range cols.children {
|
|
names = append(names, projectedName(c.text))
|
|
}
|
|
}
|
|
return names, q.section("PROJECTION").list()
|
|
}
|
|
|
|
// projectedName strips the trailing type from a `name Type` line. The type
|
|
// is a single token that may carry a parenthesised argument list; the name
|
|
// may contain spaces of its own, as `plus(id, 1)` does.
|
|
func projectedName(line string) string {
|
|
end := len(line)
|
|
if strings.HasSuffix(line, ")") {
|
|
depth := 0
|
|
for end > 0 {
|
|
end--
|
|
if line[end] == ')' {
|
|
depth++
|
|
} else if line[end] == '(' {
|
|
depth--
|
|
if depth == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for end > 0 && isWordByte(line[end-1]) {
|
|
end--
|
|
}
|
|
return strings.TrimSpace(line[:end])
|
|
}
|
|
|
|
// sourceTable resolves a COLUMN node to the table it reads from, following
|
|
// column references through subqueries and CTEs. Columns computed by an
|
|
// expression have no source and yield "".
|
|
func (t *queryTree) sourceTable(col *treeNode) string {
|
|
for depth := 0; col != nil && col.kind == "COLUMN" && depth < 32; depth++ {
|
|
id, err := strconv.Atoi(col.attrs["source_id"])
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
src := t.byID[id]
|
|
if src == nil {
|
|
return ""
|
|
}
|
|
switch src.kind {
|
|
case "TABLE":
|
|
name := src.attrs["table_name"]
|
|
if i := strings.LastIndexByte(name, '.'); i >= 0 {
|
|
name = name[i+1:]
|
|
}
|
|
return name
|
|
case "QUERY", "UNION":
|
|
// The column is one of the subquery's output columns; keep
|
|
// following whatever expression produces it there.
|
|
want := col.attrs["column_name"]
|
|
names, nodes := projection(src)
|
|
col = nil
|
|
for i, name := range names {
|
|
if name == want && i < len(nodes) {
|
|
col = nodes[i]
|
|
break
|
|
}
|
|
}
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// childrenOrNil returns a node's children, tolerating a nil node.
|
|
func (n *treeNode) childrenOrNil() []*treeNode {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
return n.children
|
|
}
|