Files
upstream-sqlc/internal/cmd/generate.go
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

377 lines
10 KiB
Go

package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime/trace"
"strings"
"sync"
"google.golang.org/grpc"
"github.com/sqlc-dev/sqlc/internal/codegen/golang"
genjson "github.com/sqlc-dev/sqlc/internal/codegen/json"
"github.com/sqlc-dev/sqlc/internal/compiler"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/config/convert"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/ext"
"github.com/sqlc-dev/sqlc/internal/ext/process"
"github.com/sqlc-dev/sqlc/internal/ext/wasm"
"github.com/sqlc-dev/sqlc/internal/multierr"
"github.com/sqlc-dev/sqlc/internal/opts"
"github.com/sqlc-dev/sqlc/internal/plugin"
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
)
var debugDumpCatalog = sqlcdebug.New("dumpcatalog")
const errMessageNoVersion = `The configuration file must have a version number.
Set the version to 1 or 2 at the top of sqlc.json:
{
"version": "1"
...
}
`
const errMessageUnknownVersion = `The configuration file has an invalid version number.
The supported version can only be "1" or "2".
`
const errMessageNoPackages = `No packages are configured`
func printFileErr(stderr io.Writer, dir string, fileErr *multierr.FileError) {
filename, err := filepath.Rel(dir, fileErr.Filename)
if err != nil {
filename = fileErr.Filename
}
fmt.Fprintf(stderr, "%s:%d:%d: %s\n", filename, fileErr.Line, fileErr.Column, fileErr.Err)
}
func findPlugin(conf config.Config, name string) (*config.Plugin, error) {
for _, plug := range conf.Plugins {
if plug.Name == name {
return &plug, nil
}
}
return nil, fmt.Errorf("plugin not found")
}
func readConfig(stderr io.Writer, dir, filename string) (string, *config.Config, error) {
configPath := ""
if filename != "" {
configPath = filepath.Join(dir, filename)
} else {
var yamlMissing, jsonMissing, ymlMissing bool
yamlPath := filepath.Join(dir, "sqlc.yaml")
ymlPath := filepath.Join(dir, "sqlc.yml")
jsonPath := filepath.Join(dir, "sqlc.json")
if _, err := os.Stat(yamlPath); os.IsNotExist(err) {
yamlMissing = true
}
if _, err := os.Stat(jsonPath); os.IsNotExist(err) {
jsonMissing = true
}
if _, err := os.Stat(ymlPath); os.IsNotExist(err) {
ymlMissing = true
}
if yamlMissing && ymlMissing && jsonMissing {
fmt.Fprintln(stderr, "error parsing configuration files. sqlc.(yaml|yml) or sqlc.json: file does not exist")
return "", nil, errors.New("config file missing")
}
if (!yamlMissing || !ymlMissing) && !jsonMissing {
fmt.Fprintln(stderr, "error: both sqlc.json and sqlc.(yaml|yml) files present")
return "", nil, errors.New("sqlc.json and sqlc.(yaml|yml) present")
}
if jsonMissing {
if yamlMissing {
configPath = ymlPath
} else {
configPath = yamlPath
}
} else {
configPath = jsonPath
}
}
base := filepath.Base(configPath)
file, err := os.Open(configPath)
if err != nil {
fmt.Fprintf(stderr, "error parsing %s: file does not exist\n", base)
return "", nil, err
}
defer file.Close()
conf, err := config.ParseConfig(file)
if err != nil {
switch err {
case config.ErrMissingVersion:
fmt.Fprint(stderr, errMessageNoVersion)
case config.ErrUnknownVersion:
fmt.Fprint(stderr, errMessageUnknownVersion)
case config.ErrNoPackages:
fmt.Fprint(stderr, errMessageNoPackages)
}
fmt.Fprintf(stderr, "error parsing %s: %s\n", base, err)
return "", nil, err
}
return configPath, &conf, nil
}
func Generate(ctx context.Context, dir, filename string, o *Options) (map[string]string, error) {
e := o.Env
stderr := o.Stderr
configPath, conf, err := o.ReadConfig(dir, filename)
if err != nil {
return nil, err
}
base := filepath.Base(configPath)
if err := config.Validate(conf); err != nil {
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
return nil, err
}
if err := e.Validate(conf); err != nil {
fmt.Fprintf(stderr, "error validating %s: %s\n", base, err)
return nil, err
}
g := &generator{
dir: dir,
output: map[string]string{},
}
if err := processQuerySets(ctx, g, conf, dir, o); err != nil {
return nil, err
}
return g.output, nil
}
type generator struct {
m sync.Mutex
dir string
output map[string]string
}
func (g *generator) Pairs(ctx context.Context, conf *config.Config) []OutputPair {
var pairs []OutputPair
for _, sql := range conf.SQL {
if sql.Gen.Go != nil {
pairs = append(pairs, OutputPair{
SQL: sql,
Gen: config.SQLGen{Go: sql.Gen.Go},
})
}
if sql.Gen.JSON != nil {
pairs = append(pairs, OutputPair{
SQL: sql,
Gen: config.SQLGen{JSON: sql.Gen.JSON},
})
}
for i := range sql.Codegen {
pairs = append(pairs, OutputPair{
SQL: sql,
Plugin: &sql.Codegen[i],
})
}
}
return pairs
}
func (g *generator) ProcessResult(ctx context.Context, combo config.CombinedSettings, sql OutputPair, result *compiler.Result) error {
out, resp, err := codegen(ctx, combo, sql, result)
if err != nil {
return err
}
files := map[string]string{}
for _, file := range resp.Files {
files[file.Name] = string(file.Contents)
}
g.m.Lock()
// out is specified by the user, not a plugin
absout := filepath.Join(g.dir, out)
// When the Go codegen is configured to emit the models file into a
// separate package directory, route that file to its own absolute path.
// This is the only file allowed to live outside of `out`.
var (
modelsFileName string
modelsAbsout string
modelsAbsfile string
)
if sql.Gen.Go != nil && sql.Gen.Go.OutputModelsPath != "" && sql.Gen.Go.ModelsEmitEnabled() {
modelsFileName = sql.Gen.Go.OutputModelsFileName
if modelsFileName == "" {
modelsFileName = "models.go"
}
modelsAbsout = filepath.Join(g.dir, sql.Gen.Go.OutputModelsPath)
modelsAbsfile = filepath.Join(modelsAbsout, modelsFileName)
}
for n, source := range files {
if modelsFileName != "" && n == modelsFileName {
// Models file routed to a separate package directory.
if strings.Contains(modelsAbsfile, "..") {
return fmt.Errorf("invalid file output path: %s", modelsAbsfile)
}
if !strings.HasPrefix(modelsAbsfile, modelsAbsout) {
return fmt.Errorf("invalid file output path: %s", modelsAbsfile)
}
g.output[modelsAbsfile] = source
continue
}
filename := filepath.Join(g.dir, out, n)
// filepath.Join calls filepath.Clean which should remove all "..", but
// double check to make sure
if strings.Contains(filename, "..") {
return fmt.Errorf("invalid file output path: %s", filename)
}
// The output file must be contained inside the output directory
if !strings.HasPrefix(filename, absout) {
return fmt.Errorf("invalid file output path: %s", filename)
}
g.output[filename] = source
}
g.m.Unlock()
return nil
}
func parse(ctx context.Context, name, dir string, sql config.SQL, combo config.CombinedSettings, parserOpts opts.Parser, stderr io.Writer) (*compiler.Result, bool) {
defer trace.StartRegion(ctx, "parse").End()
var copts []compiler.Option
if parserOpts.Experiment.CoreAnalyzer {
copts = append(copts, compiler.WithCoreAnalysis())
}
c, err := compiler.NewCompiler(sql, combo, parserOpts, copts...)
defer func() {
if c != nil {
c.Close(ctx)
}
}()
if err != nil {
fmt.Fprintf(stderr, "error creating compiler: %s\n", err)
return nil, true
}
if err := c.ParseCatalog(sql.Schema); err != nil {
fmt.Fprintf(stderr, "# package %s\n", name)
if parserErr, ok := err.(*multierr.Error); ok {
for _, fileErr := range parserErr.Errs() {
printFileErr(stderr, dir, fileErr)
}
} else {
fmt.Fprintf(stderr, "error parsing schema: %s\n", err)
}
return nil, true
}
if debugDumpCatalog.Value() == "1" {
debug.Dump(c.Catalog())
}
if err := c.ParseQueries(sql.Queries, parserOpts); err != nil {
fmt.Fprintf(stderr, "# package %s\n", name)
if parserErr, ok := err.(*multierr.Error); ok {
for _, fileErr := range parserErr.Errs() {
printFileErr(stderr, dir, fileErr)
}
} else {
fmt.Fprintf(stderr, "error parsing queries: %s\n", err)
}
return nil, true
}
return c.Result(), false
}
func codegen(ctx context.Context, combo config.CombinedSettings, sql OutputPair, result *compiler.Result) (string, *plugin.GenerateResponse, error) {
defer trace.StartRegion(ctx, "codegen").End()
req := codeGenRequest(result, combo)
var handler grpc.ClientConnInterface
var out string
switch {
case sql.Plugin != nil:
out = sql.Plugin.Out
plug, err := findPlugin(combo.Global, sql.Plugin.Plugin)
if err != nil {
return "", nil, fmt.Errorf("plugin not found: %s", err)
}
switch {
case plug.Process != nil:
handler = &process.Runner{
Cmd: plug.Process.Cmd,
Env: plug.Env,
Format: plug.Process.Format,
}
case plug.WASM != nil:
handler = &wasm.Runner{
URL: plug.WASM.URL,
SHA256: plug.WASM.SHA256,
Env: plug.Env,
}
default:
return "", nil, fmt.Errorf("unsupported plugin type")
}
opts, err := convert.YAMLtoJSON(sql.Plugin.Options)
if err != nil {
return "", nil, fmt.Errorf("invalid plugin options: %w", err)
}
req.PluginOptions = opts
global, found := combo.Global.Options[plug.Name]
if found {
opts, err := convert.YAMLtoJSON(global)
if err != nil {
return "", nil, fmt.Errorf("invalid global options: %w", err)
}
req.GlobalOptions = opts
}
case sql.Gen.Go != nil:
out = combo.Go.Out
handler = ext.HandleFunc(golang.Generate)
opts, err := json.Marshal(sql.Gen.Go)
if err != nil {
return "", nil, fmt.Errorf("opts marshal failed: %w", err)
}
req.PluginOptions = opts
if combo.Global.Overrides.Go != nil {
opts, err := json.Marshal(combo.Global.Overrides.Go)
if err != nil {
return "", nil, fmt.Errorf("opts marshal failed: %w", err)
}
req.GlobalOptions = opts
}
case sql.Gen.JSON != nil:
out = combo.JSON.Out
handler = ext.HandleFunc(genjson.Generate)
opts, err := json.Marshal(sql.Gen.JSON)
if err != nil {
return "", nil, fmt.Errorf("opts marshal failed: %w", err)
}
req.PluginOptions = opts
default:
return "", nil, fmt.Errorf("missing language backend")
}
client := plugin.NewCodegenServiceClient(handler)
resp, err := client.Generate(ctx, req)
return out, resp, err
}