snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
272
internal/goldeneye/clickhouse/analyze.go
Normal file
272
internal/goldeneye/clickhouse/analyze.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package clickhouse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/goldeneye/analysis"
|
||||
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
|
||||
)
|
||||
|
||||
// analyze runs every query against the schema and fixture and records what
|
||||
// ClickHouse reports about each.
|
||||
func analyze(ctx context.Context, l local, schema, fixture string, queries []endtoend.Query) ([]analysis.Query, error) {
|
||||
out := make([]analysis.Query, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
aq, err := analyzeQuery(ctx, l, schema, fixture, q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", q.Name, err)
|
||||
}
|
||||
out = append(out, aq)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func analyzeQuery(ctx context.Context, l local, schema, fixture string, q endtoend.Query) (analysis.Query, error) {
|
||||
sql, phs := bindPlaceholders(q.SQL)
|
||||
explain := returnsRows(sql)
|
||||
|
||||
var script strings.Builder
|
||||
for _, stmt := range []string{schema, fixture} {
|
||||
if s := strings.TrimRight(strings.TrimSpace(stmt), ";"); s != "" {
|
||||
script.WriteString(s)
|
||||
script.WriteString(";\n")
|
||||
}
|
||||
}
|
||||
if explain {
|
||||
script.WriteString("EXPLAIN QUERY TREE " + sql + ";\n")
|
||||
}
|
||||
script.WriteString(sql + ";\n")
|
||||
|
||||
results, err := l.run(ctx, script.String())
|
||||
if err != nil {
|
||||
return analysis.Query{}, err
|
||||
}
|
||||
|
||||
aq := analysis.Query{
|
||||
Name: q.Name,
|
||||
Cmd: q.Cmd,
|
||||
Columns: []analysis.Column{},
|
||||
Params: []analysis.Param{},
|
||||
}
|
||||
if !explain {
|
||||
return analyzeExec(ctx, l, script.String(), sql, phs, aq)
|
||||
}
|
||||
if len(results) != 2 {
|
||||
return analysis.Query{}, fmt.Errorf("expected the query tree and one result set, got %d results", len(results))
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for _, row := range results[0].Data {
|
||||
var line string
|
||||
if err := json.Unmarshal(row["explain"], &line); err != nil {
|
||||
return analysis.Query{}, fmt.Errorf("reading query tree: %w", err)
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
tree, err := parseQueryTree(lines)
|
||||
if err != nil {
|
||||
return analysis.Query{}, err
|
||||
}
|
||||
|
||||
// Names and types come from the block header of the executed query, the
|
||||
// same header a driver sees. The tree only adds where each came from.
|
||||
_, nodes := projection(firstNode(tree.root.children))
|
||||
for i, col := range results[1].Meta {
|
||||
ac := column(col.Name, col.Type)
|
||||
if i < len(nodes) && len(nodes) == len(results[1].Meta) {
|
||||
ac.Table = tree.sourceTable(nodes[i])
|
||||
}
|
||||
aq.Columns = append(aq.Columns, ac)
|
||||
}
|
||||
|
||||
sentinels := tree.sentinels()
|
||||
for i, ph := range phs {
|
||||
ac := analysis.Column{}
|
||||
if sentinel := sentinels[i+1]; sentinel != nil {
|
||||
ac = tree.paramColumn(sentinel)
|
||||
}
|
||||
if ph.Name != "" {
|
||||
ac.Name = ph.Name
|
||||
}
|
||||
aq.Params = append(aq.Params, analysis.Param{Number: ph.Number, Column: ac})
|
||||
}
|
||||
return aq, nil
|
||||
}
|
||||
|
||||
func column(name, typ string) analysis.Column {
|
||||
if typ == "" {
|
||||
return analysis.Column{Name: name}
|
||||
}
|
||||
return analysis.Column{Name: name, Type: parseType(typ)}
|
||||
}
|
||||
|
||||
// returnsRows reports whether a statement produces a result set and so can
|
||||
// be explained as a query tree.
|
||||
func returnsRows(sql string) bool {
|
||||
head := strings.ToLower(strings.TrimSpace(sql))
|
||||
if strings.HasPrefix(head, "(") {
|
||||
return true
|
||||
}
|
||||
for _, kw := range []string{"select", "with", "show", "describe", "desc", "exists"} {
|
||||
if strings.HasPrefix(head, kw) && (len(head) == len(kw) || !isWordByte(head[len(kw)])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// sentinels finds the constants the placeholders were substituted with,
|
||||
// keyed by placeholder ordinal.
|
||||
func (t *queryTree) sentinels() map[int]*treeNode {
|
||||
found := map[int]*treeNode{}
|
||||
var walk func(n *treeNode)
|
||||
walk = func(n *treeNode) {
|
||||
if n.kind == "CONSTANT" {
|
||||
if k, ok := sentinelOrdinal(n); ok {
|
||||
found[k] = n
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, c := range n.children {
|
||||
walk(c)
|
||||
}
|
||||
}
|
||||
walk(t.root)
|
||||
return found
|
||||
}
|
||||
|
||||
// sentinelOrdinal recognises a constant folded directly from `NULL + k` or
|
||||
// `4294967295 + k`, the shapes sentinelFor produces, and returns k. A
|
||||
// constant folded from a larger expression that merely contains a sentinel
|
||||
// does not match; the sentinel is found nested inside it instead.
|
||||
func sentinelOrdinal(c *treeNode) (int, bool) {
|
||||
fn := firstNode(c.section("EXPRESSION").childrenOrNil())
|
||||
if fn == nil || fn.kind != "FUNCTION" || fn.attrs["function_name"] != "plus" {
|
||||
return 0, false
|
||||
}
|
||||
args := fn.section("ARGUMENTS").list()
|
||||
if len(args) != 2 || args[0].kind != "CONSTANT" || args[1].kind != "CONSTANT" {
|
||||
return 0, false
|
||||
}
|
||||
base := args[0].attrs["constant_value"]
|
||||
if base != "NULL" && base != "UInt64_"+limitBase {
|
||||
return 0, false
|
||||
}
|
||||
k, err := strconv.Atoi(strings.TrimPrefix(args[1].attrs["constant_value"], "UInt64_"))
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return k, true
|
||||
}
|
||||
|
||||
// paramColumn describes what a placeholder is compared with or assigned to:
|
||||
// the other operand of the function it is an argument of, preferring a
|
||||
// column over an expression, or the projected column it stands for.
|
||||
func (t *queryTree) paramColumn(sentinel *treeNode) analysis.Column {
|
||||
list := sentinel.parent
|
||||
if list != nil && list.kind == "LIST" && list.parent != nil {
|
||||
switch owner := list.parent; {
|
||||
case owner.kind == "" && owner.text == "ARGUMENTS":
|
||||
// Prefer a column operand, then an expression, then a constant.
|
||||
rank := map[string]int{"COLUMN": 0, "FUNCTION": 1, "CONSTANT": 2}
|
||||
var best *treeNode
|
||||
for _, sib := range list.children {
|
||||
if sib == sentinel {
|
||||
continue
|
||||
}
|
||||
if r, ok := rank[sib.kind]; ok && (best == nil || r < rank[best.kind]) {
|
||||
best = sib
|
||||
}
|
||||
}
|
||||
if best != nil {
|
||||
return t.describe(best)
|
||||
}
|
||||
case owner.kind == "" && owner.text == "PROJECTION":
|
||||
names, nodes := projection(owner.parent)
|
||||
for i, n := range nodes {
|
||||
if n == sentinel && i < len(names) {
|
||||
ac := column(names[i], sentinel.attrs["constant_value_type"])
|
||||
return ac
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return column("", sentinel.attrs["constant_value_type"])
|
||||
}
|
||||
|
||||
// describe turns a tree expression into a column description.
|
||||
func (t *queryTree) describe(n *treeNode) analysis.Column {
|
||||
switch n.kind {
|
||||
case "COLUMN":
|
||||
ac := column(n.attrs["column_name"], n.attrs["result_type"])
|
||||
ac.Table = t.sourceTable(n)
|
||||
return ac
|
||||
case "FUNCTION":
|
||||
return column(n.attrs["function_name"], n.attrs["result_type"])
|
||||
case "CONSTANT":
|
||||
// A constant folded from a function call, such as toDate(now()),
|
||||
// still names the function, which is what a placeholder compared
|
||||
// with it is named after.
|
||||
name := ""
|
||||
if fn := firstNode(n.section("EXPRESSION").childrenOrNil()); fn != nil && fn.kind == "FUNCTION" {
|
||||
name = fn.attrs["function_name"]
|
||||
}
|
||||
return column(name, n.attrs["constant_value_type"])
|
||||
}
|
||||
return analysis.Column{}
|
||||
}
|
||||
|
||||
var insertValuesRe = regexp.MustCompile(`(?is)^insert\s+into\s+(?:table\s+)?([\w.` + "`" + `"]+)\s*(?:\(([^)]*)\))?\s*(?:format\s+)?values\b`)
|
||||
|
||||
// analyzeExec runs a statement that returns no rows. The only parameters it
|
||||
// can describe are those of an INSERT ... VALUES, which map positionally
|
||||
// onto the target columns reported by DESCRIBE TABLE.
|
||||
func analyzeExec(ctx context.Context, l local, script, sql string, phs []placeholder, aq analysis.Query) (analysis.Query, error) {
|
||||
m := insertValuesRe.FindStringSubmatch(sql)
|
||||
if m != nil {
|
||||
script += "DESCRIBE TABLE " + m[1] + ";\n"
|
||||
}
|
||||
results, err := l.run(ctx, script)
|
||||
if err != nil {
|
||||
return analysis.Query{}, err
|
||||
}
|
||||
|
||||
var targets []analysis.Column
|
||||
if m != nil && len(results) == 1 {
|
||||
byName := map[string]analysis.Column{}
|
||||
var all []analysis.Column
|
||||
table := strings.Trim(m[1][strings.LastIndexByte(m[1], '.')+1:], "`\"")
|
||||
for _, row := range results[0].Data {
|
||||
var name, typ string
|
||||
json.Unmarshal(row["name"], &name)
|
||||
json.Unmarshal(row["type"], &typ)
|
||||
ac := column(name, typ)
|
||||
ac.Table = table
|
||||
byName[name] = ac
|
||||
all = append(all, ac)
|
||||
}
|
||||
if strings.TrimSpace(m[2]) == "" {
|
||||
targets = all
|
||||
} else {
|
||||
for _, name := range strings.Split(m[2], ",") {
|
||||
targets = append(targets, byName[strings.Trim(strings.TrimSpace(name), "`\"")])
|
||||
}
|
||||
}
|
||||
}
|
||||
for i, ph := range phs {
|
||||
ac := analysis.Column{}
|
||||
if len(targets) > 0 {
|
||||
ac = targets[i%len(targets)]
|
||||
}
|
||||
if ph.Name != "" {
|
||||
ac.Name = ph.Name
|
||||
}
|
||||
aq.Params = append(aq.Params, analysis.Param{Number: ph.Number, Column: ac})
|
||||
}
|
||||
return aq, nil
|
||||
}
|
||||
Reference in New Issue
Block a user