Files
upstream-mirror 0f4df23290
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
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

301 lines
8.3 KiB
Go

package compiler
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/sync/errgroup"
"github.com/sqlc-dev/sqlc/internal/core"
coreschema "github.com/sqlc-dev/sqlc/internal/core/schema"
"github.com/sqlc-dev/sqlc/internal/migrations"
"github.com/sqlc-dev/sqlc/internal/multierr"
"github.com/sqlc-dev/sqlc/internal/opts"
"github.com/sqlc-dev/sqlc/internal/rpc"
"github.com/sqlc-dev/sqlc/internal/source"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/preprocess"
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
"github.com/sqlc-dev/sqlc/internal/sql/sqlpath"
)
// TODO: Rename this interface Engine
type Parser interface {
Parse(io.Reader) ([]ast.Statement, error)
CommentSyntax() source.CommentSyntax
IsReservedKeyword(string) bool
}
// schemaFile is a schema file's contents, after the rewrites that are applied
// before any of it is parsed.
type schemaFile struct {
name string
contents string
}
func (c *Compiler) parseCatalog(schemas []string) error {
paths, err := sqlpath.Glob(schemas)
if err != nil {
return err
}
merr := multierr.New()
files := make([]schemaFile, 0, len(paths))
for _, path := range paths {
blob, err := os.ReadFile(path)
if err != nil {
merr.Add(path, "", 0, err)
continue
}
contents := migrations.RemoveRollbackStatements(string(blob))
contents = migrations.RemovePsqlMetaCommands(contents)
files = append(files, schemaFile{name: path, contents: contents})
c.schema = append(c.schema, contents)
}
if c.coreAnalysis {
// A schema file that could not be read is not part of the schema the
// action was keyed on, so there is no catalog to restore and nothing
// further to report.
if len(merr.Errs()) > 0 {
return merr
}
if err := c.parseCatalogCore(files, merr); err != nil {
return err
}
} else {
c.parseCatalogLegacy(files, merr)
}
if len(merr.Errs()) > 0 {
return merr
}
return nil
}
func (c *Compiler) parseCatalogLegacy(files []schemaFile, merr *multierr.Error) {
for _, file := range files {
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
if err != nil {
// A schema file and a query file are often the same file, so a
// query's sqlc syntax can fail here. Look for an explanation
// before reporting the syntax error it caused.
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
merr.Add(file.name, file.contents, 0, err)
}
continue
}
for i := range stmts {
if err := c.catalog.Update(stmts[i], c); err != nil {
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
continue
}
}
}
}
// parseCatalogCore builds the catalog the analysis core works against: the
// dialect's seed with the schema's DDL applied. The two are one cached action,
// so a run that has built this catalog before restores it and never parses the
// schema at all.
func (c *Compiler) parseCatalogCore(files []schemaFile, merr *multierr.Error) error {
contents := make([]string, 0, len(files))
for _, file := range files {
contents = append(contents, file.contents)
}
cat, err := core.NewCached(string(c.conf.Engine), contents, func(cat *core.Catalog) error {
for _, file := range files {
stmts, err := c.parser.Parse(strings.NewReader(file.contents))
if err != nil {
// A schema file and a query file are often the same file, so a
// query's sqlc syntax can fail here. Look for an explanation
// before reporting the syntax error it caused.
if reported := addSyntaxErrors(merr, file.name, file.contents, preprocess.File(c.conf.Engine, file.contents)); !reported {
merr.Add(file.name, file.contents, 0, err)
}
continue
}
for i := range stmts {
if err := coreschema.Apply(cat, stmts[i].Raw); err != nil {
merr.Add(file.name, file.contents, stmts[i].Pos(), err)
continue
}
}
}
// A catalog the whole schema did not make it into is not the catalog
// this action describes, so it is not one to keep.
if len(merr.Errs()) > 0 {
return merr
}
return nil
}, c.coreDialect)
if cat == nil {
return fmt.Errorf("%s: init catalog: %w", c.conf.Engine, err)
}
// Whatever apply reported is already in merr, which the caller returns.
c.coreCatalog = cat
// Codegen consumes the catalog through the Result, in the legacy shape.
legacy, err := coreResultCatalog(cat)
if err != nil {
return fmt.Errorf("%s: dump catalog: %w", c.conf.Engine, err)
}
c.catalog = legacy
return nil
}
// statement is a parsed query awaiting analysis, with what error reporting
// needs to place it back in the file it came from.
type statement struct {
filename string
src string
pp *preprocess.Result
raw *ast.RawStmt
}
func (c *Compiler) parseQueries(o opts.Parser) (*Result, error) {
merr := multierr.New()
files, err := sqlpath.Glob(c.conf.Queries)
if err != nil {
return nil, err
}
var stmts []statement
for _, filename := range files {
blob, err := os.ReadFile(filename)
if err != nil {
merr.Add(filename, "", 0, err)
continue
}
src := string(blob)
// sqlc syntax is not SQL. Rewrite it to native placeholders before the
// engine parser ever sees the query, so parsers only handle SQL.
pp := preprocess.File(c.conf.Engine, src)
parsed, err := c.parser.Parse(strings.NewReader(pp.Text))
if err != nil {
if reported := addSyntaxErrors(merr, filename, src, pp); !reported {
merr.Add(filename, src, 0, err)
}
continue
}
for _, stmt := range parsed {
stmts = append(stmts, statement{filename: filename, src: src, pp: pp, raw: stmt.Raw})
}
}
queries, errs := c.analyzeStatements(stmts, o)
var q []*Query
set := map[string]struct{}{}
for i, stmt := range stmts {
if err := errs[i]; err != nil {
var e *sqlerr.Error
loc := stmt.raw.Pos()
if errors.As(err, &e) && e.Location != 0 {
loc = e.Location
}
// Locations are reported against the rewritten query; map them
// back so errors point at what the user wrote.
loc = stmt.pp.Origin(loc)
if e != nil && e.Location != 0 {
e.Location = loc
}
merr.Add(stmt.filename, stmt.src, loc, err)
// If this rpc unauthenticated error bubbles up, then all future parsing/analysis will fail
if errors.Is(err, rpc.ErrUnauthenticated) {
return nil, merr
}
continue
}
query := queries[i]
if query == nil {
continue
}
query.Metadata.Filename = filepath.Base(stmt.filename)
queryName := query.Metadata.Name
if queryName != "" {
if _, exists := set[queryName]; exists {
merr.Add(stmt.filename, stmt.src, stmt.pp.Origin(stmt.raw.Pos()), fmt.Errorf("duplicate query name: %s", queryName))
continue
}
set[queryName] = struct{}{}
}
q = append(q, query)
}
if len(merr.Errs()) > 0 {
return nil, merr
}
if len(q) == 0 {
return nil, fmt.Errorf("no queries contained in paths %s", strings.Join(c.conf.Queries, ","))
}
return &Result{
Catalog: c.catalog,
Queries: q,
}, nil
}
// analyzeStatements analyzes each statement, returning the results in the
// order the statements were given so that queries and errors come out in
// source order however they were produced.
//
// The analysis core reads the catalog and nothing else, so its statements are
// analyzed concurrently. Every other path holds state that a second goroutine
// would race — a database connection, the legacy catalog — and stays serial.
func (c *Compiler) analyzeStatements(stmts []statement, o opts.Parser) ([]*Query, []error) {
queries := make([]*Query, len(stmts))
errs := make([]error, len(stmts))
analyze := func(i int) {
queries[i], errs[i] = c.parseQuery(stmts[i].raw, stmts[i].pp, o)
}
workers := runtime.GOMAXPROCS(0)
if !c.coreAnalysis || workers < 2 || len(stmts) < 2 {
for i := range stmts {
analyze(i)
}
return queries, errs
}
var g errgroup.Group
g.SetLimit(workers)
for i := range stmts {
g.Go(func() error {
analyze(i)
return nil
})
}
g.Wait()
return queries, errs
}
func addSyntaxErrors(merr *multierr.Error, filename, src string, pp *preprocess.Result) bool {
var found bool
for _, stmt := range pp.Statements() {
if stmt.Err == nil {
continue
}
found = true
loc := pp.Origin(stmt.Start)
var e *sqlerr.Error
if errors.As(stmt.Err, &e) && e.Location != 0 {
loc = pp.Origin(e.Location)
e.Location = loc
}
merr.Add(filename, src, loc, stmt.Err)
}
return found
}