Skip to content

add support for custom Go struct tags #1569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ mysqlsh:
# $ protoc --version
# libprotoc 3.19.1
# $ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# $ go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto
# $ go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@latest
proto: internal/plugin/codegen.pb.go internal/python/ast/ast.pb.go

internal/plugin/codegen.pb.go: protos/plugin/codegen.proto
Expand Down
4 changes: 4 additions & 0 deletions docs/howto/structs.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ type Author struct {
CreatedAt time.Time `json:"created_at"`
}
```

## More control

See the Type Overrides section of the Configuration File docs for fine-grained control over struct field types and tags.
3 changes: 3 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ Each override document has the following keys:
- The PostgreSQL or MySQL type to override. Find the full list of supported types in [postgresql_type.go](https://github.com/kyleconroy/sqlc/blob/main/internal/codegen/golang/postgresql_type.go#L12) or [mysql_type.go](https://github.com/kyleconroy/sqlc/blob/main/internal/codegen/golang/mysql_type.go#L12). Note that for Postgres you must use the pg_catalog prefixed names where available.
- `go_type`:
- A fully qualified name to a Go type to use in the generated code.
- `go_struct_tag`:
- A reflect-style struct tag to use in the generated code, e.g. `a:"b" x:"y,z"`.
If you want general json/db tags for all fields, use `emit_db_tags` and/or `emit_json_tags` instead.
- `nullable`:
- If true, use this type when a column is nullable. Defaults to `false`.

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/kyleconroy/sqlc

go 1.17
go 1.18

require (
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220209173558-ad29539cd2e9
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/shim.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func pluginGoType(o config.Override) *plugin.ParsedGoType {
Package: o.GoPackage,
TypeName: o.GoTypeName,
BasicType: o.GoBasicType,
StructTags: o.GoStructTags,
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/codegen/golang/field.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type Field struct {
func (gf Field) Tag() string {
tags := make([]string, 0, len(gf.Tags))
for key, val := range gf.Tags {
tags = append(tags, fmt.Sprintf("%s\"%s\"", key, val))
tags = append(tags, fmt.Sprintf("%s:\"%s\"", key, val))
}
if len(tags) == 0 {
return ""
Expand Down
20 changes: 20 additions & 0 deletions internal/codegen/golang/go_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ import (
"github.com/kyleconroy/sqlc/internal/plugin"
)

func addExtraGoStructTags(tags map[string]string, req *plugin.CodeGenRequest, col *plugin.Column) {
for _, oride := range req.Settings.Overrides {
if oride.GoType.StructTags == nil {
continue
}
if !sdk.Matches(oride, col.Table, req.Catalog.DefaultSchema) {
// Different table.
continue
}
if !sdk.MatchString(oride.ColumnName, col.Name) {
// Different column.
continue
}
// Add the extra tags.
for k, v := range oride.GoType.StructTags {
tags[k] = v
}
}
}

func goType(req *plugin.CodeGenRequest, col *plugin.Column) string {
// Check if the column's type has been overridden
for _, oride := range req.Settings.Overrides {
Expand Down
9 changes: 5 additions & 4 deletions internal/codegen/golang/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,12 @@ func buildStructs(req *plugin.CodeGenRequest) []Struct {
for _, column := range table.Columns {
tags := map[string]string{}
if req.Settings.Go.EmitDbTags {
tags["db:"] = column.Name
tags["db"] = column.Name
}
if req.Settings.Go.EmitJsonTags {
tags["json:"] = JSONTagName(column.Name, req.Settings)
tags["json"] = JSONTagName(column.Name, req.Settings)
}
addExtraGoStructTags(tags, req, column)
s.Fields = append(s.Fields, Field{
Name: StructName(column.Name, req.Settings),
Type: goType(req, column),
Expand Down Expand Up @@ -283,10 +284,10 @@ func columnsToStruct(req *plugin.CodeGenRequest, name string, columns []goColumn
}
tags := map[string]string{}
if req.Settings.Go.EmitDbTags {
tags["db:"] = tagName
tags["db"] = tagName
}
if req.Settings.Go.EmitJsonTags {
tags["json:"] = JSONTagName(tagName, req.Settings)
tags["json"] = JSONTagName(tagName, req.Settings)
}
gs.Fields = append(gs.Fields, Field{
Name: fieldName,
Expand Down
4 changes: 2 additions & 2 deletions internal/codegen/golang/templates/pgx/batchCode.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ type {{.MethodName}}BatchResults struct {

{{if .Arg.EmitStruct}}
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}

{{if .Ret.EmitStruct}}
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
Expand Down
4 changes: 2 additions & 2 deletions internal/codegen/golang/templates/pgx/queryCode.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
{{if ne (hasPrefix .Cmd ":batch") true}}
{{if .Arg.EmitStruct}}
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}

{{if .Ret.EmitStruct}}
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
Expand Down
4 changes: 2 additions & 2 deletions internal/codegen/golang/templates/stdlib/queryCode.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}

{{if .Arg.EmitStruct}}
type {{.Arg.Type}} struct { {{- range .Arg.UniqueFields}}
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}

{{if .Ret.EmitStruct}}
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
Expand Down
2 changes: 1 addition & 1 deletion internal/codegen/golang/templates/template.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ type {{.Name}} struct { {{- range .Fields}}
{{- if .Comment}}
{{comment .Comment}}{{else}}
{{- end}}
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
{{- end}}
}
{{end}}
Expand Down
14 changes: 14 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ 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/kyleconroy/sqlc/issues/534
GoStructTag GoStructTag `json:"go_struct_tag" yaml:"go_struct_tag"`

// name of the python type to use, e.g. `mymodule.TypeName`
PythonType PythonType `json:"python_type" yaml:"python_type"`

Expand All @@ -193,6 +197,9 @@ type Override struct {
GoPackage string
GoTypeName string
GoBasicType bool

// Parsed form of GoStructTag, e.g. {"validate:", "required"}
GoStructTags map[string]string
}

func (o *Override) Matches(n *ast.TableName, defaultSchema string) bool {
Expand Down Expand Up @@ -305,6 +312,13 @@ func (o *Override) Parse() (err error) {
o.GoTypeName = parsed.TypeName
o.GoBasicType = parsed.BasicType

// validate GoStructTag
tags, err := o.GoStructTag.Parse()
if err != nil {
return err
}
o.GoStructTags = tags

return nil
}

Expand Down
45 changes: 35 additions & 10 deletions internal/config/go_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ParsedGoType struct {
Package string
TypeName string
BasicType bool
StructTag string
}

func (o *GoType) UnmarshalJSON(data []byte) error {
Expand Down Expand Up @@ -138,16 +139,12 @@ func (gt GoType) Parse() (*ParsedGoType, error) {
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:]
if strings.HasPrefix(typename, "go-") {
// 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 = typename[len("go-"):]
}
if strings.HasSuffix(typename, "-go") {
typename = typename[:len(typename)-len("-go")]
}
// 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
Expand All @@ -158,3 +155,31 @@ func (gt GoType) Parse() (*ParsedGoType, error) {
}
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)
fields := strings.Fields(string(s))
for _, f := range fields {
k, v, ok := strings.Cut(f, ":")
if !ok {
return nil, fmt.Errorf("Failed to parse Go struct tag: no colon in field %q", f)
}
if len(v) < 2 || v[0] != '"' || v[len(v)-1] != '"' {
return nil, fmt.Errorf("Failed to parse Go struct tag: missing quotes around value in field %q", f)
}
m[k] = v[1 : len(v)-1] // trim quotes off of v
}
return m, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE TABLE foo (
other text NOT NULL,
tagged text NOT NULL
);

CREATE TABLE bar (
other text NOT NULL,
also_tagged text NOT NULL
);

CREATE TABLE baz (
other text NOT NULL,
also_tagged text NOT NULL
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"version": "1",
"packages": [
{
"path": "go",
"name": "override",
"engine": "mysql",
"schema": "schema.sql",
"queries": "query.sql",
"overrides": [
{
"go_struct_tag": "abc",
"column": "foo.tagged"
},
{
"go_struct_tag": "a:b",
"column": "*.also_tagged"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
error parsing sqlc.json: Failed to parse Go struct tag: no colon in field "abc"
31 changes: 31 additions & 0 deletions internal/endtoend/testdata/overrides_go_struct_tags/mysql/go/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE TABLE foo (
other text NOT NULL,
tagged text NOT NULL
);

CREATE TABLE bar (
other text NOT NULL,
also_tagged text NOT NULL
);

CREATE TABLE baz (
other text NOT NULL,
also_tagged text NOT NULL
);
Loading