diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go index ca3ee680b5..674f46ed7c 100644 --- a/internal/cmd/generate.go +++ b/internal/cmd/generate.go @@ -204,7 +204,35 @@ func (g *generator) ProcessResult(ctx context.Context, combo config.CombinedSett // 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 diff --git a/internal/codegen/golang/gen.go b/internal/codegen/golang/gen.go index baf7fa78c5..5b81c149c3 100644 --- a/internal/codegen/golang/gen.go +++ b/internal/codegen/golang/gen.go @@ -17,13 +17,14 @@ import ( ) type tmplCtx struct { - Q string - Package string - SQLDriver opts.SQLDriver - Enums []Enum - Structs []Struct - GoQueries []Query - SqlcVersion string + Q string + Package string + ModelsPackage string + SQLDriver opts.SQLDriver + Enums []Enum + Structs []Struct + GoQueries []Query + SqlcVersion string // TODO: Race conditions SourceName string @@ -120,13 +121,13 @@ func Generate(ctx context.Context, req *plugin.GenerateRequest) (*plugin.Generat enums := buildEnums(req, options) structs := buildStructs(req, options) - queries, err := buildQueries(req, options, structs) + queries, err := buildQueries(req, options, enums, structs) if err != nil { return nil, err } if options.OmitUnusedStructs { - enums, structs = filterUnusedStructs(enums, structs, queries) + enums, structs = filterUnusedStructs(enums, structs, queries, options.ModelsTypeQualifier()) } if err := validate(options, enums, structs, queries); err != nil { @@ -186,6 +187,7 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, SQLDriver: parseDriver(options.SqlPackage), Q: "`", Package: options.Package, + ModelsPackage: options.ModelsPackage(), Enums: enums, Structs: structs, SqlcVersion: req.SqlcVersion, @@ -292,8 +294,10 @@ func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, if err := execute(dbFileName, "dbFile"); err != nil { return nil, err } - if err := execute(modelsFileName, "modelsFile"); err != nil { - return nil, err + if options.ModelsEmitEnabled() { + if err := execute(modelsFileName, "modelsFile"); err != nil { + return nil, err + } } if options.EmitInterface { if err := execute(querierFileName, "interfaceFile"); err != nil { @@ -367,25 +371,35 @@ func checkNoTimesForMySQLCopyFrom(queries []Query) error { return nil } -func filterUnusedStructs(enums []Enum, structs []Struct, queries []Query) ([]Enum, []Struct) { +func filterUnusedStructs(enums []Enum, structs []Struct, queries []Query, qualifier string) ([]Enum, []Struct) { keepTypes := make(map[string]struct{}) + keep := func(t string) { + keepTypes[t] = struct{}{} + // Also store the bare type name so that lookups against + // bare struct/enum names match even when types have been + // qualified with the models package prefix (e.g. "model.User"). + if bare := stripQualifier(t, qualifier); bare != t { + keepTypes[bare] = struct{}{} + } + } + for _, query := range queries { if !query.Arg.isEmpty() { - keepTypes[query.Arg.Type()] = struct{}{} + keep(query.Arg.Type()) if query.Arg.IsStruct() { for _, field := range query.Arg.Struct.Fields { - keepTypes[field.Type] = struct{}{} + keep(field.Type) } } } if query.hasRetType() { - keepTypes[query.Ret.Type()] = struct{}{} + keep(query.Ret.Type()) if query.Ret.IsStruct() { for _, field := range query.Ret.Struct.Fields { - keepTypes[strings.TrimPrefix(field.Type, "[]")] = struct{}{} + keep(strings.TrimPrefix(field.Type, "[]")) for _, embedField := range field.EmbedFields { - keepTypes[embedField.Type] = struct{}{} + keep(embedField.Type) } } } diff --git a/internal/codegen/golang/imports.go b/internal/codegen/golang/imports.go index ccca4f603c..76964248ef 100644 --- a/internal/codegen/golang/imports.go +++ b/internal/codegen/golang/imports.go @@ -243,6 +243,17 @@ func buildImports(options *opts.Options, queries []Query, uses func(string) bool } } + // Models package import. When models live in a separate Go package and + // any type in this file references a qualified model type, import the + // models package under a fixed `models` alias so query files always + // reference types as `models.User` regardless of how the actual + // package is named. + if options.ModelsAreExternal() { + if uses(options.ModelsTypeQualifier()) { + pkg[ImportSpec{Path: options.OutputModelsImport, ID: opts.ModelsImportAlias}] = struct{}{} + } + } + return std, pkg } diff --git a/internal/codegen/golang/opts/options.go b/internal/codegen/golang/opts/options.go index 3e956cc3d7..646bf1e066 100644 --- a/internal/codegen/golang/opts/options.go +++ b/internal/codegen/golang/opts/options.go @@ -37,6 +37,10 @@ type Options struct { OutputBatchFileName string `json:"output_batch_file_name,omitempty" yaml:"output_batch_file_name"` OutputDbFileName string `json:"output_db_file_name,omitempty" yaml:"output_db_file_name"` OutputModelsFileName string `json:"output_models_file_name,omitempty" yaml:"output_models_file_name"` + OutputModelsPath string `json:"output_models_path,omitempty" yaml:"output_models_path"` + OutputModelsPackage string `json:"output_models_package,omitempty" yaml:"output_models_package"` + OutputModelsImport string `json:"output_models_import,omitempty" yaml:"output_models_import"` + OutputModelsEmit *bool `json:"output_models_emit,omitempty" yaml:"output_models_emit"` OutputQuerierFileName string `json:"output_querier_file_name,omitempty" yaml:"output_querier_file_name"` OutputCopyfromFileName string `json:"output_copyfrom_file_name,omitempty" yaml:"output_copyfrom_file_name"` OutputFilesSuffix string `json:"output_files_suffix,omitempty" yaml:"output_files_suffix"` @@ -94,6 +98,17 @@ func parseOpts(req *plugin.GenerateRequest) (*Options, error) { } } + // Default the models package name to the base of the models path. When + // the user only configures output_models_emit: false (no path), fall + // back to the base of the import path. + if options.OutputModelsPackage == "" { + if options.OutputModelsPath != "" { + options.OutputModelsPackage = filepath.Base(options.OutputModelsPath) + } else if options.OutputModelsImport != "" { + options.OutputModelsPackage = filepath.Base(options.OutputModelsImport) + } + } + for i := range options.Overrides { if err := options.Overrides[i].parse(req); err != nil { return nil, err @@ -154,5 +169,75 @@ func ValidateOpts(opts *Options) error { return fmt.Errorf("invalid options: query parameter limit must not be negative") } + if err := validateModelsOptions(opts); err != nil { + return err + } + + return nil +} + +// ModelsEmitEnabled reports whether this codegen block should write the +// models file. Defaults to true when the option is unset. +func (o *Options) ModelsEmitEnabled() bool { + if o.OutputModelsEmit == nil { + return true + } + return *o.OutputModelsEmit +} + +// ModelsImportAlias is the fixed Go import alias used for the models +// package in query files. Using a constant alias keeps the type qualifier +// consistent regardless of how the user names the actual package. +const ModelsImportAlias = "models" + +// ModelsPackage returns the Go package name to use in the models file +// itself (i.e. the `package X` declaration). When the caller has not +// configured a separate models package, this is the same as Package. +func (o *Options) ModelsPackage() string { + if o.OutputModelsPackage != "" { + return o.OutputModelsPackage + } + return o.Package +} + +// ModelsAreExternal reports whether model types live in a different Go +// package than the queries package. When true, query files must import the +// models package and reference types as `models.Type`. +func (o *Options) ModelsAreExternal() bool { + return o.OutputModelsImport != "" +} + +// ModelsTypeQualifier returns the prefix to use when referencing a model +// type from a query file ("models."). Empty string when no qualifier is +// needed. +func (o *Options) ModelsTypeQualifier() string { + if o.ModelsAreExternal() { + return ModelsImportAlias + "." + } + return "" +} + +func validateModelsOptions(opts *Options) error { + hasAnyModelsOpt := opts.OutputModelsPath != "" || + opts.OutputModelsPackage != "" || + opts.OutputModelsImport != "" || + opts.OutputModelsEmit != nil + + if !hasAnyModelsOpt { + return nil + } + + if opts.OutputModelsImport == "" { + return fmt.Errorf("invalid options: output_models_import is required when any output_models_* option is set") + } + + if opts.ModelsEmitEnabled() && opts.OutputModelsPath == "" { + return fmt.Errorf("invalid options: output_models_path is required when emitting models to a separate package") + } + + if opts.ModelsEmitEnabled() && opts.OutputModelsPath == opts.Out { + return fmt.Errorf("invalid options: output_models_path matches out; models would overwrite the queries package") + } + return nil } diff --git a/internal/codegen/golang/qualify.go b/internal/codegen/golang/qualify.go new file mode 100644 index 0000000000..eb85b84d5c --- /dev/null +++ b/internal/codegen/golang/qualify.go @@ -0,0 +1,79 @@ +package golang + +import "strings" + +// stripQualifier removes a leading slice/pointer prefix and the given +// `pkg.` qualifier from a Go type expression. When the qualifier is empty +// or absent from the type, the input is returned unchanged. +func stripQualifier(t, qualifier string) string { + if qualifier == "" { + return t + } + prefix := "" + rest := t + for { + if strings.HasPrefix(rest, "[]") { + prefix += "[]" + rest = rest[2:] + continue + } + if strings.HasPrefix(rest, "*") { + prefix += "*" + rest = rest[1:] + continue + } + break + } + if strings.HasPrefix(rest, qualifier) { + return prefix + rest[len(qualifier):] + } + return t +} + +// modelTypeSet is the set of Go type names that live in the models file. +type modelTypeSet map[string]struct{} + +// buildModelTypeSet returns the set of type names that are declared in +// models.go for the current codegen invocation. +func buildModelTypeSet(enums []Enum, structs []Struct) modelTypeSet { + set := make(modelTypeSet, len(enums)*4+len(structs)) + for _, e := range enums { + set[e.Name] = struct{}{} + set["Null"+e.Name] = struct{}{} + } + for _, s := range structs { + if s.IsModel { + set[s.Name] = struct{}{} + } + } + return set +} + +// qualifyType prefixes a Go type expression with `qualifier` when the bare +// type name belongs to `models`. Slice and pointer prefixes are preserved. +// When qualifier is empty (i.e. models live in the queries package), the +// input is returned unchanged. +func qualifyType(t string, models modelTypeSet, qualifier string) string { + if qualifier == "" || t == "" || len(models) == 0 { + return t + } + prefix := "" + rest := t + for { + if strings.HasPrefix(rest, "[]") { + prefix += "[]" + rest = rest[2:] + continue + } + if strings.HasPrefix(rest, "*") { + prefix += "*" + rest = rest[1:] + continue + } + break + } + if _, ok := models[rest]; ok { + return prefix + qualifier + rest + } + return t +} diff --git a/internal/codegen/golang/query.go b/internal/codegen/golang/query.go index 3b4fb2fa1a..27c596c24e 100644 --- a/internal/codegen/golang/query.go +++ b/internal/codegen/golang/query.go @@ -18,6 +18,10 @@ type QueryValue struct { Typ string SQLDriver opts.SQLDriver + // ModelQualifier prefixes references to model types when the models file + // lives in a different Go package (e.g. "model."). Empty otherwise. + ModelQualifier string + // Column is kept so late in the generation process around to differentiate // between mysql slices and pg arrays Column *plugin.Column @@ -88,6 +92,14 @@ func (v QueryValue) Type() string { return v.Typ } if v.Struct != nil { + // Model structs (table-derived) live in the models file. When that + // file is generated into a different Go package, references from + // query files must be qualified. Synthetic structs (Params/Row) + // are defined in the same query file as their use, so they stay + // bare. + if v.Struct.IsModel && v.ModelQualifier != "" { + return v.ModelQualifier + v.Struct.Name + } return v.Struct.Name } panic("no type for QueryValue: " + v.Name) diff --git a/internal/codegen/golang/result.go b/internal/codegen/golang/result.go index 216f5e3372..5bfa7f795e 100644 --- a/internal/codegen/golang/result.go +++ b/internal/codegen/golang/result.go @@ -84,6 +84,7 @@ func buildStructs(req *plugin.GenerateRequest, options *opts.Options) []Struct { Table: &plugin.Identifier{Schema: schema.Name, Name: table.Rel.Name}, Name: StructName(structName, options), Comment: table.Comment, + IsModel: true, } for _, column := range table.Columns { tags := map[string]string{} @@ -181,7 +182,9 @@ func argName(name string) string { return out } -func buildQueries(req *plugin.GenerateRequest, options *opts.Options, structs []Struct) ([]Query, error) { +func buildQueries(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, structs []Struct) ([]Query, error) { + models := buildModelTypeSet(enums, structs) + qualifier := options.ModelsTypeQualifier() qs := make([]Query, 0, len(req.Queries)) for _, query := range req.Queries { if query.Name == "" { @@ -231,11 +234,12 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, structs [] if len(query.Params) == 1 && qpl != 0 { p := query.Params[0] gq.Arg = QueryValue{ - Name: escape(paramName(p)), - DBName: p.Column.GetName(), - Typ: goType(req, options, p.Column), - SQLDriver: sqlpkg, - Column: p.Column, + Name: escape(paramName(p)), + DBName: p.Column.GetName(), + Typ: qualifyType(goType(req, options, p.Column), models, qualifier), + SQLDriver: sqlpkg, + ModelQualifier: qualifier, + Column: p.Column, } } else if len(query.Params) >= 1 { var cols []goColumn @@ -245,16 +249,17 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, structs [] Column: p.Column, }) } - s, err := columnsToStruct(req, options, gq.MethodName+"Params", cols, false) + s, err := columnsToStruct(req, options, gq.MethodName+"Params", cols, false, models, qualifier) if err != nil { return nil, err } gq.Arg = QueryValue{ - Emit: true, - Name: "arg", - Struct: s, - SQLDriver: sqlpkg, - EmitPointer: options.EmitParamsStructPointers, + Emit: true, + Name: "arg", + Struct: s, + SQLDriver: sqlpkg, + EmitPointer: options.EmitParamsStructPointers, + ModelQualifier: qualifier, } // if query params is 2, and query params limit is 4 AND this is a copyfrom, we still want to emit the query's model @@ -287,10 +292,11 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, structs [] } } gq.Ret = QueryValue{ - Name: retName, - DBName: name, - Typ: goType(req, options, c), - SQLDriver: sqlpkg, + Name: retName, + DBName: name, + Typ: qualifyType(goType(req, options, c), models, qualifier), + SQLDriver: sqlpkg, + ModelQualifier: qualifier, } } else if putOutColumns(query) { var gs *Struct @@ -326,18 +332,19 @@ func buildQueries(req *plugin.GenerateRequest, options *opts.Options, structs [] }) } var err error - gs, err = columnsToStruct(req, options, gq.MethodName+"Row", columns, true) + gs, err = columnsToStruct(req, options, gq.MethodName+"Row", columns, true, models, qualifier) if err != nil { return nil, err } emit = true } gq.Ret = QueryValue{ - Emit: emit, - Name: "i", - Struct: gs, - SQLDriver: sqlpkg, - EmitPointer: options.EmitResultStructPointers, + Emit: emit, + Name: "i", + Struct: gs, + SQLDriver: sqlpkg, + EmitPointer: options.EmitResultStructPointers, + ModelQualifier: qualifier, } } @@ -367,7 +374,7 @@ func putOutColumns(query *plugin.Query) bool { // JSON tags: count, count_2, count_2 // // This is unlikely to happen, so don't fix it yet -func columnsToStruct(req *plugin.GenerateRequest, options *opts.Options, name string, columns []goColumn, useID bool) (*Struct, error) { +func columnsToStruct(req *plugin.GenerateRequest, options *opts.Options, name string, columns []goColumn, useID bool, models modelTypeSet, qualifier string) (*Struct, error) { gs := Struct{ Name: name, } @@ -413,9 +420,9 @@ func columnsToStruct(req *plugin.GenerateRequest, options *opts.Options, name st Column: c.Column, } if c.embed == nil { - f.Type = goType(req, options, c.Column) + f.Type = qualifyType(goType(req, options, c.Column), models, qualifier) } else { - f.Type = c.embed.modelType + f.Type = qualifyType(c.embed.modelType, models, qualifier) f.EmbedFields = c.embed.fields } diff --git a/internal/codegen/golang/struct.go b/internal/codegen/golang/struct.go index ed9311800e..72f91fb747 100644 --- a/internal/codegen/golang/struct.go +++ b/internal/codegen/golang/struct.go @@ -14,6 +14,10 @@ type Struct struct { Name string Fields []Field Comment string + // IsModel is true for table structs that live in the models file. When + // the models file is generated into a different Go package, references + // to these types from query files must be qualified. + IsModel bool } func StructName(name string, options *opts.Options) string { diff --git a/internal/codegen/golang/templates/template.tmpl b/internal/codegen/golang/templates/template.tmpl index afd50c01ac..9365817457 100644 --- a/internal/codegen/golang/templates/template.tmpl +++ b/internal/codegen/golang/templates/template.tmpl @@ -71,7 +71,7 @@ import ( // sqlc {{.SqlcVersion}} {{end}} -package {{.Package}} +package {{.ModelsPackage}} {{ if hasImports .SourceName }} import ( diff --git a/internal/config/v_one.go b/internal/config/v_one.go index 52925d63f8..fa1a4d8d28 100644 --- a/internal/config/v_one.go +++ b/internal/config/v_one.go @@ -50,6 +50,10 @@ type v1PackageSettings struct { OutputBatchFileName string `json:"output_batch_file_name,omitempty" yaml:"output_batch_file_name"` OutputDBFileName string `json:"output_db_file_name,omitempty" yaml:"output_db_file_name"` OutputModelsFileName string `json:"output_models_file_name,omitempty" yaml:"output_models_file_name"` + OutputModelsPath string `json:"output_models_path,omitempty" yaml:"output_models_path"` + OutputModelsPackage string `json:"output_models_package,omitempty" yaml:"output_models_package"` + OutputModelsImport string `json:"output_models_import,omitempty" yaml:"output_models_import"` + OutputModelsEmit *bool `json:"output_models_emit,omitempty" yaml:"output_models_emit"` OutputQuerierFileName string `json:"output_querier_file_name,omitempty" yaml:"output_querier_file_name"` OutputCopyFromFileName string `json:"output_copyfrom_file_name,omitempty" yaml:"output_copyfrom_file_name"` OutputFilesSuffix string `json:"output_files_suffix,omitempty" yaml:"output_files_suffix"` @@ -163,6 +167,10 @@ func (c *V1GenerateSettings) Translate() Config { OutputBatchFileName: pkg.OutputBatchFileName, OutputDbFileName: pkg.OutputDBFileName, OutputModelsFileName: pkg.OutputModelsFileName, + OutputModelsPath: pkg.OutputModelsPath, + OutputModelsPackage: pkg.OutputModelsPackage, + OutputModelsImport: pkg.OutputModelsImport, + OutputModelsEmit: pkg.OutputModelsEmit, OutputQuerierFileName: pkg.OutputQuerierFileName, OutputCopyfromFileName: pkg.OutputCopyFromFileName, OutputFilesSuffix: pkg.OutputFilesSuffix, diff --git a/internal/config/v_one.json b/internal/config/v_one.json index 36588463b7..d4ea6b6304 100644 --- a/internal/config/v_one.json +++ b/internal/config/v_one.json @@ -230,6 +230,18 @@ "output_models_file_name": { "type": "string" }, + "output_models_path": { + "type": "string" + }, + "output_models_package": { + "type": "string" + }, + "output_models_import": { + "type": "string" + }, + "output_models_emit": { + "type": "boolean" + }, "output_querier_file_name": { "type": "string" }, diff --git a/internal/config/v_two.json b/internal/config/v_two.json index 5db15cce7e..f4cfb875c0 100644 --- a/internal/config/v_two.json +++ b/internal/config/v_two.json @@ -248,6 +248,18 @@ "output_models_file_name": { "type": "string" }, + "output_models_path": { + "type": "string" + }, + "output_models_package": { + "type": "string" + }, + "output_models_import": { + "type": "string" + }, + "output_models_emit": { + "type": "boolean" + }, "output_querier_file_name": { "type": "string" }, diff --git a/internal/endtoend/testdata/output_models_path/postgresql/db/db.go b/internal/endtoend/testdata/output_models_path/postgresql/db/db.go new file mode 100644 index 0000000000..f43598b1eb --- /dev/null +++ b/internal/endtoend/testdata/output_models_path/postgresql/db/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package db + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/output_models_path/postgresql/db/querier.go b/internal/endtoend/testdata/output_models_path/postgresql/db/querier.go new file mode 100644 index 0000000000..18d562cd69 --- /dev/null +++ b/internal/endtoend/testdata/output_models_path/postgresql/db/querier.go @@ -0,0 +1,21 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package db + +import ( + "context" + + models "github.com/sqlc-dev/sqlc/endtoend/output_models_path/postgresql/model" +) + +type Querier interface { + CreateAuthor(ctx context.Context, arg CreateAuthorParams) (models.Author, error) + GetAuthor(ctx context.Context, id int64) (models.Author, error) + GetBook(ctx context.Context, id int64) (models.Book, error) + ListAuthors(ctx context.Context) ([]models.Author, error) + ListAuthorsByStatus(ctx context.Context, status models.Status) ([]models.Author, error) +} + +var _ Querier = (*Queries)(nil) diff --git a/internal/endtoend/testdata/output_models_path/postgresql/db/query.sql.go b/internal/endtoend/testdata/output_models_path/postgresql/db/query.sql.go new file mode 100644 index 0000000000..2e2561375c --- /dev/null +++ b/internal/endtoend/testdata/output_models_path/postgresql/db/query.sql.go @@ -0,0 +1,126 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package db + +import ( + "context" + "database/sql" + + models "github.com/sqlc-dev/sqlc/endtoend/output_models_path/postgresql/model" +) + +const createAuthor = `-- name: CreateAuthor :one +INSERT INTO authors (name, bio, status) VALUES ($1, $2, $3) RETURNING id, name, bio, status +` + +type CreateAuthorParams struct { + Name string + Bio sql.NullString + Status models.Status +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (models.Author, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Bio, arg.Status) + var i models.Author + err := row.Scan( + &i.ID, + &i.Name, + &i.Bio, + &i.Status, + ) + return i, err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT id, name, bio, status FROM authors WHERE id = $1 LIMIT 1 +` + +func (q *Queries) GetAuthor(ctx context.Context, id int64) (models.Author, error) { + row := q.db.QueryRowContext(ctx, getAuthor, id) + var i models.Author + err := row.Scan( + &i.ID, + &i.Name, + &i.Bio, + &i.Status, + ) + return i, err +} + +const getBook = `-- name: GetBook :one +SELECT id, author_id, title FROM books WHERE id = $1 LIMIT 1 +` + +func (q *Queries) GetBook(ctx context.Context, id int64) (models.Book, error) { + row := q.db.QueryRowContext(ctx, getBook, id) + var i models.Book + err := row.Scan(&i.ID, &i.AuthorID, &i.Title) + return i, err +} + +const listAuthors = `-- name: ListAuthors :many +SELECT id, name, bio, status FROM authors ORDER BY name +` + +func (q *Queries) ListAuthors(ctx context.Context) ([]models.Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []models.Author + for rows.Next() { + var i models.Author + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Bio, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAuthorsByStatus = `-- name: ListAuthorsByStatus :many +SELECT id, name, bio, status FROM authors WHERE status = $1 ORDER BY name +` + +func (q *Queries) ListAuthorsByStatus(ctx context.Context, status models.Status) ([]models.Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthorsByStatus, status) + if err != nil { + return nil, err + } + defer rows.Close() + var items []models.Author + for rows.Next() { + var i models.Author + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Bio, + &i.Status, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/output_models_path/postgresql/model/models.go b/internal/endtoend/testdata/output_models_path/postgresql/model/models.go new file mode 100644 index 0000000000..da987b1078 --- /dev/null +++ b/internal/endtoend/testdata/output_models_path/postgresql/model/models.go @@ -0,0 +1,67 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package model + +import ( + "database/sql" + "database/sql/driver" + "fmt" +) + +type Status string + +const ( + StatusActive Status = "active" + StatusInactive Status = "inactive" + StatusPending Status = "pending" +) + +func (e *Status) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Status(s) + case string: + *e = Status(s) + default: + return fmt.Errorf("unsupported scan type for Status: %T", src) + } + return nil +} + +type NullStatus struct { + Status Status + Valid bool // Valid is true if Status is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullStatus) Scan(value interface{}) error { + if value == nil { + ns.Status, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Status.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.Status), nil +} + +type Author struct { + ID int64 + Name string + Bio sql.NullString + Status Status +} + +type Book struct { + ID int64 + AuthorID int64 + Title string +} diff --git a/internal/endtoend/testdata/output_models_path/postgresql/query.sql b/internal/endtoend/testdata/output_models_path/postgresql/query.sql new file mode 100644 index 0000000000..5c7c377ab8 --- /dev/null +++ b/internal/endtoend/testdata/output_models_path/postgresql/query.sql @@ -0,0 +1,14 @@ +-- name: GetAuthor :one +SELECT * FROM authors WHERE id = $1 LIMIT 1; + +-- name: ListAuthors :many +SELECT * FROM authors ORDER BY name; + +-- name: CreateAuthor :one +INSERT INTO authors (name, bio, status) VALUES ($1, $2, $3) RETURNING *; + +-- name: ListAuthorsByStatus :many +SELECT * FROM authors WHERE status = $1 ORDER BY name; + +-- name: GetBook :one +SELECT * FROM books WHERE id = $1 LIMIT 1; diff --git a/internal/endtoend/testdata/output_models_path/postgresql/schema.sql b/internal/endtoend/testdata/output_models_path/postgresql/schema.sql new file mode 100644 index 0000000000..2d85828249 --- /dev/null +++ b/internal/endtoend/testdata/output_models_path/postgresql/schema.sql @@ -0,0 +1,14 @@ +CREATE TYPE status AS ENUM ('active', 'inactive', 'pending'); + +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + bio text, + status status NOT NULL DEFAULT 'active' +); + +CREATE TABLE books ( + id BIGSERIAL PRIMARY KEY, + author_id BIGINT NOT NULL REFERENCES authors(id), + title text NOT NULL +); diff --git a/internal/endtoend/testdata/output_models_path/postgresql/sqlc.json b/internal/endtoend/testdata/output_models_path/postgresql/sqlc.json new file mode 100644 index 0000000000..9fa8540070 --- /dev/null +++ b/internal/endtoend/testdata/output_models_path/postgresql/sqlc.json @@ -0,0 +1,19 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "query.sql", + "engine": "postgresql", + "gen": { + "go": { + "out": "db", + "package": "db", + "emit_interface": true, + "output_models_path": "model", + "output_models_import": "github.com/sqlc-dev/sqlc/endtoend/output_models_path/postgresql/model" + } + } + } + ] +} diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/model/models.go b/internal/endtoend/testdata/output_models_shared/postgresql/model/models.go new file mode 100644 index 0000000000..9cf4f4b75b --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/model/models.go @@ -0,0 +1,58 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package model + +import ( + "database/sql/driver" + "fmt" +) + +type Status string + +const ( + StatusActive Status = "active" + StatusInactive Status = "inactive" +) + +func (e *Status) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Status(s) + case string: + *e = Status(s) + default: + return fmt.Errorf("unsupported scan type for Status: %T", src) + } + return nil +} + +type NullStatus struct { + Status Status + Valid bool // Valid is true if Status is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullStatus) Scan(value interface{}) error { + if value == nil { + ns.Status, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Status.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullStatus) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.Status), nil +} + +type Author struct { + ID int64 + Name string + Status Status +} diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/primary/db.go b/internal/endtoend/testdata/output_models_shared/postgresql/primary/db.go new file mode 100644 index 0000000000..0e51e835e1 --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/primary/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package primary + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/primary/query.sql.go b/internal/endtoend/testdata/output_models_shared/postgresql/primary/query.sql.go new file mode 100644 index 0000000000..555d40522e --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/primary/query.sql.go @@ -0,0 +1,39 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package primary + +import ( + "context" + + models "github.com/sqlc-dev/sqlc/endtoend/output_models_shared/postgresql/model" +) + +const createAuthor = `-- name: CreateAuthor :one +INSERT INTO authors (name, status) VALUES ($1, $2) RETURNING id, name, status +` + +type CreateAuthorParams struct { + Name string + Status models.Status +} + +func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (models.Author, error) { + row := q.db.QueryRowContext(ctx, createAuthor, arg.Name, arg.Status) + var i models.Author + err := row.Scan(&i.ID, &i.Name, &i.Status) + return i, err +} + +const getAuthor = `-- name: GetAuthor :one +SELECT id, name, status FROM authors WHERE id = $1 LIMIT 1 +` + +func (q *Queries) GetAuthor(ctx context.Context, id int64) (models.Author, error) { + row := q.db.QueryRowContext(ctx, getAuthor, id) + var i models.Author + err := row.Scan(&i.ID, &i.Name, &i.Status) + return i, err +} diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/queries/primary/query.sql b/internal/endtoend/testdata/output_models_shared/postgresql/queries/primary/query.sql new file mode 100644 index 0000000000..b8646b5533 --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/queries/primary/query.sql @@ -0,0 +1,5 @@ +-- name: CreateAuthor :one +INSERT INTO authors (name, status) VALUES ($1, $2) RETURNING *; + +-- name: GetAuthor :one +SELECT * FROM authors WHERE id = $1 LIMIT 1; diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/queries/replica/query.sql b/internal/endtoend/testdata/output_models_shared/postgresql/queries/replica/query.sql new file mode 100644 index 0000000000..b533fea924 --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/queries/replica/query.sql @@ -0,0 +1,5 @@ +-- name: ListAuthors :many +SELECT * FROM authors ORDER BY name; + +-- name: ListAuthorsByStatus :many +SELECT * FROM authors WHERE status = $1 ORDER BY name; diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/replica/db.go b/internal/endtoend/testdata/output_models_shared/postgresql/replica/db.go new file mode 100644 index 0000000000..0725dd95cb --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/replica/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 + +package replica + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/replica/query.sql.go b/internal/endtoend/testdata/output_models_shared/postgresql/replica/query.sql.go new file mode 100644 index 0000000000..4831f5cd0a --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/replica/query.sql.go @@ -0,0 +1,66 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.31.1 +// source: query.sql + +package replica + +import ( + "context" + + models "github.com/sqlc-dev/sqlc/endtoend/output_models_shared/postgresql/model" +) + +const listAuthors = `-- name: ListAuthors :many +SELECT id, name, status FROM authors ORDER BY name +` + +func (q *Queries) ListAuthors(ctx context.Context) ([]models.Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthors) + if err != nil { + return nil, err + } + defer rows.Close() + var items []models.Author + for rows.Next() { + var i models.Author + if err := rows.Scan(&i.ID, &i.Name, &i.Status); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listAuthorsByStatus = `-- name: ListAuthorsByStatus :many +SELECT id, name, status FROM authors WHERE status = $1 ORDER BY name +` + +func (q *Queries) ListAuthorsByStatus(ctx context.Context, status models.Status) ([]models.Author, error) { + rows, err := q.db.QueryContext(ctx, listAuthorsByStatus, status) + if err != nil { + return nil, err + } + defer rows.Close() + var items []models.Author + for rows.Next() { + var i models.Author + if err := rows.Scan(&i.ID, &i.Name, &i.Status); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/schema.sql b/internal/endtoend/testdata/output_models_shared/postgresql/schema.sql new file mode 100644 index 0000000000..afb9a4e68e --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/schema.sql @@ -0,0 +1,7 @@ +CREATE TYPE status AS ENUM ('active', 'inactive'); + +CREATE TABLE authors ( + id BIGSERIAL PRIMARY KEY, + name text NOT NULL, + status status NOT NULL DEFAULT 'active' +); diff --git a/internal/endtoend/testdata/output_models_shared/postgresql/sqlc.json b/internal/endtoend/testdata/output_models_shared/postgresql/sqlc.json new file mode 100644 index 0000000000..98d60f8f5e --- /dev/null +++ b/internal/endtoend/testdata/output_models_shared/postgresql/sqlc.json @@ -0,0 +1,31 @@ +{ + "version": "2", + "sql": [ + { + "schema": "schema.sql", + "queries": "queries/primary", + "engine": "postgresql", + "gen": { + "go": { + "out": "primary", + "package": "primary", + "output_models_path": "model", + "output_models_import": "github.com/sqlc-dev/sqlc/endtoend/output_models_shared/postgresql/model" + } + } + }, + { + "schema": "schema.sql", + "queries": "queries/replica", + "engine": "postgresql", + "gen": { + "go": { + "out": "replica", + "package": "replica", + "output_models_import": "github.com/sqlc-dev/sqlc/endtoend/output_models_shared/postgresql/model", + "output_models_emit": false + } + } + } + ] +}