Skip to content

Commit 823e3e1

Browse files
committed
Add analyzer.skip_parser config option for PostgreSQL
This commit adds a new configuration option `analyzer.skip_parser` that allows sqlc to skip the parser and catalog entirely, relying solely on the database analyzer for query analysis. This is particularly useful when: - Working with complex PostgreSQL syntax not fully supported by the parser - Wanting to ensure queries are validated against the actual database schema - Dealing with database-specific features or extensions Key changes: - Add `skip_parser` field to `Analyzer` config struct - Implement `parseQueriesWithAnalyzer` method using sqlfile.Split - Skip parser and catalog initialization when `skip_parser` is enabled - Add validation requiring database analyzer when using skip_parser - Only PostgreSQL is supported for this feature initially Usage example: ```yaml version: "2" sql: - name: "example" engine: "postgresql" queries: "query.sql" schema: [] database: uri: "postgresql://user:pass@localhost:5432/db" analyzer: skip_parser: true gen: go: package: "db" out: "db" ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b807fe9 commit 823e3e1

File tree

4 files changed

+190
-15
lines changed

4 files changed

+190
-15
lines changed

internal/cmd/generate.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -305,20 +305,28 @@ func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.C
305305
fmt.Fprintf(stderr, "error creating compiler: %s\n", err)
306306
return nil, true
307307
}
308-
if err := c.ParseCatalog(sql.Schema); err != nil {
309-
fmt.Fprintf(stderr, "# package %s\n", name)
310-
if parserErr, ok := err.(*multierr.Error); ok {
311-
for _, fileErr := range parserErr.Errs() {
312-
printFileErr(stderr, dir, fileErr)
308+
309+
// Check if skip_parser is enabled
310+
skipParser := sql.Analyzer.SkipParser != nil && *sql.Analyzer.SkipParser
311+
312+
// Skip catalog parsing if skip_parser is enabled
313+
if !skipParser {
314+
if err := c.ParseCatalog(sql.Schema); err != nil {
315+
fmt.Fprintf(stderr, "# package %s\n", name)
316+
if parserErr, ok := err.(*multierr.Error); ok {
317+
for _, fileErr := range parserErr.Errs() {
318+
printFileErr(stderr, dir, fileErr)
319+
}
320+
} else {
321+
fmt.Fprintf(stderr, "error parsing schema: %s\n", err)
313322
}
314-
} else {
315-
fmt.Fprintf(stderr, "error parsing schema: %s\n", err)
323+
return nil, true
324+
}
325+
if parserOpts.Debug.DumpCatalog {
326+
debug.Dump(c.Catalog())
316327
}
317-
return nil, true
318-
}
319-
if parserOpts.Debug.DumpCatalog {
320-
debug.Dump(c.Catalog())
321328
}
329+
322330
if err := c.ParseQueries(sql.Queries, parserOpts); err != nil {
323331
fmt.Fprintf(stderr, "# package %s\n", name)
324332
if parserErr, ok := err.(*multierr.Error); ok {

internal/compiler/compile.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
package compiler
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
67
"io"
78
"os"
89
"path/filepath"
910
"strings"
1011

12+
"github.com/sqlc-dev/sqlc/internal/metadata"
1113
"github.com/sqlc-dev/sqlc/internal/migrations"
1214
"github.com/sqlc-dev/sqlc/internal/multierr"
1315
"github.com/sqlc-dev/sqlc/internal/opts"
1416
"github.com/sqlc-dev/sqlc/internal/rpc"
1517
"github.com/sqlc-dev/sqlc/internal/source"
1618
"github.com/sqlc-dev/sqlc/internal/sql/ast"
19+
"github.com/sqlc-dev/sqlc/internal/sql/named"
1720
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
21+
"github.com/sqlc-dev/sqlc/internal/sql/sqlfile"
1822
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
1923
)
2024

@@ -118,3 +122,132 @@ func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
118122
Queries: q,
119123
}, nil
120124
}
125+
126+
// parseQueriesWithAnalyzer parses queries using only the database analyzer,
127+
// skipping the parser and catalog entirely. Uses sqlfile.Split to extract
128+
// individual queries from .sql files.
129+
func (c *Compiler) parseQueriesWithAnalyzer(o opts.Parser) (*Result, error) {
130+
ctx := context.Background()
131+
var q []*Query
132+
merr := multierr.New()
133+
set := map[string]struct{}{}
134+
files, err := sqlpath.Glob(c.conf.Queries)
135+
if err != nil {
136+
return nil, err
137+
}
138+
139+
if c.analyzer == nil {
140+
return nil, fmt.Errorf("database analyzer is required when skip_parser is enabled")
141+
}
142+
143+
for _, filename := range files {
144+
blob, err := os.ReadFile(filename)
145+
if err != nil {
146+
merr.Add(filename, "", 0, err)
147+
continue
148+
}
149+
src := string(blob)
150+
151+
// Use sqlfile.Split to extract individual queries
152+
queries, err := sqlfile.Split(ctx, strings.NewReader(src))
153+
if err != nil {
154+
merr.Add(filename, src, 0, err)
155+
continue
156+
}
157+
158+
for _, queryText := range queries {
159+
// Extract metadata from comments
160+
name, cmd, err := metadata.ParseQueryNameAndType(queryText, metadata.CommentSyntax{Dash: true})
161+
if err != nil {
162+
merr.Add(filename, queryText, 0, err)
163+
continue
164+
}
165+
166+
// Skip queries without names (not marked with sqlc comments)
167+
if name == "" {
168+
continue
169+
}
170+
171+
// Check for duplicate query names
172+
if _, exists := set[name]; exists {
173+
merr.Add(filename, queryText, 0, fmt.Errorf("duplicate query name: %s", name))
174+
continue
175+
}
176+
set[name] = struct{}{}
177+
178+
// Extract additional metadata from comments
179+
cleanedComments, err := source.CleanedComments(queryText, source.CommentSyntax{Dash: true})
180+
if err != nil {
181+
merr.Add(filename, queryText, 0, err)
182+
continue
183+
}
184+
185+
md := metadata.Metadata{
186+
Name: name,
187+
Cmd: cmd,
188+
Filename: filepath.Base(filename),
189+
}
190+
191+
md.Params, md.Flags, md.RuleSkiplist, err = metadata.ParseCommentFlags(cleanedComments)
192+
if err != nil {
193+
merr.Add(filename, queryText, 0, err)
194+
continue
195+
}
196+
197+
// Use the database analyzer to analyze the query
198+
// We pass an empty AST node since we're not using the parser
199+
result, err := c.analyzer.Analyze(ctx, nil, queryText, c.schema, &named.ParamSet{})
200+
if err != nil {
201+
merr.Add(filename, queryText, 0, err)
202+
// If this rpc unauthenticated error bubbles up, then all future parsing/analysis will fail
203+
if errors.Is(err, rpc.ErrUnauthenticated) {
204+
return nil, merr
205+
}
206+
continue
207+
}
208+
209+
// Convert analyzer results to Query format
210+
var cols []*Column
211+
for _, col := range result.Columns {
212+
cols = append(cols, convertColumn(col))
213+
}
214+
215+
var params []Parameter
216+
for _, p := range result.Params {
217+
params = append(params, Parameter{
218+
Number: int(p.Number),
219+
Column: convertColumn(p.Column),
220+
})
221+
}
222+
223+
// Strip comments from the final SQL
224+
trimmed, comments, err := source.StripComments(queryText)
225+
if err != nil {
226+
merr.Add(filename, queryText, 0, err)
227+
continue
228+
}
229+
md.Comments = comments
230+
231+
query := &Query{
232+
SQL: trimmed,
233+
Metadata: md,
234+
Columns: cols,
235+
Params: params,
236+
}
237+
238+
q = append(q, query)
239+
}
240+
}
241+
242+
if len(merr.Errs()) > 0 {
243+
return nil, merr
244+
}
245+
if len(q) == 0 {
246+
return nil, fmt.Errorf("no queries contained in paths %s", strings.Join(c.conf.Queries, ","))
247+
}
248+
249+
return &Result{
250+
Catalog: nil, // No catalog when skip_parser is enabled
251+
Queries: q,
252+
}, nil
253+
}

internal/compiler/engine.go

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
3636
c.client = client
3737
}
3838

39+
// Check if skip_parser is enabled
40+
skipParser := conf.Analyzer.SkipParser != nil && *conf.Analyzer.SkipParser
41+
42+
// If skip_parser is enabled, we must have database analyzer enabled
43+
if skipParser {
44+
if conf.Database == nil {
45+
return nil, fmt.Errorf("skip_parser requires database configuration")
46+
}
47+
if conf.Analyzer.Database != nil && !*conf.Analyzer.Database {
48+
return nil, fmt.Errorf("skip_parser requires database analyzer to be enabled")
49+
}
50+
// Only PostgreSQL is supported for now
51+
if conf.Engine != config.EnginePostgreSQL {
52+
return nil, fmt.Errorf("skip_parser is only supported for PostgreSQL")
53+
}
54+
}
55+
3956
switch conf.Engine {
4057
case config.EngineSQLite:
4158
c.parser = sqlite.NewParser()
@@ -46,8 +63,11 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
4663
c.catalog = dolphin.NewCatalog()
4764
c.selector = newDefaultSelector()
4865
case config.EnginePostgreSQL:
49-
c.parser = postgresql.NewParser()
50-
c.catalog = postgresql.NewCatalog()
66+
// Skip parser and catalog if skip_parser is enabled
67+
if !skipParser {
68+
c.parser = postgresql.NewParser()
69+
c.catalog = postgresql.NewCatalog()
70+
}
5171
c.selector = newDefaultSelector()
5272
if conf.Database != nil {
5373
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
@@ -73,7 +93,20 @@ func (c *Compiler) ParseCatalog(schema []string) error {
7393
}
7494

7595
func (c *Compiler) ParseQueries(queries []string, o opts.Parser) error {
76-
r, err := c.parseQueries(o)
96+
// Check if skip_parser is enabled
97+
skipParser := c.conf.Analyzer.SkipParser != nil && *c.conf.Analyzer.SkipParser
98+
99+
var r *Result
100+
var err error
101+
102+
if skipParser {
103+
// Use database analyzer only, skip parser and catalog
104+
r, err = c.parseQueriesWithAnalyzer(o)
105+
} else {
106+
// Use traditional parser-based approach
107+
r, err = c.parseQueries(o)
108+
}
109+
77110
if err != nil {
78111
return err
79112
}

internal/config/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ type SQL struct {
123123
}
124124

125125
type Analyzer struct {
126-
Database *bool `json:"database" yaml:"database"`
126+
Database *bool `json:"database" yaml:"database"`
127+
SkipParser *bool `json:"skip_parser" yaml:"skip_parser"`
127128
}
128129

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

0 commit comments

Comments
 (0)