snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
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

This commit is contained in:
upstream-mirror
2026-09-16 02:17:34 +05:00
commit 0f4df23290
7340 changed files with 238743 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
package golang
import "github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
func parseDriver(sqlPackage string) opts.SQLDriver {
switch sqlPackage {
case opts.SQLPackagePGXV4:
return opts.SQLDriverPGXV4
case opts.SQLPackagePGXV5:
return opts.SQLDriverPGXV5
default:
return opts.SQLDriverLibPQ
}
}

View File

@@ -0,0 +1,64 @@
package golang
import (
"strings"
"unicode"
)
type Constant struct {
Name string
Type string
Value string
}
type Enum struct {
Name string
Comment string
Constants []Constant
NameTags map[string]string
ValidTags map[string]string
}
func (e Enum) NameTag() string {
return TagsToString(e.NameTags)
}
func (e Enum) ValidTag() string {
return TagsToString(e.ValidTags)
}
func enumReplacer(r rune) rune {
if strings.ContainsRune("-/:_", r) {
return '_'
} else if (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') {
return r
} else {
return -1
}
}
// EnumReplace removes all non ident symbols (all but letters, numbers and
// underscore) and returns valid ident name for provided name.
func EnumReplace(value string) string {
return strings.Map(enumReplacer, value)
}
// EnumValueName removes all non ident symbols (all but letters, numbers and
// underscore) and converts snake case ident to camel case.
func EnumValueName(value string) string {
parts := strings.Split(EnumReplace(value), "_")
for i, part := range parts {
parts[i] = titleFirst(part)
}
return strings.Join(parts, "")
}
func titleFirst(s string) string {
r := []rune(s)
r[0] = unicode.ToUpper(r[0])
return string(r)
}

View File

@@ -0,0 +1,143 @@
package golang
import (
"fmt"
"regexp"
"sort"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
type Field struct {
Name string // CamelCased name for Go
DBName string // Name as used in the DB
Type string
Tags map[string]string
Comment string
Column *plugin.Column
// EmbedFields contains the embedded fields that require scanning.
EmbedFields []Field
}
func (gf Field) Tag() string {
return TagsToString(gf.Tags)
}
func (gf Field) HasSqlcSlice() bool {
return gf.Column.IsSqlcSlice
}
func TagsToString(tags map[string]string) string {
if len(tags) == 0 {
return ""
}
tagParts := make([]string, 0, len(tags))
for key, val := range tags {
tagParts = append(tagParts, fmt.Sprintf("%s:%q", key, val))
}
sort.Strings(tagParts)
return strings.Join(tagParts, " ")
}
func JSONTagName(name string, options *opts.Options) string {
style := options.JsonTagsCaseStyle
idUppercase := options.JsonTagsIdUppercase
if style == "" || style == "none" {
return name
} else {
return SetJSONCaseStyle(name, style, idUppercase)
}
}
func SetCaseStyle(name string, style string) string {
switch style {
case "camel":
return toCamelCase(name)
case "pascal":
return toPascalCase(name)
case "snake":
return toSnakeCase(name)
default:
panic(fmt.Sprintf("unsupported JSON tags case style: '%s'", style))
}
}
func SetJSONCaseStyle(name string, style string, idUppercase bool) string {
switch style {
case "camel":
return toJsonCamelCase(name, idUppercase)
case "pascal":
return toPascalCase(name)
case "snake":
return toSnakeCase(name)
default:
panic(fmt.Sprintf("unsupported JSON tags case style: '%s'", style))
}
}
var camelPattern = regexp.MustCompile("[^A-Z][A-Z]+")
func toSnakeCase(s string) string {
if !strings.ContainsRune(s, '_') {
s = camelPattern.ReplaceAllStringFunc(s, func(x string) string {
return x[:1] + "_" + x[1:]
})
}
return strings.ToLower(s)
}
func toCamelCase(s string) string {
return toCamelInitCase(s, false)
}
func toPascalCase(s string) string {
return toCamelInitCase(s, true)
}
func toCamelInitCase(name string, initUpper bool) string {
var out strings.Builder
for i, p := range strings.Split(name, "_") {
if !initUpper && i == 0 {
out.WriteString(p)
continue
}
if p == "id" {
out.WriteString("ID")
} else {
out.WriteString(strings.Title(p))
}
}
return out.String()
}
func toJsonCamelCase(name string, idUppercase bool) string {
var out strings.Builder
idStr := "Id"
if idUppercase {
idStr = "ID"
}
for i, p := range strings.Split(name, "_") {
if i == 0 {
out.WriteString(p)
continue
}
if p == "id" {
out.WriteString(idStr)
} else {
out.WriteString(strings.Title(p))
}
}
return out.String()
}
func toLowerCase(str string) string {
if str == "" {
return ""
}
return strings.ToLower(str[:1]) + str[1:]
}

View File

@@ -0,0 +1,426 @@
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
}

View File

@@ -0,0 +1,92 @@
package golang
import (
"maps"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
func addExtraGoStructTags(tags map[string]string, req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) {
for _, override := range options.Overrides {
oride := override.ShimOverride
if oride.GoType.StructTags == nil {
continue
}
if override.MatchesColumn(col) {
maps.Copy(tags, oride.GoType.StructTags)
continue
}
if !override.Matches(col.Table, req.Catalog.DefaultSchema) {
// Different table.
continue
}
cname := col.Name
if col.OriginalName != "" {
cname = col.OriginalName
}
if !sdk.MatchString(oride.ColumnName, cname) {
// Different column.
continue
}
// Add the extra tags.
maps.Copy(tags, oride.GoType.StructTags)
}
}
func goType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
// Check if the column's type has been overridden
for _, override := range options.Overrides {
oride := override.ShimOverride
if oride.GoType.TypeName == "" {
continue
}
cname := col.Name
if col.OriginalName != "" {
cname = col.OriginalName
}
sameTable := override.Matches(col.Table, req.Catalog.DefaultSchema)
if oride.Column != "" && sdk.MatchString(oride.ColumnName, cname) && sameTable {
if col.IsSqlcSlice {
return "[]" + oride.GoType.TypeName
}
return oride.GoType.TypeName
}
}
typ := goInnerType(req, options, col)
if col.IsSqlcSlice {
return "[]" + typ
}
if col.IsArray {
return strings.Repeat("[]", int(col.ArrayDims)) + typ
}
return typ
}
func goInnerType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
// package overrides have a higher precedence
for _, override := range options.Overrides {
oride := override.ShimOverride
if oride.GoType.TypeName == "" {
continue
}
if override.MatchesColumn(col) {
return oride.GoType.TypeName
}
}
// TODO: Extend the engine interface to handle types
switch req.Settings.Engine {
case "mysql":
return mysqlType(req, options, col)
case "postgresql":
return postgresType(req, options, col)
case "sqlite":
return sqliteType(req, options, col)
default:
return "any"
}
}

View File

@@ -0,0 +1,537 @@
package golang
import (
"fmt"
"sort"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/metadata"
)
type fileImports struct {
Std []ImportSpec
Dep []ImportSpec
}
type ImportSpec struct {
ID string
Path string
}
func (s ImportSpec) String() string {
if s.ID != "" {
return fmt.Sprintf("%s %q", s.ID, s.Path)
} else {
return fmt.Sprintf("%q", s.Path)
}
}
func mergeImports(imps ...fileImports) [][]ImportSpec {
if len(imps) == 1 {
return [][]ImportSpec{
imps[0].Std,
imps[0].Dep,
}
}
var stds, pkgs []ImportSpec
seenStd := map[string]struct{}{}
seenPkg := map[string]struct{}{}
for i := range imps {
for _, spec := range imps[i].Std {
if _, ok := seenStd[spec.Path]; ok {
continue
}
stds = append(stds, spec)
seenStd[spec.Path] = struct{}{}
}
for _, spec := range imps[i].Dep {
if _, ok := seenPkg[spec.Path]; ok {
continue
}
pkgs = append(pkgs, spec)
seenPkg[spec.Path] = struct{}{}
}
}
return [][]ImportSpec{stds, pkgs}
}
type importer struct {
Options *opts.Options
Queries []Query
Enums []Enum
Structs []Struct
}
func (i *importer) usesType(typ string) bool {
for _, strct := range i.Structs {
for _, f := range strct.Fields {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, typ) {
return true
}
}
}
return false
}
func (i *importer) HasImports(filename string) bool {
imports := i.Imports(filename)
return len(imports[0]) != 0 || len(imports[1]) != 0
}
func (i *importer) Imports(filename string) [][]ImportSpec {
dbFileName := "db.go"
if i.Options.OutputDbFileName != "" {
dbFileName = i.Options.OutputDbFileName
}
modelsFileName := "models.go"
if i.Options.OutputModelsFileName != "" {
modelsFileName = i.Options.OutputModelsFileName
}
querierFileName := "querier.go"
if i.Options.OutputQuerierFileName != "" {
querierFileName = i.Options.OutputQuerierFileName
}
copyfromFileName := "copyfrom.go"
if i.Options.OutputCopyfromFileName != "" {
copyfromFileName = i.Options.OutputCopyfromFileName
}
batchFileName := "batch.go"
if i.Options.OutputBatchFileName != "" {
batchFileName = i.Options.OutputBatchFileName
}
switch filename {
case dbFileName:
return mergeImports(i.dbImports())
case modelsFileName:
return mergeImports(i.modelImports())
case querierFileName:
return mergeImports(i.interfaceImports())
case copyfromFileName:
return mergeImports(i.copyfromImports())
case batchFileName:
return mergeImports(i.batchImports())
default:
return mergeImports(i.queryImports(filename))
}
}
func (i *importer) dbImports() fileImports {
var pkg []ImportSpec
std := []ImportSpec{
{Path: "context"},
}
sqlpkg := parseDriver(i.Options.SqlPackage)
switch sqlpkg {
case opts.SQLDriverPGXV4:
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgconn"})
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v4"})
case opts.SQLDriverPGXV5:
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"})
pkg = append(pkg, ImportSpec{Path: "github.com/jackc/pgx/v5"})
default:
std = append(std, ImportSpec{Path: "database/sql"})
if i.Options.EmitPreparedQueries {
std = append(std, ImportSpec{Path: "fmt"})
}
}
sort.Slice(std, func(i, j int) bool { return std[i].Path < std[j].Path })
sort.Slice(pkg, func(i, j int) bool { return pkg[i].Path < pkg[j].Path })
return fileImports{Std: std, Dep: pkg}
}
var stdlibTypes = map[string]string{
"json.RawMessage": "encoding/json",
"time.Time": "time",
"net.IP": "net",
"net.HardwareAddr": "net",
"netip.Addr": "net/netip",
"netip.Prefix": "net/netip",
}
var pqtypeTypes = map[string]struct{}{
"pqtype.CIDR": {},
"pqtype.Inet": {},
"pqtype.Macaddr": {},
"pqtype.NullRawMessage": {},
}
func buildImports(options *opts.Options, queries []Query, uses func(string) bool) (map[string]struct{}, map[ImportSpec]struct{}) {
pkg := make(map[ImportSpec]struct{})
std := make(map[string]struct{})
if uses("sql.Null") {
std["database/sql"] = struct{}{}
}
sqlpkg := parseDriver(options.SqlPackage)
for _, q := range queries {
if q.Cmd == metadata.CmdExecResult {
switch sqlpkg {
case opts.SQLDriverPGXV4:
pkg[ImportSpec{Path: "github.com/jackc/pgconn"}] = struct{}{}
case opts.SQLDriverPGXV5:
pkg[ImportSpec{Path: "github.com/jackc/pgx/v5/pgconn"}] = struct{}{}
default:
std["database/sql"] = struct{}{}
}
}
}
for typeName, pkg := range stdlibTypes {
if uses(typeName) {
std[pkg] = struct{}{}
}
}
if uses("pgtype.") {
if sqlpkg == opts.SQLDriverPGXV5 {
pkg[ImportSpec{Path: "github.com/jackc/pgx/v5/pgtype"}] = struct{}{}
} else {
pkg[ImportSpec{Path: "github.com/jackc/pgtype"}] = struct{}{}
}
}
for typeName := range pqtypeTypes {
if uses(typeName) {
pkg[ImportSpec{Path: "github.com/sqlc-dev/pqtype"}] = struct{}{}
break
}
}
overrideTypes := map[string]string{}
for _, override := range options.Overrides {
o := override.ShimOverride
if o.GoType.BasicType || o.GoType.TypeName == "" {
continue
}
overrideTypes[o.GoType.TypeName] = o.GoType.ImportPath
}
_, overrideNullTime := overrideTypes["pq.NullTime"]
if uses("pq.NullTime") && !overrideNullTime {
pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{}
}
_, overrideUUID := overrideTypes["uuid.UUID"]
if uses("uuid.UUID") && !overrideUUID {
pkg[ImportSpec{Path: "github.com/google/uuid"}] = struct{}{}
}
_, overrideNullUUID := overrideTypes["uuid.NullUUID"]
if uses("uuid.NullUUID") && !overrideNullUUID {
pkg[ImportSpec{Path: "github.com/google/uuid"}] = struct{}{}
}
_, overrideVector := overrideTypes["pgvector.Vector"]
if uses("pgvector.Vector") && !overrideVector {
pkg[ImportSpec{Path: "github.com/pgvector/pgvector-go"}] = struct{}{}
}
// Custom imports
for _, override := range options.Overrides {
o := override.ShimOverride
if o.GoType.BasicType || o.GoType.TypeName == "" {
continue
}
_, alreadyImported := std[o.GoType.ImportPath]
hasPackageAlias := o.GoType.Package != ""
if (!alreadyImported || hasPackageAlias) && uses(o.GoType.TypeName) {
pkg[ImportSpec{Path: o.GoType.ImportPath, ID: o.GoType.Package}] = struct{}{}
}
}
// 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
}
func (i *importer) interfaceImports() fileImports {
std, pkg := buildImports(i.Options, i.Queries, func(name string) bool {
for _, q := range i.Queries {
if q.hasRetType() {
if usesBatch([]Query{q}) {
continue
}
if hasPrefixIgnoringSliceAndPointerPrefix(q.Ret.Type(), name) {
return true
}
}
for _, f := range q.Arg.Pairs() {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
}
}
}
return false
})
std["context"] = struct{}{}
return sortedImports(std, pkg)
}
func (i *importer) modelImports() fileImports {
std, pkg := buildImports(i.Options, nil, i.usesType)
if len(i.Enums) > 0 {
std["fmt"] = struct{}{}
std["database/sql/driver"] = struct{}{}
}
return sortedImports(std, pkg)
}
func sortedImports(std map[string]struct{}, pkg map[ImportSpec]struct{}) fileImports {
pkgs := make([]ImportSpec, 0, len(pkg))
for spec := range pkg {
pkgs = append(pkgs, spec)
}
stds := make([]ImportSpec, 0, len(std))
for path := range std {
stds = append(stds, ImportSpec{Path: path})
}
sort.Slice(stds, func(i, j int) bool { return stds[i].Path < stds[j].Path })
sort.Slice(pkgs, func(i, j int) bool { return pkgs[i].Path < pkgs[j].Path })
return fileImports{stds, pkgs}
}
func (i *importer) queryImports(filename string) fileImports {
var gq []Query
anyNonCopyFrom := false
for _, query := range i.Queries {
if usesBatch([]Query{query}) {
continue
}
if query.SourceName == filename {
gq = append(gq, query)
if query.Cmd != metadata.CmdCopyFrom {
anyNonCopyFrom = true
}
}
}
std, pkg := buildImports(i.Options, gq, func(name string) bool {
for _, q := range gq {
if q.hasRetType() {
if q.Ret.EmitStruct() {
for _, f := range q.Ret.Struct.Fields {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
}
}
}
if hasPrefixIgnoringSliceAndPointerPrefix(q.Ret.Type(), name) {
return true
}
}
// Check the fields of the argument struct if it's emitted
if q.Arg.EmitStruct() {
for _, f := range q.Arg.Struct.Fields {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
}
}
}
// Check the argument pairs inside the method definition
for _, f := range q.Arg.Pairs() {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
}
}
}
return false
})
sliceScan := func() bool {
for _, q := range gq {
if q.hasRetType() {
if q.Ret.IsStruct() {
for _, f := range q.Ret.Struct.Fields {
if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" {
return true
}
for _, embed := range f.EmbedFields {
if strings.HasPrefix(embed.Type, "[]") && embed.Type != "[]byte" {
return true
}
}
}
} else {
if strings.HasPrefix(q.Ret.Type(), "[]") && q.Ret.Type() != "[]byte" {
return true
}
}
}
if !q.Arg.isEmpty() {
if q.Arg.IsStruct() {
for _, f := range q.Arg.Struct.Fields {
if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" && !f.HasSqlcSlice() {
return true
}
}
} else {
if strings.HasPrefix(q.Arg.Type(), "[]") && q.Arg.Type() != "[]byte" && !q.Arg.HasSqlcSlices() {
return true
}
}
}
}
return false
}
// Search for sqlc.slice() calls
sqlcSliceScan := func() bool {
for _, q := range gq {
if q.Arg.HasSqlcSlices() {
return true
}
}
return false
}
if anyNonCopyFrom {
std["context"] = struct{}{}
}
sqlpkg := parseDriver(i.Options.SqlPackage)
if sqlcSliceScan() && !sqlpkg.IsPGX() {
std["strings"] = struct{}{}
}
if sliceScan() && !sqlpkg.IsPGX() {
pkg[ImportSpec{Path: "github.com/lib/pq"}] = struct{}{}
}
if i.Options.WrapErrors {
std["fmt"] = struct{}{}
}
return sortedImports(std, pkg)
}
func (i *importer) copyfromImports() fileImports {
copyFromQueries := make([]Query, 0, len(i.Queries))
for _, q := range i.Queries {
if q.Cmd == metadata.CmdCopyFrom {
copyFromQueries = append(copyFromQueries, q)
}
}
std, pkg := buildImports(i.Options, copyFromQueries, func(name string) bool {
for _, q := range copyFromQueries {
if q.hasRetType() {
if strings.HasPrefix(q.Ret.Type(), name) {
return true
}
}
if !q.Arg.isEmpty() {
if strings.HasPrefix(q.Arg.Type(), name) {
return true
}
}
}
return false
})
std["context"] = struct{}{}
if i.Options.SqlDriver == opts.SQLDriverGoSQLDriverMySQL {
std["io"] = struct{}{}
std["fmt"] = struct{}{}
std["sync/atomic"] = struct{}{}
pkg[ImportSpec{Path: "github.com/go-sql-driver/mysql"}] = struct{}{}
pkg[ImportSpec{Path: "github.com/hexon/mysqltsv"}] = struct{}{}
}
return sortedImports(std, pkg)
}
func (i *importer) batchImports() fileImports {
batchQueries := make([]Query, 0, len(i.Queries))
for _, q := range i.Queries {
if usesBatch([]Query{q}) {
batchQueries = append(batchQueries, q)
}
}
std, pkg := buildImports(i.Options, batchQueries, func(name string) bool {
for _, q := range batchQueries {
if q.hasRetType() {
if q.Ret.EmitStruct() {
for _, f := range q.Ret.Struct.Fields {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
}
}
}
if hasPrefixIgnoringSliceAndPointerPrefix(q.Ret.Type(), name) {
return true
}
}
if q.Arg.EmitStruct() {
for _, f := range q.Arg.Struct.Fields {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
}
}
}
for _, f := range q.Arg.Pairs() {
if hasPrefixIgnoringSliceAndPointerPrefix(f.Type, name) {
return true
}
}
}
return false
})
std["context"] = struct{}{}
std["errors"] = struct{}{}
sqlpkg := parseDriver(i.Options.SqlPackage)
switch sqlpkg {
case opts.SQLDriverPGXV4:
pkg[ImportSpec{Path: "github.com/jackc/pgx/v4"}] = struct{}{}
case opts.SQLDriverPGXV5:
pkg[ImportSpec{Path: "github.com/jackc/pgx/v5"}] = struct{}{}
}
return sortedImports(std, pkg)
}
func trimSliceAndPointerPrefix(v string) string {
v = strings.TrimPrefix(v, "[]")
v = strings.TrimPrefix(v, "*")
return v
}
func hasPrefixIgnoringSliceAndPointerPrefix(s, prefix string) bool {
trimmedS := trimSliceAndPointerPrefix(s)
trimmedPrefix := trimSliceAndPointerPrefix(prefix)
return strings.HasPrefix(trimmedS, trimmedPrefix)
}
func replaceConflictedArg(imports [][]ImportSpec, queries []Query) []Query {
m := make(map[string]struct{})
for _, is := range imports {
for _, i := range is {
paths := strings.Split(i.Path, "/")
m[paths[len(paths)-1]] = struct{}{}
}
}
replacedQueries := make([]Query, 0, len(queries))
for _, query := range queries {
if _, exist := m[query.Arg.Name]; exist {
query.Arg.Name = toCamelCase(fmt.Sprintf("arg_%s", query.Arg.Name))
}
replacedQueries = append(replacedQueries, query)
}
return replacedQueries
}

View File

@@ -0,0 +1,144 @@
package golang
import (
"log"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
func mysqlType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
columnType := sdk.DataType(col.Type)
notNull := col.NotNull || col.IsArray
unsigned := col.Unsigned
switch columnType {
case "varchar", "text", "char", "tinytext", "mediumtext", "longtext":
if notNull {
return "string"
}
return "sql.NullString"
case "tinyint":
if col.Length == 1 {
if notNull {
return "bool"
}
return "sql.NullBool"
} else {
if notNull {
if unsigned {
return "uint8"
}
return "int8"
}
// The database/sql package does not have a sql.NullInt8 type, so we
// use the smallest type they have which is NullInt16
return "sql.NullInt16"
}
case "year":
if notNull {
return "int16"
}
return "sql.NullInt16"
case "smallint":
if notNull {
if unsigned {
return "uint16"
}
return "int16"
}
return "sql.NullInt16"
case "int", "integer", "mediumint":
if notNull {
if unsigned {
return "uint32"
}
return "int32"
}
return "sql.NullInt32"
case "bigint", "bigint unsigned", "bigint signed":
// "bigint unsigned" and "bigint signed" are MySQL CAST types
// Note: We use int64 for CAST AS UNSIGNED to match original behavior,
// even though uint64 would be more semantically correct.
// The Unsigned flag on columns (from table schema) still uses uint64.
if notNull {
if unsigned {
return "uint64"
}
return "int64"
}
return "sql.NullInt64"
case "blob", "binary", "varbinary", "tinyblob", "mediumblob", "longblob":
if notNull {
return "[]byte"
}
return "sql.NullString"
case "double", "double precision", "real", "float":
if notNull {
return "float64"
}
return "sql.NullFloat64"
case "decimal", "dec", "fixed":
if notNull {
return "string"
}
return "sql.NullString"
case "enum":
// TODO: Proper Enum support
return "string"
case "date", "timestamp", "datetime", "time":
if notNull {
return "time.Time"
}
return "sql.NullTime"
case "boolean", "bool":
if notNull {
return "bool"
}
return "sql.NullBool"
case "json":
return "json.RawMessage"
case "any":
return "any"
default:
for _, schema := range req.Catalog.Schemas {
for _, enum := range schema.Enums {
if enum.Name == columnType {
if notNull {
if schema.Name == req.Catalog.DefaultSchema {
return StructName(enum.Name, options)
}
return StructName(schema.Name+"_"+enum.Name, options)
} else {
if schema.Name == req.Catalog.DefaultSchema {
return "Null" + StructName(enum.Name, options)
}
return "Null" + StructName(schema.Name+"_"+enum.Name, options)
}
}
}
}
if debug.Active {
log.Printf("Unknown MySQL type: %s\n", columnType)
}
return "any"
}
}

View File

@@ -0,0 +1,64 @@
package opts
import "fmt"
type SQLDriver string
const (
SQLPackagePGXV4 string = "pgx/v4"
SQLPackagePGXV5 string = "pgx/v5"
SQLPackageStandard string = "database/sql"
)
var validPackages = map[string]struct{}{
string(SQLPackagePGXV4): {},
string(SQLPackagePGXV5): {},
string(SQLPackageStandard): {},
}
func validatePackage(sqlPackage string) error {
if _, found := validPackages[sqlPackage]; !found {
return fmt.Errorf("unknown SQL package: %s", sqlPackage)
}
return nil
}
const (
SQLDriverPGXV4 SQLDriver = "github.com/jackc/pgx/v4"
SQLDriverPGXV5 = "github.com/jackc/pgx/v5"
SQLDriverLibPQ = "github.com/lib/pq"
SQLDriverGoSQLDriverMySQL = "github.com/go-sql-driver/mysql"
)
var validDrivers = map[string]struct{}{
string(SQLDriverPGXV4): {},
string(SQLDriverPGXV5): {},
string(SQLDriverLibPQ): {},
string(SQLDriverGoSQLDriverMySQL): {},
}
func validateDriver(sqlDriver string) error {
if _, found := validDrivers[sqlDriver]; !found {
return fmt.Errorf("unknown SQL driver: %s", sqlDriver)
}
return nil
}
func (d SQLDriver) IsPGX() bool {
return d == SQLDriverPGXV4 || d == SQLDriverPGXV5
}
func (d SQLDriver) IsGoSQLDriverMySQL() bool {
return d == SQLDriverGoSQLDriverMySQL
}
func (d SQLDriver) Package() string {
switch d {
case SQLDriverPGXV4:
return SQLPackagePGXV4
case SQLDriverPGXV5:
return SQLPackagePGXV5
default:
return SQLPackageStandard
}
}

View File

@@ -0,0 +1,192 @@
package opts
import (
"encoding/json"
"fmt"
"go/types"
"regexp"
"strings"
"github.com/fatih/structtag"
)
type GoType struct {
Path string `json:"import" yaml:"import"`
Package string `json:"package" yaml:"package"`
Name string `json:"type" yaml:"type"`
Pointer bool `json:"pointer" yaml:"pointer"`
Slice bool `json:"slice" yaml:"slice"`
Spec string `json:"-"`
BuiltIn bool `json:"-"`
}
type ParsedGoType struct {
ImportPath string
Package string
TypeName string
BasicType bool
StructTag string
}
func (o *GoType) MarshalJSON() ([]byte, error) {
if o.Spec != "" {
return json.Marshal(o.Spec)
}
type alias GoType
return json.Marshal(alias(*o))
}
func (o *GoType) UnmarshalJSON(data []byte) error {
var spec string
if err := json.Unmarshal(data, &spec); err == nil {
*o = GoType{Spec: spec}
return nil
}
type alias GoType
var a alias
if err := json.Unmarshal(data, &a); err != nil {
return err
}
*o = GoType(a)
return nil
}
func (o *GoType) UnmarshalYAML(unmarshal func(any) error) error {
var spec string
if err := unmarshal(&spec); err == nil {
*o = GoType{Spec: spec}
return nil
}
type alias GoType
var a alias
if err := unmarshal(&a); err != nil {
return err
}
*o = GoType(a)
return nil
}
var validIdentifier = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
var versionNumber = regexp.MustCompile(`^v[0-9]+$`)
var invalidIdentifier = regexp.MustCompile(`[^a-zA-Z0-9_]`)
func generatePackageID(importPath string) (string, bool) {
parts := strings.Split(importPath, "/")
name := parts[len(parts)-1]
// If the last part of the import path is a valid identifier, assume that's the package name
if versionNumber.MatchString(name) && len(parts) >= 2 {
name = parts[len(parts)-2]
return invalidIdentifier.ReplaceAllString(strings.ToLower(name), "_"), true
}
if validIdentifier.MatchString(name) {
return name, false
}
return invalidIdentifier.ReplaceAllString(strings.ToLower(name), "_"), true
}
// validate GoType
func (gt GoType) parse() (*ParsedGoType, error) {
var o ParsedGoType
if gt.Spec == "" {
// TODO: Validation
if gt.Path == "" && gt.Package != "" {
return nil, fmt.Errorf("Package override `go_type`: package name requires an import path")
}
var pkg string
var pkgNeedsAlias bool
if gt.Package == "" && gt.Path != "" {
pkg, pkgNeedsAlias = generatePackageID(gt.Path)
if pkgNeedsAlias {
o.Package = pkg
}
} else {
pkg = gt.Package
o.Package = gt.Package
}
o.ImportPath = gt.Path
o.TypeName = gt.Name
o.BasicType = gt.Path == "" && gt.Package == ""
if pkg != "" {
o.TypeName = pkg + "." + o.TypeName
}
if gt.Pointer {
o.TypeName = "*" + o.TypeName
}
if gt.Slice {
o.TypeName = "[]" + o.TypeName
}
return &o, nil
}
input := gt.Spec
lastDot := strings.LastIndex(input, ".")
lastSlash := strings.LastIndex(input, "/")
typename := input
if lastDot == -1 && lastSlash == -1 {
// if the type name has no slash and no dot, validate that the type is a basic Go type
var found bool
for _, typ := range types.Typ {
info := typ.Info()
if info == 0 {
continue
}
if info&types.IsUntyped != 0 {
continue
}
if typename == typ.Name() {
found = true
}
}
if !found {
return nil, fmt.Errorf("Package override `go_type` specifier %q is not a Go basic type e.g. 'string'", input)
}
o.BasicType = true
} else {
// assume the type lives in a Go package
if lastDot == -1 {
return nil, fmt.Errorf("Package override `go_type` specifier %q is not the proper format, expected 'package.type', e.g. 'github.com/segmentio/ksuid.KSUID'", input)
}
typename = input[lastSlash+1:]
// a package name beginning with "go-" will give syntax errors in
// generated code. We should do the right thing and get the actual
// import name, but in lieu of that, stripping the leading "go-" may get
// us what we want.
typename = strings.TrimPrefix(typename, "go-")
typename = strings.TrimSuffix(typename, "-go")
o.ImportPath = input[:lastDot]
}
o.TypeName = typename
isPointer := input[0] == '*'
if isPointer {
o.ImportPath = o.ImportPath[1:]
o.TypeName = "*" + o.TypeName
}
return &o, nil
}
// GoStructTag is a raw Go struct tag.
type GoStructTag string
// Parse parses and validates a GoStructTag.
// The output is in a form convenient for codegen.
//
// Sample valid inputs/outputs:
//
// In Out
// empty string {}
// `a:"b"` {"a": "b"}
// `a:"b" x:"y,z"` {"a": "b", "x": "y,z"}
func (s GoStructTag) parse() (map[string]string, error) {
m := make(map[string]string)
tags, err := structtag.Parse(string(s))
if err != nil {
return nil, err
}
for _, tag := range tags.Tags() {
m[tag.Key] = tag.Value()
}
return m, nil
}

View File

@@ -0,0 +1,243 @@
package opts
import (
"encoding/json"
"fmt"
"maps"
"path/filepath"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
type Options struct {
EmitInterface bool `json:"emit_interface" yaml:"emit_interface"`
EmitJsonTags bool `json:"emit_json_tags" yaml:"emit_json_tags"`
JsonTagsIdUppercase bool `json:"json_tags_id_uppercase" yaml:"json_tags_id_uppercase"`
EmitDbTags bool `json:"emit_db_tags" yaml:"emit_db_tags"`
EmitPreparedQueries bool `json:"emit_prepared_queries" yaml:"emit_prepared_queries"`
EmitExactTableNames bool `json:"emit_exact_table_names,omitempty" yaml:"emit_exact_table_names"`
EmitEmptySlices bool `json:"emit_empty_slices,omitempty" yaml:"emit_empty_slices"`
EmitExportedQueries bool `json:"emit_exported_queries" yaml:"emit_exported_queries"`
EmitResultStructPointers bool `json:"emit_result_struct_pointers" yaml:"emit_result_struct_pointers"`
EmitParamsStructPointers bool `json:"emit_params_struct_pointers" yaml:"emit_params_struct_pointers"`
EmitMethodsWithDbArgument bool `json:"emit_methods_with_db_argument,omitempty" yaml:"emit_methods_with_db_argument"`
EmitPointersForNullTypes bool `json:"emit_pointers_for_null_types" yaml:"emit_pointers_for_null_types"`
// nil inherits EmitPointersForNullTypes; non-nil overrides for enums only.
EmitPointersForNullEnumTypes *bool `json:"emit_pointers_for_null_enum_types,omitempty" yaml:"emit_pointers_for_null_enum_types"`
EmitEnumValidMethod bool `json:"emit_enum_valid_method,omitempty" yaml:"emit_enum_valid_method"`
EmitAllEnumValues bool `json:"emit_all_enum_values,omitempty" yaml:"emit_all_enum_values"`
EmitSqlAsComment bool `json:"emit_sql_as_comment,omitempty" yaml:"emit_sql_as_comment"`
JsonTagsCaseStyle string `json:"json_tags_case_style,omitempty" yaml:"json_tags_case_style"`
Package string `json:"package" yaml:"package"`
Out string `json:"out" yaml:"out"`
Overrides []Override `json:"overrides,omitempty" yaml:"overrides"`
Rename map[string]string `json:"rename,omitempty" yaml:"rename"`
SqlPackage string `json:"sql_package" yaml:"sql_package"`
SqlDriver string `json:"sql_driver" yaml:"sql_driver"`
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"`
InflectionExcludeTableNames []string `json:"inflection_exclude_table_names,omitempty" yaml:"inflection_exclude_table_names"`
WrapErrors bool `json:"wrap_errors,omitempty" yaml:"wrap_errors"`
QueryParameterLimit *int32 `json:"query_parameter_limit,omitempty" yaml:"query_parameter_limit"`
OmitSqlcVersion bool `json:"omit_sqlc_version,omitempty" yaml:"omit_sqlc_version"`
OmitUnusedStructs bool `json:"omit_unused_structs,omitempty" yaml:"omit_unused_structs"`
BuildTags string `json:"build_tags,omitempty" yaml:"build_tags"`
Initialisms *[]string `json:"initialisms,omitempty" yaml:"initialisms"`
InitialismsMap map[string]struct{} `json:"-" yaml:"-"`
}
type GlobalOptions struct {
Overrides []Override `json:"overrides,omitempty" yaml:"overrides"`
Rename map[string]string `json:"rename,omitempty" yaml:"rename"`
}
func Parse(req *plugin.GenerateRequest) (*Options, error) {
options, err := parseOpts(req)
if err != nil {
return nil, err
}
global, err := parseGlobalOpts(req)
if err != nil {
return nil, err
}
if len(global.Overrides) > 0 {
options.Overrides = append(global.Overrides, options.Overrides...)
}
if len(global.Rename) > 0 {
if options.Rename == nil {
options.Rename = map[string]string{}
}
maps.Copy(options.Rename, global.Rename)
}
return options, nil
}
func parseOpts(req *plugin.GenerateRequest) (*Options, error) {
var options Options
if len(req.PluginOptions) == 0 {
return &options, nil
}
if err := json.Unmarshal(req.PluginOptions, &options); err != nil {
return nil, fmt.Errorf("unmarshalling plugin options: %w", err)
}
if options.Package == "" {
if options.Out != "" {
options.Package = filepath.Base(options.Out)
} else {
return nil, fmt.Errorf("invalid options: missing package name")
}
}
// 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
}
}
if options.SqlPackage != "" {
if err := validatePackage(options.SqlPackage); err != nil {
return nil, fmt.Errorf("invalid options: %s", err)
}
}
if options.SqlDriver != "" {
if err := validateDriver(options.SqlDriver); err != nil {
return nil, fmt.Errorf("invalid options: %s", err)
}
}
if options.QueryParameterLimit == nil {
options.QueryParameterLimit = new(int32)
*options.QueryParameterLimit = 1
}
if options.Initialisms == nil {
options.Initialisms = new([]string)
*options.Initialisms = []string{"id"}
}
options.InitialismsMap = map[string]struct{}{}
for _, initial := range *options.Initialisms {
options.InitialismsMap[initial] = struct{}{}
}
return &options, nil
}
func parseGlobalOpts(req *plugin.GenerateRequest) (*GlobalOptions, error) {
var options GlobalOptions
if len(req.GlobalOptions) == 0 {
return &options, nil
}
if err := json.Unmarshal(req.GlobalOptions, &options); err != nil {
return nil, fmt.Errorf("unmarshalling global options: %w", err)
}
for i := range options.Overrides {
if err := options.Overrides[i].parse(req); err != nil {
return nil, err
}
}
return &options, nil
}
func ValidateOpts(opts *Options) error {
if opts.EmitMethodsWithDbArgument && opts.EmitPreparedQueries {
return fmt.Errorf("invalid options: emit_methods_with_db_argument and emit_prepared_queries options are mutually exclusive")
}
if *opts.QueryParameterLimit < 0 {
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
}

View File

@@ -0,0 +1,176 @@
package opts
import (
"fmt"
"os"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/pattern"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
type Override struct {
// name of the golang type to use, e.g. `github.com/segmentio/ksuid.KSUID`
GoType GoType `json:"go_type" yaml:"go_type"`
// additional Go struct tags to add to this field, in raw Go struct tag form, e.g. `validate:"required" x:"y,z"`
// see https://github.com/sqlc-dev/sqlc/issues/534
GoStructTag GoStructTag `json:"go_struct_tag" yaml:"go_struct_tag"`
// fully qualified name of the Go type, e.g. `github.com/segmentio/ksuid.KSUID`
DBType string `json:"db_type" yaml:"db_type"`
Deprecated_PostgresType string `json:"postgres_type" yaml:"postgres_type"`
// for global overrides only when two different engines are in use
Engine string `json:"engine,omitempty" yaml:"engine"`
// True if the GoType should override if the matching type is nullable
Nullable bool `json:"nullable" yaml:"nullable"`
// True if the GoType should override if the matching type is unsiged.
Unsigned bool `json:"unsigned" yaml:"unsigned"`
// Deprecated. Use the `nullable` property instead
Deprecated_Null bool `json:"null" yaml:"null"`
// fully qualified name of the column, e.g. `accounts.id`
Column string `json:"column" yaml:"column"`
ColumnName *pattern.Match `json:"-"`
TableCatalog *pattern.Match `json:"-"`
TableSchema *pattern.Match `json:"-"`
TableRel *pattern.Match `json:"-"`
GoImportPath string `json:"-"`
GoPackage string `json:"-"`
GoTypeName string `json:"-"`
GoBasicType bool `json:"-"`
// Parsed form of GoStructTag, e.g. {"validate:", "required"}
GoStructTags map[string]string `json:"-"`
ShimOverride *ShimOverride `json:"-"`
}
func (o *Override) Matches(n *plugin.Identifier, defaultSchema string) bool {
if n == nil {
return false
}
schema := n.Schema
if n.Schema == "" {
schema = defaultSchema
}
if o.TableCatalog != nil && !o.TableCatalog.MatchString(n.Catalog) {
return false
}
if o.TableSchema == nil && schema != "" {
return false
}
if o.TableSchema != nil && !o.TableSchema.MatchString(schema) {
return false
}
if o.TableRel == nil && n.Name != "" {
return false
}
if o.TableRel != nil && !o.TableRel.MatchString(n.Name) {
return false
}
return true
}
func (o *Override) MatchesColumn(col *plugin.Column) bool {
columnType := sdk.DataType(col.Type)
notNull := col.NotNull || col.IsArray
return o.DBType != "" && o.DBType == columnType && o.Nullable != notNull && o.Unsigned == col.Unsigned
}
func (o *Override) parse(req *plugin.GenerateRequest) (err error) {
// validate deprecated postgres_type field
if o.Deprecated_PostgresType != "" {
fmt.Fprintf(os.Stderr, "WARNING: \"postgres_type\" is deprecated. Instead, use \"db_type\" to specify a type override.\n")
if o.DBType != "" {
return fmt.Errorf(`Type override configurations cannot have "db_type" and "postres_type" together. Use "db_type" alone`)
}
o.DBType = o.Deprecated_PostgresType
}
// validate deprecated null field
if o.Deprecated_Null {
fmt.Fprintf(os.Stderr, "WARNING: \"null\" is deprecated. Instead, use the \"nullable\" field.\n")
o.Nullable = true
}
schema := "public"
if req != nil && req.Catalog != nil {
schema = req.Catalog.DefaultSchema
}
// validate option combinations
switch {
case o.Column != "" && o.DBType != "":
return fmt.Errorf("Override specifying both `column` (%q) and `db_type` (%q) is not valid.", o.Column, o.DBType)
case o.Column == "" && o.DBType == "":
return fmt.Errorf("Override must specify one of either `column` or `db_type`")
}
// validate Column
if o.Column != "" {
colParts := strings.Split(o.Column, ".")
switch len(colParts) {
case 2:
if o.ColumnName, err = pattern.MatchCompile(colParts[1]); err != nil {
return err
}
if o.TableRel, err = pattern.MatchCompile(colParts[0]); err != nil {
return err
}
if o.TableSchema, err = pattern.MatchCompile(schema); err != nil {
return err
}
case 3:
if o.ColumnName, err = pattern.MatchCompile(colParts[2]); err != nil {
return err
}
if o.TableRel, err = pattern.MatchCompile(colParts[1]); err != nil {
return err
}
if o.TableSchema, err = pattern.MatchCompile(colParts[0]); err != nil {
return err
}
case 4:
if o.ColumnName, err = pattern.MatchCompile(colParts[3]); err != nil {
return err
}
if o.TableRel, err = pattern.MatchCompile(colParts[2]); err != nil {
return err
}
if o.TableSchema, err = pattern.MatchCompile(colParts[1]); err != nil {
return err
}
if o.TableCatalog, err = pattern.MatchCompile(colParts[0]); err != nil {
return err
}
default:
return fmt.Errorf("Override `column` specifier %q is not the proper format, expected '[catalog.][schema.]tablename.colname'", o.Column)
}
}
// validate GoType
parsed, err := o.GoType.parse()
if err != nil {
return err
}
o.GoImportPath = parsed.ImportPath
o.GoPackage = parsed.Package
o.GoTypeName = parsed.TypeName
o.GoBasicType = parsed.BasicType
// validate GoStructTag
tags, err := o.GoStructTag.parse()
if err != nil {
return err
}
o.GoStructTags = tags
o.ShimOverride = shimOverride(req, o)
return nil
}

View File

@@ -0,0 +1,117 @@
package opts
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestTypeOverrides(t *testing.T) {
for _, test := range []struct {
override Override
pkg string
typeName string
basic bool
}{
{
Override{
DBType: "uuid",
GoType: GoType{Spec: "github.com/segmentio/ksuid.KSUID"},
},
"github.com/segmentio/ksuid",
"ksuid.KSUID",
false,
},
// TODO: Add test for struct pointers
//
// {
// Override{
// DBType: "uuid",
// GoType: "github.com/segmentio/*ksuid.KSUID",
// },
// "github.com/segmentio/ksuid",
// "*ksuid.KSUID",
// false,
// },
{
Override{
DBType: "citext",
GoType: GoType{Spec: "string"},
},
"",
"string",
true,
},
{
Override{
DBType: "timestamp",
GoType: GoType{Spec: "time.Time"},
},
"time",
"time.Time",
false,
},
} {
tt := test
t.Run(tt.override.GoType.Spec, func(t *testing.T) {
if err := tt.override.parse(nil); err != nil {
t.Fatalf("override parsing failed; %s", err)
}
if diff := cmp.Diff(tt.pkg, tt.override.GoImportPath); diff != "" {
t.Errorf("package mismatch;\n%s", diff)
}
if diff := cmp.Diff(tt.typeName, tt.override.GoTypeName); diff != "" {
t.Errorf("type name mismatch;\n%s", diff)
}
if diff := cmp.Diff(tt.basic, tt.override.GoBasicType); diff != "" {
t.Errorf("basic mismatch;\n%s", diff)
}
})
}
for _, test := range []struct {
override Override
err string
}{
{
Override{
DBType: "uuid",
GoType: GoType{Spec: "Pointer"},
},
"Package override `go_type` specifier \"Pointer\" is not a Go basic type e.g. 'string'",
},
{
Override{
DBType: "uuid",
GoType: GoType{Spec: "untyped rune"},
},
"Package override `go_type` specifier \"untyped rune\" is not a Go basic type e.g. 'string'",
},
} {
tt := test
t.Run(tt.override.GoType.Spec, func(t *testing.T) {
err := tt.override.parse(nil)
if err == nil {
t.Fatalf("expected parse to fail; got nil")
}
if diff := cmp.Diff(tt.err, err.Error()); diff != "" {
t.Errorf("error mismatch;\n%s", diff)
}
})
}
}
func FuzzOverride(f *testing.F) {
for _, spec := range []string{
"string",
"github.com/gofrs/uuid.UUID",
"github.com/segmentio/ksuid.KSUID",
} {
f.Add(spec)
}
f.Fuzz(func(t *testing.T, s string) {
o := Override{
GoType: GoType{Spec: s},
}
o.parse(nil)
})
}

View File

@@ -0,0 +1,77 @@
package opts
import (
"strings"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
// The ShimOverride struct exists to bridge the gap between the Override struct
// and the previous Override struct defined in codegen.proto. Eventually these
// shim structs should be removed in favor of using the existing Override and
// GoType structs, but it's easier to provide these shim structs to not change
// the existing, working code.
type ShimOverride struct {
DbType string
Nullable bool
Column string
Table *plugin.Identifier
ColumnName string
Unsigned bool
GoType *ShimGoType
}
func shimOverride(req *plugin.GenerateRequest, o *Override) *ShimOverride {
var column string
var table plugin.Identifier
if o.Column != "" {
colParts := strings.Split(o.Column, ".")
switch len(colParts) {
case 2:
table.Schema = req.Catalog.DefaultSchema
table.Name = colParts[0]
column = colParts[1]
case 3:
table.Schema = colParts[0]
table.Name = colParts[1]
column = colParts[2]
case 4:
table.Catalog = colParts[0]
table.Schema = colParts[1]
table.Name = colParts[2]
column = colParts[3]
}
}
return &ShimOverride{
DbType: o.DBType,
Nullable: o.Nullable,
Unsigned: o.Unsigned,
Column: o.Column,
ColumnName: column,
Table: &table,
GoType: shimGoType(o),
}
}
type ShimGoType struct {
ImportPath string
Package string
TypeName string
BasicType bool
StructTags map[string]string
}
func shimGoType(o *Override) *ShimGoType {
// Note that there is a slight mismatch between this and the
// proto api. The GoType on the override is the unparsed type,
// which could be a qualified path or an object, as per
// https://docs.sqlc.dev/en/v1.18.0/reference/config.html#type-overriding
return &ShimGoType{
ImportPath: o.GoImportPath,
Package: o.GoPackage,
TypeName: o.GoTypeName,
BasicType: o.GoBasicType,
StructTags: o.GoStructTags,
}
}

View File

@@ -0,0 +1,619 @@
package golang
import (
"fmt"
"log"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
func parseIdentifierString(name string) (*plugin.Identifier, error) {
parts := strings.Split(name, ".")
switch len(parts) {
case 1:
return &plugin.Identifier{
Name: parts[0],
}, nil
case 2:
return &plugin.Identifier{
Schema: parts[0],
Name: parts[1],
}, nil
case 3:
return &plugin.Identifier{
Catalog: parts[0],
Schema: parts[1],
Name: parts[2],
}, nil
default:
return nil, fmt.Errorf("invalid name: %s", name)
}
}
func postgresType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
columnType := sdk.DataType(col.Type)
notNull := col.NotNull || col.IsArray
driver := parseDriver(options.SqlPackage)
emitPointersForNull := driver.IsPGX() && options.EmitPointersForNullTypes
emitPointersForNullEnums := emitPointersForNull
if options.EmitPointersForNullEnumTypes != nil {
emitPointersForNullEnums = driver.IsPGX() && *options.EmitPointersForNullEnumTypes
}
switch columnType {
case "serial", "serial4", "pg_catalog.serial4":
if notNull {
return "int32"
}
if emitPointersForNull {
return "*int32"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Int4"
}
return "sql.NullInt32"
case "bigserial", "serial8", "pg_catalog.serial8":
if notNull {
return "int64"
}
if emitPointersForNull {
return "*int64"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Int8"
}
return "sql.NullInt64"
case "smallserial", "serial2", "pg_catalog.serial2":
if notNull {
return "int16"
}
if emitPointersForNull {
return "*int16"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Int2"
}
return "sql.NullInt16"
case "integer", "int", "int4", "pg_catalog.int4":
if notNull {
return "int32"
}
if emitPointersForNull {
return "*int32"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Int4"
}
return "sql.NullInt32"
case "bigint", "int8", "pg_catalog.int8":
if notNull {
return "int64"
}
if emitPointersForNull {
return "*int64"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Int8"
}
return "sql.NullInt64"
case "smallint", "int2", "pg_catalog.int2":
if notNull {
return "int16"
}
if emitPointersForNull {
return "*int16"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Int2"
}
return "sql.NullInt16"
case "float", "double precision", "float8", "pg_catalog.float8":
if notNull {
return "float64"
}
if emitPointersForNull {
return "*float64"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Float8"
}
return "sql.NullFloat64"
case "real", "float4", "pg_catalog.float4":
if notNull {
return "float32"
}
if emitPointersForNull {
return "*float32"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Float4"
}
return "sql.NullFloat64" // TODO: Change to sql.NullFloat32 after updating the go.mod file
case "numeric", "pg_catalog.numeric", "money":
if driver.IsPGX() {
return "pgtype.Numeric"
}
// Since the Go standard library does not have a decimal type, lib/pq
// returns numerics as strings.
//
// https://github.com/lib/pq/issues/648
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
return "sql.NullString"
case "boolean", "bool", "pg_catalog.bool":
if notNull {
return "bool"
}
if emitPointersForNull {
return "*bool"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Bool"
}
return "sql.NullBool"
case "json", "pg_catalog.json":
switch driver {
case opts.SQLDriverPGXV5:
return "[]byte"
case opts.SQLDriverPGXV4:
return "pgtype.JSON"
case opts.SQLDriverLibPQ:
if notNull {
return "json.RawMessage"
} else {
return "pqtype.NullRawMessage"
}
default:
return "any"
}
case "jsonb", "pg_catalog.jsonb":
switch driver {
case opts.SQLDriverPGXV5:
return "[]byte"
case opts.SQLDriverPGXV4:
return "pgtype.JSONB"
case opts.SQLDriverLibPQ:
if notNull {
return "json.RawMessage"
} else {
return "pqtype.NullRawMessage"
}
default:
return "any"
}
case "bytea", "blob", "pg_catalog.bytea":
return "[]byte"
case "date":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Date"
}
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"
case "pg_catalog.time", "time", "time without time zone":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Time"
}
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"
case "pg_catalog.timetz", "timetz", "time with time zone":
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"
case "pg_catalog.timestamp", "timestamp", "timestamp without time zone":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Timestamp"
}
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"
case "pg_catalog.timestamptz", "timestamptz", "timestamp with time zone":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Timestamptz"
}
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"
case "text", "pg_catalog.varchar", "varchar", "character varying", "pg_catalog.bpchar", "bpchar", "character", "string", "citext", "name":
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Text"
}
return "sql.NullString"
case "uuid":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.UUID"
}
if notNull {
return "uuid.UUID"
}
if emitPointersForNull {
return "*uuid.UUID"
}
return "uuid.NullUUID"
case "inet":
switch driver {
case opts.SQLDriverPGXV5:
if notNull {
return "netip.Addr"
}
return "*netip.Addr"
case opts.SQLDriverPGXV4:
return "pgtype.Inet"
case opts.SQLDriverLibPQ:
return "pqtype.Inet"
default:
return "any"
}
case "cidr":
switch driver {
case opts.SQLDriverPGXV5:
if notNull {
return "netip.Prefix"
}
return "*netip.Prefix"
case opts.SQLDriverPGXV4:
return "pgtype.CIDR"
case opts.SQLDriverLibPQ:
return "pqtype.CIDR"
default:
return "any"
}
case "macaddr", "macaddr8":
switch driver {
case opts.SQLDriverPGXV5:
return "net.HardwareAddr"
case opts.SQLDriverPGXV4:
return "pgtype.Macaddr"
case opts.SQLDriverLibPQ:
return "pqtype.Macaddr"
default:
return "any"
}
case "ltree", "lquery", "ltxtquery":
// This module implements a data type ltree for representing labels
// of data stored in a hierarchical tree-like structure. Extensive
// facilities for searching through label trees are provided.
//
// https://www.postgresql.org/docs/current/ltree.html
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Text"
}
return "sql.NullString"
case "interval", "pg_catalog.interval":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Interval"
}
if notNull {
return "int64"
}
if emitPointersForNull {
return "*int64"
}
return "sql.NullInt64"
case "daterange":
switch driver {
case opts.SQLDriverPGXV4:
return "pgtype.Daterange"
case opts.SQLDriverPGXV5:
return "pgtype.Range[pgtype.Date]"
default:
return "any"
}
case "datemultirange":
switch driver {
case opts.SQLDriverPGXV5:
return "pgtype.Multirange[pgtype.Range[pgtype.Date]]"
default:
return "any"
}
case "tsrange":
switch driver {
case opts.SQLDriverPGXV4:
return "pgtype.Tsrange"
case opts.SQLDriverPGXV5:
return "pgtype.Range[pgtype.Timestamp]"
default:
return "any"
}
case "tsmultirange":
switch driver {
case opts.SQLDriverPGXV5:
return "pgtype.Multirange[pgtype.Range[pgtype.Timestamp]]"
default:
return "any"
}
case "tstzrange":
switch driver {
case opts.SQLDriverPGXV4:
return "pgtype.Tstzrange"
case opts.SQLDriverPGXV5:
return "pgtype.Range[pgtype.Timestamptz]"
default:
return "any"
}
case "tstzmultirange":
switch driver {
case opts.SQLDriverPGXV5:
return "pgtype.Multirange[pgtype.Range[pgtype.Timestamptz]]"
default:
return "any"
}
case "numrange":
switch driver {
case opts.SQLDriverPGXV4:
return "pgtype.Numrange"
case opts.SQLDriverPGXV5:
return "pgtype.Range[pgtype.Numeric]"
default:
return "any"
}
case "nummultirange":
switch driver {
case opts.SQLDriverPGXV5:
return "pgtype.Multirange[pgtype.Range[pgtype.Numeric]]"
default:
return "any"
}
case "int4range":
switch driver {
case opts.SQLDriverPGXV4:
return "pgtype.Int4range"
case opts.SQLDriverPGXV5:
return "pgtype.Range[pgtype.Int4]"
default:
return "any"
}
case "int4multirange":
switch driver {
case opts.SQLDriverPGXV5:
return "pgtype.Multirange[pgtype.Range[pgtype.Int4]]"
default:
return "any"
}
case "int8range":
switch driver {
case opts.SQLDriverPGXV4:
return "pgtype.Int8range"
case opts.SQLDriverPGXV5:
return "pgtype.Range[pgtype.Int8]"
default:
return "any"
}
case "int8multirange":
switch driver {
case opts.SQLDriverPGXV5:
return "pgtype.Multirange[pgtype.Range[pgtype.Int8]]"
default:
return "any"
}
case "hstore":
if driver.IsPGX() {
return "pgtype.Hstore"
}
return "any"
case "bit", "varbit", "bit varying", "pg_catalog.bit", "pg_catalog.varbit":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Bits"
}
if driver == opts.SQLDriverPGXV4 {
return "pgtype.Varbit"
}
case "cid":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Uint32"
}
if driver == opts.SQLDriverPGXV4 {
return "pgtype.CID"
}
case "oid":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Uint32"
}
if driver == opts.SQLDriverPGXV4 {
return "pgtype.OID"
}
case "tid":
if driver.IsPGX() {
return "pgtype.TID"
}
case "xid":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Uint32"
}
if driver == opts.SQLDriverPGXV4 {
return "pgtype.XID"
}
case "xid8":
if driver == opts.SQLDriverPGXV5 {
return "pgtype.Uint64"
}
case "box":
if driver.IsPGX() {
return "pgtype.Box"
}
case "circle":
if driver.IsPGX() {
return "pgtype.Circle"
}
case "line":
if driver.IsPGX() {
return "pgtype.Line"
}
case "lseg":
if driver.IsPGX() {
return "pgtype.Lseg"
}
case "path":
if driver.IsPGX() {
return "pgtype.Path"
}
case "point":
if driver.IsPGX() {
return "pgtype.Point"
}
case "polygon":
if driver.IsPGX() {
return "pgtype.Polygon"
}
case "vector":
if driver == opts.SQLDriverPGXV5 {
if emitPointersForNull {
return "*pgvector.Vector"
} else {
return "pgvector.Vector"
}
}
case "void":
// A void value can only be scanned into an empty interface.
return "any"
case "any":
return "any"
default:
rel, err := parseIdentifierString(columnType)
if err != nil {
// TODO: Should this actually return an error here?
return "any"
}
if rel.Schema == "" {
rel.Schema = req.Catalog.DefaultSchema
}
for _, schema := range req.Catalog.Schemas {
if schema.Name == "pg_catalog" || schema.Name == "information_schema" {
continue
}
for _, enum := range schema.Enums {
if rel.Name == enum.Name && rel.Schema == schema.Name {
if notNull {
if schema.Name == req.Catalog.DefaultSchema {
return StructName(enum.Name, options)
}
return StructName(schema.Name+"_"+enum.Name, options)
} else {
nullPrefix := "Null"
if emitPointersForNullEnums {
nullPrefix = "*"
}
if schema.Name == req.Catalog.DefaultSchema {
return nullPrefix + StructName(enum.Name, options)
}
return nullPrefix + StructName(schema.Name+"_"+enum.Name, options)
}
}
}
for _, ct := range schema.CompositeTypes {
if rel.Name == ct.Name && rel.Schema == schema.Name {
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
return "sql.NullString"
}
}
}
}
if debug.Active {
log.Printf("unknown PostgreSQL type: %s\n", columnType)
}
return "any"
}

View File

@@ -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
}
var prefix strings.Builder
rest := t
for {
if strings.HasPrefix(rest, "[]") {
prefix.WriteString("[]")
rest = rest[2:]
continue
}
if strings.HasPrefix(rest, "*") {
prefix.WriteString("*")
rest = rest[1:]
continue
}
break
}
if strings.HasPrefix(rest, qualifier) {
return prefix.String() + 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
}
var prefix strings.Builder
rest := t
for {
if strings.HasPrefix(rest, "[]") {
prefix.WriteString("[]")
rest = rest[2:]
continue
}
if strings.HasPrefix(rest, "*") {
prefix.WriteString("*")
rest = rest[1:]
continue
}
break
}
if _, ok := models[rest]; ok {
return prefix.String() + qualifier + rest
}
return t
}

View File

@@ -0,0 +1,308 @@
package golang
import (
"fmt"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
type QueryValue struct {
Emit bool
EmitPointer bool
Name string
DBName string // The name of the field in the database. Only set if Struct==nil.
Struct *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
}
func (v QueryValue) EmitStruct() bool {
return v.Emit
}
func (v QueryValue) IsStruct() bool {
return v.Struct != nil
}
func (v QueryValue) IsPointer() bool {
return v.EmitPointer && v.Struct != nil
}
func (v QueryValue) isEmpty() bool {
return v.Typ == "" && v.Name == "" && v.Struct == nil
}
type Argument struct {
Name string
Type string
}
func (v QueryValue) Pair() string {
var out []string
for _, arg := range v.Pairs() {
out = append(out, arg.Name+" "+arg.Type)
}
return strings.Join(out, ",")
}
// Return the argument name and type for query methods. Should only be used in
// the context of method arguments.
func (v QueryValue) Pairs() []Argument {
if v.isEmpty() {
return nil
}
if !v.EmitStruct() && v.IsStruct() {
var out []Argument
for _, f := range v.Struct.Fields {
out = append(out, Argument{
Name: escape(toLowerCase(f.Name)),
Type: f.Type,
})
}
return out
}
return []Argument{
{
Name: escape(v.Name),
Type: v.DefineType(),
},
}
}
func (v QueryValue) SlicePair() string {
if v.isEmpty() {
return ""
}
return v.Name + " []" + v.DefineType()
}
func (v QueryValue) Type() string {
if v.Typ != "" {
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)
}
func (v *QueryValue) DefineType() string {
t := v.Type()
if v.IsPointer() {
return "*" + t
}
return t
}
func (v *QueryValue) ReturnName() string {
if v.IsPointer() {
return "&" + escape(v.Name)
}
return escape(v.Name)
}
func (v QueryValue) UniqueFields() []Field {
seen := map[string]struct{}{}
fields := make([]Field, 0, len(v.Struct.Fields))
for _, field := range v.Struct.Fields {
if _, found := seen[field.Name]; found {
continue
}
seen[field.Name] = struct{}{}
fields = append(fields, field)
}
return fields
}
func (v QueryValue) Params() string {
if v.isEmpty() {
return ""
}
var out []string
if v.Struct == nil {
if !v.Column.IsSqlcSlice && strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" && !v.SQLDriver.IsPGX() {
out = append(out, "pq.Array("+escape(v.Name)+")")
} else {
out = append(out, escape(v.Name))
}
} else {
for _, f := range v.Struct.Fields {
if !f.HasSqlcSlice() && strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" && !v.SQLDriver.IsPGX() {
out = append(out, "pq.Array("+escape(v.VariableForField(f))+")")
} else {
out = append(out, escape(v.VariableForField(f)))
}
}
}
if len(out) <= 3 {
return strings.Join(out, ",")
}
out = append(out, "")
return "\n" + strings.Join(out, ",\n")
}
func (v QueryValue) ColumnNames() []string {
if v.Struct == nil {
return []string{v.DBName}
}
names := make([]string, len(v.Struct.Fields))
for i, f := range v.Struct.Fields {
names[i] = f.DBName
}
return names
}
func (v QueryValue) ColumnNamesAsGoSlice() string {
if v.Struct == nil {
return fmt.Sprintf("[]string{%q}", v.DBName)
}
escapedNames := make([]string, len(v.Struct.Fields))
for i, f := range v.Struct.Fields {
if f.Column != nil && f.Column.OriginalName != "" {
escapedNames[i] = fmt.Sprintf("%q", f.Column.OriginalName)
} else {
escapedNames[i] = fmt.Sprintf("%q", f.DBName)
}
}
return "[]string{" + strings.Join(escapedNames, ", ") + "}"
}
// When true, we have to build the arguments to q.db.QueryContext in addition to
// munging the SQL
func (v QueryValue) HasSqlcSlices() bool {
if v.Struct == nil {
return v.Column != nil && v.Column.IsSqlcSlice
}
for _, v := range v.Struct.Fields {
if v.Column.IsSqlcSlice {
return true
}
}
return false
}
func (v QueryValue) Scan() string {
var out []string
if v.Struct == nil {
if strings.HasPrefix(v.Typ, "[]") && v.Typ != "[]byte" && !v.SQLDriver.IsPGX() {
out = append(out, "pq.Array(&"+v.Name+")")
} else {
out = append(out, "&"+v.Name)
}
} else {
for _, f := range v.Struct.Fields {
// append any embedded fields
if len(f.EmbedFields) > 0 {
for _, embed := range f.EmbedFields {
if strings.HasPrefix(embed.Type, "[]") && embed.Type != "[]byte" && !v.SQLDriver.IsPGX() {
out = append(out, "pq.Array(&"+v.Name+"."+f.Name+"."+embed.Name+")")
} else {
out = append(out, "&"+v.Name+"."+f.Name+"."+embed.Name)
}
}
continue
}
if strings.HasPrefix(f.Type, "[]") && f.Type != "[]byte" && !v.SQLDriver.IsPGX() {
out = append(out, "pq.Array(&"+v.Name+"."+f.Name+")")
} else {
out = append(out, "&"+v.Name+"."+f.Name)
}
}
}
if len(out) <= 3 {
return strings.Join(out, ",")
}
out = append(out, "")
return "\n" + strings.Join(out, ",\n")
}
// Deprecated: This method does not respect the Emit field set on the
// QueryValue. It's used by the go-sql-driver-mysql/copyfromCopy.tmpl and should
// not be used other places.
func (v QueryValue) CopyFromMySQLFields() []Field {
// fmt.Printf("%#v\n", v)
if v.Struct != nil {
return v.Struct.Fields
}
return []Field{
{
Name: v.Name,
DBName: v.DBName,
Type: v.Typ,
},
}
}
func (v QueryValue) VariableForField(f Field) string {
if !v.IsStruct() {
return v.Name
}
if !v.EmitStruct() {
return toLowerCase(f.Name)
}
return v.Name + "." + f.Name
}
// A struct used to generate methods and fields on the Queries struct
type Query struct {
Cmd string
Comments []string
MethodName string
FieldName string
ConstantName string
SQL string
SourceName string
Ret QueryValue
Arg QueryValue
// Used for :copyfrom
Table *plugin.Identifier
}
func (q Query) hasRetType() bool {
scanned := q.Cmd == metadata.CmdOne || q.Cmd == metadata.CmdMany ||
q.Cmd == metadata.CmdBatchMany || q.Cmd == metadata.CmdBatchOne
return scanned && !q.Ret.isEmpty()
}
func (q Query) TableIdentifierAsGoSlice() string {
escapedNames := make([]string, 0, 3)
for _, p := range []string{q.Table.Catalog, q.Table.Schema, q.Table.Name} {
if p != "" {
escapedNames = append(escapedNames, fmt.Sprintf("%q", p))
}
}
return "[]string{" + strings.Join(escapedNames, ", ") + "}"
}
func (q Query) TableIdentifierForMySQL() string {
escapedNames := make([]string, 0, 3)
for _, p := range []string{q.Table.Catalog, q.Table.Schema, q.Table.Name} {
if p != "" {
escapedNames = append(escapedNames, fmt.Sprintf("`%s`", p))
}
}
return strings.Join(escapedNames, ".")
}

View File

@@ -0,0 +1,71 @@
package golang
func escape(s string) string {
if IsReserved(s) {
return s + "_"
}
return s
}
func IsReserved(s string) bool {
switch s {
// Not a keyword, but generated code refers to the predeclared type
// `any`, so a package-scope identifier with that name would shadow it.
case "any":
return true
case "break":
return true
case "default":
return true
case "func":
return true
case "interface":
return true
case "select":
return true
case "case":
return true
case "defer":
return true
case "go":
return true
case "map":
return true
case "struct":
return true
case "chan":
return true
case "else":
return true
case "goto":
return true
case "package":
return true
case "switch":
return true
case "const":
return true
case "fallthrough":
return true
case "if":
return true
case "range":
return true
case "type":
return true
case "continue":
return true
case "for":
return true
case "import":
return true
case "return":
return true
case "var":
return true
case "q":
return true
default:
return false
}
}

View File

@@ -0,0 +1,473 @@
package golang
import (
"bufio"
"fmt"
"sort"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/inflection"
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
func buildEnums(req *plugin.GenerateRequest, options *opts.Options) []Enum {
var enums []Enum
for _, schema := range req.Catalog.Schemas {
if schema.Name == "pg_catalog" || schema.Name == "information_schema" {
continue
}
for _, enum := range schema.Enums {
var enumName string
if schema.Name == req.Catalog.DefaultSchema {
enumName = enum.Name
} else {
enumName = schema.Name + "_" + enum.Name
}
e := Enum{
Name: StructName(enumName, options),
Comment: enum.Comment,
NameTags: map[string]string{},
ValidTags: map[string]string{},
}
if options.EmitJsonTags {
e.NameTags["json"] = JSONTagName(enumName, options)
e.ValidTags["json"] = JSONTagName("valid", options)
}
seen := make(map[string]struct{}, len(enum.Vals))
for i, v := range enum.Vals {
value := EnumReplace(v)
if _, found := seen[value]; found || value == "" {
value = fmt.Sprintf("value_%d", i)
}
e.Constants = append(e.Constants, Constant{
Name: StructName(enumName+"_"+value, options),
Value: v,
Type: e.Name,
})
seen[value] = struct{}{}
}
enums = append(enums, e)
}
}
if len(enums) > 0 {
sort.Slice(enums, func(i, j int) bool { return enums[i].Name < enums[j].Name })
}
return enums
}
func buildStructs(req *plugin.GenerateRequest, options *opts.Options) []Struct {
var structs []Struct
for _, schema := range req.Catalog.Schemas {
if schema.Name == "pg_catalog" || schema.Name == "information_schema" {
continue
}
for _, table := range schema.Tables {
var tableName string
if schema.Name == req.Catalog.DefaultSchema {
tableName = table.Rel.Name
} else {
tableName = schema.Name + "_" + table.Rel.Name
}
structName := tableName
if !options.EmitExactTableNames {
structName = inflection.Singular(inflection.SingularParams{
Name: structName,
Exclusions: options.InflectionExcludeTableNames,
})
}
s := 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{}
if options.EmitDbTags {
tags["db"] = column.Name
}
if options.EmitJsonTags {
tags["json"] = JSONTagName(column.Name, options)
}
addExtraGoStructTags(tags, req, options, column)
s.Fields = append(s.Fields, Field{
Name: StructName(column.Name, options),
Type: goType(req, options, column),
Tags: tags,
Comment: column.Comment,
})
}
structs = append(structs, s)
}
}
if len(structs) > 0 {
sort.Slice(structs, func(i, j int) bool { return structs[i].Name < structs[j].Name })
}
return structs
}
type goColumn struct {
id int
*plugin.Column
embed *goEmbed
}
type goEmbed struct {
modelType string
modelName string
fields []Field
}
// look through all the structs and attempt to find a matching one to embed
// We need the name of the struct and its field names.
func newGoEmbed(embed *plugin.Identifier, structs []Struct, defaultSchema string) *goEmbed {
if embed == nil {
return nil
}
for _, s := range structs {
embedSchema := defaultSchema
if embed.Schema != "" {
embedSchema = embed.Schema
}
// compare the other attributes
if embed.Catalog != s.Table.Catalog || embed.Name != s.Table.Name || embedSchema != s.Table.Schema {
continue
}
fields := make([]Field, len(s.Fields))
copy(fields, s.Fields)
return &goEmbed{
modelType: s.Name,
modelName: s.Name,
fields: fields,
}
}
return nil
}
func columnName(c *plugin.Column, pos int) string {
if c.Name != "" {
return c.Name
}
return fmt.Sprintf("column_%d", pos+1)
}
func paramName(p *plugin.Parameter) string {
if p.Column.Name != "" {
return argName(p.Column.Name)
}
return fmt.Sprintf("dollar_%d", p.Number)
}
func argName(name string) string {
var out strings.Builder
for i, p := range strings.Split(name, "_") {
if i == 0 {
out.WriteString(strings.ToLower(p))
} else if p == "id" {
out.WriteString("ID")
} else {
out.WriteString(strings.Title(p))
}
}
return out.String()
}
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 == "" {
continue
}
if query.Cmd == "" {
continue
}
var constantName string
if options.EmitExportedQueries {
constantName = sdk.Title(query.Name)
} else {
constantName = escape(sdk.LowerTitle(query.Name))
}
comments := query.Comments
if options.EmitSqlAsComment {
if len(comments) == 0 {
comments = append(comments, query.Name)
}
comments = append(comments, " ")
scanner := bufio.NewScanner(strings.NewReader(query.Text))
for scanner.Scan() {
line := scanner.Text()
comments = append(comments, " "+line)
}
if err := scanner.Err(); err != nil {
return nil, err
}
}
gq := Query{
Cmd: query.Cmd,
ConstantName: constantName,
FieldName: sdk.LowerTitle(query.Name) + "Stmt",
MethodName: query.Name,
SourceName: query.Filename,
SQL: query.Text,
Comments: comments,
Table: query.InsertIntoTable,
}
sqlpkg := parseDriver(options.SqlPackage)
qpl := int(*options.QueryParameterLimit)
if len(query.Params) == 1 && qpl != 0 {
p := query.Params[0]
gq.Arg = QueryValue{
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
for _, p := range query.Params {
cols = append(cols, goColumn{
id: int(p.Number),
Column: p.Column,
})
}
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,
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
// otherwise we end up with a copyfrom using a struct without the struct definition
if len(query.Params) <= qpl && query.Cmd != ":copyfrom" {
gq.Arg.Emit = false
}
}
if len(query.Columns) == 1 && query.Columns[0].EmbedTable == nil {
c := query.Columns[0]
name := columnName(c, 0)
name = strings.Replace(name, "$", "_", -1)
retName := escape(name)
// For :one queries the scan destination lives in the same scope as
// the query parameters, so reusing a parameter's name would cause
// Scan to overwrite the input and leak it back to the caller on
// sql.ErrNoRows (see sqlc-dev/sqlc#4354). Rename the return
// variable when it would collide.
if query.Cmd == metadata.CmdOne {
argNames := map[string]struct{}{}
for _, p := range gq.Arg.Pairs() {
argNames[p.Name] = struct{}{}
}
for {
if _, conflict := argNames[retName]; !conflict {
break
}
retName += "_2"
}
}
gq.Ret = QueryValue{
Name: retName,
DBName: name,
Typ: qualifyType(goType(req, options, c), models, qualifier),
SQLDriver: sqlpkg,
ModelQualifier: qualifier,
}
} else if putOutColumns(query) {
var gs *Struct
var emit bool
for _, s := range structs {
if len(s.Fields) != len(query.Columns) {
continue
}
same := true
for i, f := range s.Fields {
c := query.Columns[i]
sameName := f.Name == StructName(columnName(c, i), options)
sameType := f.Type == goType(req, options, c)
sameTable := sdk.SameTableName(c.Table, s.Table, req.Catalog.DefaultSchema)
if !sameName || !sameType || !sameTable {
same = false
}
}
if same {
gs = &s
break
}
}
if gs == nil {
var columns []goColumn
for i, c := range query.Columns {
columns = append(columns, goColumn{
id: i,
Column: c,
embed: newGoEmbed(c.EmbedTable, structs, req.Catalog.DefaultSchema),
})
}
var err error
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,
ModelQualifier: qualifier,
}
}
qs = append(qs, gq)
}
sort.Slice(qs, func(i, j int) bool { return qs[i].MethodName < qs[j].MethodName })
return qs, nil
}
var cmdReturnsData = map[string]struct{}{
metadata.CmdBatchMany: {},
metadata.CmdBatchOne: {},
metadata.CmdMany: {},
metadata.CmdOne: {},
}
func putOutColumns(query *plugin.Query) bool {
_, found := cmdReturnsData[query.Cmd]
return found
}
// It's possible that this method will generate duplicate JSON tag values
//
// Columns: count, count, count_2
// Fields: Count, Count_2, Count2
//
// 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, models modelTypeSet, qualifier string) (*Struct, error) {
gs := Struct{
Name: name,
}
seen := map[string][]int{}
suffixes := map[int]int{}
for i, c := range columns {
colName := columnName(c.Column, i)
tagName := colName
// override col/tag with expected model name
if c.embed != nil {
colName = c.embed.modelName
tagName = SetCaseStyle(colName, "snake")
}
fieldName := StructName(colName, options)
baseFieldName := fieldName
// Track suffixes by the ID of the column, so that columns referring to the same numbered parameter can be
// reused.
suffix := 0
if o, ok := suffixes[c.id]; ok && useID {
suffix = o
} else if v := len(seen[fieldName]); v > 0 && !c.IsNamedParam {
suffix = v + 1
}
suffixes[c.id] = suffix
if suffix > 0 {
tagName = fmt.Sprintf("%s_%d", tagName, suffix)
fieldName = fmt.Sprintf("%s_%d", fieldName, suffix)
}
tags := map[string]string{}
if options.EmitDbTags {
tags["db"] = tagName
}
if options.EmitJsonTags {
tags["json"] = JSONTagName(tagName, options)
}
addExtraGoStructTags(tags, req, options, c.Column)
f := Field{
Name: fieldName,
DBName: colName,
Tags: tags,
Column: c.Column,
}
if c.embed == nil {
f.Type = qualifyType(goType(req, options, c.Column), models, qualifier)
} else {
f.Type = qualifyType(c.embed.modelType, models, qualifier)
f.EmbedFields = c.embed.fields
}
gs.Fields = append(gs.Fields, f)
if _, found := seen[baseFieldName]; !found {
seen[baseFieldName] = []int{i}
} else {
seen[baseFieldName] = append(seen[baseFieldName], i)
}
}
// If a field does not have a known type, but another
// field with the same name has a known type, assign
// the known type to the field without a known type
for i, field := range gs.Fields {
if len(seen[field.Name]) > 1 && field.Type == "any" {
for _, j := range seen[field.Name] {
if i == j {
continue
}
otherField := gs.Fields[j]
if otherField.Type != field.Type {
field.Type = otherField.Type
}
gs.Fields[i] = field
}
}
}
err := checkIncompatibleFieldTypes(gs.Fields)
if err != nil {
return nil, err
}
return &gs, nil
}
func checkIncompatibleFieldTypes(fields []Field) error {
fieldTypes := map[string]string{}
for _, field := range fields {
if fieldType, found := fieldTypes[field.Name]; !found {
fieldTypes[field.Name] = field.Type
} else if field.Type != fieldType {
return fmt.Errorf("named param %s has incompatible types: %s, %s", field.Name, field.Type, fieldType)
}
}
return nil
}

View File

@@ -0,0 +1,78 @@
package golang
import (
"testing"
"github.com/sqlc-dev/sqlc/internal/metadata"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
func TestPutOutColumns_ForZeroColumns(t *testing.T) {
tests := []struct {
cmd string
want bool
}{
{
cmd: metadata.CmdExec,
want: false,
},
{
cmd: metadata.CmdExecResult,
want: false,
},
{
cmd: metadata.CmdExecRows,
want: false,
},
{
cmd: metadata.CmdExecLastId,
want: false,
},
{
cmd: metadata.CmdMany,
want: true,
},
{
cmd: metadata.CmdOne,
want: true,
},
{
cmd: metadata.CmdCopyFrom,
want: false,
},
{
cmd: metadata.CmdBatchExec,
want: false,
},
{
cmd: metadata.CmdBatchMany,
want: true,
},
{
cmd: metadata.CmdBatchOne,
want: true,
},
}
for _, tc := range tests {
t.Run(tc.cmd, func(t *testing.T) {
query := &plugin.Query{
Cmd: tc.cmd,
Columns: []*plugin.Column{},
}
got := putOutColumns(query)
if got != tc.want {
t.Errorf("putOutColumns failed. want %v, got %v", tc.want, got)
}
})
}
}
func TestPutOutColumns_AlwaysTrueWhenQueryHasColumns(t *testing.T) {
query := &plugin.Query{
Cmd: metadata.CmdMany,
Columns: []*plugin.Column{{}},
}
if putOutColumns(query) != true {
t.Error("should be true when we have columns")
}
}

View File

@@ -0,0 +1,102 @@
package golang
import (
"log"
"strings"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/codegen/sdk"
"github.com/sqlc-dev/sqlc/internal/debug"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
func sqliteType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.Column) string {
dt := strings.ToLower(sdk.DataType(col.Type))
notNull := col.NotNull || col.IsArray
emitPointersForNull := options.EmitPointersForNullTypes
switch dt {
case "int", "integer", "tinyint", "smallint", "mediumint", "bigint", "unsignedbigint", "int2", "int8":
if notNull {
return "int64"
}
if emitPointersForNull {
return "*int64"
}
return "sql.NullInt64"
case "blob":
return "[]byte"
case "real", "double", "doubleprecision", "float":
if notNull {
return "float64"
}
if emitPointersForNull {
return "*float64"
}
return "sql.NullFloat64"
case "boolean", "bool":
if notNull {
return "bool"
}
if emitPointersForNull {
return "*bool"
}
return "sql.NullBool"
case "date", "datetime", "timestamp":
if notNull {
return "time.Time"
}
if emitPointersForNull {
return "*time.Time"
}
return "sql.NullTime"
case "json", "jsonb":
return "json.RawMessage"
case "any":
return "any"
}
switch {
case strings.HasPrefix(dt, "character"),
strings.HasPrefix(dt, "varchar"),
strings.HasPrefix(dt, "varyingcharacter"),
strings.HasPrefix(dt, "nchar"),
strings.HasPrefix(dt, "nativecharacter"),
strings.HasPrefix(dt, "nvarchar"),
dt == "text",
dt == "clob":
if notNull {
return "string"
}
if emitPointersForNull {
return "*string"
}
return "sql.NullString"
case strings.HasPrefix(dt, "decimal"), dt == "numeric":
if notNull {
return "float64"
}
if emitPointersForNull {
return "*float64"
}
return "sql.NullFloat64"
default:
if debug.Active {
log.Printf("unknown SQLite type: %s\n", dt)
}
return "any"
}
}

View File

@@ -0,0 +1,53 @@
package golang
import (
"strings"
"unicode"
"unicode/utf8"
"github.com/sqlc-dev/sqlc/internal/codegen/golang/opts"
"github.com/sqlc-dev/sqlc/internal/plugin"
)
type Struct struct {
Table *plugin.Identifier
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 {
if rename := options.Rename[name]; rename != "" {
return rename
}
var out strings.Builder
name = strings.Map(func(r rune) rune {
if unicode.IsLetter(r) {
return r
}
if unicode.IsDigit(r) {
return r
}
return rune('_')
}, name)
for p := range strings.SplitSeq(name, "_") {
if _, found := options.InitialismsMap[p]; found {
out.WriteString(strings.ToUpper(p))
} else {
out.WriteString(strings.Title(p))
}
}
// If a name has a digit as its first char, prepand an underscore to make it a valid Go name.
r, _ := utf8.DecodeRuneInString(out.String())
if unicode.IsDigit(r) {
return "_" + out.String()
} else {
return out.String()
}
}

View File

@@ -0,0 +1,7 @@
package golang
import "embed"
//go:embed templates/*
//go:embed templates/*/*
var templates embed.FS

View File

@@ -0,0 +1,52 @@
{{define "copyfromCodeGoSqlDriver"}}
{{range .GoQueries}}
{{if eq .Cmd ":copyfrom" }}
var readerHandlerSequenceFor{{.MethodName}} uint32 = 1
func convertRowsFor{{.MethodName}}(w *io.PipeWriter, {{.Arg.SlicePair}}) {
e := mysqltsv.NewEncoder(w, {{ len .Arg.CopyFromMySQLFields }}, nil)
for _, row := range {{.Arg.Name}} {
{{- with $arg := .Arg }}
{{- range $arg.CopyFromMySQLFields}}
{{- if eq .Type "string"}}
e.AppendString({{if $arg.Struct}}row.{{.Name}}{{else}}row{{end}})
{{- else if or (eq .Type "[]byte") (eq .Type "json.RawMessage")}}
e.AppendBytes({{if $arg.Struct}}row.{{.Name}}{{else}}row{{end}})
{{- else}}
e.AppendValue({{if $arg.Struct}}row.{{.Name}}{{else}}row{{end}})
{{- end}}
{{- end}}
{{- end}}
}
w.CloseWithError(e.Close())
}
{{range .Comments}}//{{.}}
{{end -}}
// {{.MethodName}} uses MySQL's LOAD DATA LOCAL INFILE and is not atomic.
//
// Errors and duplicate keys are treated as warnings and insertion will
// continue, even without an error for some cases. Use this in a transaction
// and use SHOW WARNINGS to check for any problems and roll back if you want to.
//
// Check the documentation for more information:
// https://dev.mysql.com/doc/refman/8.0/en/load-data.html#load-data-error-handling
func (q *Queries) {{.MethodName}}(ctx context.Context{{if $.EmitMethodsWithDBArgument}}, db DBTX{{end}}, {{.Arg.SlicePair}}) (int64, error) {
pr, pw := io.Pipe()
defer pr.Close()
rh := fmt.Sprintf("{{.MethodName}}_%d", atomic.AddUint32(&readerHandlerSequenceFor{{.MethodName}}, 1))
mysql.RegisterReaderHandler(rh, func() io.Reader { return pr })
defer mysql.DeregisterReaderHandler(rh)
go convertRowsFor{{.MethodName}}(pw, {{.Arg.Name}})
// The string interpolation is necessary because LOAD DATA INFILE requires
// the file name to be given as a literal string.
result, err := {{if (not $.EmitMethodsWithDBArgument)}}q.{{end}}db.ExecContext(ctx, fmt.Sprintf("LOAD DATA LOCAL INFILE '%s' INTO TABLE {{.TableIdentifierForMySQL}} %s ({{range $index, $name := .Arg.ColumnNames}}{{if gt $index 0}}, {{end}}{{$name}}{{end}})", "Reader::" + rh, mysqltsv.Escaping))
if err != nil {
return 0, err
}
return result.RowsAffected()
}
{{end}}
{{end}}
{{end}}

View File

@@ -0,0 +1,134 @@
{{define "batchCodePgx"}}
var (
ErrBatchAlreadyClosed = errors.New("batch already closed")
)
{{range .GoQueries}}
{{if eq (hasPrefix .Cmd ":batch") true }}
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
{{escape .SQL}}
{{$.Q}}
type {{.MethodName}}BatchResults struct {
br pgx.BatchResults
tot int
closed bool
}
{{if .Arg.Struct}}
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
{{if .Ret.EmitStruct}}
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ if $.EmitMethodsWithDBArgument}}db DBTX,{{end}} {{.Arg.SlicePair}}) *{{.MethodName}}BatchResults {
batch := &pgx.Batch{}
for _, a := range {{index .Arg.Name}} {
vals := []any{
{{- if .Arg.Struct }}
{{- range .Arg.Struct.Fields }}
a.{{.Name}},
{{- end }}
{{- else }}
a,
{{- end }}
}
batch.Queue({{.ConstantName}}, vals...)
}
br := {{if not $.EmitMethodsWithDBArgument}}q.{{end}}db.SendBatch(ctx, batch)
return &{{.MethodName}}BatchResults{br,len({{.Arg.Name}}),false}
}
{{if eq .Cmd ":batchexec"}}
func (b *{{.MethodName}}BatchResults) Exec(f func(int, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
if b.closed {
if f != nil {
f(t, ErrBatchAlreadyClosed)
}
continue
}
_, err := b.br.Exec()
if f != nil {
f(t, err)
}
}
}
{{end}}
{{if eq .Cmd ":batchmany"}}
func (b *{{.MethodName}}BatchResults) Query(f func(int, []{{.Ret.DefineType}}, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
{{- if $.EmitEmptySlices}}
items := []{{.Ret.DefineType}}{}
{{else}}
var items []{{.Ret.DefineType}}
{{end -}}
if b.closed {
if f != nil {
f(t, items, ErrBatchAlreadyClosed)
}
continue
}
err := func() error {
rows, err := b.br.Query()
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var {{.Ret.Name}} {{.Ret.Type}}
if err := rows.Scan({{.Ret.Scan}}); err != nil {
return err
}
items = append(items, {{.Ret.ReturnName}})
}
return rows.Err()
}()
if f != nil {
f(t, items, err)
}
}
}
{{end}}
{{if eq .Cmd ":batchone"}}
func (b *{{.MethodName}}BatchResults) QueryRow(f func(int, {{.Ret.DefineType}}, error)) {
defer b.br.Close()
for t := 0; t < b.tot; t++ {
var {{.Ret.Name}} {{.Ret.Type}}
if b.closed {
if f != nil {
f(t, {{if .Ret.IsPointer}}nil{{else}}{{.Ret.Name}}{{end}}, ErrBatchAlreadyClosed)
}
continue
}
row := b.br.QueryRow()
err := row.Scan({{.Ret.Scan}})
if f != nil {
f(t, {{.Ret.ReturnName}}, err)
}
}
}
{{end}}
func (b *{{.MethodName}}BatchResults) Close() error {
b.closed = true
return b.br.Close()
}
{{end}}
{{end}}
{{end}}

View File

@@ -0,0 +1,51 @@
{{define "copyfromCodePgx"}}
{{range .GoQueries}}
{{if eq .Cmd ":copyfrom" }}
// iteratorFor{{.MethodName}} implements pgx.CopyFromSource.
type iteratorFor{{.MethodName}} struct {
rows []{{.Arg.DefineType}}
skippedFirstNextCall bool
}
func (r *iteratorFor{{.MethodName}}) Next() bool {
if len(r.rows) == 0 {
return false
}
if !r.skippedFirstNextCall {
r.skippedFirstNextCall = true
return true
}
r.rows = r.rows[1:]
return len(r.rows) > 0
}
func (r iteratorFor{{.MethodName}}) Values() ([]any, error) {
return []any{
{{- if .Arg.Struct }}
{{- range .Arg.Struct.Fields }}
r.rows[0].{{.Name}},
{{- end }}
{{- else }}
r.rows[0],
{{- end }}
}, nil
}
func (r iteratorFor{{.MethodName}}) Err() error {
return nil
}
{{range .Comments}}//{{.}}
{{end -}}
{{- if $.EmitMethodsWithDBArgument -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.SlicePair}}) (int64, error) {
return db.CopyFrom(ctx, {{.TableIdentifierAsGoSlice}}, {{.Arg.ColumnNamesAsGoSlice}}, &iteratorFor{{.MethodName}}{rows: {{.Arg.Name}}})
{{- else -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.SlicePair}}) (int64, error) {
return q.db.CopyFrom(ctx, {{.TableIdentifierAsGoSlice}}, {{.Arg.ColumnNamesAsGoSlice}}, &iteratorFor{{.MethodName}}{rows: {{.Arg.Name}}})
{{- end}}
}
{{end}}
{{end}}
{{end}}

View File

@@ -0,0 +1,37 @@
{{define "dbCodeTemplatePgx"}}
type DBTX interface {
Exec(context.Context, string, ...any) (pgconn.CommandTag, error)
Query(context.Context, string, ...any) (pgx.Rows, error)
QueryRow(context.Context, string, ...any) pgx.Row
{{- if .UsesCopyFrom }}
CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error)
{{- end }}
{{- if .UsesBatch }}
SendBatch(context.Context, *pgx.Batch) pgx.BatchResults
{{- end }}
}
{{ if .EmitMethodsWithDBArgument}}
func New() *Queries {
return &Queries{}
{{- else -}}
func New(db DBTX) *Queries {
return &Queries{db: db}
{{- end}}
}
type Queries struct {
{{if not .EmitMethodsWithDBArgument}}
db DBTX
{{end}}
}
{{if not .EmitMethodsWithDBArgument}}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}
{{end}}
{{end}}

View File

@@ -0,0 +1,73 @@
{{define "interfaceCodePgx"}}
type Querier interface {
{{- $dbtxParam := .EmitMethodsWithDBArgument -}}
{{- range .GoQueries}}
{{- if and (eq .Cmd ":one") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
{{- else if eq .Cmd ":one" }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
{{- end}}
{{- if and (eq .Cmd ":many") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
{{- else if eq .Cmd ":many" }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
{{- end}}
{{- if and (eq .Cmd ":exec") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error
{{- else if eq .Cmd ":exec" }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error
{{- end}}
{{- if and (eq .Cmd ":execrows") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error)
{{- else if eq .Cmd ":execrows" }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error)
{{- end}}
{{- if and (eq .Cmd ":execresult") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (pgconn.CommandTag, error)
{{- else if eq .Cmd ":execresult" }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (pgconn.CommandTag, error)
{{- end}}
{{- if and (eq .Cmd ":copyfrom") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.SlicePair}}) (int64, error)
{{- else if eq .Cmd ":copyfrom" }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.SlicePair}}) (int64, error)
{{- end}}
{{- if and (or (eq .Cmd ":batchexec") (eq .Cmd ":batchmany") (eq .Cmd ":batchone")) ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.SlicePair}}) *{{.MethodName}}BatchResults
{{- else if or (eq .Cmd ":batchexec") (eq .Cmd ":batchmany") (eq .Cmd ":batchone") }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.SlicePair}}) *{{.MethodName}}BatchResults
{{- end}}
{{- end}}
}
var _ Querier = (*Queries)(nil)
{{end}}

View File

@@ -0,0 +1,142 @@
{{define "queryCodePgx"}}
{{range .GoQueries}}
{{if $.OutputQuery .SourceName}}
{{if and (ne .Cmd ":copyfrom") (ne (hasPrefix .Cmd ":batch") true)}}
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
{{escape .SQL}}
{{$.Q}}
{{end}}
{{if ne (hasPrefix .Cmd ":batch") true}}
{{if .Arg.EmitStruct}}
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
{{if .Ret.EmitStruct}}
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
{{end}}
{{if eq .Cmd ":one"}}
{{range .Comments}}//{{.}}
{{end -}}
{{- if $.EmitMethodsWithDBArgument -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) {
row := db.QueryRow(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- else -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) {
row := q.db.QueryRow(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- end}}
{{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }}
var {{.Ret.Name}} {{.Ret.Type}}
{{- end}}
err := row.Scan({{.Ret.Scan}})
{{- if $.WrapErrors}}
if err != nil {
err = fmt.Errorf("query {{.MethodName}}: %w", err)
}
{{- end}}
return {{.Ret.ReturnName}}, err
}
{{end}}
{{if eq .Cmd ":many"}}
{{range .Comments}}//{{.}}
{{end -}}
{{- if $.EmitMethodsWithDBArgument -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {
rows, err := db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- else -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {
rows, err := q.db.Query(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- end}}
if err != nil {
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
defer rows.Close()
{{- if $.EmitEmptySlices}}
items := []{{.Ret.DefineType}}{}
{{else}}
var items []{{.Ret.DefineType}}
{{end -}}
for rows.Next() {
var {{.Ret.Name}} {{.Ret.Type}}
if err := rows.Scan({{.Ret.Scan}}); err != nil {
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
items = append(items, {{.Ret.ReturnName}})
}
if err := rows.Err(); err != nil {
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
return items, nil
}
{{end}}
{{if eq .Cmd ":exec"}}
{{range .Comments}}//{{.}}
{{end -}}
{{- if $.EmitMethodsWithDBArgument -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error {
_, err := db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- else -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error {
_, err := q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- end}}
{{- if $.WrapErrors }}
if err != nil {
return fmt.Errorf("query {{.MethodName}}: %w", err)
}
return nil
{{- else }}
return err
{{- end }}
}
{{end}}
{{if eq .Cmd ":execrows"}}
{{range .Comments}}//{{.}}
{{end -}}
{{if $.EmitMethodsWithDBArgument -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error) {
result, err := db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- else -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error) {
result, err := q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- end}}
if err != nil {
return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
return result.RowsAffected(), nil
}
{{end}}
{{if eq .Cmd ":execresult"}}
{{range .Comments}}//{{.}}
{{end -}}
{{- if $.EmitMethodsWithDBArgument -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (pgconn.CommandTag, error) {
{{queryRetval .}} db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- else -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (pgconn.CommandTag, error) {
{{queryRetval .}} q.db.Exec(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- end}}
{{- if $.WrapErrors}}
if err != nil {
err = fmt.Errorf("query {{.MethodName}}: %w", err)
}
return result, err
{{- end}}
}
{{end}}
{{end}}
{{end}}
{{end}}

View File

@@ -0,0 +1,105 @@
{{define "dbCodeTemplateStd"}}
type DBTX interface {
ExecContext(context.Context, string, ...any) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...any) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...any) *sql.Row
}
{{ if .EmitMethodsWithDBArgument}}
func New() *Queries {
return &Queries{}
{{- else -}}
func New(db DBTX) *Queries {
return &Queries{db: db}
{{- end}}
}
{{if .EmitPreparedQueries}}
func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
q := Queries{db: db}
var err error
{{- if eq (len .GoQueries) 0 }}
_ = err
{{- end }}
{{- range .GoQueries }}
if q.{{.FieldName}}, err = db.PrepareContext(ctx, {{.ConstantName}}); err != nil {
return nil, fmt.Errorf("error preparing query {{.MethodName}}: %w", err)
}
{{- end}}
return &q, nil
}
func (q *Queries) Close() error {
var err error
{{- range .GoQueries }}
if q.{{.FieldName}} != nil {
if cerr := q.{{.FieldName}}.Close(); cerr != nil {
err = fmt.Errorf("error closing {{.FieldName}}: %w", cerr)
}
}
{{- end}}
return err
}
func (q *Queries) exec(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (sql.Result, error) {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).ExecContext(ctx, args...)
case stmt != nil:
return stmt.ExecContext(ctx, args...)
default:
return q.db.ExecContext(ctx, query, args...)
}
}
func (q *Queries) query(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (*sql.Rows, error) {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).QueryContext(ctx, args...)
case stmt != nil:
return stmt.QueryContext(ctx, args...)
default:
return q.db.QueryContext(ctx, query, args...)
}
}
func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, args ...any) (*sql.Row) {
switch {
case stmt != nil && q.tx != nil:
return q.tx.StmtContext(ctx, stmt).QueryRowContext(ctx, args...)
case stmt != nil:
return stmt.QueryRowContext(ctx, args...)
default:
return q.db.QueryRowContext(ctx, query, args...)
}
}
{{end}}
type Queries struct {
{{- if not .EmitMethodsWithDBArgument}}
db DBTX
{{- end}}
{{- if .EmitPreparedQueries}}
tx *sql.Tx
{{- range .GoQueries}}
{{.FieldName}} *sql.Stmt
{{- end}}
{{- end}}
}
{{if not .EmitMethodsWithDBArgument}}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
{{- if .EmitPreparedQueries}}
tx: tx,
{{- range .GoQueries}}
{{.FieldName}}: q.{{.FieldName}},
{{- end}}
{{- end}}
}
}
{{end}}
{{end}}

View File

@@ -0,0 +1,63 @@
{{define "interfaceCodeStd"}}
type Querier interface {
{{- $dbtxParam := .EmitMethodsWithDBArgument -}}
{{- range .GoQueries}}
{{- if and (eq .Cmd ":one") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
{{- else if eq .Cmd ":one"}}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ({{.Ret.DefineType}}, error)
{{- end}}
{{- if and (eq .Cmd ":many") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
{{- else if eq .Cmd ":many"}}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error)
{{- end}}
{{- if and (eq .Cmd ":exec") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) error
{{- else if eq .Cmd ":exec"}}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) error
{{- end}}
{{- if and (eq .Cmd ":execrows") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error)
{{- else if eq .Cmd ":execrows"}}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error)
{{- end}}
{{- if and (eq .Cmd ":execlastid") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (int64, error)
{{- else if eq .Cmd ":execlastid"}}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (int64, error)
{{- end}}
{{- if and (eq .Cmd ":execresult") ($dbtxParam) }}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, db DBTX, {{.Arg.Pair}}) (sql.Result, error)
{{- else if eq .Cmd ":execresult"}}
{{range .Comments}}//{{.}}
{{end -}}
{{.MethodName}}(ctx context.Context, {{.Arg.Pair}}) (sql.Result, error)
{{- end}}
{{- end}}
}
var _ Querier = (*Queries)(nil)
{{end}}

View File

@@ -0,0 +1,171 @@
{{define "queryCodeStd"}}
{{range .GoQueries}}
{{if $.OutputQuery .SourceName}}
const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
{{escape .SQL}}
{{$.Q}}
{{if .Arg.EmitStruct}}
type {{.Arg.Type}} struct { {{- range .Arg.UniqueFields}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
{{if .Ret.EmitStruct}}
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
{{if eq .Cmd ":one"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ({{.Ret.DefineType}}, error) {
{{- template "queryCodeStdExec" . }}
{{- if or (ne .Arg.Pair .Ret.Pair) (ne .Arg.DefineType .Ret.DefineType) }}
var {{.Ret.Name}} {{.Ret.Type}}
{{- end}}
err := row.Scan({{.Ret.Scan}})
{{- if $.WrapErrors}}
if err != nil {
err = fmt.Errorf("query {{.MethodName}}: %w", err)
}
{{- end}}
return {{.Ret.ReturnName}}, err
}
{{end}}
{{if eq .Cmd ":many"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) ([]{{.Ret.DefineType}}, error) {
{{- template "queryCodeStdExec" . }}
if err != nil {
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
defer rows.Close()
{{- if $.EmitEmptySlices}}
items := []{{.Ret.DefineType}}{}
{{else}}
var items []{{.Ret.DefineType}}
{{end -}}
for rows.Next() {
var {{.Ret.Name}} {{.Ret.Type}}
if err := rows.Scan({{.Ret.Scan}}); err != nil {
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
items = append(items, {{.Ret.ReturnName}})
}
if err := rows.Close(); err != nil {
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
if err := rows.Err(); err != nil {
return nil, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
return items, nil
}
{{end}}
{{if eq .Cmd ":exec"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) error {
{{- template "queryCodeStdExec" . }}
{{- if $.WrapErrors}}
if err != nil {
err = fmt.Errorf("query {{.MethodName}}: %w", err)
}
{{- end}}
return err
}
{{end}}
{{if eq .Cmd ":execrows"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (int64, error) {
{{- template "queryCodeStdExec" . }}
if err != nil {
return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
return result.RowsAffected()
}
{{end}}
{{if eq .Cmd ":execlastid"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (int64, error) {
{{- template "queryCodeStdExec" . }}
if err != nil {
return 0, {{if $.WrapErrors}}fmt.Errorf("query {{.MethodName}}: %w", err){{else}}err{{end}}
}
return result.LastInsertId()
}
{{end}}
{{if eq .Cmd ":execresult"}}
{{range .Comments}}//{{.}}
{{end -}}
func (q *Queries) {{.MethodName}}(ctx context.Context, {{ dbarg }} {{.Arg.Pair}}) (sql.Result, error) {
{{- template "queryCodeStdExec" . }}
{{- if $.WrapErrors}}
if err != nil {
err = fmt.Errorf("query {{.MethodName}}: %w", err)
}
return result, err
{{- end}}
}
{{end}}
{{end}}
{{end}}
{{end}}
{{define "queryCodeStdExec"}}
{{- if .Arg.HasSqlcSlices }}
query := {{.ConstantName}}
var queryParams []any
{{- if .Arg.Struct }}
{{- $arg := .Arg }}
{{- range .Arg.Struct.Fields }}
{{- if .HasSqlcSlice }}
if len({{$arg.VariableForField .}}) > 0 {
for _, v := range {{$arg.VariableForField .}} {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", strings.Repeat(",?", len({{$arg.VariableForField .}}))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:{{.Column.Name}}*/?", "NULL", 1)
}
{{- else }}
queryParams = append(queryParams, {{$arg.VariableForField .}})
{{- end }}
{{- end }}
{{- else }}
{{- /* Single argument parameter to this goroutine (they are not packed
in a struct), because .Arg.HasSqlcSlices further up above was true,
this section is 100% a slice (impossible to get here otherwise).
*/}}
if len({{.Arg.Name}}) > 0 {
for _, v := range {{.Arg.Name}} {
queryParams = append(queryParams, v)
}
query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", strings.Repeat(",?", len({{.Arg.Name}}))[1:], 1)
} else {
query = strings.Replace(query, "/*SLICE:{{.Arg.Column.Name}}*/?", "NULL", 1)
}
{{- end }}
{{- if emitPreparedQueries }}
{{ queryRetval . }} {{ queryMethod . }}(ctx, nil, query, queryParams...)
{{- else}}
{{ queryRetval . }} {{ queryMethod . }}(ctx, query, queryParams...)
{{- end -}}
{{- else if emitPreparedQueries }}
{{- queryRetval . }} {{ queryMethod . }}(ctx, q.{{.FieldName}}, {{.ConstantName}}, {{.Arg.Params}})
{{- else}}
{{- queryRetval . }} {{ queryMethod . }}(ctx, {{.ConstantName}}, {{.Arg.Params}})
{{- end -}}
{{end}}

View File

@@ -0,0 +1,254 @@
{{define "dbFile"}}
{{if .BuildTags}}
//go:build {{.BuildTags}}
{{end}}// Code generated by sqlc. DO NOT EDIT.
{{if not .OmitSqlcVersion}}// versions:
// sqlc {{.SqlcVersion}}
{{end}}
package {{.Package}}
{{ if hasImports .SourceName }}
import (
{{range imports .SourceName}}
{{range .}}{{.}}
{{end}}
{{end}}
)
{{end}}
{{template "dbCode" . }}
{{end}}
{{define "dbCode"}}
{{if .SQLDriver.IsPGX }}
{{- template "dbCodeTemplatePgx" .}}
{{else}}
{{- template "dbCodeTemplateStd" .}}
{{end}}
{{end}}
{{define "interfaceFile"}}
{{if .BuildTags}}
//go:build {{.BuildTags}}
{{end}}// Code generated by sqlc. DO NOT EDIT.
{{if not .OmitSqlcVersion}}// versions:
// sqlc {{.SqlcVersion}}
{{end}}
package {{.Package}}
{{ if hasImports .SourceName }}
import (
{{range imports .SourceName}}
{{range .}}{{.}}
{{end}}
{{end}}
)
{{end}}
{{template "interfaceCode" . }}
{{end}}
{{define "interfaceCode"}}
{{if .SQLDriver.IsPGX }}
{{- template "interfaceCodePgx" .}}
{{else}}
{{- template "interfaceCodeStd" .}}
{{end}}
{{end}}
{{define "modelsFile"}}
{{if .BuildTags}}
//go:build {{.BuildTags}}
{{end}}// Code generated by sqlc. DO NOT EDIT.
{{if not .OmitSqlcVersion}}// versions:
// sqlc {{.SqlcVersion}}
{{end}}
package {{.ModelsPackage}}
{{ if hasImports .SourceName }}
import (
{{range imports .SourceName}}
{{range .}}{{.}}
{{end}}
{{end}}
)
{{end}}
{{template "modelsCode" . }}
{{end}}
{{define "modelsCode"}}
{{range .Enums}}
{{if .Comment}}{{comment .Comment}}{{end}}
type {{.Name}} string
const (
{{- range .Constants}}
{{.Name}} {{.Type}} = "{{.Value}}"
{{- end}}
)
func (e *{{.Name}}) Scan(src any) error {
switch s := src.(type) {
case []byte:
*e = {{.Name}}(s)
case string:
*e = {{.Name}}(s)
default:
return fmt.Errorf("unsupported scan type for {{.Name}}: %T", src)
}
return nil
}
type Null{{.Name}} struct {
{{.Name}} {{.Name}} {{if .NameTag}}{{$.Q}}{{.NameTag}}{{$.Q}}{{end}}
Valid bool {{if .ValidTag}}{{$.Q}}{{.ValidTag}}{{$.Q}}{{end}} // Valid is true if {{.Name}} is not NULL
}
// Scan implements the Scanner interface.
func (ns *Null{{.Name}}) Scan(value any) error {
if value == nil {
ns.{{.Name}}, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.{{.Name}}.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns Null{{.Name}}) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.{{.Name}}), nil
}
{{ if $.EmitEnumValidMethod }}
func (e {{.Name}}) Valid() bool {
switch e {
case {{ range $idx, $name := .Constants }}{{ if ne $idx 0 }},{{ "\n" }}{{ end }}{{ .Name }}{{ end }}:
return true
}
return false
}
{{ end }}
{{ if $.EmitAllEnumValues }}
func All{{ .Name }}Values() []{{ .Name }} {
return []{{ .Name }}{ {{ range .Constants}}{{ "\n" }}{{ .Name }},{{ end }}
}
}
{{ end }}
{{end}}
{{range .Structs}}
{{if .Comment}}{{comment .Comment}}{{end}}
type {{.Name}} struct { {{- range .Fields}}
{{- if .Comment}}
{{comment .Comment}}{{else}}
{{- end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
{{end}}
{{define "queryFile"}}
{{if .BuildTags}}
//go:build {{.BuildTags}}
{{end}}// Code generated by sqlc. DO NOT EDIT.
{{if not .OmitSqlcVersion}}// versions:
// sqlc {{.SqlcVersion}}
{{end}}// source: {{.SourceName}}
package {{.Package}}
{{ if hasImports .SourceName }}
import (
{{range imports .SourceName}}
{{range .}}{{.}}
{{end}}
{{end}}
)
{{end}}
{{template "queryCode" . }}
{{end}}
{{define "queryCode"}}
{{if .SQLDriver.IsPGX }}
{{- template "queryCodePgx" .}}
{{else}}
{{- template "queryCodeStd" .}}
{{end}}
{{end}}
{{define "copyfromFile"}}
{{if .BuildTags}}
//go:build {{.BuildTags}}
{{end}}// Code generated by sqlc. DO NOT EDIT.
{{if not .OmitSqlcVersion}}// versions:
// sqlc {{.SqlcVersion}}
{{end}}// source: {{.SourceName}}
package {{.Package}}
{{ if hasImports .SourceName }}
import (
{{range imports .SourceName}}
{{range .}}{{.}}
{{end}}
{{end}}
)
{{end}}
{{template "copyfromCode" . }}
{{end}}
{{define "copyfromCode"}}
{{if .SQLDriver.IsPGX }}
{{- template "copyfromCodePgx" .}}
{{else if .SQLDriver.IsGoSQLDriverMySQL }}
{{- template "copyfromCodeGoSqlDriver" .}}
{{end}}
{{end}}
{{define "batchFile"}}
{{if .BuildTags}}
//go:build {{.BuildTags}}
{{end}}// Code generated by sqlc. DO NOT EDIT.
{{if not .OmitSqlcVersion}}// versions:
// sqlc {{.SqlcVersion}}
{{end}}// source: {{.SourceName}}
package {{.Package}}
{{ if hasImports .SourceName }}
import (
{{range imports .SourceName}}
{{range .}}{{.}}
{{end}}
{{end}}
)
{{end}}
{{template "batchCode" . }}
{{end}}
{{define "batchCode"}}
{{if .SQLDriver.IsPGX }}
{{- template "batchCodePgx" .}}
{{end}}
{{end}}