Skip to content

Commit 304ba5d

Browse files
authored
add support for custom Go struct tags (#1569)
* internal/config: use strings.Trim{Prefix,Suffix} This is equivalent and slightly simpler. * Makefile: fix vtproto 'go install' command * internal/codegen/golang: simplify template tag condition Rather than modeling when .Tag will be empty, check directly whether .Tag is empty. This simplifies the template and reduces the number of places that must be touched when adding new sources of struct tags. * internal/codegen/golang: tweak tag formatting Rather than inserting the colon at tag construction time, insert it at tag formatting time. This makes the input look a bit more natural. This matters more, as we are about to add another, more distant, place where we insert tags. * all: add support for custom Go struct tags This change adds a new type of override: go_struct_tag. When provided for a field, it adds that struct tag to the generated code. The provided struct tag is parsed according to the standard package reflect rules, and its components are updated independently. This allows struct tag overrides to be compatible with (and optionally override) autogenerated json and db struct tags. Fixes #534 * go.mod: bump to Go 1.18 The code uses some 1.18-only features, like strings.Cut and testing.F. The CI requires Go 1.18. Since Go 1.18 is now required, reflect that in the go.mod.
1 parent 8c3d70b commit 304ba5d

36 files changed

+817
-272
lines changed

Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ mysqlsh:
3838
# $ protoc --version
3939
# libprotoc 3.19.1
4040
# $ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
41-
# $ go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto
41+
# $ go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@latest
4242
proto: internal/plugin/codegen.pb.go internal/python/ast/ast.pb.go
4343

4444
internal/plugin/codegen.pb.go: protos/plugin/codegen.proto

docs/howto/structs.md

+4
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ type Author struct {
4646
CreatedAt time.Time `json:"created_at"`
4747
}
4848
```
49+
50+
## More control
51+
52+
See the Type Overrides section of the Configuration File docs for fine-grained control over struct field types and tags.

docs/reference/config.md

+3
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,9 @@ Each override document has the following keys:
225225
- 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.
226226
- `go_type`:
227227
- A fully qualified name to a Go type to use in the generated code.
228+
- `go_struct_tag`:
229+
- A reflect-style struct tag to use in the generated code, e.g. `a:"b" x:"y,z"`.
230+
If you want general json/db tags for all fields, use `emit_db_tags` and/or `emit_json_tags` instead.
228231
- `nullable`:
229232
- If true, use this type when a column is nullable. Defaults to `false`.
230233

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/kyleconroy/sqlc
22

3-
go 1.17
3+
go 1.18
44

55
require (
66
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220209173558-ad29539cd2e9

internal/cmd/shim.go

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ func pluginGoType(o config.Override) *plugin.ParsedGoType {
109109
Package: o.GoPackage,
110110
TypeName: o.GoTypeName,
111111
BasicType: o.GoBasicType,
112+
StructTags: o.GoStructTags,
112113
}
113114
}
114115

internal/codegen/golang/field.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type Field struct {
1919
func (gf Field) Tag() string {
2020
tags := make([]string, 0, len(gf.Tags))
2121
for key, val := range gf.Tags {
22-
tags = append(tags, fmt.Sprintf("%s\"%s\"", key, val))
22+
tags = append(tags, fmt.Sprintf("%s:\"%s\"", key, val))
2323
}
2424
if len(tags) == 0 {
2525
return ""

internal/codegen/golang/go_type.go

+20
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ import (
55
"github.com/kyleconroy/sqlc/internal/plugin"
66
)
77

8+
func addExtraGoStructTags(tags map[string]string, req *plugin.CodeGenRequest, col *plugin.Column) {
9+
for _, oride := range req.Settings.Overrides {
10+
if oride.GoType.StructTags == nil {
11+
continue
12+
}
13+
if !sdk.Matches(oride, col.Table, req.Catalog.DefaultSchema) {
14+
// Different table.
15+
continue
16+
}
17+
if !sdk.MatchString(oride.ColumnName, col.Name) {
18+
// Different column.
19+
continue
20+
}
21+
// Add the extra tags.
22+
for k, v := range oride.GoType.StructTags {
23+
tags[k] = v
24+
}
25+
}
26+
}
27+
828
func goType(req *plugin.CodeGenRequest, col *plugin.Column) string {
929
// Check if the column's type has been overridden
1030
for _, oride := range req.Settings.Overrides {

internal/codegen/golang/result.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,12 @@ func buildStructs(req *plugin.CodeGenRequest) []Struct {
7474
for _, column := range table.Columns {
7575
tags := map[string]string{}
7676
if req.Settings.Go.EmitDbTags {
77-
tags["db:"] = column.Name
77+
tags["db"] = column.Name
7878
}
7979
if req.Settings.Go.EmitJsonTags {
80-
tags["json:"] = JSONTagName(column.Name, req.Settings)
80+
tags["json"] = JSONTagName(column.Name, req.Settings)
8181
}
82+
addExtraGoStructTags(tags, req, column)
8283
s.Fields = append(s.Fields, Field{
8384
Name: StructName(column.Name, req.Settings),
8485
Type: goType(req, column),
@@ -283,10 +284,10 @@ func columnsToStruct(req *plugin.CodeGenRequest, name string, columns []goColumn
283284
}
284285
tags := map[string]string{}
285286
if req.Settings.Go.EmitDbTags {
286-
tags["db:"] = tagName
287+
tags["db"] = tagName
287288
}
288289
if req.Settings.Go.EmitJsonTags {
289-
tags["json:"] = JSONTagName(tagName, req.Settings)
290+
tags["json"] = JSONTagName(tagName, req.Settings)
290291
}
291292
gs.Fields = append(gs.Fields, Field{
292293
Name: fieldName,

internal/codegen/golang/templates/pgx/batchCode.tmpl

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ type {{.MethodName}}BatchResults struct {
1212

1313
{{if .Arg.EmitStruct}}
1414
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
15-
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
15+
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
1616
{{- end}}
1717
}
1818
{{end}}
1919

2020
{{if .Ret.EmitStruct}}
2121
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
22-
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
22+
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
2323
{{- end}}
2424
}
2525
{{end}}

internal/codegen/golang/templates/pgx/queryCode.tmpl

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
1010
{{if ne (hasPrefix .Cmd ":batch") true}}
1111
{{if .Arg.EmitStruct}}
1212
type {{.Arg.Type}} struct { {{- range .Arg.Struct.Fields}}
13-
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
13+
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
1414
{{- end}}
1515
}
1616
{{end}}
1717

1818
{{if .Ret.EmitStruct}}
1919
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
20-
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
20+
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
2121
{{- end}}
2222
}
2323
{{end}}

internal/codegen/golang/templates/stdlib/queryCode.tmpl

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ const {{.ConstantName}} = {{$.Q}}-- name: {{.MethodName}} {{.Cmd}}
77

88
{{if .Arg.EmitStruct}}
99
type {{.Arg.Type}} struct { {{- range .Arg.UniqueFields}}
10-
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
10+
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
1111
{{- end}}
1212
}
1313
{{end}}
1414

1515
{{if .Ret.EmitStruct}}
1616
type {{.Ret.Type}} struct { {{- range .Ret.Struct.Fields}}
17-
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
17+
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
1818
{{- end}}
1919
}
2020
{{end}}

internal/codegen/golang/templates/template.tmpl

+1-1
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ type {{.Name}} struct { {{- range .Fields}}
111111
{{- if .Comment}}
112112
{{comment .Comment}}{{else}}
113113
{{- end}}
114-
{{.Name}} {{.Type}} {{if or ($.EmitJSONTags) ($.EmitDBTags)}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
114+
{{.Name}} {{.Type}} {{if .Tag}}{{$.Q}}{{.Tag}}{{$.Q}}{{end}}
115115
{{- end}}
116116
}
117117
{{end}}

internal/config/config.go

+14
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,10 @@ type Override struct {
167167
// name of the golang type to use, e.g. `github.com/segmentio/ksuid.KSUID`
168168
GoType GoType `json:"go_type" yaml:"go_type"`
169169

170+
// additional Go struct tags to add to this field, in raw Go struct tag form, e.g. `validate:"required" x:"y,z"`
171+
// see https://github.com/kyleconroy/sqlc/issues/534
172+
GoStructTag GoStructTag `json:"go_struct_tag" yaml:"go_struct_tag"`
173+
170174
// name of the python type to use, e.g. `mymodule.TypeName`
171175
PythonType PythonType `json:"python_type" yaml:"python_type"`
172176

@@ -193,6 +197,9 @@ type Override struct {
193197
GoPackage string
194198
GoTypeName string
195199
GoBasicType bool
200+
201+
// Parsed form of GoStructTag, e.g. {"validate:", "required"}
202+
GoStructTags map[string]string
196203
}
197204

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

315+
// validate GoStructTag
316+
tags, err := o.GoStructTag.Parse()
317+
if err != nil {
318+
return err
319+
}
320+
o.GoStructTags = tags
321+
308322
return nil
309323
}
310324

internal/config/go_type.go

+35-10
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type ParsedGoType struct {
2222
Package string
2323
TypeName string
2424
BasicType bool
25+
StructTag string
2526
}
2627

2728
func (o *GoType) UnmarshalJSON(data []byte) error {
@@ -138,16 +139,12 @@ func (gt GoType) Parse() (*ParsedGoType, error) {
138139
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)
139140
}
140141
typename = input[lastSlash+1:]
141-
if strings.HasPrefix(typename, "go-") {
142-
// a package name beginning with "go-" will give syntax errors in
143-
// generated code. We should do the right thing and get the actual
144-
// import name, but in lieu of that, stripping the leading "go-" may get
145-
// us what we want.
146-
typename = typename[len("go-"):]
147-
}
148-
if strings.HasSuffix(typename, "-go") {
149-
typename = typename[:len(typename)-len("-go")]
150-
}
142+
// a package name beginning with "go-" will give syntax errors in
143+
// generated code. We should do the right thing and get the actual
144+
// import name, but in lieu of that, stripping the leading "go-" may get
145+
// us what we want.
146+
typename = strings.TrimPrefix(typename, "go-")
147+
typename = strings.TrimSuffix(typename, "-go")
151148
o.ImportPath = input[:lastDot]
152149
}
153150
o.TypeName = typename
@@ -158,3 +155,31 @@ func (gt GoType) Parse() (*ParsedGoType, error) {
158155
}
159156
return &o, nil
160157
}
158+
159+
// GoStructTag is a raw Go struct tag.
160+
type GoStructTag string
161+
162+
// Parse parses and validates a GoStructTag.
163+
// The output is in a form convenient for codegen.
164+
//
165+
// Sample valid inputs/outputs:
166+
//
167+
// In Out
168+
// empty string {}
169+
// `a:"b"` {"a": "b"}
170+
// `a:"b" x:"y,z"` {"a": "b", "x": "y,z"}
171+
func (s GoStructTag) Parse() (map[string]string, error) {
172+
m := make(map[string]string)
173+
fields := strings.Fields(string(s))
174+
for _, f := range fields {
175+
k, v, ok := strings.Cut(f, ":")
176+
if !ok {
177+
return nil, fmt.Errorf("Failed to parse Go struct tag: no colon in field %q", f)
178+
}
179+
if len(v) < 2 || v[0] != '"' || v[len(v)-1] != '"' {
180+
return nil, fmt.Errorf("Failed to parse Go struct tag: missing quotes around value in field %q", f)
181+
}
182+
m[k] = v[1 : len(v)-1] // trim quotes off of v
183+
}
184+
return m, nil
185+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE TABLE foo (
2+
other text NOT NULL,
3+
tagged text NOT NULL
4+
);
5+
6+
CREATE TABLE bar (
7+
other text NOT NULL,
8+
also_tagged text NOT NULL
9+
);
10+
11+
CREATE TABLE baz (
12+
other text NOT NULL,
13+
also_tagged text NOT NULL
14+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"version": "1",
3+
"packages": [
4+
{
5+
"path": "go",
6+
"name": "override",
7+
"engine": "mysql",
8+
"schema": "schema.sql",
9+
"queries": "query.sql",
10+
"overrides": [
11+
{
12+
"go_struct_tag": "abc",
13+
"column": "foo.tagged"
14+
},
15+
{
16+
"go_struct_tag": "a:b",
17+
"column": "*.also_tagged"
18+
}
19+
]
20+
}
21+
]
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
error parsing sqlc.json: Failed to parse Go struct tag: no colon in field "abc"

internal/endtoend/testdata/overrides_go_struct_tags/mysql/go/db.go

+31
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/overrides_go_struct_tags/mysql/go/models.go

+22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE TABLE foo (
2+
other text NOT NULL,
3+
tagged text NOT NULL
4+
);
5+
6+
CREATE TABLE bar (
7+
other text NOT NULL,
8+
also_tagged text NOT NULL
9+
);
10+
11+
CREATE TABLE baz (
12+
other text NOT NULL,
13+
also_tagged text NOT NULL
14+
);

0 commit comments

Comments
 (0)