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

427 lines
11 KiB
Go

package golang
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"go/format"
"slices"
"strings"
"text/template"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
type tmplCtx struct {
Q string
Package string
ModelsPackage string
SQLDriver opts.SQLDriver
Enums []Enum
Structs []Struct
GoQueries []Query
SqlcVersion string
// TODO: Race conditions
SourceName string
EmitJSONTags bool
JsonTagsIDUppercase bool
EmitDBTags bool
EmitPreparedQueries bool
EmitInterface bool
EmitEmptySlices bool
EmitMethodsWithDBArgument bool
EmitEnumValidMethod bool
EmitAllEnumValues bool
UsesCopyFrom bool
UsesBatch bool
OmitSqlcVersion bool
BuildTags string
WrapErrors bool
}
func (t *tmplCtx) OutputQuery(sourceName string) bool {
return t.SourceName == sourceName
}
func (t *tmplCtx) codegenDbarg() string {
if t.EmitMethodsWithDBArgument {
return "db DBTX, "
}
return ""
}
// Called as a global method since subtemplate queryCodeStdExec does not have
// access to the toplevel tmplCtx
func (t *tmplCtx) codegenEmitPreparedQueries() bool {
return t.EmitPreparedQueries
}
func (t *tmplCtx) codegenQueryMethod(q Query) string {
db := "q.db"
if t.EmitMethodsWithDBArgument {
db = "db"
}
switch q.Cmd {
case ":one":
if t.EmitPreparedQueries {
return "q.queryRow"
}
return db + ".QueryRowContext"
case ":many":
if t.EmitPreparedQueries {
return "q.query"
}
return db + ".QueryContext"
default:
if t.EmitPreparedQueries {
return "q.exec"
}
return db + ".ExecContext"
}
}
func (t *tmplCtx) codegenQueryRetval(q Query) (string, error) {
switch q.Cmd {
case ":one":
return "row :=", nil
case ":many":
return "rows, err :=", nil
case ":exec":
return "_, err :=", nil
case ":execrows", ":execlastid":
return "result, err :=", nil
case ":execresult":
if t.WrapErrors {
return "result, err :=", nil
}
return "return", nil
default:
return "", fmt.Errorf("unhandled q.Cmd case %q", q.Cmd)
}
}
func Generate(ctx context.Context, req *plugin.GenerateRequest) (*plugin.GenerateResponse, error) {
options, err := opts.Parse(req)
if err != nil {
return nil, err
}
if err := opts.ValidateOpts(options); err != nil {
return nil, err
}
enums := buildEnums(req, options)
structs := buildStructs(req, options)
queries, err := buildQueries(req, options, enums, structs)
if err != nil {
return nil, err
}
if options.OmitUnusedStructs {
enums, structs = filterUnusedStructs(enums, structs, queries, options.ModelsTypeQualifier())
}
if err := validate(options, enums, structs, queries); err != nil {
return nil, err
}
return generate(req, options, enums, structs, queries)
}
func validate(options *opts.Options, enums []Enum, structs []Struct, queries []Query) error {
enumNames := make(map[string]struct{})
for _, enum := range enums {
enumNames[enum.Name] = struct{}{}
enumNames["Null"+enum.Name] = struct{}{}
}
structNames := make(map[string]struct{})
for _, struckt := range structs {
if _, ok := enumNames[struckt.Name]; ok {
return fmt.Errorf("struct name conflicts with enum name: %s", struckt.Name)
}
structNames[struckt.Name] = struct{}{}
}
if !options.EmitExportedQueries {
return nil
}
for _, query := range queries {
if _, ok := enumNames[query.ConstantName]; ok {
return fmt.Errorf("query constant name conflicts with enum name: %s", query.ConstantName)
}
if _, ok := structNames[query.ConstantName]; ok {
return fmt.Errorf("query constant name conflicts with struct name: %s", query.ConstantName)
}
}
return nil
}
func generate(req *plugin.GenerateRequest, options *opts.Options, enums []Enum, structs []Struct, queries []Query) (*plugin.GenerateResponse, error) {
i := &importer{
Options: options,
Queries: queries,
Enums: enums,
Structs: structs,
}
tctx := tmplCtx{
EmitInterface: options.EmitInterface,
EmitJSONTags: options.EmitJsonTags,
JsonTagsIDUppercase: options.JsonTagsIdUppercase,
EmitDBTags: options.EmitDbTags,
EmitPreparedQueries: options.EmitPreparedQueries,
EmitEmptySlices: options.EmitEmptySlices,
EmitMethodsWithDBArgument: options.EmitMethodsWithDbArgument,
EmitEnumValidMethod: options.EmitEnumValidMethod,
EmitAllEnumValues: options.EmitAllEnumValues,
UsesCopyFrom: usesCopyFrom(queries),
UsesBatch: usesBatch(queries),
SQLDriver: parseDriver(options.SqlPackage),
Q: "`",
Package: options.Package,
ModelsPackage: options.ModelsPackage(),
Enums: enums,
Structs: structs,
SqlcVersion: req.SqlcVersion,
BuildTags: options.BuildTags,
OmitSqlcVersion: options.OmitSqlcVersion,
WrapErrors: options.WrapErrors,
}
if tctx.UsesCopyFrom && !tctx.SQLDriver.IsPGX() && options.SqlDriver != opts.SQLDriverGoSQLDriverMySQL {
return nil, errors.New(":copyfrom is only supported by pgx and github.com/go-sql-driver/mysql")
}
if tctx.UsesCopyFrom && options.SqlDriver == opts.SQLDriverGoSQLDriverMySQL {
if err := checkNoTimesForMySQLCopyFrom(queries); err != nil {
return nil, err
}
tctx.SQLDriver = opts.SQLDriverGoSQLDriverMySQL
}
if tctx.UsesBatch && !tctx.SQLDriver.IsPGX() {
return nil, errors.New(":batch* commands are only supported by pgx")
}
funcMap := template.FuncMap{
"lowerTitle": sdk.LowerTitle,
"comment": sdk.DoubleSlashComment,
"escape": sdk.EscapeBacktick,
"imports": i.Imports,
"hasImports": i.HasImports,
"hasPrefix": strings.HasPrefix,
// These methods are Go specific, they do not belong in the codegen package
// (as that is language independent)
"dbarg": tctx.codegenDbarg,
"emitPreparedQueries": tctx.codegenEmitPreparedQueries,
"queryMethod": tctx.codegenQueryMethod,
"queryRetval": tctx.codegenQueryRetval,
}
tmpl := template.Must(
template.New("table").
Funcs(funcMap).
ParseFS(
templates,
"templates/*.tmpl",
"templates/*/*.tmpl",
),
)
output := map[string]string{}
execute := func(name, templateName string) error {
imports := i.Imports(name)
replacedQueries := replaceConflictedArg(imports, queries)
var b bytes.Buffer
w := bufio.NewWriter(&b)
tctx.SourceName = name
tctx.GoQueries = replacedQueries
err := tmpl.ExecuteTemplate(w, templateName, &tctx)
w.Flush()
if err != nil {
return err
}
code, err := format.Source(b.Bytes())
if err != nil {
fmt.Println(b.String())
return fmt.Errorf("source error: %w", err)
}
if templateName == "queryFile" && options.OutputFilesSuffix != "" {
name += options.OutputFilesSuffix
}
if !strings.HasSuffix(name, ".go") {
name += ".go"
}
output[name] = string(code)
return nil
}
dbFileName := "db.go"
if options.OutputDbFileName != "" {
dbFileName = options.OutputDbFileName
}
modelsFileName := "models.go"
if options.OutputModelsFileName != "" {
modelsFileName = options.OutputModelsFileName
}
querierFileName := "querier.go"
if options.OutputQuerierFileName != "" {
querierFileName = options.OutputQuerierFileName
}
copyfromFileName := "copyfrom.go"
if options.OutputCopyfromFileName != "" {
copyfromFileName = options.OutputCopyfromFileName
}
batchFileName := "batch.go"
if options.OutputBatchFileName != "" {
batchFileName = options.OutputBatchFileName
}
if err := execute(dbFileName, "dbFile"); 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 {
return nil, err
}
}
if tctx.UsesCopyFrom {
if err := execute(copyfromFileName, "copyfromFile"); err != nil {
return nil, err
}
}
if tctx.UsesBatch {
if err := execute(batchFileName, "batchFile"); err != nil {
return nil, err
}
}
files := map[string]struct{}{}
for _, gq := range queries {
files[gq.SourceName] = struct{}{}
}
for source := range files {
if err := execute(source, "queryFile"); err != nil {
return nil, err
}
}
resp := plugin.GenerateResponse{}
for filename, code := range output {
resp.Files = append(resp.Files, &plugin.File{
Name: filename,
Contents: []byte(code),
})
}
return &resp, nil
}
func usesCopyFrom(queries []Query) bool {
for _, q := range queries {
if q.Cmd == metadata.CmdCopyFrom {
return true
}
}
return false
}
func usesBatch(queries []Query) bool {
for _, q := range queries {
if slices.Contains([]string{metadata.CmdBatchExec, metadata.CmdBatchMany, metadata.CmdBatchOne}, q.Cmd) {
return true
}
}
return false
}
func checkNoTimesForMySQLCopyFrom(queries []Query) error {
for _, q := range queries {
if q.Cmd != metadata.CmdCopyFrom {
continue
}
for _, f := range q.Arg.CopyFromMySQLFields() {
if f.Type == "time.Time" {
return fmt.Errorf("values with a timezone are not yet supported")
}
}
}
return nil
}
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() {
keep(query.Arg.Type())
if query.Arg.IsStruct() {
for _, field := range query.Arg.Struct.Fields {
keep(field.Type)
}
}
}
if query.hasRetType() {
keep(query.Ret.Type())
if query.Ret.IsStruct() {
for _, field := range query.Ret.Struct.Fields {
keep(strings.TrimPrefix(field.Type, "[]"))
for _, embedField := range field.EmbedFields {
keep(embedField.Type)
}
}
}
}
}
keepEnums := make([]Enum, 0, len(enums))
for _, enum := range enums {
_, keep := keepTypes[enum.Name]
_, keepNull := keepTypes["Null"+enum.Name]
_, keepPointer := keepTypes["*"+enum.Name]
if keep || keepNull || keepPointer {
keepEnums = append(keepEnums, enum)
}
}
keepStructs := make([]Struct, 0, len(structs))
for _, st := range structs {
if _, ok := keepTypes[st.Name]; ok {
keepStructs = append(keepStructs, st)
}
}
return keepEnums, keepStructs
}