-
Notifications
You must be signed in to change notification settings - Fork 27
/
templated_file.go
527 lines (491 loc) · 16 KB
/
templated_file.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
package golang
import (
"fmt"
"github.com/jschaf/pggen/internal/ast"
"github.com/jschaf/pggen/internal/codegen/golang/gotype"
"github.com/jschaf/pggen/internal/pginfer"
"strconv"
"strings"
)
// TemplatedPackage is all templated files in a pggen invocation. The templated
// files do not necessarily reside in the same directory.
type TemplatedPackage struct {
Files []TemplatedFile // sorted lexicographically by path
}
// TemplatedFile is the Go version of a SQL query file with all information
// needed to execute the codegen template.
type TemplatedFile struct {
Pkg TemplatedPackage // the parent package containing this file
PkgPath string // full package path, like "github.com/foo/bar"
GoPkg string // the name of the Go package to use for the "package foo" declaration
SourcePath string // absolute path to source SQL file
Queries []TemplatedQuery // the queries with all template information
Imports []string // Go imports
// True if this file is the leader file. The leader defines common code used
// by all queries in the same directory. Only one leader per directory.
IsLeader bool
// Any declarations this file should declare. Only set on leader.
Declarers []Declarer
}
// TemplatedQuery is a query with all information required to execute the
// codegen template.
type TemplatedQuery struct {
Name string // name of the query, from the comment preceding the query
SQLVarName string // name of the string variable containing the SQL
ResultKind ast.ResultKind // kind of result: :one, :many, or :exec
Doc string // doc from the source query file, formatted for Go
PreparedSQL string // SQL query, ready to run with PREPARE statement
Inputs []TemplatedParam // input parameters to the query
Outputs []TemplatedColumn // output columns of the query
InlineParamCount int // inclusive count of params that will be inlined
}
type TemplatedParam struct {
UpperName string // name of the param in UpperCamelCase, like 'FirstName' from pggen.arg('first_name')
LowerName string // name of the param in lowerCamelCase, like 'firstName' from pggen.arg('first_name')
QualType string // package-qualified Go type to use for this param
Type gotype.Type
RawName pginfer.InputParam
}
type TemplatedColumn struct {
PgName string // original name of the Postgres column
UpperName string // name in Go-style (UpperCamelCase) to use for the column
LowerName string // name in Go-style (lowerCamelCase)
Type gotype.Type
QualType string // package qualified Go type to use for the column, like "pgtype.Text"
}
func (tf TemplatedFile) needsPgconnImport() bool {
if tf.IsLeader {
// Leader files define genericConn.Exec which returns pgconn.CommandTag.
return true
}
for _, query := range tf.Queries {
if query.ResultKind == ast.ResultKindExec {
return true // :exec queries return pgconn.CommandTag
}
}
return false
}
// EmitPreparedSQL emits the prepared SQL query with appropriate quoting.
func (tq TemplatedQuery) EmitPreparedSQL() string {
if strings.ContainsRune(tq.PreparedSQL, '`') {
return strconv.Quote(tq.PreparedSQL)
}
return "`" + tq.PreparedSQL + "`"
}
// EmitParams emits the TemplatedQuery.Inputs into method parameters with both
// a name and type based on the number of params. For use in a method
// definition.
func (tq TemplatedQuery) EmitParams() string {
if !tq.isInlineParams() {
return ", params " + tq.Name + "Params"
}
sb := strings.Builder{}
for _, input := range tq.Inputs {
sb.WriteString(", ")
sb.WriteString(input.LowerName)
sb.WriteRune(' ')
sb.WriteString(input.QualType)
}
return sb.String()
}
// getLongestInput returns the length of the longest name and type name in all
// columns. Useful for struct definition alignment.
func getLongestInput(inputs []TemplatedParam) (int, int) {
nameLen := 0
for _, out := range inputs {
if len(out.UpperName) > nameLen {
nameLen = len(out.UpperName)
}
}
nameLen++ // 1 space to separate name from type
typeLen := 0
for _, out := range inputs {
if len(out.QualType) > typeLen {
typeLen = len(out.QualType)
}
}
typeLen++ // 1 space to separate type from struct tags.
return nameLen, typeLen
}
// EmitParamStruct emits the struct definition for query params if needed.
func (tq TemplatedQuery) EmitParamStruct() string {
if tq.isInlineParams() {
return ""
}
sb := &strings.Builder{}
sb.WriteString("\n\ntype ")
sb.WriteString(tq.Name)
sb.WriteString("Params struct {\n")
maxNameLen, maxTypeLen := getLongestInput(tq.Inputs)
for _, out := range tq.Inputs {
// Name
sb.WriteString("\t")
sb.WriteString(out.UpperName)
// Type
sb.WriteString(strings.Repeat(" ", maxNameLen-len(out.UpperName)))
sb.WriteString(out.QualType)
// JSON struct tag
sb.WriteString(strings.Repeat(" ", maxTypeLen-len(out.QualType)))
sb.WriteString("`json:")
sb.WriteString(strconv.Quote(out.RawName.PgName))
sb.WriteString("`")
sb.WriteRune('\n')
}
sb.WriteString("}")
return sb.String()
}
// EmitParamNames emits the TemplatedQuery.Inputs into comma separated names
// for use in a method invocation.
func (tq TemplatedQuery) EmitParamNames() string {
appendParam := func(sb *strings.Builder, typ gotype.Type, name string) {
switch typ := gotype.UnwrapNestedType(typ).(type) {
case *gotype.CompositeType:
sb.WriteString("q.types.")
sb.WriteString(NameCompositeInitFunc(typ))
sb.WriteString("(")
sb.WriteString(name)
sb.WriteString(")")
case *gotype.ArrayType:
if gotype.IsPgxSupportedArray(typ) {
sb.WriteString(name)
break
}
switch gotype.UnwrapNestedType(typ.Elem).(type) {
case *gotype.CompositeType, *gotype.EnumType:
sb.WriteString("q.types.")
sb.WriteString(NameArrayInitFunc(typ))
sb.WriteString("(")
sb.WriteString(name)
sb.WriteString(")")
default:
sb.WriteString(name)
}
default:
sb.WriteString(name)
}
}
switch {
case tq.isInlineParams():
sb := &strings.Builder{}
for _, input := range tq.Inputs {
sb.WriteString(", ")
appendParam(sb, input.Type, input.LowerName)
}
return sb.String()
default:
sb := &strings.Builder{}
for _, input := range tq.Inputs {
sb.WriteString(", ")
appendParam(sb, input.Type, "params."+input.UpperName)
}
return sb.String()
}
}
func (tq TemplatedQuery) isInlineParams() bool {
return len(tq.Inputs) <= tq.InlineParamCount
}
// EmitRowScanArgs emits the args to scan a single row from a pgx.Row or
// pgx.Rows.
func (tq TemplatedQuery) EmitRowScanArgs() (string, error) {
switch tq.ResultKind {
case ast.ResultKindExec:
return "", fmt.Errorf("cannot EmitRowScanArgs for :exec query %s", tq.Name)
case ast.ResultKindMany, ast.ResultKindOne:
break // okay
default:
return "", fmt.Errorf("unhandled EmitRowScanArgs type: %s", tq.ResultKind)
}
hasOnlyOneNonVoid := len(removeVoidColumns(tq.Outputs)) == 1
sb := strings.Builder{}
sb.Grow(15 * len(tq.Outputs))
for i, out := range tq.Outputs {
switch typ := gotype.UnwrapNestedType(out.Type).(type) {
case *gotype.ArrayType:
switch gotype.UnwrapNestedType(typ.Elem).(type) {
case *gotype.EnumType, *gotype.CompositeType:
sb.WriteString(out.LowerName)
sb.WriteString("Array")
default:
if hasOnlyOneNonVoid {
sb.WriteString("&item")
} else {
sb.WriteString("&item.")
sb.WriteString(out.UpperName)
}
}
case *gotype.CompositeType:
sb.WriteString(out.LowerName)
sb.WriteString("Row")
case *gotype.EnumType, *gotype.OpaqueType:
if hasOnlyOneNonVoid {
sb.WriteString("&item")
} else {
sb.WriteString("&item.")
sb.WriteString(out.UpperName)
}
case *gotype.VoidType:
sb.WriteString("nil")
default:
return "", fmt.Errorf("unhandled type to emit row scan: %s %T", typ.BaseName(), typ)
}
if i < len(tq.Outputs)-1 {
sb.WriteString(", ")
}
}
return sb.String(), nil
}
// EmitResultType returns the string representing the overall query result type,
// meaning the return result.
func (tq TemplatedQuery) EmitResultType() (string, error) {
outs := removeVoidColumns(tq.Outputs)
switch tq.ResultKind {
case ast.ResultKindExec:
return "pgconn.CommandTag", nil
case ast.ResultKindMany:
switch len(outs) {
case 0:
return "pgconn.CommandTag", nil
case 1:
return "[]" + outs[0].QualType, nil
default:
return "[]" + tq.Name + "Row", nil
}
case ast.ResultKindOne:
switch len(outs) {
case 0:
return "pgconn.CommandTag", nil
case 1:
return outs[0].QualType, nil
default:
return tq.Name + "Row", nil
}
default:
return "", fmt.Errorf("unhandled EmitResultType kind: %s", tq.ResultKind)
}
}
// EmitResultTypeInit returns the initialization code for the result type with
// name, typically "item" or "items". For array types, we take care to not use a
// var declaration so that JSON serialization returns an empty array instead of
// null.
func (tq TemplatedQuery) EmitResultTypeInit(name string) (string, error) {
switch tq.ResultKind {
case ast.ResultKindOne:
result, err := tq.EmitResultType()
if err != nil {
return "", fmt.Errorf("create result type for EmitResultTypeInit: %w", err)
}
isArr := strings.HasPrefix(result, "[]")
if isArr {
return name + " := " + result + "{}", nil
}
return "var " + name + " " + result, nil
case ast.ResultKindMany:
result, err := tq.EmitResultType()
if err != nil {
return "", fmt.Errorf("create result type for EmitResultTypeInit: %w", err)
}
isArr := strings.HasPrefix(result, "[]")
if isArr {
return name + " := " + result + "{}", nil
}
// Remove pointer. Return the right type by adding an address operator, "&",
// where needed.
result = strings.TrimPrefix(result, "*")
return "var " + name + " " + result, nil
default:
return "", fmt.Errorf("unhandled EmitResultTypeInit for kind %s", tq.ResultKind)
}
}
// EmitResultDecoders declares all initialization required for output types.
func (tq TemplatedQuery) EmitResultDecoders() (string, error) {
sb := &strings.Builder{}
const indent = "\n\t" // 1 level indent inside querier method
for _, out := range tq.Outputs {
switch typ := gotype.UnwrapNestedType(out.Type).(type) {
case *gotype.CompositeType:
sb.WriteString(indent)
sb.WriteString(out.LowerName)
sb.WriteString("Row := q.types.")
sb.WriteString(NameCompositeTranscoderFunc(typ))
sb.WriteString("()")
case *gotype.ArrayType:
switch gotype.UnwrapNestedType(typ.Elem).(type) {
case *gotype.EnumType, *gotype.CompositeType:
// For all other array elems, a normal array works.
sb.WriteString(indent)
sb.WriteString(out.LowerName)
sb.WriteString("Array := q.types.")
sb.WriteString(NameArrayTranscoderFunc(typ))
sb.WriteString("()")
}
default:
continue
}
}
return sb.String(), nil
}
// EmitResultAssigns writes all the assign statements after scanning the result
// from pgx.
//
// Copies pgtype.CompositeFields representing a Postgres composite type into the
// output struct.
//
// Copies pgtype.EnumArray fields into Go enum array types.
func (tq TemplatedQuery) EmitResultAssigns(zeroVal string) (string, error) {
sb := &strings.Builder{}
indent := "\n\t"
if tq.ResultKind == ast.ResultKindMany {
indent += "\t" // a :many query processes items in a for loop
}
for _, out := range tq.Outputs {
switch typ := gotype.UnwrapNestedType(out.Type).(type) {
case *gotype.CompositeType:
sb.WriteString(indent)
sb.WriteString("if err := ")
sb.WriteString(out.LowerName)
sb.WriteString("Row.AssignTo(&item")
if len(removeVoidColumns(tq.Outputs)) > 1 {
sb.WriteRune('.')
sb.WriteString(out.UpperName)
}
sb.WriteString("); err != nil {")
sb.WriteString(indent)
sb.WriteString("\treturn ")
sb.WriteString(zeroVal)
sb.WriteString(", fmt.Errorf(\"assign ")
sb.WriteString(tq.Name)
sb.WriteString(" row: %w\", err)")
sb.WriteString(indent)
sb.WriteString("}")
case *gotype.ArrayType:
switch gotype.UnwrapNestedType(typ.Elem).(type) {
case *gotype.CompositeType, *gotype.EnumType:
sb.WriteString(indent)
sb.WriteString("if err := ")
sb.WriteString(out.LowerName)
sb.WriteString("Array.AssignTo(&item")
if len(removeVoidColumns(tq.Outputs)) > 1 {
sb.WriteRune('.')
sb.WriteString(out.UpperName)
}
sb.WriteString("); err != nil {")
sb.WriteString(indent)
sb.WriteString("\treturn ")
sb.WriteString(zeroVal)
sb.WriteString(", fmt.Errorf(\"assign ")
sb.WriteString(tq.Name)
sb.WriteString(" row: %w\", err)")
sb.WriteString(indent)
sb.WriteString("}")
}
}
}
return sb.String(), nil
}
// EmitResultElem returns the string representing a single item in the overall
// query result type. For :one and :exec queries, this is the same as
// EmitResultType. For :many queries, this is the element type of the slice
// result type.
func (tq TemplatedQuery) EmitResultElem() (string, error) {
result, err := tq.EmitResultType()
if err != nil {
return "", fmt.Errorf("unhandled EmitResultElem type: %w", err)
}
// Unwrap arrays because we build the array with append.
arr := strings.TrimPrefix(result, "[]")
// Unwrap pointers because we add "&" to return the correct types.
ptr := strings.TrimPrefix(arr, "*")
return ptr, nil
}
// EmitResultExpr returns the string representation of a single item to return
// for :one queries or to append for :many queries. Useful for figuring out if
// we need to use the address operator. Controls the string item and &item in:
//
// items = append(items, item)
// items = append(items, &item)
func (tq TemplatedQuery) EmitResultExpr(name string) (string, error) {
switch tq.ResultKind {
case ast.ResultKindOne:
return name, nil
case ast.ResultKindMany:
result, err := tq.EmitResultType()
if err != nil {
return "", fmt.Errorf("unhandled EmitResultExpr type: %w", err)
}
isPtr := strings.HasPrefix(result, "[]*") || strings.HasPrefix(result, "*")
if isPtr {
return "&" + name, nil
}
return name, nil
default:
return "", fmt.Errorf("unhandled EmitResultExpr type: %s", tq.ResultKind)
}
}
// getLongestOutput returns the length of the longest name and type name in all
// columns. Useful for struct definition alignment.
func getLongestOutput(outs []TemplatedColumn) (int, int) {
nameLen := 0
for _, out := range outs {
if len(out.UpperName) > nameLen {
nameLen = len(out.UpperName)
}
}
nameLen++ // 1 space to separate name from type
typeLen := 0
for _, out := range outs {
if len(out.QualType) > typeLen {
typeLen = len(out.QualType)
}
}
typeLen++ // 1 space to separate type from struct tags.
return nameLen, typeLen
}
// EmitRowStruct writes the struct definition for query output row if one is
// needed.
func (tq TemplatedQuery) EmitRowStruct() string {
switch tq.ResultKind {
case ast.ResultKindExec:
return ""
case ast.ResultKindOne, ast.ResultKindMany:
outs := removeVoidColumns(tq.Outputs)
if len(outs) <= 1 {
return "" // if there's only 1 output column, return it directly
}
sb := &strings.Builder{}
sb.WriteString("\n\ntype ")
sb.WriteString(tq.Name)
sb.WriteString("Row struct {\n")
maxNameLen, maxTypeLen := getLongestOutput(outs)
for _, out := range outs {
// Name
sb.WriteString("\t")
sb.WriteString(out.UpperName)
// Type
sb.WriteString(strings.Repeat(" ", maxNameLen-len(out.UpperName)))
sb.WriteString(out.QualType)
// JSON struct tag
sb.WriteString(strings.Repeat(" ", maxTypeLen-len(out.QualType)))
sb.WriteString("`json:")
sb.WriteString(strconv.Quote(out.PgName))
sb.WriteString("`")
sb.WriteRune('\n')
}
sb.WriteString("}")
return sb.String()
default:
panic("unhandled result type: " + tq.ResultKind)
}
}
// removeVoidColumns makes a copy of cols with all VoidType columns removed.
// Useful because return types shouldn't contain the void type, but we need
// to use a nil placeholder for void types when scanning a pgx.Row.
func removeVoidColumns(cols []TemplatedColumn) []TemplatedColumn {
outs := make([]TemplatedColumn, 0, len(cols))
for _, col := range cols {
if _, ok := col.Type.(*gotype.VoidType); ok {
continue
}
outs = append(outs, col)
}
return outs
}