Skip to content

Commit 4483721

Browse files
committed
fix #237: add option NoExpand to control default field "expand" behavior
1 parent caa42d4 commit 4483721

4 files changed

Lines changed: 179 additions & 4 deletions

File tree

README.md

Lines changed: 23 additions & 1 deletion
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+
Expanded Meta `db:"expanded" fieldopt:"expand"` // Force a nested struct to expand even when sqlbuilder.NoExpand is true.
275276
Payload Meta `db:"payload" fieldopt:"noexpand"` // Treat a nested struct as one column instead of expanding it.
276277

277278
// The `omitempty` can be written as a function.
@@ -284,7 +285,7 @@ type ATable struct {
284285
}
285286
```
286287

287-
If a field is itself a struct and has a `db` tag, `Struct` treats that tag as a database alias and expands the nested fields. This is useful for `JOIN` projections built from reusable structs.
288+
If a field is itself a struct and has a `db` tag, `Struct` treats that tag as a database alias and expands the nested fields by default. This is useful for `JOIN` projections built from reusable structs.
288289

289290
```go
290291
type Post struct {
@@ -312,6 +313,27 @@ fmt.Println(sql)
312313
// SELECT post.id, post.text, comment.body FROM posts post JOIN comments comment ON post.id = comment.post_id
313314
```
314315

316+
Set `sqlbuilder.NoExpand = true` before first use of a `Struct` to keep tagged nested structs as a single column by default. When the flag is enabled, add `fieldopt:"expand"` to opt a specific field back into expansion.
317+
318+
```go
319+
type Payload struct {
320+
Key string `json:"key"`
321+
}
322+
323+
type Row struct {
324+
Payload Payload `db:"payload"`
325+
Post Post `db:"post" fieldopt:"expand"`
326+
}
327+
328+
sqlbuilder.NoExpand = true
329+
st := sqlbuilder.NewStruct(new(Row))
330+
331+
fmt.Println(st.Columns())
332+
333+
// Output:
334+
// [payload post.id post.text]
335+
```
336+
315337
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.
316338

317339
```go

struct.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,23 @@ var (
2121
FieldTag = "fieldtag"
2222

2323
// FieldOpt is the options for a struct field.
24+
// Supported options include withquote, omitempty, expand, and noexpand.
2425
// As db column can contain "," in theory, field options should be provided in a separated tag.
2526
FieldOpt = "fieldopt"
2627

2728
// FieldAs is the column alias (AS) for a struct field.
2829
FieldAs = "fieldas"
30+
31+
// NoExpand changes the default behavior for tagged nested struct fields.
32+
// When true, tagged nested structs stay as a single column unless fieldopt:"expand" is set.
33+
// Set it before first use of a Struct if that Struct should use the non-expanding default.
34+
NoExpand = false
2935
)
3036

3137
const (
3238
fieldOptWithQuote = "withquote"
3339
fieldOptOmitEmpty = "omitempty"
40+
fieldOptExpand = "expand"
3441
fieldOptNoExpand = "noexpand"
3542

3643
optName = "optName"

struct_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,97 @@ func TestStructTaggedNestedFieldNoExpand(t *testing.T) {
403403
a.Equal(addrs[1], &scanned.LiabVar)
404404
}
405405

406+
func TestStructTaggedNestedFieldDisableExpand(t *testing.T) {
407+
type varRec struct {
408+
ChnlPH string `db:"ph"`
409+
ExpVK int64 `db:"vk"`
410+
}
411+
412+
type decRec struct {
413+
DecID string `db:"dec_id"`
414+
LiabVar varRec `db:"liab_var"`
415+
}
416+
417+
a := assert.New(t)
418+
old := NoExpand
419+
NoExpand = true
420+
defer func() {
421+
NoExpand = old
422+
}()
423+
424+
st := NewStruct(new(decRec)).For(PostgreSQL)
425+
rec := &decRec{
426+
DecID: "dec-1",
427+
LiabVar: varRec{ChnlPH: "ph", ExpVK: 7},
428+
}
429+
430+
selectSQL, selectArgs := st.SelectFrom("decs d").Build()
431+
a.Equal(selectSQL, "SELECT d.dec_id, d.liab_var FROM decs d")
432+
a.Equal(selectArgs, nil)
433+
a.Equal(st.Columns(), []string{"dec_id", "liab_var"})
434+
a.Equal(st.Values(rec), []interface{}{rec.DecID, rec.LiabVar})
435+
436+
updateSQL, updateArgs := st.Update("decs", rec).Build()
437+
a.Equal(updateSQL, "UPDATE decs SET dec_id = $1, liab_var = $2")
438+
a.Equal(updateArgs, []interface{}{rec.DecID, rec.LiabVar})
439+
440+
insertSQL, insertArgs := st.InsertInto("decs", rec).Build()
441+
a.Equal(insertSQL, "INSERT INTO decs (dec_id, liab_var) VALUES ($1, $2)")
442+
a.Equal(insertArgs, []interface{}{rec.DecID, rec.LiabVar})
443+
444+
var scanned decRec
445+
addrs := st.Addr(&scanned)
446+
a.Equal(len(addrs), 2)
447+
a.Equal(addrs[0], &scanned.DecID)
448+
a.Equal(addrs[1], &scanned.LiabVar)
449+
}
450+
451+
func TestStructTaggedNestedFieldExpandOverridesNoExpand(t *testing.T) {
452+
type varRec struct {
453+
ChnlPH string `db:"ph"`
454+
ExpVK int64 `db:"vk"`
455+
}
456+
457+
type decRec struct {
458+
DecID string `db:"dec_id"`
459+
LiabVar varRec `db:"liab_var" fieldopt:"expand"`
460+
}
461+
462+
a := assert.New(t)
463+
old := NoExpand
464+
NoExpand = true
465+
defer func() {
466+
NoExpand = old
467+
}()
468+
469+
st := NewStruct(new(decRec)).For(PostgreSQL)
470+
rec := &decRec{
471+
DecID: "dec-1",
472+
LiabVar: varRec{ChnlPH: "ph", ExpVK: 7},
473+
}
474+
475+
selectSQL, selectArgs := st.SelectFrom("decs d").Build()
476+
a.Equal(selectSQL, "SELECT d.dec_id, liab_var.ph, liab_var.vk FROM decs d")
477+
a.Equal(selectArgs, nil)
478+
a.Equal(st.Columns(), []string{"dec_id", "liab_var.ph", "liab_var.vk"})
479+
a.Equal(st.Values(rec), []interface{}{rec.DecID, rec.LiabVar.ChnlPH, rec.LiabVar.ExpVK})
480+
481+
updateSQL, updateArgs := st.Update("decs", rec).Build()
482+
a.Equal(updateSQL, "UPDATE decs SET dec_id = $1, liab_var.ph = $2, liab_var.vk = $3")
483+
a.Equal(updateArgs, []interface{}{rec.DecID, rec.LiabVar.ChnlPH, rec.LiabVar.ExpVK})
484+
485+
insertSQL, insertArgs := st.InsertInto("decs", rec).Build()
486+
a.Equal(insertSQL, "INSERT INTO decs (dec_id, liab_var) VALUES ($1, $2)")
487+
a.Equal(insertArgs, []interface{}{rec.DecID, rec.LiabVar})
488+
489+
var scanned decRec
490+
addrs := st.Addr(&scanned)
491+
a.Equal(len(addrs), 3)
492+
a.Equal(addrs[0], &scanned.DecID)
493+
a.Equal(addrs[1], &scanned.LiabVar.ChnlPH)
494+
a.Equal(addrs[2], &scanned.LiabVar.ExpVK)
495+
}
496+
406497
func TestStructInsertIntoAnonymousFieldHasNoPrefix(t *testing.T) {
407498
type embedded struct {
408499
ID string `db:"id"`
@@ -525,6 +616,38 @@ const (
525616
OrderStatePaid
526617
)
527618

619+
func ExampleNoExpand() {
620+
type Post struct {
621+
ID string `db:"id"`
622+
Text string `db:"text"`
623+
}
624+
625+
type DefaultRow struct {
626+
Post Post `db:"post"`
627+
}
628+
629+
type ExpandedRow struct {
630+
Post Post `db:"post" fieldopt:"expand"`
631+
}
632+
633+
old := NoExpand
634+
defer func() {
635+
NoExpand = old
636+
}()
637+
638+
NoExpand = false
639+
fmt.Println(NewStruct(new(DefaultRow)).Columns())
640+
641+
NoExpand = true
642+
fmt.Println(NewStruct(new(DefaultRow)).Columns())
643+
fmt.Println(NewStruct(new(ExpandedRow)).Columns())
644+
645+
// Output:
646+
// [post.id post.text]
647+
// [post]
648+
// [post.id post.text]
649+
}
650+
528651
func ExampleStruct_useStructAsORM() {
529652
// Suppose we defined following type for user db.
530653
type User struct {

structfields.go

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,18 @@ type structField struct {
4242
omitEmptyTags omitEmptyTagMap
4343
}
4444

45+
type structFieldExpandMode uint8
46+
47+
const (
48+
structFieldExpandDefault structFieldExpandMode = iota
49+
structFieldExpandEnabled
50+
structFieldExpandDisabled
51+
)
52+
4553
type structFieldOptions struct {
4654
isQuoted bool
4755
omitEmptyTags omitEmptyTagMap
48-
noExpand bool
56+
expandMode structFieldExpandMode
4957
}
5058

5159
type structFieldsParser func() *structFields
@@ -182,8 +190,11 @@ func parseStructFieldOptions(field reflect.StructField) structFieldOptions {
182190
case fieldOptWithQuote:
183191
fieldOpts.isQuoted = true
184192

193+
case fieldOptExpand:
194+
fieldOpts.expandMode = structFieldExpandEnabled
195+
185196
case fieldOptNoExpand:
186-
fieldOpts.noExpand = true
197+
fieldOpts.expandMode = structFieldExpandDisabled
187198
}
188199
}
189200

@@ -223,7 +234,19 @@ func shouldExpandAnonymousStructField(t reflect.Type) bool {
223234
}
224235

225236
func shouldExpandTaggedStructField(t reflect.Type, dbtag string, fieldOpts structFieldOptions) bool {
226-
return dbtag != "" && !fieldOpts.noExpand && canExpandStructType(t)
237+
if dbtag == "" || !canExpandStructType(t) {
238+
return false
239+
}
240+
241+
switch fieldOpts.expandMode {
242+
case structFieldExpandEnabled:
243+
return true
244+
245+
case structFieldExpandDisabled:
246+
return false
247+
}
248+
249+
return !NoExpand
227250
}
228251

229252
func canExpandStructType(t reflect.Type) bool {

0 commit comments

Comments
 (0)