Skip to content

Commit be9f5fd

Browse files
committed
core/seed: describe dialects in JSON rather than Go
Each engine now embeds a dialect.json and hands it to seed.Dialect, which parses it into a Spec and applies it. An unknown field is an error, so a misspelled key is caught where it is written rather than silently ignored. ClickHouse and GoogleSQL move onto the seed package at the same time, replacing their hand-rolled type and operator loops. Both gain the two things the shared spec gives every dialect and their loops left out: the type each literal takes on, and a count() aggregate. Generated function catalogs stay in Go — pg_catalog and the MySQL and SQLite stdlibs are passed to seed.Dialect rather than restated as JSON — and a dialect can declare a handful of functions inline where there is no generated list. A new test loads every engine's dialect.json into a catalog and checks that its literals and a sample of its type spellings resolve and compare. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MnoUabwBWW9gaEn2Nj7eG
1 parent 2185eef commit be9f5fd

12 files changed

Lines changed: 560 additions & 436 deletions

File tree

internal/core/seed/seed.go

Lines changed: 119 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
// Package seed builds a core catalog for a SQL dialect from a declarative
22
// description of its type system: the types it defines, the operators and
33
// casts between them, and the functions it ships with.
4+
//
5+
// A dialect is described by a JSON document, which each engine package embeds
6+
// as its dialect.json and hands to Dialect. Load parses one.
47
package seed
58

69
import (
10+
"bytes"
11+
"encoding/json"
712
"fmt"
13+
"slices"
814
"strings"
915

1016
"github.com/sqlc-dev/sqlc/internal/core"
@@ -16,66 +22,108 @@ import (
1622
// that a schema may use in a column definition; each becomes its own catalog
1723
// type, with implicit casts registered between them.
1824
type Type struct {
19-
Name string
20-
Category string
21-
Aliases []string
25+
Name string `json:"name"`
26+
Category string `json:"category"`
27+
Aliases []string `json:"aliases,omitempty"`
2228
}
2329

2430
// Operator is a single operator overload.
2531
type Operator struct {
26-
Name string
27-
Left string
28-
Right string
29-
Result string
32+
Name string `json:"name"`
33+
Left string `json:"left"`
34+
Right string `json:"right"`
35+
Result string `json:"result"`
3036
}
3137

3238
// Cast is a single cast between two types. Context is 'i'mplicit, 'a'ssignment
3339
// or 'e'xplicit.
3440
type Cast struct {
35-
Source string
36-
Target string
37-
Context string
41+
Source string `json:"source"`
42+
Target string `json:"target"`
43+
Context string `json:"context,omitempty"`
44+
}
45+
46+
// Function is a function the dialect ships with. Kind is 'f'unction,
47+
// 'a'ggregate, 'w'indow or 'p'rocedure.
48+
type Function struct {
49+
Name string `json:"name"`
50+
Kind string `json:"kind,omitempty"`
51+
Args []string `json:"args,omitempty"`
52+
Returns string `json:"returns"`
53+
Nullable bool `json:"nullable,omitempty"`
3854
}
3955

4056
// Spec describes a dialect. Apply turns it into catalog rows.
4157
type Spec struct {
4258
// Dialect is the name recorded in the catalog, e.g. "postgresql".
43-
Dialect string
59+
Dialect string `json:"dialect"`
4460

4561
// Types are the dialect's types.
46-
Types []Type
62+
Types []Type `json:"types"`
4763

4864
// Const names the type each kind of literal takes on, keyed by the
4965
// core.Const* constants. Kinds left out fall back to PostgreSQL's names.
50-
Const map[string]string
66+
Const map[string]string `json:"const,omitempty"`
5167

5268
// Comparison operators are registered as (T, T) -> Bool for every type in
5369
// ComparisonCategories.
54-
Comparison []string
55-
ComparisonCategories string
70+
Comparison []string `json:"comparison,omitempty"`
71+
ComparisonCategories string `json:"comparison_categories,omitempty"`
5672

5773
// Arithmetic operators are registered as (T, T) -> T for every type in
5874
// ArithmeticCategories.
59-
Arithmetic []string
60-
ArithmeticCategories string
75+
Arithmetic []string `json:"arithmetic,omitempty"`
76+
ArithmeticCategories string `json:"arithmetic_categories,omitempty"`
6177

6278
// Bool names the type comparisons return.
63-
Bool string
79+
Bool string `json:"bool,omitempty"`
6480

6581
// Operators and Casts are registered as given, on top of the ones the
6682
// category rules above generate.
67-
Operators []Operator
68-
Casts []Cast
83+
Operators []Operator `json:"operators,omitempty"`
84+
Casts []Cast `json:"casts,omitempty"`
6985

7086
// CastCategories lists the categories whose types are all implicitly
7187
// castable to one another, so that an operator on two spellings of the
7288
// same kind of value resolves. "*" makes every seeded type implicitly
7389
// castable to every other, for dialects that compare across categories.
74-
CastCategories string
90+
CastCategories string `json:"cast_categories,omitempty"`
91+
92+
// Functions are the dialect's built-in functions.
93+
Functions []Function `json:"functions,omitempty"`
94+
95+
// Funcs are further built-in functions, in the form the engine packages
96+
// already describe them. A dialect whose function catalog is generated
97+
// passes it here rather than restating it as JSON.
98+
Funcs []*catalog.Function `json:"-"`
99+
}
75100

76-
// Funcs are the dialect's built-in functions, in the form the engine
77-
// packages already describe them.
78-
Funcs []*catalog.Function
101+
// Load parses a dialect description. An unknown field is an error rather than
102+
// something to ignore, so that a misspelled key is caught where it is written.
103+
func Load(data []byte) (Spec, error) {
104+
dec := json.NewDecoder(bytes.NewReader(data))
105+
dec.DisallowUnknownFields()
106+
var s Spec
107+
if err := dec.Decode(&s); err != nil {
108+
return Spec{}, fmt.Errorf("seed: parse dialect: %w", err)
109+
}
110+
if s.Dialect == "" {
111+
return Spec{}, fmt.Errorf("seed: dialect has no name")
112+
}
113+
return s, nil
114+
}
115+
116+
// Dialect returns the catalog option that seeds the dialect the JSON
117+
// describes, together with any function catalog the engine generates.
118+
func Dialect(data []byte, funcs []*catalog.Function) core.Option {
119+
return core.WithSeed(func(cat *core.Catalog) error {
120+
spec, err := Load(data)
121+
if err != nil {
122+
return err
123+
}
124+
spec.Funcs = funcs
125+
return spec.Apply(cat)
126+
})
79127
}
80128

81129
// Apply registers everything the spec describes on cat.
@@ -103,6 +151,9 @@ func (s Spec) Apply(cat *core.Catalog) error {
103151
if err := b.casts(s); err != nil {
104152
return err
105153
}
154+
if err := b.functions(s); err != nil {
155+
return err
156+
}
106157
return b.funcs(s)
107158
}
108159

@@ -175,8 +226,15 @@ func (b *builder) createType(name, category string) (int64, error) {
175226
return oid, nil
176227
}
177228

229+
// constKinds are the kinds of literal a dialect may name a type for.
230+
var constKinds = []string{core.ConstInteger, core.ConstFloat, core.ConstString, core.ConstBool}
231+
178232
func (b *builder) consts(s Spec) error {
179233
for kind, name := range s.Const {
234+
if !slices.Contains(constKinds, kind) {
235+
return fmt.Errorf("seed %s: unknown constant kind %q, want one of %s",
236+
s.Dialect, kind, strings.Join(constKinds, ", "))
237+
}
180238
if _, ok := b.oids[strings.ToLower(name)]; !ok {
181239
return fmt.Errorf("seed %s: constant %s names unknown type %q", s.Dialect, kind, name)
182240
}
@@ -307,6 +365,34 @@ func (b *builder) cast(c Cast) error {
307365
})
308366
}
309367

368+
func (b *builder) functions(s Spec) error {
369+
for _, fn := range s.Functions {
370+
returnOID, err := b.funcTypeName(fn.Returns)
371+
if err != nil {
372+
return fmt.Errorf("seed %s function %q: %w", s.Dialect, fn.Name, err)
373+
}
374+
args := make([]core.ProcArg, 0, len(fn.Args))
375+
for _, arg := range fn.Args {
376+
argOID, err := b.funcTypeName(arg)
377+
if err != nil {
378+
return fmt.Errorf("seed %s function %q: %w", s.Dialect, fn.Name, err)
379+
}
380+
args = append(args, core.ProcArg{TypeOID: argOID})
381+
}
382+
if _, err := b.cat.CreateProc(core.ProcSpec{
383+
Name: fn.Name,
384+
DialectOID: b.dialectOID,
385+
Kind: fn.Kind,
386+
ReturnTypeOID: returnOID,
387+
ReturnNullable: fn.Nullable,
388+
Args: args,
389+
}); err != nil {
390+
return fmt.Errorf("seed %s function %q: %w", s.Dialect, fn.Name, err)
391+
}
392+
}
393+
return nil
394+
}
395+
310396
func (b *builder) funcs(s Spec) error {
311397
for _, fn := range s.Funcs {
312398
returnOID, err := b.funcType(fn.ReturnType)
@@ -342,8 +428,15 @@ func (b *builder) funcs(s Spec) error {
342428
// pseudo types ("any", "record") and types no dialect spec bothers to list, so
343429
// an unknown name is registered rather than rejected.
344430
func (b *builder) funcType(tn *ast.TypeName) (int64, error) {
345-
if tn == nil || tn.Name == "" {
431+
if tn == nil {
432+
return 0, nil
433+
}
434+
return b.funcTypeName(tn.Name)
435+
}
436+
437+
func (b *builder) funcTypeName(name string) (int64, error) {
438+
if name == "" {
346439
return 0, nil
347440
}
348-
return b.createType(tn.Name, "U")
441+
return b.createType(name, "U")
349442
}

internal/core/seed/seed_test.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package seed_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/sqlc-dev/sqlc/internal/core"
7+
"github.com/sqlc-dev/sqlc/internal/core/seed"
8+
"github.com/sqlc-dev/sqlc/internal/engine/clickhouse"
9+
"github.com/sqlc-dev/sqlc/internal/engine/dolphin"
10+
"github.com/sqlc-dev/sqlc/internal/engine/googlesql"
11+
"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
12+
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
13+
)
14+
15+
// TestDialects loads every engine's dialect.json into a catalog. A malformed
16+
// document, a misspelled key or an operator over a type the document does not
17+
// define fails here rather than on the first query analyzed.
18+
func TestDialects(t *testing.T) {
19+
t.Parallel()
20+
21+
dialects := map[string]struct {
22+
option core.Option
23+
// types are spellings a schema written for the dialect may use, each
24+
// of which has to resolve and to compare against itself.
25+
types []string
26+
}{
27+
"postgresql": {postgresql.Dialect(), []string{"int4", "integer", "bigserial", "text", "timestamptz"}},
28+
"mysql": {dolphin.Dialect(), []string{"bigint", "varchar", "datetime", "signed"}},
29+
"sqlite": {sqlite.Dialect(), []string{"integer", "text", "real", "blob"}},
30+
"clickhouse": {clickhouse.Dialect(), []string{"uint64", "string", "datetime"}},
31+
"googlesql": {googlesql.Dialect(), []string{"int64", "string", "timestamp"}},
32+
}
33+
34+
for name, d := range dialects {
35+
t.Run(name, func(t *testing.T) {
36+
t.Parallel()
37+
38+
cat, err := core.New(d.option)
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
defer cat.Close()
43+
44+
if _, err := cat.DialectOID(name); err != nil {
45+
t.Errorf("dialect %s not registered: %s", name, err)
46+
}
47+
48+
// Every dialect types the four kinds of literal.
49+
for _, kind := range []string{core.ConstInteger, core.ConstFloat, core.ConstString, core.ConstBool} {
50+
if _, err := cat.ConstTypeOID(kind); err != nil {
51+
t.Errorf("%s literal: %s", kind, err)
52+
}
53+
}
54+
55+
for _, typeName := range d.types {
56+
oid, err := cat.TypeOID(typeName)
57+
if err != nil {
58+
t.Errorf("type %q: %s", typeName, err)
59+
continue
60+
}
61+
ops, err := cat.FindOperators("=", oid, oid)
62+
if err != nil {
63+
t.Fatal(err)
64+
}
65+
if len(ops) == 0 {
66+
t.Errorf("type %q has no %q operator", typeName, "=")
67+
}
68+
}
69+
})
70+
}
71+
}
72+
73+
func TestLoadRejectsUnknownField(t *testing.T) {
74+
t.Parallel()
75+
76+
if _, err := seed.Load([]byte(`{"dialect": "x", "typos": []}`)); err == nil {
77+
t.Fatal("expected an unknown field to be rejected")
78+
}
79+
}
80+
81+
func TestLoadRequiresName(t *testing.T) {
82+
t.Parallel()
83+
84+
if _, err := seed.Load([]byte(`{"types": []}`)); err == nil {
85+
t.Fatal("expected a dialect without a name to be rejected")
86+
}
87+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"dialect": "clickhouse",
3+
"types": [
4+
{ "name": "UInt8", "category": "N" },
5+
{ "name": "UInt16", "category": "N" },
6+
{ "name": "UInt32", "category": "N" },
7+
{ "name": "UInt64", "category": "N" },
8+
{ "name": "UInt128", "category": "N" },
9+
{ "name": "UInt256", "category": "N" },
10+
{ "name": "Int8", "category": "N" },
11+
{ "name": "Int16", "category": "N" },
12+
{ "name": "Int32", "category": "N" },
13+
{ "name": "Int64", "category": "N" },
14+
{ "name": "Int128", "category": "N" },
15+
{ "name": "Int256", "category": "N" },
16+
{ "name": "Float32", "category": "N" },
17+
{ "name": "Float64", "category": "N" },
18+
{ "name": "BFloat16", "category": "N" },
19+
{ "name": "Decimal", "category": "N" },
20+
{ "name": "Decimal32", "category": "N" },
21+
{ "name": "Decimal64", "category": "N" },
22+
{ "name": "Decimal128", "category": "N" },
23+
{ "name": "Decimal256", "category": "N" },
24+
25+
{ "name": "Bool", "category": "B" },
26+
27+
{ "name": "String", "category": "S" },
28+
{ "name": "FixedString", "category": "S" },
29+
{ "name": "UUID", "category": "S" },
30+
31+
{ "name": "Date", "category": "D" },
32+
{ "name": "Date32", "category": "D" },
33+
{ "name": "DateTime", "category": "D" },
34+
{ "name": "DateTime64", "category": "D" },
35+
36+
{ "name": "IPv4", "category": "S" },
37+
{ "name": "IPv6", "category": "S" },
38+
{ "name": "JSON", "category": "U" },
39+
{ "name": "Enum8", "category": "U" },
40+
{ "name": "Enum16", "category": "U" },
41+
{ "name": "Nullable", "category": "U" },
42+
{ "name": "LowCardinality", "category": "U" },
43+
{ "name": "Array", "category": "A" },
44+
{ "name": "Map", "category": "U" },
45+
{ "name": "Tuple", "category": "U" },
46+
{ "name": "Nested", "category": "U" },
47+
{ "name": "Nothing", "category": "U" }
48+
],
49+
"const": {
50+
"integer": "Int64",
51+
"float": "Float64",
52+
"string": "String",
53+
"bool": "Bool"
54+
},
55+
"bool": "Bool",
56+
"comparison": ["=", "<>", "!=", "<", "<=", ">", ">="],
57+
"comparison_categories": "NBSD",
58+
"arithmetic": ["+", "-", "*", "/"],
59+
"arithmetic_categories": "N",
60+
"functions": [
61+
{ "name": "count", "kind": "a", "returns": "UInt64" }
62+
]
63+
}

0 commit comments

Comments
 (0)