• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

huandu / go-sqlbuilder / 23343207800

20 Mar 2026 12:39PM UTC coverage: 96.639% (-0.5%) from 97.124%
23343207800

push

github

huandu
Support nested struct db aliases in Struct (ref #233)

79 of 100 new or added lines in 2 files covered. (79.0%)

1 existing line in 1 file now uncovered.

3911 of 4047 relevant lines covered (96.64%)

1.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.58
/structfields.go
1
package sqlbuilder
2

3
import (
4
        "database/sql"
5
        "fmt"
6
        "reflect"
7
        "strings"
8
        "sync"
9
)
10

11
var typeOfSQLScanner = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
12

13
type structFields struct {
14
        noTag  *structTaggedFields
15
        tagged map[string]*structTaggedFields
16
}
17

18
type structTaggedFields struct {
19
        // All columns for SELECT.
20
        ForRead     []*structField
21
        colsForRead map[string]*structField
22

23
        // All columns which can be used in INSERT and UPDATE.
24
        ForWrite     []*structField
25
        colsForWrite map[string]struct{}
26
}
27

28
type structField struct {
29
        Name     string
30
        Alias    string
31
        As       string
32
        Tags     []string
33
        IsQuoted bool
34
        DBTag    string
35
        Field    reflect.StructField
36
        Index    []int
37

38
        omitEmptyTags omitEmptyTagMap
39
}
40

41
type structFieldsParser func() *structFields
42

43
func makeDefaultFieldsParser(t reflect.Type) structFieldsParser {
1✔
44
        return makeFieldsParser(t, nil, true)
1✔
45
}
1✔
46

47
func makeCustomFieldsParser(t reflect.Type, mapper FieldMapperFunc) structFieldsParser {
1✔
48
        return makeFieldsParser(t, mapper, false)
1✔
49
}
1✔
50

51
func makeFieldsParser(t reflect.Type, mapper FieldMapperFunc, useDefault bool) structFieldsParser {
1✔
52
        var once sync.Once
1✔
53
        sfs := &structFields{
1✔
54
                noTag:  makeStructTaggedFields(),
1✔
55
                tagged: map[string]*structTaggedFields{},
1✔
56
        }
1✔
57

1✔
58
        return func() *structFields {
2✔
59
                once.Do(func() {
2✔
60
                        if useDefault {
2✔
61
                                mapper = DefaultFieldMapper
1✔
62
                        }
1✔
63

64
                        sfs.parse(t, mapper, "", nil)
1✔
65
                })
66

67
                return sfs
1✔
68
        }
69
}
70

71
func (sfs *structFields) parse(t reflect.Type, mapper FieldMapperFunc, prefix string, index []int) {
1✔
72
        l := t.NumField()
1✔
73
        var anonymous []reflect.StructField
1✔
74

1✔
75
        for i := 0; i < l; i++ {
2✔
76
                field := t.Field(i)
1✔
77

1✔
78
                // Skip unexported fields that are not embedded structs.
1✔
79
                if field.PkgPath != "" && !field.Anonymous {
2✔
80
                        continue
1✔
81
                }
82

83
                if field.Anonymous {
2✔
84
                        ft := field.Type
1✔
85

1✔
86
                        // If field is an anonymous struct or pointer to struct, parse it later.
1✔
87
                        if shouldExpandAnonymousStructField(ft) {
2✔
88
                                anonymous = append(anonymous, field)
1✔
89
                                continue
1✔
90
                        }
91
                }
92

93
                // Parse DBTag.
94
                alias, dbtag := DefaultGetAlias(&field)
1✔
95

1✔
96
                if alias == "-" {
2✔
97
                        continue
1✔
98
                }
99

100
                if shouldExpandTaggedStructField(field.Type, dbtag) {
2✔
101
                        sfs.parse(dereferencedType(field.Type), mapper, dbtag+".", appendFieldIndex(index, i))
1✔
102
                        continue
1✔
103
                }
104

105
                if alias == "" {
2✔
106
                        alias = field.Name
1✔
107
                        if mapper != nil {
2✔
108
                                alias = mapper(alias)
1✔
109
                        }
1✔
110
                }
111

112
                if prefix != "" && !strings.ContainsRune(alias, '.') {
2✔
113
                        alias = prefix + alias
1✔
114
                }
1✔
115

116
                // Parse FieldOpt.
117
                fieldopt := field.Tag.Get(FieldOpt)
1✔
118
                opts := optRegex.FindAllString(fieldopt, -1)
1✔
119
                isQuoted := false
1✔
120
                omitEmptyTags := omitEmptyTagMap{}
1✔
121

1✔
122
                for _, opt := range opts {
2✔
123
                        optMap := getOptMatchedMap(opt)
1✔
124

1✔
125
                        switch optMap[optName] {
1✔
126
                        case fieldOptOmitEmpty:
1✔
127
                                tags := getTagsFromOptParams(optMap[optParams])
1✔
128

1✔
129
                                for _, tag := range tags {
2✔
130
                                        omitEmptyTags[tag] = struct{}{}
1✔
131
                                }
1✔
132

133
                        case fieldOptWithQuote:
1✔
134
                                isQuoted = true
1✔
135
                        }
136
                }
137

138
                // Parse FieldAs.
139
                fieldas := field.Tag.Get(FieldAs)
1✔
140

1✔
141
                // Parse FieldTag.
1✔
142
                fieldtag := field.Tag.Get(FieldTag)
1✔
143
                tags := splitTags(fieldtag)
1✔
144

1✔
145
                // Make struct field.
1✔
146
                structField := &structField{
1✔
147
                        Name:          field.Name,
1✔
148
                        Alias:         alias,
1✔
149
                        As:            fieldas,
1✔
150
                        Tags:          tags,
1✔
151
                        IsQuoted:      isQuoted,
1✔
152
                        DBTag:         dbtag,
1✔
153
                        Field:         field,
1✔
154
                        Index:         appendFieldIndex(index, i),
1✔
155
                        omitEmptyTags: omitEmptyTags,
1✔
156
                }
1✔
157

1✔
158
                // Make sure all fields can be added to noTag without conflict.
1✔
159
                sfs.noTag.Add(structField)
1✔
160

1✔
161
                for _, tag := range tags {
2✔
162
                        sfs.taggedFields(tag).Add(structField)
1✔
163
                }
1✔
164
        }
165

166
        for _, field := range anonymous {
2✔
167
                ft := dereferencedType(field.Type)
1✔
168
                sfs.parse(ft, mapper, prefix, appendFieldIndex(index, field.Index...))
1✔
169
        }
1✔
170
}
171

172
func appendFieldIndex(prefix []int, index ...int) []int {
1✔
173
        path := make([]int, 0, len(prefix)+len(index))
1✔
174
        path = append(path, prefix...)
1✔
175
        path = append(path, index...)
1✔
176
        return path
1✔
177
}
1✔
178

179
func shouldExpandAnonymousStructField(t reflect.Type) bool {
1✔
180
        return canExpandStructType(t)
1✔
181
}
1✔
182

183
func shouldExpandTaggedStructField(t reflect.Type, dbtag string) bool {
1✔
184
        return dbtag != "" && canExpandStructType(t)
1✔
185
}
1✔
186

187
func canExpandStructType(t reflect.Type) bool {
1✔
188
        if t == nil {
1✔
NEW
189
                return false
×
UNCOV
190
        }
×
191

192
        dt := dereferencedType(t)
1✔
193
        if dt.Kind() != reflect.Struct {
2✔
194
                return false
1✔
195
        }
1✔
196

197
        if implementsScannerOrValuer(t) || implementsScannerOrValuer(dt) {
1✔
NEW
198
                return false
×
NEW
199
        }
×
200

201
        if dt != t && implementsScannerOrValuer(reflect.PtrTo(dt)) {
1✔
NEW
202
                return false
×
NEW
203
        }
×
204

205
        for i := 0; i < dt.NumField(); i++ {
2✔
206
                field := dt.Field(i)
1✔
207
                if field.PkgPath == "" || field.Anonymous {
2✔
208
                        return true
1✔
209
                }
1✔
210
        }
211

212
        return false
1✔
213
}
214

215
func implementsScannerOrValuer(t reflect.Type) bool {
1✔
216
        if t == nil {
1✔
NEW
217
                return false
×
NEW
218
        }
×
219

220
        return t.Implements(typeOfSQLDriverValuer) || t.Implements(typeOfSQLScanner)
1✔
221
}
222

223
func (sfs *structFields) FilterTags(with, without []string) *structTaggedFields {
1✔
224
        if len(with) == 0 && len(without) == 0 {
2✔
225
                return sfs.noTag
1✔
226
        }
1✔
227

228
        // Simply return the tagged fields.
229
        if len(with) == 1 && len(without) == 0 {
2✔
230
                return sfs.tagged[with[0]]
1✔
231
        }
1✔
232

233
        // Find out all with and without fields.
234
        taggedFields := makeStructTaggedFields()
1✔
235
        filteredReadFields := make(map[string]struct{}, len(sfs.noTag.colsForRead))
1✔
236

1✔
237
        for _, tag := range without {
2✔
238
                if field, ok := sfs.tagged[tag]; ok {
2✔
239
                        for k := range field.colsForRead {
2✔
240
                                filteredReadFields[k] = struct{}{}
1✔
241
                        }
1✔
242
                }
243
        }
244

245
        if len(with) == 0 {
2✔
246
                for _, field := range sfs.noTag.ForRead {
2✔
247
                        k := field.Key()
1✔
248

1✔
249
                        if _, ok := filteredReadFields[k]; !ok {
2✔
250
                                taggedFields.Add(field)
1✔
251
                        }
1✔
252
                }
253
        } else {
1✔
254
                for _, tag := range with {
2✔
255
                        if fields, ok := sfs.tagged[tag]; ok {
2✔
256
                                for _, field := range fields.ForRead {
2✔
257
                                        k := field.Key()
1✔
258

1✔
259
                                        if _, ok := filteredReadFields[k]; !ok {
2✔
260
                                                taggedFields.Add(field)
1✔
261
                                        }
1✔
262
                                }
263
                        }
264
                }
265
        }
266

267
        return taggedFields
1✔
268
}
269

270
func (sfs *structFields) taggedFields(tag string) *structTaggedFields {
1✔
271
        fields, ok := sfs.tagged[tag]
1✔
272

1✔
273
        if !ok {
2✔
274
                fields = makeStructTaggedFields()
1✔
275
                sfs.tagged[tag] = fields
1✔
276
        }
1✔
277

278
        return fields
1✔
279
}
280

281
func makeStructTaggedFields() *structTaggedFields {
1✔
282
        return &structTaggedFields{
1✔
283
                colsForRead:  map[string]*structField{},
1✔
284
                colsForWrite: map[string]struct{}{},
1✔
285
        }
1✔
286
}
1✔
287

288
// Add a new field to stfs.
289
// If field's key exists in stfs.fields, the field is ignored.
290
func (stfs *structTaggedFields) Add(field *structField) {
1✔
291
        key := field.Key()
1✔
292

1✔
293
        if _, ok := stfs.colsForRead[key]; !ok {
2✔
294
                stfs.colsForRead[key] = field
1✔
295
                stfs.ForRead = append(stfs.ForRead, field)
1✔
296
        }
1✔
297

298
        key = field.Alias
1✔
299

1✔
300
        if _, ok := stfs.colsForWrite[key]; !ok {
2✔
301
                stfs.colsForWrite[key] = struct{}{}
1✔
302
                stfs.ForWrite = append(stfs.ForWrite, field)
1✔
303
        }
1✔
304
}
305

306
// Cols returns the fields whose key is one of cols.
307
// If any column in cols doesn't exist in sfs.fields, returns nil.
308
func (stfs *structTaggedFields) Cols(cols []string) []*structField {
1✔
309
        fields := make([]*structField, 0, len(cols))
1✔
310

1✔
311
        for _, col := range cols {
2✔
312
                field := stfs.colsForRead[col]
1✔
313

1✔
314
                if field == nil {
2✔
315
                        return nil
1✔
316
                }
1✔
317

318
                fields = append(fields, field)
1✔
319
        }
320

321
        return fields
1✔
322
}
323

324
// Key returns the key name to identify a field.
325
func (sf *structField) Key() string {
1✔
326
        if sf.As != "" {
2✔
327
                return sf.As
1✔
328
        }
1✔
329

330
        if sf.Alias != "" {
2✔
331
                return sf.Alias
1✔
332
        }
1✔
333

334
        return sf.Name
×
335
}
336

337
// NameForSelect returns the name for SELECT.
338
func (sf *structField) NameForSelect(flavor Flavor) string {
1✔
339
        if sf.As == "" {
2✔
340
                return sf.Quote(flavor)
1✔
341
        }
1✔
342

343
        return fmt.Sprintf("%s AS %s", sf.Quote(flavor), sf.As)
1✔
344
}
345

346
// Quote the Alias in sf with flavor.
347
func (sf *structField) Quote(flavor Flavor) string {
1✔
348
        if !sf.IsQuoted {
2✔
349
                return sf.Alias
1✔
350
        }
1✔
351

352
        return flavor.Quote(sf.Alias)
1✔
353
}
354

355
// ShouldOmitEmpty returns true only if any one of tags is in the omitted tags map.
356
func (sf *structField) ShouldOmitEmpty(tags ...string) (ret bool) {
1✔
357
        omit := sf.omitEmptyTags
1✔
358

1✔
359
        if len(omit) == 0 {
2✔
360
                return
1✔
361
        }
1✔
362

363
        // Always check default tag.
364
        if _, ret = omit[""]; ret {
2✔
365
                return
1✔
366
        }
1✔
367

368
        for _, tag := range tags {
2✔
369
                if _, ret = omit[tag]; ret {
2✔
370
                        return
1✔
371
                }
1✔
372
        }
373

374
        return
1✔
375
}
376

377
type omitEmptyTagMap map[string]struct{}
378

379
func getOptMatchedMap(opt string) (res map[string]string) {
1✔
380
        res = map[string]string{}
1✔
381
        sm := optRegex.FindStringSubmatch(opt)
1✔
382

1✔
383
        for i, name := range optRegex.SubexpNames() {
2✔
384
                if name != "" {
2✔
385
                        res[name] = sm[i]
1✔
386
                }
1✔
387
        }
388

389
        return
1✔
390
}
391

392
func getTagsFromOptParams(opts string) (tags []string) {
1✔
393
        tags = splitTags(opts)
1✔
394

1✔
395
        if len(tags) == 0 {
2✔
396
                tags = append(tags, "")
1✔
397
        }
1✔
398

399
        return
1✔
400
}
401

402
func splitTags(fieldtag string) (tags []string) {
1✔
403
        parts := strings.Split(fieldtag, ",")
1✔
404

1✔
405
        for _, v := range parts {
2✔
406
                tag := strings.TrimSpace(v)
1✔
407

1✔
408
                if tag == "" {
2✔
409
                        continue
1✔
410
                }
411

412
                tags = append(tags, tag)
1✔
413
        }
414

415
        return
1✔
416
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc