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

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View 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
}

View File

@@ -0,0 +1,43 @@
package clickhouse
import (
"context"
"os"
"github.com/sqlc-dev/sqlc/internal/goldeneye/analysis"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// Analyze runs a case's queries through the clickhouse binary and returns
// the analysis in the JSON shape sqlc analyze prints.
func Analyze(ctx context.Context, binary string, c endtoend.Case) ([]byte, error) {
schema, err := os.ReadFile(c.Schema)
if err != nil {
return nil, err
}
var fixture []byte
if c.Fixture != "" {
if fixture, err = os.ReadFile(c.Fixture); err != nil {
return nil, err
}
}
queries, err := c.Queries()
if err != nil {
return nil, err
}
out, err := analyze(ctx, local{binary: binary}, string(schema), string(fixture), queries)
if err != nil {
return nil, err
}
return analysis.Encode(out)
}
// Check compares what ClickHouse reports for a case with the output the
// case committed, returning a diff when they differ.
func Check(ctx context.Context, binary string, c endtoend.Case) (string, error) {
got, err := Analyze(ctx, binary, c)
if err != nil {
return "", err
}
return c.Compare(got)
}

View File

@@ -0,0 +1,146 @@
// Package clickhouse generates the ClickHouse dialect seed under
// internal/engine/clickhouse/dialect from a clickhouse binary, run as an
// ephemeral `clickhouse local` process that needs no server.
//
// types.jsonl comes from system.data_type_families: every family that is not
// an alias becomes a type, the families that alias it become its aliases,
// and its category is decided by its name, since ClickHouse records no such
// thing. ClickHouse describes its functions no further than their names —
// system.functions carries no signatures — so functions.jsonl is written by
// hand and is not this package's business.
//
// The package also verifies the ClickHouse analyze cases under
// internal/endtoend/testdata against the same binary: each case's schema and
// fixture are loaded into a `clickhouse local` process and its queries run
// there. Result column types come from the executed query's result header,
// provenance from EXPLAIN QUERY TREE, and parameters from sentinel constants
// substituted for the placeholders, since ClickHouse itself never sees a ?.
// The answer is printed in the JSON shape sqlc analyze prints and compared
// with the case's committed stdout.json byte for byte.
//
// The binary is downloaded once per pinned version by Install, or supplied
// through the CLICKHOUSE environment variable.
package clickhouse
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"sort"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
)
// Engine is the name of the engine directory the dialect lives under.
const Engine = "clickhouse"
// typeFamilies lists every data type ClickHouse knows and, for a spelling
// such as INT or VARCHAR, the type it stands for.
const typeFamilies = `
SELECT name, alias_to
FROM system.data_type_families
ORDER BY name`
// Version reports the release a binary is.
func Version(ctx context.Context, binary string) (string, error) {
out, err := exec.CommandContext(ctx, binary, "local", "--version").Output()
if err != nil {
return "", fmt.Errorf("clickhouse local --version: %w", err)
}
return strings.TrimSpace(string(out)), nil
}
// Generate reads the dialect from the binary.
func Generate(ctx context.Context, binary string) (dialect.Files, error) {
results, err := local{binary: binary}.run(ctx, typeFamilies)
if err != nil {
return nil, err
}
if len(results) != 1 {
return nil, fmt.Errorf("clickhouse: expected one result set, got %d", len(results))
}
var families []family
for _, row := range results[0].Data {
var f family
if err := json.Unmarshal(row["name"], &f.Name); err != nil {
return nil, fmt.Errorf("clickhouse: system.data_type_families: %w", err)
}
if err := json.Unmarshal(row["alias_to"], &f.AliasTo); err != nil {
return nil, fmt.Errorf("clickhouse: system.data_type_families: %w", err)
}
families = append(families, f)
}
types, err := dialect.JSONL(readTypes(families))
if err != nil {
return nil, err
}
return dialect.Files{dialect.TypesFile: types}, nil
}
type family struct {
Name string
AliasTo string
}
// readTypes turns the families into types: one per family that is not an
// alias, carrying the spellings that alias it. The seed reads names without
// regard to case, so an alias that only differs in case from its type —
// bool for Bool, ENUM for Enum — is dropped.
func readTypes(families []family) []dialect.Type {
byName := map[string]*dialect.Type{}
var names []string
for _, f := range families {
if f.AliasTo != "" {
continue
}
byName[f.Name] = &dialect.Type{Name: f.Name, Category: category(f.Name)}
names = append(names, f.Name)
}
for _, f := range families {
if f.AliasTo == "" || strings.EqualFold(f.Name, f.AliasTo) {
continue
}
if t, ok := byName[f.AliasTo]; ok {
t.Aliases = append(t.Aliases, f.Name)
}
}
sort.Strings(names)
types := make([]dialect.Type, 0, len(names))
for _, name := range names {
types = append(types, *byName[name])
}
return types
}
// category assigns a type the PostgreSQL category letter the seed package
// uses, by the family's name: N for numbers, B for booleans, S for strings
// and the identifiers stored as them, D for dates and times, T for time
// spans, A for arrays and U for everything else.
func category(name string) string {
switch {
case name == "Bool":
return "B"
case name == "Array":
return "A"
case strings.HasPrefix(name, "Interval"):
return "T"
case hasPrefix(name, "UInt", "Int", "Float", "BFloat", "Decimal"):
return "N"
case name == "String", name == "FixedString", name == "UUID", name == "IPv4", name == "IPv6":
return "S"
case hasPrefix(name, "Date", "Time"):
return "D"
}
return "U"
}
func hasPrefix(name string, prefixes ...string) bool {
for _, prefix := range prefixes {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}

View File

@@ -0,0 +1,66 @@
package clickhouse
import (
"context"
"testing"
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// TestDialect verifies the committed ClickHouse dialect against what the
// pinned clickhouse binary reports. It skips unless the binary is installed.
func TestDialect(t *testing.T) {
binary, err := Locate()
if err != nil {
t.Skip(err)
}
ctx := context.Background()
version, err := Version(ctx, binary)
if err != nil {
t.Fatal(err)
}
files, err := Generate(ctx, binary)
if err != nil {
t.Fatal(err)
}
dir, err := dialect.Dir(Engine)
if err != nil {
t.Fatal(err)
}
report, err := dialect.Check(dir, files)
if err != nil {
t.Fatal(err)
}
if report != "" {
t.Errorf("%s does not match what %s reports:\n%s", dir, version, report)
}
}
// TestAnalyzeCases verifies every ClickHouse analyze case under
// internal/endtoend/testdata against what ClickHouse reports. It skips
// unless the binary is installed.
func TestAnalyzeCases(t *testing.T) {
binary, err := Locate()
if err != nil {
t.Skip(err)
}
cases, err := endtoend.Cases(Engine)
if err != nil {
t.Fatal(err)
}
if len(cases) == 0 {
t.Fatal("no clickhouse analyze cases found")
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
diff, err := Check(context.Background(), binary, c)
if err != nil {
t.Fatal(err)
}
if diff != "" {
t.Errorf("%s does not match what ClickHouse reports (-committed +clickhouse):\n%s", c.Output, diff)
}
})
}
}

View File

@@ -0,0 +1,221 @@
package clickhouse
import (
"archive/tar"
"compress/gzip"
"context"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
// DefaultVersion is the ClickHouse release the dialect is generated from.
// Bumping it is a deliberate change: the query tree format and type
// inference can shift between releases, so regenerate and review the dialect
// after changing it, and add the new release's assets to the table below.
const DefaultVersion = "25.8.2.29"
// asset is one downloadable build of ClickHouse. Linux builds are published
// as clickhouse-common-static tarballs holding the binary at
// usr/bin/clickhouse; macOS builds are published as bare binaries.
type asset struct {
Version string
OS string
Arch string
Name string
SHA512 string
}
// assets lists every build Install knows how to fetch, with the SHA-512 of
// the download. A version that is not in this table cannot be installed:
// verifying the download is the point of the table.
//
// The tarball checksums are the ones in the .sha512 files ClickHouse
// publishes next to them. ClickHouse publishes no checksum for the macOS
// binaries, so those were computed from the downloads.
var assets = []asset{
{"25.8.2.29", "linux", "amd64", "clickhouse-common-static-25.8.2.29-amd64.tgz", "6ff0aa1ffac6e564970174422ecde0d645cdb96812247a6e544d39cad6d78a514265f90a2bc7b4bad49903cea96eddd16a415a45b2aeaf9164461be76331bdee"},
{"25.8.2.29", "linux", "arm64", "clickhouse-common-static-25.8.2.29-arm64.tgz", "68204ca4d4e472790f808ee376251fae82e58066a31f35a40d15d442ce5988d697f18a1208d28b8bb8e2dfad4b20b7fcb5107e2178472abcd97251b8de7f058e"},
{"25.8.2.29", "darwin", "amd64", "clickhouse-macos", "2805805ad2506e37a3e71b4ae9e797bdc010a9368dc28e99bcaaa2c70a72cfdd031c0fce8fc304248fad73211d28d645f75c6432cfae1e1e54d72d04e8626cd4"},
{"25.8.2.29", "darwin", "arm64", "clickhouse-macos-aarch64", "4c9237e85c8d4e1aced2b339b32e086b4f23fa14f99754b056a7e33dda88c4fb5d52e09e29d20181ec5b580caadb00f36613802e71755ff34927535eb8babf79"},
}
// releaseTag returns the GitHub release tag for a version. ClickHouse tags
// its March and August releases as LTS and everything else as stable.
func releaseTag(version string) (string, error) {
parts := strings.Split(version, ".")
if len(parts) != 4 {
return "", fmt.Errorf("invalid ClickHouse version %q: want MAJOR.MINOR.PATCH.BUILD", version)
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return "", fmt.Errorf("invalid ClickHouse version %q: %w", version, err)
}
suffix := "-stable"
if minor == 3 || minor == 8 {
suffix = "-lts"
}
return "v" + version + suffix, nil
}
// releaseAsset finds the build for a platform in the table.
func releaseAsset(version, goos, goarch string) (asset, error) {
for _, a := range assets {
if a.Version == version && a.OS == goos && a.Arch == goarch {
return a, nil
}
}
for _, a := range assets {
if a.Version == version {
return asset{}, fmt.Errorf("no ClickHouse %s build is listed for %s/%s", version, goos, goarch)
}
}
return asset{}, fmt.Errorf("ClickHouse %s is not in the asset table; add its downloads and checksums to install.go", version)
}
// url is the asset's download address on GitHub.
func (a asset) url() (string, error) {
tag, err := releaseTag(a.Version)
if err != nil {
return "", err
}
return "https://github.com/ClickHouse/ClickHouse/releases/download/" + tag + "/" + a.Name, nil
}
// tarball reports whether the download is an archive rather than the binary.
func (a asset) tarball() bool {
return strings.HasSuffix(a.Name, ".tgz")
}
// cachedBinary is where Install puts the binary for a version.
func cachedBinary(version string) (string, error) {
dir, err := os.UserCacheDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "sqlc-clickhouse", version, "clickhouse"), nil
}
// Locate finds a clickhouse binary: the CLICKHOUSE environment variable wins,
// then the cached copy of DefaultVersion.
func Locate() (string, error) {
if path := os.Getenv("CLICKHOUSE"); path != "" {
return path, nil
}
path, err := cachedBinary(DefaultVersion)
if err != nil {
return "", err
}
if _, err := os.Stat(path); err != nil {
return "", fmt.Errorf("clickhouse %s is not installed: run `go run ./cmd/goldeneye install clickhouse` in internal/goldeneye, or set CLICKHOUSE to a clickhouse binary", DefaultVersion)
}
return path, nil
}
// Install downloads the clickhouse binary for a version into the cache and
// returns its path. It is a no-op when the version is already cached. The
// download is checked against the table's SHA-512 before it is installed.
func Install(ctx context.Context, version, goos, goarch string, progress io.Writer) (string, error) {
dest, err := cachedBinary(version)
if err != nil {
return "", err
}
if _, err := os.Stat(dest); err == nil {
return dest, nil
}
a, err := releaseAsset(version, goos, goarch)
if err != nil {
return "", err
}
url, err := a.url()
if err != nil {
return "", err
}
if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
return "", err
}
fmt.Fprintf(progress, "downloading %s\n", url)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("downloading %s: %s", url, resp.Status)
}
// Write next to the destination and rename so a partial or corrupt
// download never masquerades as an installed binary.
tmp, err := os.CreateTemp(filepath.Dir(dest), "clickhouse-*.partial")
if err != nil {
return "", err
}
defer os.Remove(tmp.Name())
// Hash every byte that comes off the wire, including the parts of a
// tarball after the binary, which extraction would otherwise not read.
sum := sha512.New()
body := io.TeeReader(resp.Body, sum)
var src io.Reader = body
if a.tarball() {
src, err = binaryInTarball(body)
if err != nil {
return "", fmt.Errorf("downloading %s: %w", url, err)
}
}
if _, err := io.Copy(tmp, src); err != nil {
tmp.Close()
return "", err
}
if _, err := io.Copy(io.Discard, body); err != nil {
tmp.Close()
return "", err
}
if err := tmp.Close(); err != nil {
return "", err
}
if got := hex.EncodeToString(sum.Sum(nil)); got != a.SHA512 {
return "", fmt.Errorf("downloading %s: SHA-512 mismatch: got %s, want %s", url, got, a.SHA512)
}
if err := os.Chmod(tmp.Name(), 0o755); err != nil {
return "", err
}
if err := os.Rename(tmp.Name(), dest); err != nil {
return "", err
}
return dest, nil
}
// binaryInTarball positions a reader at the clickhouse binary inside a
// clickhouse-common-static tarball.
func binaryInTarball(r io.Reader) (io.Reader, error) {
gz, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
tr := tar.NewReader(gz)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
return nil, errors.New("tarball does not contain usr/bin/clickhouse")
}
if err != nil {
return nil, err
}
if hdr.Typeflag == tar.TypeReg && strings.HasSuffix(hdr.Name, "/usr/bin/clickhouse") {
return tr, nil
}
}
}

View File

@@ -0,0 +1,82 @@
package clickhouse
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
// local runs SQL through an ephemeral `clickhouse local` process.
type local struct {
binary string
}
// resultSet is one JSON-format result printed by clickhouse local. Only
// statements that return rows print one; DDL and INSERT print nothing.
type resultSet struct {
Meta []resultColumn `json:"meta"`
Data []map[string]json.RawMessage `json:"data"`
Rows int `json:"rows"`
}
type resultColumn struct {
Name string `json:"name"`
Type string `json:"type"`
}
// run executes a multi-statement script in a fresh process with fresh
// storage and returns the result sets in statement order. Any statement
// failing fails the whole run with ClickHouse's own error message.
func (l local) run(ctx context.Context, script string) ([]resultSet, error) {
dir, err := os.MkdirTemp("", "sqlc-goldeneye-clickhouse-*")
if err != nil {
return nil, err
}
defer os.RemoveAll(dir)
queries := filepath.Join(dir, "queries.sql")
if err := os.WriteFile(queries, []byte(script), 0o600); err != nil {
return nil, err
}
// stdin must not be inherited: clickhouse local reads it as table data
// and blocks until it is closed.
cmd := exec.CommandContext(ctx, l.binary, "local",
"--multiquery",
"--queries-file", queries,
"--output-format", "JSON",
"--path", filepath.Join(dir, "data"),
)
cmd.Stdin = nil
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
msg := strings.TrimSpace(stderr.String())
if msg == "" {
msg = err.Error()
}
return nil, errors.New(msg)
}
var results []resultSet
dec := json.NewDecoder(&stdout)
for {
var rs resultSet
err := dec.Decode(&rs)
if errors.Is(err, io.EOF) {
return results, nil
}
if err != nil {
return nil, fmt.Errorf("decoding clickhouse local output: %w", err)
}
results = append(results, rs)
}
}

View File

@@ -0,0 +1,68 @@
package clickhouse
import (
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/endtoend"
)
// placeholder is one parameter reference in a query, in order of appearance.
type placeholder struct {
Number int
Name string // sqlc.arg / sqlc.narg name, empty for ?
}
// Placeholders are substituted with constant expressions that carry their
// ordinal, so they can be told apart from each other and from literal NULLs
// once ClickHouse has folded them: the query tree prints the expression a
// folded constant came from. NULL coerces to any type, so a comparison
// against it analyzes with the other operand's type. LIMIT and OFFSET
// reject NULL and only accept unsigned integers, so those get a value no
// query would plausibly contain.
const limitBase = "4294967295"
func sentinelFor(lastWord string, ordinal int) string {
switch strings.ToLower(lastWord) {
case "limit", "offset":
return fmt.Sprintf("toUInt64(%s + %d)", limitBase, ordinal)
}
return fmt.Sprintf("(NULL + %d)", ordinal)
}
// bindPlaceholders rewrites sqlc's parameter syntax (?, sqlc.arg(name),
// sqlc.narg(name)) into constants ClickHouse can analyze. ClickHouse binds
// every ? positionally, so each placeholder is its own parameter even when
// a name repeats, which is how sqlc numbers them too.
func bindPlaceholders(sql string) (string, []placeholder) {
var phs []placeholder
out := endtoend.Rewrite(sql, func(name, lastWord string) string {
phs = append(phs, placeholder{Number: len(phs) + 1, Name: name})
return sentinelFor(lastWord, len(phs))
})
return out, phs
}
func isWordByte(c byte) bool {
return c == '_' || c == '.' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
}
// skipQuoted returns the index just past the quoted token starting at i,
// honouring backslash escapes and doubled quotes.
func skipQuoted(s string, i int) int {
q := s[i]
j := i + 1
for j < len(s) {
switch {
case s[j] == '\\' && j+1 < len(s):
j += 2
case s[j] == q && j+1 < len(s) && s[j+1] == q:
j += 2
case s[j] == q:
return j + 1
default:
j++
}
}
return len(s)
}

View File

@@ -0,0 +1,236 @@
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
}

View File

@@ -0,0 +1,160 @@
package clickhouse
import (
"strconv"
"strings"
"github.com/sqlc-dev/sqlc/internal/goldeneye/analysis"
)
// A type is a call expression, the way ClickHouse itself models one: a
// lowercased name applied to an ordered argument list. Each argument is
// another type, an integer, a boolean or a quoted string, optionally
// labelled, so Array, Map and LowCardinality are ordinary names and nothing
// about a nested type is lost. Nullability is an attribute of a type rather
// than a wrapper, since every engine has it and only ClickHouse spells it as
// a type: Nullable(T) becomes T with nullable set, at whatever depth it
// appears. The shape maps one to one onto a protobuf message with a oneof
// for the argument value:
//
// Map(String, Nullable(UInt32))
// {"name": "map", "args": [
// {"type": {"name": "string"}},
// {"type": {"name": "uint32", "nullable": true}}]}
//
// Tuple(lat Float64, lon Float64)
// {"name": "tuple", "args": [
// {"label": "lat", "type": {"name": "float64"}},
// {"label": "lon", "type": {"name": "float64"}}]}
//
// Enum8('a' = 1, 'b' = 2)
// {"name": "enum8", "args": [{"label": "a", "int": 1}, {"label": "b", "int": 2}]}
//
// DateTime64(3, 'UTC')
// {"name": "datetime64", "args": [{"int": 3}, {"string": "UTC"}]}
//
// An identifier argument such as the function in AggregateFunction(uniq,
// String) is a word rather than a type. Resolving names against the
// catalog is the reader's job; the output only records what was said.
// parseType turns a ClickHouse type string into its expression.
func parseType(t string) *analysis.TypeExpr {
name, args := splitType(t)
name = strings.ToLower(strings.TrimSpace(name))
if name == "nullable" && len(args) == 1 {
expr := parseType(args[0])
expr.Nullable = true
return expr
}
if name == "" {
name = "nothing"
}
expr := &analysis.TypeExpr{Name: name}
for _, a := range args {
expr.Args = append(expr.Args, parseArg(a))
}
// LowCardinality(Nullable(T)) is the only order ClickHouse accepts for
// a nullable low-cardinality column, and the nullability is the
// column's: it reads as a nullable LowCardinality(T).
if name == "lowcardinality" && len(expr.Args) == 1 && expr.Args[0].Type != nil && expr.Args[0].Type.Nullable {
expr.Nullable = true
expr.Args[0].Type.Nullable = false
}
// The function an aggregate-function type names is a word, not a type.
if name == "aggregatefunction" || name == "simpleaggregatefunction" {
if len(expr.Args) > 0 && expr.Args[0].Type != nil && len(expr.Args[0].Type.Args) == 0 {
fn := expr.Args[0].Type.Name
expr.Args[0] = analysis.TypeArg{Ident: &fn}
}
}
return expr
}
// parseArg parses one argument: a quoted string, an integer, a boolean, a
// labelled argument (`lat Float64` in a Tuple, `'a' = 1` in an Enum), or a
// type.
func parseArg(a string) analysis.TypeArg {
a = strings.TrimSpace(a)
if strings.HasPrefix(a, "'") {
end := skipQuoted(a, 0)
lit := unquote(a[1 : end-1])
if rest := strings.TrimSpace(a[end:]); strings.HasPrefix(rest, "=") {
arg := parseArg(rest[1:])
arg.Label = lit
return arg
}
return analysis.TypeArg{String: &lit}
}
if n, err := strconv.ParseInt(a, 10, 64); err == nil {
return analysis.TypeArg{Int: &n}
}
switch strings.ToLower(a) {
case "true", "false":
b := strings.EqualFold(a, "true")
return analysis.TypeArg{Bool: &b}
}
if i := labelEnd(a); i > 0 {
arg := parseArg(a[i+1:])
arg.Label = a[:i]
return arg
}
return analysis.TypeArg{Type: parseType(a)}
}
// labelEnd returns the index of the space separating a label from the type
// it labels, or -1 when the argument has no label: a space that comes before
// any parenthesis, as in `lat Float64` or `tags Array(String)`.
func labelEnd(a string) int {
head := a
if p := strings.IndexByte(a, '('); p >= 0 {
head = a[:p]
}
return strings.IndexByte(head, ' ')
}
// unquote undoes the escaping inside a single-quoted ClickHouse literal.
func unquote(s string) string {
s = strings.ReplaceAll(s, `\'`, `'`)
s = strings.ReplaceAll(s, `''`, `'`)
return strings.ReplaceAll(s, `\\`, `\`)
}
// splitType splits `Base(arg, arg)` into its base name and top-level
// arguments, leaving nested parentheses and quoted strings intact.
func splitType(t string) (string, []string) {
t = strings.TrimSpace(t)
open := strings.IndexByte(t, '(')
if open < 0 || !strings.HasSuffix(t, ")") {
return t, nil
}
base := strings.TrimSpace(t[:open])
inner := t[open+1 : len(t)-1]
var (
args []string
depth int
quote byte
start int
)
for i := 0; i < len(inner); i++ {
c := inner[i]
switch {
case quote != 0:
if c == '\\' {
i++
} else if c == quote {
quote = 0
}
case c == '\'' || c == '"' || c == '`':
quote = c
case c == '(':
depth++
case c == ')':
depth--
case c == ',' && depth == 0:
args = append(args, strings.TrimSpace(inner[start:i]))
start = i + 1
}
}
args = append(args, strings.TrimSpace(inner[start:]))
return base, args
}