Skip to content

Commit aa392c7

Browse files
kyleconroyclaude
andcommitted
feat(postgresql): add accurate analyzer mode for database-only analysis
Add an optional `analyzer.accurate: true` mode for PostgreSQL that bypasses the internal catalog and uses only database-backed analysis. Key features: - Uses database PREPARE for all type resolution (columns, parameters) - Uses expander package for SELECT * and RETURNING * expansion - Queries pg_catalog to build catalog structures for code generation - Skips internal catalog building from schema files Configuration: ```yaml sql: - engine: postgresql database: uri: "postgres://..." # or managed: true analyzer: accurate: true ``` This mode requires a database connection and the schema must exist in the database. It provides more accurate type information for complex queries. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 21e6557 commit aa392c7

File tree

7 files changed

+348
-3
lines changed

7 files changed

+348
-3
lines changed

internal/compiler/compile.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package compiler
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"io"
@@ -39,6 +40,13 @@ func (c *Compiler) parseCatalog(schemas []string) error {
3940
}
4041
contents := migrations.RemoveRollbackStatements(string(blob))
4142
c.schema = append(c.schema, contents)
43+
44+
// In accurate mode, we only need to collect schema files for migrations
45+
// but don't build the internal catalog from them
46+
if c.accurateMode {
47+
continue
48+
}
49+
4250
stmts, err := c.parser.Parse(strings.NewReader(contents))
4351
if err != nil {
4452
merr.Add(filename, contents, 0, err)
@@ -58,6 +66,15 @@ func (c *Compiler) parseCatalog(schemas []string) error {
5866
}
5967

6068
func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
69+
ctx := context.Background()
70+
71+
// In accurate mode, initialize the database connection pool before parsing queries
72+
if c.accurateMode && c.pgAnalyzer != nil {
73+
if err := c.pgAnalyzer.EnsurePool(ctx, c.schema); err != nil {
74+
return nil, fmt.Errorf("failed to initialize database connection: %w", err)
75+
}
76+
}
77+
6178
var q []*Query
6279
merr := multierr.New()
6380
set := map[string]struct{}{}
@@ -113,6 +130,18 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
113130
if len(q) == 0 {
114131
return nil, fmt.Errorf("no queries contained in paths %s", strings.Join(c.conf.Queries, ","))
115132
}
133+
134+
// In accurate mode, build the catalog from the database after parsing all queries
135+
if c.accurateMode && c.pgAnalyzer != nil {
136+
// Default to "public" schema if no specific schemas are specified
137+
schemas := []string{"public"}
138+
cat, err := c.pgAnalyzer.IntrospectSchema(ctx, schemas)
139+
if err != nil {
140+
return nil, fmt.Errorf("failed to introspect database schema: %w", err)
141+
}
142+
c.catalog = cat
143+
}
144+
116145
return &Result{
117146
Catalog: c.catalog,
118147
Queries: q,

internal/compiler/engine.go

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
sqliteanalyze "github.com/sqlc-dev/sqlc/internal/engine/sqlite/analyzer"
1515
"github.com/sqlc-dev/sqlc/internal/opts"
1616
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
17+
"github.com/sqlc-dev/sqlc/internal/x/expander"
1718
)
1819

1920
type Compiler struct {
@@ -27,6 +28,15 @@ type Compiler struct {
2728
selector selector
2829

2930
schema []string
31+
32+
// accurateMode indicates that the compiler should use database-only analysis
33+
// and skip building the internal catalog from schema files
34+
accurateMode bool
35+
// pgAnalyzer is the PostgreSQL-specific analyzer used in accurate mode
36+
// for schema introspection
37+
pgAnalyzer *pganalyze.Analyzer
38+
// expander is used to expand SELECT * and RETURNING * in accurate mode
39+
expander *expander.Expander
3040
}
3141

3242
func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, error) {
@@ -37,6 +47,9 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
3747
c.client = client
3848
}
3949

50+
// Check for accurate mode
51+
accurateMode := conf.Analyzer.Accurate != nil && *conf.Analyzer.Accurate
52+
4053
switch conf.Engine {
4154
case config.EngineSQLite:
4255
c.parser = sqlite.NewParser()
@@ -56,10 +69,32 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
5669
c.catalog = dolphin.NewCatalog()
5770
c.selector = newDefaultSelector()
5871
case config.EnginePostgreSQL:
59-
c.parser = postgresql.NewParser()
72+
parser := postgresql.NewParser()
73+
c.parser = parser
6074
c.catalog = postgresql.NewCatalog()
6175
c.selector = newDefaultSelector()
62-
if conf.Database != nil {
76+
77+
if accurateMode {
78+
// Accurate mode requires a database connection
79+
if conf.Database == nil {
80+
return nil, fmt.Errorf("accurate mode requires database configuration")
81+
}
82+
if conf.Database.URI == "" && !conf.Database.Managed {
83+
return nil, fmt.Errorf("accurate mode requires database.uri or database.managed")
84+
}
85+
c.accurateMode = true
86+
// Create the PostgreSQL analyzer for schema introspection
87+
c.pgAnalyzer = pganalyze.New(c.client, *conf.Database)
88+
// Use the analyzer wrapped with cache for query analysis
89+
c.analyzer = analyzer.Cached(
90+
c.pgAnalyzer,
91+
combo.Global,
92+
*conf.Database,
93+
)
94+
// Create the expander using the pgAnalyzer as the column getter
95+
// The parser implements both Parser and format.Dialect interfaces
96+
c.expander = expander.New(c.pgAnalyzer, parser, parser)
97+
} else if conf.Database != nil {
6398
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
6499
c.analyzer = analyzer.Cached(
65100
pganalyze.New(c.client, *conf.Database),

internal/compiler/parse.go

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,56 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
7171
}
7272

7373
var anlys *analysis
74-
if c.analyzer != nil {
74+
if c.accurateMode && c.expander != nil {
75+
// In accurate mode, use the expander for star expansion
76+
// and rely entirely on the database analyzer for type resolution
77+
expandedQuery, err := c.expander.Expand(ctx, rawSQL)
78+
if err != nil {
79+
return nil, fmt.Errorf("star expansion failed: %w", err)
80+
}
81+
82+
// Parse named parameters from the expanded query
83+
expandedStmts, err := c.parser.Parse(strings.NewReader(expandedQuery))
84+
if err != nil {
85+
return nil, fmt.Errorf("parsing expanded query failed: %w", err)
86+
}
87+
if len(expandedStmts) == 0 {
88+
return nil, errors.New("no statements in expanded query")
89+
}
90+
expandedRaw := expandedStmts[0].Raw
91+
92+
// Use the analyzer to get type information from the database
93+
result, err := c.analyzer.Analyze(ctx, expandedRaw, expandedQuery, c.schema, nil)
94+
if err != nil {
95+
return nil, err
96+
}
97+
98+
// Convert the analyzer result to the internal analysis format
99+
var cols []*Column
100+
for _, col := range result.Columns {
101+
cols = append(cols, convertColumn(col))
102+
}
103+
var params []Parameter
104+
for _, p := range result.Params {
105+
params = append(params, Parameter{
106+
Number: int(p.Number),
107+
Column: convertColumn(p.Column),
108+
})
109+
}
110+
111+
// Determine the insert table if applicable
112+
var table *ast.TableName
113+
if insert, ok := expandedRaw.Stmt.(*ast.InsertStmt); ok {
114+
table, _ = ParseTableName(insert.Relation)
115+
}
116+
117+
anlys = &analysis{
118+
Table: table,
119+
Columns: cols,
120+
Parameters: params,
121+
Query: expandedQuery,
122+
}
123+
} else if c.analyzer != nil {
75124
inference, _ := c.inferQuery(raw, rawSQL)
76125
if inference == nil {
77126
inference = &analysis{}

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ type SQL struct {
124124

125125
type Analyzer struct {
126126
Database *bool `json:"database" yaml:"database"`
127+
Accurate *bool `json:"accurate" yaml:"accurate"`
127128
}
128129

129130
// TODO: Figure out a better name for this

internal/config/v_one.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@
8080
"properties": {
8181
"database": {
8282
"type": "boolean"
83+
},
84+
"accurate": {
85+
"type": "boolean"
8386
}
8487
}
8588
},

internal/config/v_two.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@
8383
"properties": {
8484
"database": {
8585
"type": "boolean"
86+
},
87+
"accurate": {
88+
"type": "boolean"
8689
}
8790
}
8891
},

0 commit comments

Comments
 (0)