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
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:
64
internal/codegen/golang/opts/enum.go
Normal file
64
internal/codegen/golang/opts/enum.go
Normal 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
|
||||
}
|
||||
}
|
||||
192
internal/codegen/golang/opts/go_type.go
Normal file
192
internal/codegen/golang/opts/go_type.go
Normal 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
|
||||
}
|
||||
243
internal/codegen/golang/opts/options.go
Normal file
243
internal/codegen/golang/opts/options.go
Normal 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
|
||||
}
|
||||
176
internal/codegen/golang/opts/override.go
Normal file
176
internal/codegen/golang/opts/override.go
Normal 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
|
||||
}
|
||||
117
internal/codegen/golang/opts/override_test.go
Normal file
117
internal/codegen/golang/opts/override_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
77
internal/codegen/golang/opts/shim.go
Normal file
77
internal/codegen/golang/opts/shim.go
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user