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
377 lines
10 KiB
Go
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
|
|
}
|