Skip to content

Commit caa42d4

Browse files
committed
fix #236: New fieldopt "noexpand" to treat a nested struct as one column
1 parent e118b20 commit caa42d4

4 files changed

Lines changed: 99 additions & 25 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ type ATable struct {
272272
unexported int // Unexported field is not visible to Struct.
273273
Quoted string `db:"quoted" fieldopt:"withquote"` // Add quote to the field using back quote or double quote. See `Flavor#Quote`.
274274
Empty uint `db:"empty" fieldopt:"omitempty"` // Omit the field in UPDATE if it is a nil or zero value.
275+
Payload Meta `db:"payload" fieldopt:"noexpand"` // Treat a nested struct as one column instead of expanding it.
275276

276277
// The `omitempty` can be written as a function.
277278
// In this case, omit empty field `Tagged` when UPDATE for tag `tag1` and `tag3` but not `tag2`.
@@ -311,6 +312,27 @@ fmt.Println(sql)
311312
// SELECT post.id, post.text, comment.body FROM posts post JOIN comments comment ON post.id = comment.post_id
312313
```
313314

315+
If a nested struct should stay as a single column, add `fieldopt:"noexpand"` to opt out of the automatic expansion. This is useful for JSON columns scanned into Go structs by the database driver.
316+
317+
```go
318+
type Payload struct {
319+
Key string `json:"key"`
320+
}
321+
322+
type Row struct {
323+
ID string `db:"id"`
324+
Payload Payload `db:"payload" fieldopt:"noexpand"`
325+
}
326+
327+
st := sqlbuilder.NewStruct(new(Row)).For(sqlbuilder.PostgreSQL)
328+
sql, _ := st.SelectFrom("events e").Build()
329+
330+
fmt.Println(sql)
331+
332+
// Output:
333+
// SELECT e.id, e.payload FROM events e
334+
```
335+
314336
For detailed instructions on utilizing `Struct`, refer to the [examples](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Struct).
315337

316338
Furthermore, `Struct` can be employed as a zero-configuration ORM. Unlike most ORM implementations that necessitate preliminary configurations for database connectivity, `Struct` operates without any configuration, functioning seamlessly with any SQL driver compatible with `database/sql`. `Struct` does not invoke any `database/sql` APIs; it solely generates the appropriate SQL statements with arguments for `DB#Query`/`DB#Exec` or an array of struct field addresses for `Rows#Scan`/`Row#Scan`.

struct.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var (
3131
const (
3232
fieldOptWithQuote = "withquote"
3333
fieldOptOmitEmpty = "omitempty"
34+
fieldOptNoExpand = "noexpand"
3435

3536
optName = "optName"
3637
optParams = "optParams"

struct_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,41 @@ func TestStructInsertIntoTaggedNestedFieldRemainsScalar(t *testing.T) {
368368
a.Equal(args, []interface{}{rec.DecID, rec.LiabVar, rec.AssetVars})
369369
}
370370

371+
func TestStructTaggedNestedFieldNoExpand(t *testing.T) {
372+
type varRec struct {
373+
ChnlPH string `json:"ph"`
374+
ExpVK int64 `json:"vk"`
375+
}
376+
377+
type decRec struct {
378+
DecID string `db:"dec_id"`
379+
LiabVar varRec `db:"liab_var" fieldopt:"noexpand"`
380+
}
381+
382+
a := assert.New(t)
383+
st := NewStruct(new(decRec)).For(PostgreSQL)
384+
rec := &decRec{
385+
DecID: "dec-1",
386+
LiabVar: varRec{ChnlPH: "ph", ExpVK: 7},
387+
}
388+
389+
selectSQL, selectArgs := st.SelectFrom("decs d").Build()
390+
a.Equal(selectSQL, "SELECT d.dec_id, d.liab_var FROM decs d")
391+
a.Equal(selectArgs, nil)
392+
a.Equal(st.Columns(), []string{"dec_id", "liab_var"})
393+
a.Equal(st.Values(rec), []interface{}{rec.DecID, rec.LiabVar})
394+
395+
updateSQL, updateArgs := st.Update("decs", rec).Build()
396+
a.Equal(updateSQL, "UPDATE decs SET dec_id = $1, liab_var = $2")
397+
a.Equal(updateArgs, []interface{}{rec.DecID, rec.LiabVar})
398+
399+
var scanned decRec
400+
addrs := st.Addr(&scanned)
401+
a.Equal(len(addrs), 2)
402+
a.Equal(addrs[0], &scanned.DecID)
403+
a.Equal(addrs[1], &scanned.LiabVar)
404+
}
405+
371406
func TestStructInsertIntoAnonymousFieldHasNoPrefix(t *testing.T) {
372407
type embedded struct {
373408
ID string `db:"id"`

structfields.go

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ type structField struct {
4242
omitEmptyTags omitEmptyTagMap
4343
}
4444

45+
type structFieldOptions struct {
46+
isQuoted bool
47+
omitEmptyTags omitEmptyTagMap
48+
noExpand bool
49+
}
50+
4551
type structFieldsParser func() *structFields
4652

4753
func makeDefaultFieldsParser(t reflect.Type) structFieldsParser {
@@ -101,16 +107,18 @@ func (sfs *structFields) parse(t reflect.Type, mapper FieldMapperFunc, prefix st
101107
continue
102108
}
103109

104-
if shouldExpandTaggedStructField(field.Type, dbtag) {
105-
structField := makeStructField(field, alias, dbtag, mapper, prefix, index, i)
110+
fieldOpts := parseStructFieldOptions(field)
111+
112+
if shouldExpandTaggedStructField(field.Type, dbtag, fieldOpts) {
113+
structField := makeStructField(field, alias, dbtag, mapper, fieldOpts, prefix, index, i)
106114
if allowInsert {
107115
sfs.addInsertField(structField)
108116
}
109117
sfs.parse(dereferencedType(field.Type), mapper, dbtag+".", appendFieldIndex(index, i), false)
110118
continue
111119
}
112120

113-
structField := makeStructField(field, alias, dbtag, mapper, prefix, index, i)
121+
structField := makeStructField(field, alias, dbtag, mapper, fieldOpts, prefix, index, i)
114122
if allowInsert {
115123
sfs.addField(structField)
116124
} else {
@@ -124,7 +132,7 @@ func (sfs *structFields) parse(t reflect.Type, mapper FieldMapperFunc, prefix st
124132
}
125133
}
126134

127-
func makeStructField(field reflect.StructField, alias, dbtag string, mapper FieldMapperFunc, prefix string, index []int, fieldIndex int) *structField {
135+
func makeStructField(field reflect.StructField, alias, dbtag string, mapper FieldMapperFunc, fieldOpts structFieldOptions, prefix string, index []int, fieldIndex int) *structField {
128136
if alias == "" {
129137
alias = field.Name
130138
if mapper != nil {
@@ -136,10 +144,29 @@ func makeStructField(field reflect.StructField, alias, dbtag string, mapper Fiel
136144
alias = prefix + alias
137145
}
138146

147+
fieldas := field.Tag.Get(FieldAs)
148+
fieldtag := field.Tag.Get(FieldTag)
149+
tags := splitTags(fieldtag)
150+
151+
return &structField{
152+
Name: field.Name,
153+
Alias: alias,
154+
As: fieldas,
155+
Tags: tags,
156+
IsQuoted: fieldOpts.isQuoted,
157+
DBTag: dbtag,
158+
Field: field,
159+
Index: appendFieldIndex(index, fieldIndex),
160+
omitEmptyTags: fieldOpts.omitEmptyTags,
161+
}
162+
}
163+
164+
func parseStructFieldOptions(field reflect.StructField) structFieldOptions {
139165
fieldopt := field.Tag.Get(FieldOpt)
140166
opts := optRegex.FindAllString(fieldopt, -1)
141-
isQuoted := false
142-
omitEmptyTags := omitEmptyTagMap{}
167+
fieldOpts := structFieldOptions{
168+
omitEmptyTags: omitEmptyTagMap{},
169+
}
143170

144171
for _, opt := range opts {
145172
optMap := getOptMatchedMap(opt)
@@ -149,29 +176,18 @@ func makeStructField(field reflect.StructField, alias, dbtag string, mapper Fiel
149176
tags := getTagsFromOptParams(optMap[optParams])
150177

151178
for _, tag := range tags {
152-
omitEmptyTags[tag] = struct{}{}
179+
fieldOpts.omitEmptyTags[tag] = struct{}{}
153180
}
154181

155182
case fieldOptWithQuote:
156-
isQuoted = true
183+
fieldOpts.isQuoted = true
184+
185+
case fieldOptNoExpand:
186+
fieldOpts.noExpand = true
157187
}
158188
}
159189

160-
fieldas := field.Tag.Get(FieldAs)
161-
fieldtag := field.Tag.Get(FieldTag)
162-
tags := splitTags(fieldtag)
163-
164-
return &structField{
165-
Name: field.Name,
166-
Alias: alias,
167-
As: fieldas,
168-
Tags: tags,
169-
IsQuoted: isQuoted,
170-
DBTag: dbtag,
171-
Field: field,
172-
Index: appendFieldIndex(index, fieldIndex),
173-
omitEmptyTags: omitEmptyTags,
174-
}
190+
return fieldOpts
175191
}
176192

177193
func (sfs *structFields) addField(field *structField) {
@@ -206,8 +222,8 @@ func shouldExpandAnonymousStructField(t reflect.Type) bool {
206222
return canExpandStructType(t)
207223
}
208224

209-
func shouldExpandTaggedStructField(t reflect.Type, dbtag string) bool {
210-
return dbtag != "" && canExpandStructType(t)
225+
func shouldExpandTaggedStructField(t reflect.Type, dbtag string, fieldOpts structFieldOptions) bool {
226+
return dbtag != "" && !fieldOpts.noExpand && canExpandStructType(t)
211227
}
212228

213229
func canExpandStructType(t reflect.Type) bool {

0 commit comments

Comments
 (0)