Skip to content

Commit c3f1473

Browse files
inconshreveablekyleconroy
authored andcommitted
Implement per-column type overrides. (sqlc-dev#123)
* Implement per-column type overrides. Often a user may want to override the Go type used for model generation and query generation, but it's not a 1-to-1 mapping of SQL types to Go type. Instead, the override to use depends entirely on the column of a particular table. This commit adds the functionality to override types of generated columns in models and query parameters by adding a new 'overrides' configuration settings object on a per-package basis. This required tracking down a number of places where the column's Table FQN wasn't properly propagated through to the the model or query parameter Column. This change necessitated updating a bunch of tests to match. Lastly, I've added a test to prove the functionality works (in addition to testing it on a large code base) and added a documentation section in the README. * update docs
1 parent 7a0601e commit c3f1473

File tree

8 files changed

+227
-75
lines changed

8 files changed

+227
-75
lines changed

README.md

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,7 @@ instead.
324324
"packages": [...],
325325
"overrides": [
326326
{
327-
"go_type": "uuid.UUID",
328-
"package": "github.com/gofrs/uuid",
327+
"go_type": "github.com/gofrs/uuid.UUID",
329328
"postgres_type": "uuid"
330329
}
331330
]
@@ -336,12 +335,50 @@ Each override document has the following keys:
336335
- `postgres_type`:
337336
- The PostgreSQL type to override. Find the full list of supported types in [gen.go](https://github.com/kyleconroy/sqlc/blob/master/internal/dinosql/gen.go#L438).
338337
- `go_type`:
339-
- The Go type, with package name, to use in the generated code.
340-
- `package`:
341-
- The full import path for the package.
338+
- A fully qualified name to a Go type to use in the generated code.
342339
- `null`:
343340
- If true, use this type when a column in nullable. Defaults to `false`.
344341

342+
### Per-Column Type Overrides
343+
344+
Sometimes you would like to override the Go type used in model or query generation for
345+
a specific field of a table and not on a type basis as described in the previous section.
346+
347+
This may be configured by specifying the `column` property in the override definition. `column`
348+
should be of the form `table.column` buy you may be even more specify by specifying `schema.table.column`
349+
or `catalog.schema.table.column`.
350+
351+
```
352+
{
353+
"version": "1",
354+
"packages": [...],
355+
"overrides": [
356+
{
357+
"column": "authors.id",
358+
"go_type": "github.com/segmentio/ksuid.KSUID"
359+
}
360+
]
361+
}
362+
```
363+
364+
### Package Level Overrides
365+
366+
Overrides can be configured globally, as demonstrated in the previous sections, or they can be configured on a per-package which
367+
scopes the override behavior to just a single package:
368+
369+
```
370+
{
371+
"version": "1",
372+
"packages": [
373+
{
374+
...
375+
"overrides": [...]
376+
}
377+
],
378+
}
379+
```
380+
381+
345382
### Renaming Struct Fields
346383

347384
Struct field names are generated from column names using a simple algorithm:

internal/catalog/build.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ func Update(c *pg.Catalog, stmt nodes.Node) error {
151151
DataType: join(d.TypeName.Names, "."),
152152
NotNull: isNotNull(d),
153153
IsArray: isArray(d.TypeName),
154+
Table: fqn,
154155
})
155156

156157
case nodes.AT_AlterColumnType:
@@ -197,6 +198,7 @@ func Update(c *pg.Catalog, stmt nodes.Node) error {
197198
DataType: join(n.TypeName.Names, "."),
198199
NotNull: isNotNull(n),
199200
IsArray: isArray(n.TypeName),
201+
Table: fqn,
200202
})
201203
}
202204
}

internal/catalog/build_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ func TestUpdate(t *testing.T) {
105105
Tables: map[string]pg.Table{
106106
"foo": pg.Table{
107107
Name: "foo",
108-
Columns: []pg.Column{{Name: "bar", DataType: "text", NotNull: true}},
108+
Columns: []pg.Column{{Name: "bar", DataType: "text", NotNull: true, Table: pg.FQN{Schema: "public", Rel: "foo"}}},
109109
},
110110
},
111111
},
@@ -123,7 +123,7 @@ func TestUpdate(t *testing.T) {
123123
"foo": pg.Table{
124124
Name: "foo",
125125
Columns: []pg.Column{
126-
{Name: "bar", DataType: "text", IsArray: true, NotNull: true},
126+
{Name: "bar", DataType: "text", IsArray: true, NotNull: true, Table: pg.FQN{Schema: "public", Rel: "foo"}},
127127
},
128128
},
129129
},
@@ -142,7 +142,7 @@ func TestUpdate(t *testing.T) {
142142
Tables: map[string]pg.Table{
143143
"foo": pg.Table{
144144
Name: "foo",
145-
Columns: []pg.Column{{Name: "bar", DataType: "text"}},
145+
Columns: []pg.Column{{Name: "bar", DataType: "text", Table: pg.FQN{Schema: "public", Rel: "foo"}}},
146146
},
147147
},
148148
},
@@ -160,7 +160,7 @@ func TestUpdate(t *testing.T) {
160160
Tables: map[string]pg.Table{
161161
"foo": pg.Table{
162162
Name: "foo",
163-
Columns: []pg.Column{{Name: "bar", DataType: "text"}},
163+
Columns: []pg.Column{{Name: "bar", DataType: "text", Table: pg.FQN{Schema: "public", Rel: "foo"}}},
164164
},
165165
},
166166
},
@@ -178,7 +178,7 @@ func TestUpdate(t *testing.T) {
178178
Tables: map[string]pg.Table{
179179
"foo": pg.Table{
180180
Name: "foo",
181-
Columns: []pg.Column{{Name: "baz", DataType: "text"}},
181+
Columns: []pg.Column{{Name: "baz", DataType: "text", Table: pg.FQN{Schema: "public", Rel: "foo"}}},
182182
},
183183
},
184184
},
@@ -196,7 +196,7 @@ func TestUpdate(t *testing.T) {
196196
Tables: map[string]pg.Table{
197197
"foo": pg.Table{
198198
Name: "foo",
199-
Columns: []pg.Column{{Name: "bar", DataType: "bool"}},
199+
Columns: []pg.Column{{Name: "bar", DataType: "bool", Table: pg.FQN{Schema: "public", Rel: "foo"}}},
200200
},
201201
},
202202
},
@@ -335,7 +335,7 @@ func TestUpdate(t *testing.T) {
335335
"venues": pg.Table{
336336
Name: "venues",
337337
Columns: []pg.Column{
338-
{Name: "id", DataType: "serial", NotNull: true},
338+
{Name: "id", DataType: "serial", NotNull: true, Table: pg.FQN{Schema: "public", Rel: "venues"}},
339339
},
340340
},
341341
},

internal/dinosql/config.go

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,96 @@ package dinosql
33
import (
44
"encoding/json"
55
"errors"
6+
"fmt"
67
"io"
7-
)
8+
"strings"
89

9-
type PackageSettings struct {
10-
Name string `json:"name"`
11-
Path string `json:"path"`
12-
Schema string `json:"schema"`
13-
Queries string `json:"queries"`
14-
EmitPreparedQueries bool `json:"emit_prepared_queries"`
15-
EmitJSONTags bool `json:"emit_json_tags"`
16-
}
10+
"github.com/kyleconroy/sqlc/internal/pg"
11+
)
1712

1813
type GenerateSettings struct {
1914
Version string `json:"version"`
2015
Packages []PackageSettings `json:"packages"`
21-
Overrides []TypeOverride `json:"overrides,omitempty"`
16+
Overrides []Override `json:"overrides,omitempty"`
2217
Rename map[string]string `json:"rename,omitempty"`
2318
}
2419

20+
type PackageSettings struct {
21+
Name string `json:"name"`
22+
Path string `json:"path"`
23+
Schema string `json:"schema"`
24+
Queries string `json:"queries"`
25+
EmitPreparedQueries bool `json:"emit_prepared_queries"`
26+
EmitJSONTags bool `json:"emit_json_tags"`
27+
Overrides []Override `json:"overrides"`
28+
}
29+
30+
type Override struct {
31+
// name of the golang type to use, e.g. `github.com/segmentio/ksuid.KSUID`
32+
GoType string `json:"go_type"`
33+
34+
// fully qualified name of the Go type, e.g. `github.com/segmentio/ksuid.KSUID`
35+
PostgresType string `json:"postgres_type"`
36+
37+
// True if the GoType should override if the maching postgres type is nullable
38+
Null bool `json:"null"`
39+
40+
// fully qualified name of the column, e.g. `accounts.id`
41+
Column string `json:"column"`
42+
43+
columnName string
44+
table pg.FQN
45+
goTypeName string
46+
goPackage string
47+
}
48+
49+
func (o *Override) Parse() error {
50+
// validate option combinations
51+
switch {
52+
case o.Column != "" && o.PostgresType != "":
53+
return fmt.Errorf("Override specifying both `column` (%q) and `postgres_type` (%q) is not valid.", o.Column, o.PostgresType)
54+
case o.Column == "" && o.PostgresType == "":
55+
return fmt.Errorf("Override must specify one of either `column` or `postgres_type`")
56+
}
57+
58+
// validate Column
59+
if o.Column != "" {
60+
colParts := strings.Split(o.Column, ".")
61+
switch len(colParts) {
62+
case 2:
63+
o.columnName = colParts[1]
64+
o.table = pg.FQN{Schema: "public", Rel: colParts[0]}
65+
case 3:
66+
o.columnName = colParts[2]
67+
o.table = pg.FQN{Schema: colParts[0], Rel: colParts[1]}
68+
case 4:
69+
o.columnName = colParts[3]
70+
o.table = pg.FQN{Catalog: colParts[0], Schema: colParts[1], Rel: colParts[2]}
71+
default:
72+
return fmt.Errorf("Override `column` specifier %q is not the proper format, expected '[catalog.][schema.]colname.tablename'", o.Column)
73+
}
74+
}
75+
76+
// validate GoType
77+
lastDot := strings.LastIndex(o.GoType, ".")
78+
if lastDot == -1 {
79+
return fmt.Errorf("Package override `go_type` specificier %q is not the proper format, expected 'package.type', e.g. 'github.com/segmentio/ksuid.KSUID'", o.GoType)
80+
}
81+
lastSlash := strings.LastIndex(o.GoType, "/")
82+
if lastSlash == -1 {
83+
return fmt.Errorf("Package override `go_type` specificier %q is not the proper format, expected 'package.type', e.g. 'github.com/segmentio/ksuid.KSUID'", o.GoType)
84+
}
85+
o.goTypeName = o.GoType[lastSlash+1:]
86+
o.goPackage = o.GoType[:lastDot]
87+
isPointer := o.GoType[0] == '*'
88+
if isPointer {
89+
o.goPackage = o.goPackage[1:]
90+
o.goTypeName = "*" + o.goTypeName
91+
}
92+
93+
return nil
94+
}
95+
2596
var ErrMissingVersion = errors.New("no version number")
2697
var ErrUnknownVersion = errors.New("invalid version number")
2798
var ErrNoPackages = errors.New("no packages")
@@ -42,5 +113,17 @@ func ParseConfig(rd io.Reader) (GenerateSettings, error) {
42113
if len(config.Packages) == 0 {
43114
return config, ErrNoPackages
44115
}
116+
for i := range config.Overrides {
117+
if err := config.Overrides[i].Parse(); err != nil {
118+
return config, err
119+
}
120+
}
121+
for j := range config.Packages {
122+
for i := range config.Packages[j].Overrides {
123+
if err := config.Packages[j].Overrides[i].Parse(); err != nil {
124+
return config, err
125+
}
126+
}
127+
}
45128
return config, nil
46129
}

internal/dinosql/gen.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,8 @@ func (r Result) ModelImports() [][]string {
208208
// Custom imports
209209
var pkg []string
210210
overrideTypes := map[string]string{}
211-
for _, o := range r.Settings.Overrides {
212-
overrideTypes[o.GoType] = o.Package
211+
for _, o := range append(r.Settings.Overrides, r.packageSettings.Overrides...) {
212+
overrideTypes[o.goTypeName] = o.goPackage
213213
}
214214

215215
_, overrideNullTime := overrideTypes["pq.NullTime"]
@@ -321,8 +321,8 @@ func (r Result) QueryImports(filename string) [][]string {
321321

322322
var pkg []string
323323
overrideTypes := map[string]string{}
324-
for _, o := range r.Settings.Overrides {
325-
overrideTypes[o.GoType] = o.Package
324+
for _, o := range append(r.Settings.Overrides, r.packageSettings.Overrides...) {
325+
overrideTypes[o.goTypeName] = o.goPackage
326326
}
327327

328328
if sliceScan() {
@@ -432,17 +432,24 @@ func (r Result) Structs() []GoStruct {
432432
}
433433

434434
func (r Result) goType(col core.Column) string {
435-
typ := r.goInnerType(col.DataType, col.NotNull || col.IsArray)
435+
typ := r.goInnerType(col)
436436
if col.IsArray {
437437
return "[]" + typ
438438
}
439439
return typ
440440
}
441441

442-
func (r Result) goInnerType(columnType string, notNull bool) string {
443-
for _, oride := range r.Settings.Overrides {
442+
func (r Result) goInnerType(col core.Column) string {
443+
columnType := col.DataType
444+
notNull := col.NotNull || col.IsArray
445+
446+
// package overrides have a higher precedence
447+
for _, oride := range append(r.Settings.Overrides, r.packageSettings.Overrides...) {
444448
if oride.PostgresType == columnType && oride.Null != notNull {
445-
return oride.GoType
449+
return oride.goTypeName
450+
}
451+
if oride.columnName == col.Name && oride.table == col.Table {
452+
return oride.goTypeName
446453
}
447454
}
448455

@@ -964,6 +971,7 @@ func lowerTitle(s string) string {
964971
}
965972

966973
func Generate(r *Result, global GenerateSettings, settings PackageSettings) (map[string]string, error) {
974+
r.packageSettings = settings
967975
funcMap := template.FuncMap{
968976
"lowerTitle": lowerTitle,
969977
"imports": r.Imports(settings),

internal/dinosql/gen_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,30 @@ func TestColumnsToStruct(t *testing.T) {
3535
DataType: "bytea",
3636
NotNull: true,
3737
},
38+
{
39+
Name: "retyped",
40+
DataType: "text",
41+
NotNull: true,
42+
},
43+
}
44+
45+
// all of the columns are on the 'foo' table
46+
for i := range cols {
47+
cols[i].Table = pg.FQN{Schema: "public", Rel: "foo"}
3848
}
3949

4050
r := Result{}
51+
52+
// set up column-based override test
53+
o := Override{
54+
GoType: "example.com/pkg.CustomType",
55+
Column: "foo.retyped",
56+
}
57+
o.Parse()
58+
r.packageSettings = PackageSettings{
59+
Overrides: []Override{o},
60+
}
61+
4162
actual := r.columnsToStruct("Foo", cols)
4263
expected := &GoStruct{
4364
Name: "Foo",
@@ -47,6 +68,7 @@ func TestColumnsToStruct(t *testing.T) {
4768
{Name: "Count_2", Type: "int64", Tags: map[string]string{"json:": "count_2"}},
4869
{Name: "Tags", Type: "[]string", Tags: map[string]string{"json:": "tags"}},
4970
{Name: "ByteSeq", Type: "[]byte", Tags: map[string]string{"json:": "byte_seq"}},
71+
{Name: "Retyped", Type: "pkg.CustomType", Tags: map[string]string{"json:": "retyped"}},
5072
},
5173
}
5274
if diff := cmp.Diff(expected, actual); diff != "" {

0 commit comments

Comments
 (0)