Skip to content

Commit 2185eef

Browse files
committed
core: analyze every dialect through the new analysis core
`sqlc analyze` ran ClickHouse and GoogleSQL on the core catalog and analyzer while PostgreSQL, MySQL and SQLite still went through the legacy compiler. This routes all five dialects through the core. - compiler: NewCompiler takes options; WithCoreAnalysis, which the analyze command passes, builds a core catalog seeded with the engine's dialect and skips the legacy catalog and the analyzer connection entirely. ClickHouse and GoogleSQL keep doing so unconditionally. `generate`, `vet` and `compile` are untouched. - core/seed: a declarative description of a dialect — its types and their aliases, the operators and casts between them, and the functions it ships with — that Apply turns into catalog rows. PostgreSQL, MySQL and SQLite seeds are built on it, reusing each engine's existing function catalog (pg_catalog and the MySQL and SQLite stdlibs). - core: literals take the type each dialect names rather than PostgreSQL's, a type a schema declares gains the dialect's comparison operators, and array types are named after their element. - core/schema: enums, functions, views, CREATE TABLE AS, ALTER TABLE and the rename statements now load into the catalog, and a column's array-ness is read from either the type name or the column. - core/analyzer: CTEs, set operations, subqueries in FROM and as expressions, correlated references, functions in FROM, IN, BETWEEN, CASE, COALESCE, array expressions, casts and output-name references in GROUP BY and HAVING. Overloads are chosen by argument type, polymorphic return types resolve to the argument's, and an unknown function or operator leaves the expression untyped instead of failing the query. The SQLite golden changes because the core reports the type name the catalog stores, which is lower case. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MnoUabwBWW9gaEn2Nj7eG
1 parent 5ce31fc commit 2185eef

60 files changed

Lines changed: 3547 additions & 57 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/howto/analyze.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ Unlike [`generate`](generate.md), this command does not require a configuration
77
file and does not connect to a database. It uses sqlc's native static analysis
88
to infer types directly from the provided schema.
99

10+
Every dialect is analyzed by the same engine-neutral analysis core: the schema
11+
is loaded into a catalog seeded with the dialect's types, operators and
12+
functions, and each query is resolved against it. `generate` still uses each
13+
engine's own analysis path, so the two can report a type differently — most
14+
visibly, `analyze` reports type names as the catalog stores them, in lower
15+
case.
16+
1017
## Usage
1118

1219
```sh

internal/cmd/analyze.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Examples:
130130
parserOpts := opts.Parser{}
131131

132132
ctx := cmd.Context()
133-
c, err := compiler.NewCompiler(sql, combo, parserOpts)
133+
c, err := compiler.NewCompiler(sql, combo, parserOpts, compiler.WithCoreAnalysis())
134134
if err != nil {
135135
return fmt.Errorf("error creating compiler: %w", err)
136136
}

internal/compiler/engine.go

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ type Compiler struct {
3232

3333
coreCatalog *core.Catalog
3434

35+
// coreAnalysis routes every engine through the core catalog and analyzer.
36+
coreAnalysis bool
37+
3538
schema []string
3639

3740
// databaseOnlyMode indicates that the compiler should use database-only analysis
@@ -41,8 +44,33 @@ type Compiler struct {
4144
expander *expander.Expander
4245
}
4346

44-
func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts.Parser) (*Compiler, error) {
47+
// Option configures a Compiler.
48+
type Option func(*Compiler)
49+
50+
// WithCoreAnalysis analyzes queries with the core catalog and analyzer rather
51+
// than each engine's own analysis path. ClickHouse and GoogleSQL always do;
52+
// this opts the remaining engines in.
53+
func WithCoreAnalysis() Option {
54+
return func(c *Compiler) { c.coreAnalysis = true }
55+
}
56+
57+
func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, options ...Option) (*Compiler, error) {
4558
c := &Compiler{conf: conf, combo: combo}
59+
for _, o := range options {
60+
o(c)
61+
}
62+
63+
// ClickHouse and GoogleSQL have no legacy analysis path to fall back to.
64+
switch conf.Engine {
65+
case config.EngineClickHouse, config.EngineGoogleSQL:
66+
c.coreAnalysis = true
67+
}
68+
if c.coreAnalysis {
69+
if err := c.initCore(); err != nil {
70+
return nil, err
71+
}
72+
return c, nil
73+
}
4674

4775
if conf.Database != nil && conf.Database.Managed {
4876
client := dbmanager.NewClient(combo.Global.Servers)
@@ -116,26 +144,46 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings, parserOpts opts
116144
)
117145
}
118146
}
147+
default:
148+
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
149+
}
150+
return c, nil
151+
}
152+
153+
// initCore wires up the engine's parser and a core catalog seeded with its
154+
// dialect. No legacy catalog and no analyzer connection are involved.
155+
func (c *Compiler) initCore() error {
156+
var dialect core.Option
157+
switch c.conf.Engine {
158+
case config.EngineSQLite:
159+
c.parser = sqlite.NewParser()
160+
c.selector = newSQLiteSelector()
161+
dialect = sqlite.Dialect()
162+
case config.EngineMySQL:
163+
c.parser = dolphin.NewParser()
164+
c.selector = newDefaultSelector()
165+
dialect = dolphin.Dialect()
166+
case config.EnginePostgreSQL:
167+
c.parser = postgresql.NewParser()
168+
c.selector = newDefaultSelector()
169+
dialect = postgresql.Dialect()
119170
case config.EngineClickHouse:
120171
c.parser = clickhouse.NewParser()
121172
c.selector = newDefaultSelector()
122-
cat, err := core.New(clickhouse.Dialect())
123-
if err != nil {
124-
return nil, fmt.Errorf("clickhouse: init catalog: %w", err)
125-
}
126-
c.coreCatalog = cat
173+
dialect = clickhouse.Dialect()
127174
case config.EngineGoogleSQL:
128175
c.parser = googlesql.NewParser()
129176
c.selector = newDefaultSelector()
130-
cat, err := core.New(googlesql.Dialect())
131-
if err != nil {
132-
return nil, fmt.Errorf("googlesql: init catalog: %w", err)
133-
}
134-
c.coreCatalog = cat
177+
dialect = googlesql.Dialect()
135178
default:
136-
return nil, fmt.Errorf("unknown engine: %s", conf.Engine)
179+
return fmt.Errorf("unknown engine: %s", c.conf.Engine)
137180
}
138-
return c, nil
181+
cat, err := core.New(dialect)
182+
if err != nil {
183+
return fmt.Errorf("%s: init catalog: %w", c.conf.Engine, err)
184+
}
185+
c.coreCatalog = cat
186+
return nil
139187
}
140188

141189
func (c *Compiler) Catalog() *catalog.Catalog {

internal/compiler/parse_core.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func coreColumn(c core.Column) *Column {
9292
Name: c.Name,
9393
DataType: c.DataType,
9494
NotNull: c.NotNull,
95+
IsArray: c.IsArray,
9596
}
9697
if c.Source != nil && c.Source.Table != "" {
9798
col.Table = &ast.TableName{Schema: c.Source.Schema, Name: c.Source.Table}
@@ -110,6 +111,7 @@ func coreParamColumn(p core.Parameter, params *named.ParamSet) *Column {
110111
Name: p.Name,
111112
DataType: p.DataType,
112113
NotNull: p.NotNull,
114+
IsArray: p.IsArray,
113115
}
114116
if p.Source != nil && p.Source.Table != "" {
115117
col.Table = &ast.TableName{Schema: p.Source.Schema, Name: p.Source.Table}

internal/core/analysis.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Column struct {
2727
DataType string `json:"data_type"`
2828
TypeOID int64 `json:"type_oid,omitempty"`
2929
NotNull bool `json:"not_null"`
30+
IsArray bool `json:"is_array,omitempty"`
3031
SourceClassOID int64 `json:"source_class_oid,omitempty"`
3132
SourceAttributeOID int64 `json:"source_attribute_oid,omitempty"`
3233
Source *ColumnSource `json:"source,omitempty"`
@@ -44,5 +45,6 @@ type Parameter struct {
4445
DataType string `json:"data_type,omitempty"`
4546
TypeOID int64 `json:"type_oid,omitempty"`
4647
NotNull bool `json:"not_null"`
48+
IsArray bool `json:"is_array,omitempty"`
4749
Source *ColumnSource `json:"source,omitempty"`
4850
}

internal/core/analyzer/analyzer.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package analyzer
22

33
import (
44
"fmt"
5+
"strings"
56

67
"github.com/sqlc-dev/sqlc/internal/core"
78
"github.com/sqlc-dev/sqlc/internal/sql/ast"
@@ -42,12 +43,83 @@ func Prepare(cat *core.Catalog, stmt ast.Node) (core.PrepareResult, error) {
4243
return a.result(), nil
4344
}
4445

46+
// dataType reports the type's name and whether it is an array of that type.
47+
// The catalog names an array after its element, so a caller that reports the
48+
// two separately gets them apart here.
49+
func (a *analyzer) dataType(oid int64) (string, bool) {
50+
if oid == 0 {
51+
return "", false
52+
}
53+
name, err := a.cat.TypeName(oid)
54+
if err != nil {
55+
return "", false
56+
}
57+
if element, ok := strings.CutSuffix(name, core.ArraySuffix); ok {
58+
return element, true
59+
}
60+
return name, false
61+
}
62+
4563
type analyzer struct {
4664
cat *core.Catalog
4765
scope *scope
4866
columns []core.Column
4967
params map[int]core.Parameter
5068
command core.Command
69+
70+
// outer is the scope of the query this one is nested in, which a
71+
// correlated subquery refers to.
72+
outer *scope
73+
74+
// ctes are the relations a WITH clause defined, visible to this query and
75+
// to the ones nested in it.
76+
ctes map[string]scopeRel
77+
78+
// aliases are the names the target list gives its expressions. GROUP BY,
79+
// HAVING and ORDER BY may refer to a result column by its output name.
80+
aliases map[string]ast.Node
81+
82+
// resolving guards against an alias that refers to itself.
83+
resolving map[string]bool
84+
}
85+
86+
// subquery analyzes a nested SELECT. It shares the parameter set, so a
87+
// placeholder inside the subquery is reported with the rest, and it sees this
88+
// query's scope, so a correlated reference resolves.
89+
func (a *analyzer) subquery(s *ast.SelectStmt) (*analyzer, error) {
90+
sub := &analyzer{
91+
cat: a.cat,
92+
params: a.params,
93+
outer: a.scope,
94+
ctes: a.ctes,
95+
}
96+
if err := sub.analyzeSelect(s); err != nil {
97+
return nil, err
98+
}
99+
return sub, nil
100+
}
101+
102+
func (a *analyzer) subqueryColumns(s *ast.SelectStmt) ([]core.Column, error) {
103+
sub, err := a.subquery(s)
104+
if err != nil {
105+
return nil, err
106+
}
107+
return sub.columns, nil
108+
}
109+
110+
// derivedRel turns a subquery's result columns into a relation, so that the
111+
// query selecting from it resolves columns the same way as from a table.
112+
func derivedRel(alias string, cols []core.Column) scopeRel {
113+
rel := scopeRel{alias: alias, cols: make([]core.ClassColumn, 0, len(cols))}
114+
for _, col := range cols {
115+
rel.cols = append(rel.cols, core.ClassColumn{
116+
AttOID: col.SourceAttributeOID,
117+
Name: col.Name,
118+
TypeOID: col.TypeOID,
119+
NotNull: col.NotNull,
120+
})
121+
}
122+
return rel
51123
}
52124

53125
func (a *analyzer) result() core.PrepareResult {
@@ -78,12 +150,27 @@ func orderedParams(m map[int]core.Parameter) []core.Parameter {
78150
}
79151

80152
func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
153+
if err := a.bindCTEs(s.WithClause); err != nil {
154+
return err
155+
}
156+
157+
// A set operation reports the columns of its first branch. The other
158+
// branches still get analyzed, for the placeholders they hold.
159+
if s.Larg != nil && s.Rarg != nil {
160+
if err := a.analyzeSetOperation(s); err != nil {
161+
return err
162+
}
163+
return nil
164+
}
165+
81166
sc, err := a.buildScope(s.FromClause)
82167
if err != nil {
83168
return err
84169
}
85170
a.scope = sc
86171

172+
a.bindAliases(s.TargetList)
173+
87174
for _, item := range listItems(s.FromClause) {
88175
if err := a.typeJoinConditions(item); err != nil {
89176
return fmt.Errorf("join: %w", err)
@@ -110,6 +197,10 @@ func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
110197

111198
targets := listItems(s.TargetList)
112199
if targets == nil {
200+
// A VALUES list produces rows without naming columns.
201+
if s.ValuesLists != nil {
202+
return a.typeValuesLists(s.ValuesLists)
203+
}
113204
return fmt.Errorf("select: empty target list")
114205
}
115206
a.columns = make([]core.Column, 0, len(targets))
@@ -125,6 +216,107 @@ func (a *analyzer) analyzeSelect(s *ast.SelectStmt) error {
125216
return nil
126217
}
127218

219+
func (a *analyzer) typeValuesLists(l *ast.List) error {
220+
for _, row := range listItems(l) {
221+
if _, err := a.typeExpr(row); err != nil {
222+
return err
223+
}
224+
}
225+
return nil
226+
}
227+
228+
// analyzeSetOperation types both sides of UNION, INTERSECT or EXCEPT and
229+
// reports the first branch's columns, the way the databases name the result.
230+
func (a *analyzer) analyzeSetOperation(s *ast.SelectStmt) error {
231+
left, err := a.subquery(s.Larg)
232+
if err != nil {
233+
return err
234+
}
235+
if _, err := a.subquery(s.Rarg); err != nil {
236+
return err
237+
}
238+
a.columns = left.columns
239+
a.scope = left.scope
240+
return nil
241+
}
242+
243+
// bindCTEs analyzes a WITH clause, making each common table expression
244+
// available to the query as a relation.
245+
func (a *analyzer) bindCTEs(with *ast.WithClause) error {
246+
if with == nil {
247+
return nil
248+
}
249+
for _, item := range listItems(with.Ctes) {
250+
cte, ok := item.(*ast.CommonTableExpr)
251+
if !ok || cte.Ctename == nil {
252+
continue
253+
}
254+
sel, ok := cte.Ctequery.(*ast.SelectStmt)
255+
if !ok {
256+
continue
257+
}
258+
cols, err := a.subqueryColumns(sel)
259+
if err != nil {
260+
return fmt.Errorf("with %s: %w", *cte.Ctename, err)
261+
}
262+
rel := derivedRel(*cte.Ctename, cols)
263+
renameColumns(&rel, cte.Aliascolnames)
264+
if a.ctes == nil {
265+
a.ctes = map[string]scopeRel{}
266+
}
267+
a.ctes[*cte.Ctename] = rel
268+
}
269+
return nil
270+
}
271+
272+
// bindAliases records the output names the target list assigns, so a later
273+
// clause can refer to a result column by name.
274+
func (a *analyzer) bindAliases(targets *ast.List) {
275+
for _, item := range listItems(targets) {
276+
rt, ok := item.(*ast.ResTarget)
277+
if !ok || rt.Name == nil || *rt.Name == "" {
278+
continue
279+
}
280+
if a.aliases == nil {
281+
a.aliases = map[string]ast.Node{}
282+
}
283+
a.aliases[*rt.Name] = rt.Val
284+
}
285+
}
286+
287+
// typeAlias types the expression an output name stands for, which is how a
288+
// GROUP BY or HAVING clause refers to a result column.
289+
func (a *analyzer) typeAlias(name string) (exprType, bool, error) {
290+
val, ok := a.aliases[name]
291+
if !ok || a.resolving[name] {
292+
return exprType{}, false, nil
293+
}
294+
if a.resolving == nil {
295+
a.resolving = map[string]bool{}
296+
}
297+
a.resolving[name] = true
298+
defer delete(a.resolving, name)
299+
300+
t, err := a.typeExpr(val)
301+
if err != nil {
302+
return exprType{}, false, err
303+
}
304+
return t, true, nil
305+
}
306+
307+
// renameColumns applies the column aliases a derived relation was declared
308+
// with, as in "WITH t(a, b) AS (...)".
309+
func renameColumns(rel *scopeRel, names *ast.List) {
310+
for i, item := range listItems(names) {
311+
if i >= len(rel.cols) {
312+
break
313+
}
314+
if s, ok := item.(*ast.String); ok && s.Str != "" {
315+
rel.cols[i].Name = s.Str
316+
}
317+
}
318+
}
319+
128320
func listItems(l *ast.List) []ast.Node {
129321
if l == nil {
130322
return nil

0 commit comments

Comments
 (0)