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
147 lines
4.7 KiB
Go
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
|
|
}
|