diff --git a/examples/authors/sqlc.json b/examples/authors/sqlc.json new file mode 100644 index 0000000000..3db9676f1f --- /dev/null +++ b/examples/authors/sqlc.json @@ -0,0 +1,16 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "postgresql", + "gen": { + "go": { + "package": "authors", + "out": "." + } + } + } + ] +} diff --git a/examples/booktest/sqlc.json b/examples/booktest/sqlc.json new file mode 100644 index 0000000000..419a759f19 --- /dev/null +++ b/examples/booktest/sqlc.json @@ -0,0 +1,19 @@ +{ + "version": "1", + "packages": [ + { + "name": "booktest", + "path": "postgresql", + "schema": "postgresql/schema.sql", + "queries": "postgresql/query.sql", + "engine": "postgresql" + }, + { + "name": "booktest", + "path": "mysql", + "schema": "mysql/schema.sql", + "queries": "mysql/query.sql", + "engine": "mysql" + } + ] +} diff --git a/examples/jets/sqlc.json b/examples/jets/sqlc.json new file mode 100644 index 0000000000..0bca6f48df --- /dev/null +++ b/examples/jets/sqlc.json @@ -0,0 +1,12 @@ +{ + "version": "1", + "packages": [ + { + "path": ".", + "name": "jets", + "schema": "schema.sql", + "queries": "query-building.sql", + "engine": "postgresql" + } + ] +} diff --git a/examples/ondeck/sqlc.json b/examples/ondeck/sqlc.json new file mode 100644 index 0000000000..3b604f4c63 --- /dev/null +++ b/examples/ondeck/sqlc.json @@ -0,0 +1,15 @@ +{ + "version": "1", + "packages": [ + { + "path": ".", + "name": "ondeck", + "schema": "schema", + "queries": "query", + "engine": "postgresql", + "emit_json_tags": true, + "emit_prepared_queries": true, + "emit_interface": true + } + ] +} diff --git a/examples/sqlc.json b/examples/sqlc.json deleted file mode 100644 index 9662784e93..0000000000 --- a/examples/sqlc.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "version": "1", - "packages": [ - { - "path": "authors", - "schema": "authors/schema.sql", - "queries": "authors/query.sql", - "engine": "postgresql" - }, - { - "path": "ondeck", - "schema": "ondeck/schema", - "queries": "ondeck/query", - "engine": "postgresql", - "emit_json_tags": true, - "emit_prepared_queries": true, - "emit_interface": true - }, - { - "path": "jets", - "schema": "jets/schema.sql", - "queries": "jets/query-building.sql", - "engine": "postgresql" - }, - { - "name": "booktest", - "path": "booktest/postgresql", - "schema": "booktest/postgresql/schema.sql", - "queries": "booktest/postgresql/query.sql", - "engine": "postgresql" - }, - { - "name": "booktest", - "path": "booktest/mysql", - "schema": "booktest/mysql/schema.sql", - "queries": "booktest/mysql/query.sql", - "engine": "mysql" - } - ] -} diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 751171d460..7d8a8e346f 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -12,7 +12,6 @@ import ( "github.com/spf13/cobra" "github.com/kyleconroy/sqlc/internal/config" - "github.com/kyleconroy/sqlc/internal/dinosql" ) // Do runs the command logic. @@ -53,7 +52,7 @@ var initCmd = &cobra.Command{ if _, err := os.Stat("sqlc.json"); !os.IsNotExist(err) { return nil } - blob, err := json.MarshalIndent(config.GenerateSettings{Version: "1"}, "", " ") + blob, err := json.MarshalIndent(config.Config{Version: "1"}, "", " ") if err != nil { return err } @@ -91,24 +90,14 @@ var checkCmd = &cobra.Command{ Use: "compile", Short: "Statically check SQL for syntax and type errors", RunE: func(cmd *cobra.Command, args []string) error { - file, err := os.Open("sqlc.json") - if err != nil { - return err - } - - settings, err := config.ParseConfig(file) + stderr := cmd.ErrOrStderr() + dir, err := os.Getwd() if err != nil { - return err + fmt.Fprintln(stderr, "error parsing sqlc.json: file does not exist") + os.Exit(1) } - - for _, pkg := range settings.Packages { - c, err := dinosql.ParseCatalog(pkg.Schema) - if err != nil { - return err - } - if _, err := dinosql.ParseQueries(c, pkg); err != nil { - return err - } + if _, err := Generate(dir, stderr); err != nil { + os.Exit(1) } return nil }, diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index b30a7c06e7..03a81e3870 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -40,7 +40,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { return nil, err } - settings, err := config.ParseConfig(bytes.NewReader(blob)) + conf, err := config.ParseConfig(bytes.NewReader(blob)) if err != nil { switch err { case config.ErrMissingVersion: @@ -57,20 +57,19 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { output := map[string]string{} errored := false - for _, pkg := range settings.Packages { - name := pkg.Name - combo := config.Combine(settings, pkg) + for _, sql := range conf.SQL { + combo := config.Combine(conf, sql) + name := combo.Go.Package var result dinosql.Generateable // TODO: This feels like a hack that will bite us later - pkg.Schema = filepath.Join(dir, pkg.Schema) - pkg.Queries = filepath.Join(dir, pkg.Queries) - - switch pkg.Engine { + sql.Schema = filepath.Join(dir, sql.Schema) + sql.Queries = filepath.Join(dir, sql.Queries) + switch sql.Engine { case config.EngineMySQL: // Experimental MySQL support - q, err := mysql.GeneratePkg(name, pkg.Schema, pkg.Queries, combo) + q, err := mysql.GeneratePkg(name, sql.Schema, sql.Queries, combo) if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) if parserErr, ok := err.(*dinosql.ParserErr); ok { @@ -86,7 +85,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { result = q case config.EnginePostgreSQL: - c, err := dinosql.ParseCatalog(pkg.Schema) + c, err := dinosql.ParseCatalog(sql.Schema) if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) if parserErr, ok := err.(*dinosql.ParserErr); ok { @@ -100,7 +99,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { continue } - q, err := dinosql.ParseQueries(c, pkg) + q, err := dinosql.ParseQueries(c, sql) if err != nil { fmt.Fprintf(stderr, "# package %s\n", name) if parserErr, ok := err.(*dinosql.ParserErr); ok { @@ -126,7 +125,7 @@ func Generate(dir string, stderr io.Writer) (map[string]string, error) { } for n, source := range files { - filename := filepath.Join(dir, pkg.Path, n) + filename := filepath.Join(dir, combo.Go.Out, n) output[filename] = source } } diff --git a/internal/config/config.go b/internal/config/config.go index 16cbf1852a..d61c02b9b8 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,13 +1,13 @@ package config import ( + "bytes" "encoding/json" "errors" "fmt" "go/types" "io" "os" - "path/filepath" "strings" "github.com/kyleconroy/sqlc/internal/pg" @@ -28,11 +28,8 @@ The only supported version is "1". const errMessageNoPackages = `No packages are configured` -type GenerateSettings struct { - Version string `json:"version"` - Packages []PackageSettings `json:"packages"` - Overrides []Override `json:"overrides,omitempty"` - Rename map[string]string `json:"rename,omitempty"` +type versionSetting struct { + Number string `json:"version"` } type Engine string @@ -42,16 +39,40 @@ const ( EnginePostgreSQL Engine = "postgresql" ) -type PackageSettings struct { - Name string `json:"name"` - Engine Engine `json:"engine,omitempty"` - Path string `json:"path"` - Schema string `json:"schema"` - Queries string `json:"queries"` - EmitInterface bool `json:"emit_interface"` - EmitJSONTags bool `json:"emit_json_tags"` - EmitPreparedQueries bool `json:"emit_prepared_queries"` - Overrides []Override `json:"overrides"` +type Config struct { + Version string `json:"version"` + SQL []SQL `json:"sql"` + Gen Gen `json:"overrides,omitempty"` +} + +type Gen struct { + Go *GenGo `json:"go,omitempty"` +} + +type GenGo struct { + Overrides []Override `json:"overrides,omitempty"` + Rename map[string]string `json:"rename,omitempty"` +} + +type SQL struct { + Engine Engine `json:"engine,omitempty"` + Schema string `json:"schema"` + Queries string `json:"queries"` + Gen SQLGen `json:"gen"` +} + +type SQLGen struct { + Go *SQLGo `json:"go,omitempty"` +} + +type SQLGo struct { + EmitInterface bool `json:"emit_interface"` + EmitJSONTags bool `json:"emit_json_tags"` + EmitPreparedQueries bool `json:"emit_prepared_queries"` + Package string `json:"package"` + Out string `json:"out"` + Overrides []Override `json:"overrides,omitempty"` + Rename map[string]string `json:"rename,omitempty"` } type Override struct { @@ -78,23 +99,6 @@ type Override struct { GoBasicType bool } -func (c *GenerateSettings) ValidateGlobalOverrides() error { - engines := map[Engine]struct{}{} - for _, pkg := range c.Packages { - if _, ok := engines[pkg.Engine]; !ok { - engines[pkg.Engine] = struct{}{} - } - } - - usesMultipleEngines := len(engines) > 1 - for _, oride := range c.Overrides { - if usesMultipleEngines && oride.Engine == "" { - return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`) - } - } - return nil -} - func (o *Override) Parse() error { // validate deprecated postgres_type field @@ -188,63 +192,54 @@ func (o *Override) Parse() error { var ErrMissingVersion = errors.New("no version number") var ErrUnknownVersion = errors.New("invalid version number") +var ErrMissingEngine = errors.New("unknown engine") +var ErrUnknownEngine = errors.New("invalid engine") var ErrNoPackages = errors.New("no packages") var ErrNoPackageName = errors.New("missing package name") var ErrNoPackagePath = errors.New("missing package path") -func ParseConfig(rd io.Reader) (GenerateSettings, error) { - dec := json.NewDecoder(rd) - dec.DisallowUnknownFields() - var config GenerateSettings - if err := dec.Decode(&config); err != nil { +func ParseConfig(rd io.Reader) (Config, error) { + var buf bytes.Buffer + var config Config + var version versionSetting + ver := io.TeeReader(rd, &buf) + dec := json.NewDecoder(ver) + if err := dec.Decode(&version); err != nil { return config, err } - if config.Version == "" { + if version.Number == "" { return config, ErrMissingVersion } - if config.Version != "1" { + switch version.Number { + case "1": + return v1ParseConfig(&buf) + case "2": + return v2ParseConfig(&buf) + default: return config, ErrUnknownVersion } - if len(config.Packages) == 0 { - return config, ErrNoPackages - } - if err := config.ValidateGlobalOverrides(); err != nil { - return config, err - } - for i := range config.Overrides { - if err := config.Overrides[i].Parse(); err != nil { - return config, err - } - } - for j := range config.Packages { - if config.Packages[j].Path == "" { - return config, ErrNoPackagePath - } - for i := range config.Packages[j].Overrides { - if err := config.Packages[j].Overrides[i].Parse(); err != nil { - return config, err - } - } - if config.Packages[j].Name == "" { - config.Packages[j].Name = filepath.Base(config.Packages[j].Path) - } - if config.Packages[j].Engine == "" { - config.Packages[j].Engine = EnginePostgreSQL - } - } - return config, nil } type CombinedSettings struct { - Global GenerateSettings - Package PackageSettings + Global Config + Package SQL + Go SQLGo + Rename map[string]string Overrides []Override } -func Combine(gen GenerateSettings, pkg PackageSettings) CombinedSettings { - return CombinedSettings{ - Global: gen, - Package: pkg, - Overrides: append(gen.Overrides, pkg.Overrides...), +func Combine(conf Config, pkg SQL) CombinedSettings { + cs := CombinedSettings{ + Global: conf, + Package: pkg, + } + if conf.Gen.Go != nil { + cs.Rename = conf.Gen.Go.Rename + cs.Overrides = append(cs.Overrides, conf.Gen.Go.Overrides...) + } + if pkg.Gen.Go != nil { + cs.Go = *pkg.Gen.Go + cs.Overrides = append(cs.Overrides, pkg.Gen.Go.Overrides...) } + return cs } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index eeabb5d88e..da09091c35 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -19,6 +19,7 @@ const unknownVersion = `{ }` const unknownFields = `{ + "version": "1", "foo": "bar" }` diff --git a/internal/config/v_one.go b/internal/config/v_one.go new file mode 100644 index 0000000000..29d3deb5f1 --- /dev/null +++ b/internal/config/v_one.go @@ -0,0 +1,121 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "path/filepath" +) + +type v1GenerateSettings struct { + Version string `json:"version"` + Packages []v1PackageSettings `json:"packages"` + Overrides []Override `json:"overrides,omitempty"` + Rename map[string]string `json:"rename,omitempty"` +} + +type v1PackageSettings struct { + Name string `json:"name"` + Engine Engine `json:"engine,omitempty"` + Path string `json:"path"` + Schema string `json:"schema"` + Queries string `json:"queries"` + EmitInterface bool `json:"emit_interface"` + EmitJSONTags bool `json:"emit_json_tags"` + EmitPreparedQueries bool `json:"emit_prepared_queries"` + Overrides []Override `json:"overrides"` +} + +func v1ParseConfig(rd io.Reader) (Config, error) { + dec := json.NewDecoder(rd) + dec.DisallowUnknownFields() + var settings v1GenerateSettings + var config Config + if err := dec.Decode(&settings); err != nil { + return config, err + } + if settings.Version == "" { + return config, ErrMissingVersion + } + if settings.Version != "1" { + return config, ErrUnknownVersion + } + if len(settings.Packages) == 0 { + return config, ErrNoPackages + } + if err := settings.ValidateGlobalOverrides(); err != nil { + return config, err + } + for i := range settings.Overrides { + if err := settings.Overrides[i].Parse(); err != nil { + return config, err + } + } + for j := range settings.Packages { + if settings.Packages[j].Path == "" { + return config, ErrNoPackagePath + } + for i := range settings.Packages[j].Overrides { + if err := settings.Packages[j].Overrides[i].Parse(); err != nil { + return config, err + } + } + if settings.Packages[j].Name == "" { + settings.Packages[j].Name = filepath.Base(settings.Packages[j].Path) + } + if settings.Packages[j].Engine == "" { + settings.Packages[j].Engine = EnginePostgreSQL + } + } + return settings.Translate(), nil +} + +func (c *v1GenerateSettings) ValidateGlobalOverrides() error { + engines := map[Engine]struct{}{} + for _, pkg := range c.Packages { + if _, ok := engines[pkg.Engine]; !ok { + engines[pkg.Engine] = struct{}{} + } + } + + usesMultipleEngines := len(engines) > 1 + for _, oride := range c.Overrides { + if usesMultipleEngines && oride.Engine == "" { + return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`) + } + } + return nil +} + +func (c *v1GenerateSettings) Translate() Config { + conf := Config{ + Version: c.Version, + } + + for _, pkg := range c.Packages { + conf.SQL = append(conf.SQL, SQL{ + Engine: pkg.Engine, + Schema: pkg.Schema, + Queries: pkg.Queries, + Gen: SQLGen{ + Go: &SQLGo{ + EmitInterface: pkg.EmitInterface, + EmitJSONTags: pkg.EmitJSONTags, + EmitPreparedQueries: pkg.EmitPreparedQueries, + Package: pkg.Name, + Out: pkg.Path, + Overrides: pkg.Overrides, + }, + }, + }) + } + + if len(c.Overrides) > 0 || len(c.Rename) > 0 { + conf.Gen.Go = &GenGo{ + Overrides: c.Overrides, + Rename: c.Rename, + } + } + + return conf +} diff --git a/internal/config/v_two.go b/internal/config/v_two.go new file mode 100644 index 0000000000..78ff059843 --- /dev/null +++ b/internal/config/v_two.go @@ -0,0 +1,74 @@ +package config + +import ( + "encoding/json" + "fmt" + "io" + "path/filepath" +) + +func v2ParseConfig(rd io.Reader) (Config, error) { + dec := json.NewDecoder(rd) + dec.DisallowUnknownFields() + var conf Config + if err := dec.Decode(&conf); err != nil { + return conf, err + } + if conf.Version == "" { + return conf, ErrMissingVersion + } + if conf.Version != "2" { + return conf, ErrUnknownVersion + } + if len(conf.SQL) == 0 { + return conf, ErrNoPackages + } + if err := conf.validateGlobalOverrides(); err != nil { + return conf, err + } + if conf.Gen.Go != nil { + for i := range conf.Gen.Go.Overrides { + if err := conf.Gen.Go.Overrides[i].Parse(); err != nil { + return conf, err + } + } + } + for j := range conf.SQL { + if conf.SQL[j].Engine == "" { + return conf, ErrMissingEngine + } + if conf.SQL[j].Gen.Go != nil { + if conf.SQL[j].Gen.Go.Out == "" { + return conf, ErrNoPackagePath + } + if conf.SQL[j].Gen.Go.Package == "" { + conf.SQL[j].Gen.Go.Package = filepath.Base(conf.SQL[j].Gen.Go.Out) + } + for i := range conf.SQL[j].Gen.Go.Overrides { + if err := conf.SQL[j].Gen.Go.Overrides[i].Parse(); err != nil { + return conf, err + } + } + } + } + return conf, nil +} + +func (c *Config) validateGlobalOverrides() error { + engines := map[Engine]struct{}{} + for _, pkg := range c.SQL { + if _, ok := engines[pkg.Engine]; !ok { + engines[pkg.Engine] = struct{}{} + } + } + if c.Gen.Go == nil { + return nil + } + usesMultipleEngines := len(engines) > 1 + for _, oride := range c.Gen.Go.Overrides { + if usesMultipleEngines && oride.Engine == "" { + return fmt.Errorf(`the "engine" field is required for global type overrides because your configuration uses multiple database engines`) + } + } + return nil +} diff --git a/internal/dinosql/gen.go b/internal/dinosql/gen.go index 8c9dca8a25..728374b85a 100644 --- a/internal/dinosql/gen.go +++ b/internal/dinosql/gen.go @@ -192,7 +192,7 @@ func Imports(r Generateable, settings config.CombinedSettings) func(string) [][] return func(filename string) [][]string { if filename == "db.go" { imps := []string{"context", "database/sql"} - if settings.Package.EmitPreparedQueries { + if settings.Go.EmitPreparedQueries { imps = append(imps, "fmt") } return [][]string{imps} @@ -524,7 +524,7 @@ func (r Result) Enums(settings config.CombinedSettings) []GoEnum { } func StructName(name string, settings config.CombinedSettings) string { - if rename := settings.Global.Rename[name]; rename != "" { + if rename := settings.Rename[name]; rename != "" { return rename } out := "" @@ -1183,7 +1183,7 @@ type tmplCtx struct { Enums []GoEnum Structs []GoStruct GoQueries []GoQuery - Settings config.GenerateSettings + Settings config.Config // TODO: Race conditions SourceName string @@ -1210,14 +1210,14 @@ func Generate(r Generateable, settings config.CombinedSettings) (map[string]stri sqlFile := template.Must(template.New("table").Funcs(funcMap).Parse(sqlTmpl)) ifaceFile := template.Must(template.New("table").Funcs(funcMap).Parse(ifaceTmpl)) - pkg := settings.Package + golang := settings.Go tctx := tmplCtx{ Settings: settings.Global, - EmitInterface: pkg.EmitInterface, - EmitJSONTags: pkg.EmitJSONTags, - EmitPreparedQueries: pkg.EmitPreparedQueries, + EmitInterface: golang.EmitInterface, + EmitJSONTags: golang.EmitJSONTags, + EmitPreparedQueries: golang.EmitPreparedQueries, Q: "`", - Package: pkg.Name, + Package: golang.Package, GoQueries: r.GoQueries(settings), Enums: r.Enums(settings), Structs: r.Structs(settings), @@ -1252,7 +1252,7 @@ func Generate(r Generateable, settings config.CombinedSettings) (map[string]stri if err := execute("models.go", modelsFile); err != nil { return nil, err } - if pkg.EmitInterface { + if golang.EmitInterface { if err := execute("querier.go", ifaceFile); err != nil { return nil, err } diff --git a/internal/dinosql/parser.go b/internal/dinosql/parser.go index 06d18ebcdd..7131149f66 100644 --- a/internal/dinosql/parser.go +++ b/internal/dinosql/parser.go @@ -193,7 +193,7 @@ type Result struct { Catalog core.Catalog } -func ParseQueries(c core.Catalog, pkg config.PackageSettings) (*Result, error) { +func ParseQueries(c core.Catalog, pkg config.SQL) (*Result, error) { f, err := os.Stat(pkg.Queries) if err != nil { return nil, fmt.Errorf("path %s does not exist", pkg.Queries) diff --git a/internal/endtoend/endtoend_test.go b/internal/endtoend/endtoend_test.go index 9611d38f17..d028f3f1b0 100644 --- a/internal/endtoend/endtoend_test.go +++ b/internal/endtoend/endtoend_test.go @@ -16,16 +16,32 @@ import ( func TestExamples(t *testing.T) { t.Parallel() + examples, err := filepath.Abs(filepath.Join("..", "..", "examples")) + if err != nil { + t.Fatal(err) + } - examples, _ := filepath.Abs(filepath.Join("..", "..", "examples")) - var stderr bytes.Buffer - - output, err := cmd.Generate(examples, &stderr) + files, err := ioutil.ReadDir(examples) if err != nil { - t.Fatalf("%s", stderr.String()) + t.Fatal(err) } - cmpDirectory(t, examples, output) + for _, replay := range files { + if !replay.IsDir() { + continue + } + tc := replay.Name() + t.Run(tc, func(t *testing.T) { + t.Parallel() + path := filepath.Join(examples, tc) + var stderr bytes.Buffer + output, err := cmd.Generate(path, &stderr) + if err != nil { + t.Fatalf("sqlc generate failed: %s", stderr.String()) + } + cmpDirectory(t, path, output) + }) + } } func TestReplay(t *testing.T) { @@ -37,6 +53,9 @@ func TestReplay(t *testing.T) { } for _, replay := range files { + if !replay.IsDir() { + continue + } tc := replay.Name() t.Run(tc, func(t *testing.T) { t.Parallel()