Files
upstream-mirror 0f4df23290
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
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

147 lines
4.7 KiB
Go

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