From 20402210f4e613b595ae39a3ee97f7f5d53da84c Mon Sep 17 00:00:00 2001 From: doug-martin Date: Wed, 31 Jul 2019 22:18:03 -0500 Subject: [PATCH] Fix for #118 * [FIX] Fix reflection errors related to nil pointers and unexported fields #118 * Unexported fields are ignored when creating a columnMap * Nil embedded pointers will no longer cause a panic * Fields on nil embedded pointers will be ignored when creating update or insert statements. * [ADDED] You can now ingore embedded structs and their fields by using `db:"-"` tag on the embedded struct. --- HISTORY.md | 8 + docs/inserting.md | 81 ++++ docs/updating.md | 75 ++++ exec/scanner_test.go | 774 +++++++++++++++++++++------------ exp/exp.go | 4 +- exp/insert.go | 54 +-- exp/insert_test.go | 254 ++++++++--- exp/record.go | 64 +++ exp/update.go | 14 +- exp/update_test.go | 181 ++++++++ insert_dataset_example_test.go | 66 +++ internal/util/reflect.go | 28 +- internal/util/reflect_test.go | 149 +++++++ issues_test.go | 150 +++++++ update_dataset_example_test.go | 60 +++ 15 files changed, 1579 insertions(+), 383 deletions(-) create mode 100644 exp/record.go create mode 100644 exp/update_test.go diff --git a/HISTORY.md b/HISTORY.md index 1ba2a7c5..55329f44 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,11 @@ +## v8.2.0 + +* [FIX] Fix reflection errors related to nil pointers and unexported fields [#118](https://github.com/doug-martin/goqu/issues/118) + * Unexported fields are ignored when creating a columnMap + * Nil embedded pointers will no longer cause a panic + * Fields on nil embedded pointers will be ignored when creating update or insert statements. +* [ADDED] You can now ingore embedded structs and their fields by using `db:"-"` tag on the embedded struct. + ## v8.1.0 * [ADDED] Support column DEFAULT when inserting/updating via struct [#27](https://github.com/doug-martin/goqu/issues/27) diff --git a/docs/inserting.md b/docs/inserting.md index c81cebe7..a10b4854 100644 --- a/docs/inserting.md +++ b/docs/inserting.md @@ -196,6 +196,87 @@ Output: INSERT INTO "user" ("first_name", "last_name") VALUES (DEFAULT, 'Farley'), ('Jimmy', 'Stewart'), (DEFAULT, 'Jeffers') [] ``` +`goqu` will also use fields in embedded structs when creating an insert. + +**NOTE** unexported fields will be ignored! + +```go +type Address struct { + Street string `db:"address_street"` + State string `db:"address_state"` +} +type User struct { + Address + FirstName string + LastName string +} +ds := goqu.Insert("user").Rows( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + User{Address: Address{Street: "211 Street", State: "NY"}, FirstName: "Jimmy", LastName: "Stewart"}, + User{Address: Address{Street: "311 Street", State: "NY"}, FirstName: "Jeff", LastName: "Jeffers"}, +) +insertSQL, args, _ := ds.ToSQL() +fmt.Println(insertSQL, args) +``` + +Output: +``` +INSERT INTO "user" ("address_state", "address_street", "firstname", "lastname") VALUES ('NY', '111 Street', 'Greg', 'Farley'), ('NY', '211 Street', 'Jimmy', 'Stewart'), ('NY', '311 Street', 'Jeff', 'Jeffers') [] +``` + +**NOTE** When working with embedded pointers if the embedded struct is nil then the fields will be ignored. + +```go +type Address struct { + Street string + State string +} +type User struct { + *Address + FirstName string + LastName string +} +ds := goqu.Insert("user").Rows( + User{FirstName: "Greg", LastName: "Farley"}, + User{FirstName: "Jimmy", LastName: "Stewart"}, + User{FirstName: "Jeff", LastName: "Jeffers"}, +) +insertSQL, args, _ := ds.ToSQL() +fmt.Println(insertSQL, args) +``` + +Output: +``` +INSERT INTO "user" ("firstname", "lastname") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +``` + +You can ignore an embedded struct or struct pointer by using `db:"-"` + +```go +type Address struct { + Street string + State string +} +type User struct { + Address `db:"-"` + FirstName string + LastName string +} + +ds := goqu.Insert("user").Rows( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + User{Address: Address{Street: "211 Street", State: "NY"}, FirstName: "Jimmy", LastName: "Stewart"}, + User{Address: Address{Street: "311 Street", State: "NY"}, FirstName: "Jeff", LastName: "Jeffers"}, +) +insertSQL, args, _ := ds.ToSQL() +fmt.Println(insertSQL, args) +``` + +Output: +``` +INSERT INTO "user" ("firstname", "lastname") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +``` + **Insert `map[string]interface{}`** diff --git a/docs/updating.md b/docs/updating.md index 1c5fba54..398d5287 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -171,6 +171,81 @@ Output: UPDATE "items" SET "address"='111 Test Addr',"name"=DEFAULT [] ``` +`goqu` will also use fields in embedded structs when creating an update. + +**NOTE** unexported fields will be ignored! + +```go +type Address struct { + Street string `db:"address_street"` + State string `db:"address_state"` +} +type User struct { + Address + FirstName string + LastName string +} +ds := goqu.Update("user").Set( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, +) +updateSQL, args, _ := ds.ToSQL() +fmt.Println(updateSQL, args) +``` + +Output: +``` +UPDATE "user" SET "address_state"='NY',"address_street"='111 Street',"firstname"='Greg',"lastname"='Farley' [] +``` + +**NOTE** When working with embedded pointers if the embedded struct is nil then the fields will be ignored. + +```go +type Address struct { + Street string + State string +} +type User struct { + *Address + FirstName string + LastName string +} +ds := goqu.Update("user").Set( + User{FirstName: "Greg", LastName: "Farley"}, +) +updateSQL, args, _ := ds.ToSQL() +fmt.Println(updateSQL, args) +``` + +Output: +``` +UPDATE "user" SET "firstname"='Greg',"lastname"='Farley' [] +``` + +You can ignore an embedded struct or struct pointer by using `db:"-"` + +```go +type Address struct { + Street string + State string +} +type User struct { + Address `db:"-"` + FirstName string + LastName string +} +ds := goqu.Update("user").Set( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, +) +updateSQL, args, _ := ds.ToSQL() +fmt.Println(updateSQL, args) +``` + +Output: +``` +UPDATE "user" SET "firstname"='Greg',"lastname"='Farley' [] +``` + + **[Set with Map](https://godoc.org/github.com/doug-martin/goqu/#UpdateDataset.Set)** diff --git a/exec/scanner_test.go b/exec/scanner_test.go index a59fcba2..febdbb43 100644 --- a/exec/scanner_test.go +++ b/exec/scanner_test.go @@ -8,49 +8,9 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) -type TestCrudActionItem struct { - Address string `db:"address"` - Name string `db:"name"` -} - -type TestCrudActionNoTagsItem struct { - Address string - Name string -} - -type TestCrudActionWithPointerField struct { - Address *string - Name *string -} - -type TestComposedCrudActionItem struct { - TestCrudActionItem - PhoneNumber string `db:"phone_number"` - Age int64 `db:"age"` -} - -type TestEmbeddedPtrCrudActionItem struct { - *TestCrudActionItem - PhoneNumber string `db:"phone_number"` - Age int64 `db:"age"` -} - -type TestComposedDuplicateFieldsItem struct { - TestCrudActionItem - Address string `db:"other_address"` - Name string `db:"other_name"` -} - -type TestComposedPointerDuplicateFieldsItem struct { - *TestCrudActionItem - Address string `db:"other_address"` - Name string `db:"other_name"` -} - var ( testAddr1 = "111 Test Addr" testAddr2 = "211 Test Addr" @@ -63,37 +23,45 @@ type crudExecTest struct { } func (cet *crudExecTest) TestWithError() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + ctx := context.Background() db, _, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) expectedErr := fmt.Errorf("crud exec error") e := newQueryExecutor(db, expectedErr, `SELECT * FROM "items"`) - var items []TestCrudActionItem - assert.EqualError(t, e.ScanStructs(&items), expectedErr.Error()) - assert.EqualError(t, e.ScanStructsContext(ctx, &items), expectedErr.Error()) - found, err := e.ScanStruct(&TestCrudActionItem{}) - assert.EqualError(t, err, expectedErr.Error()) - assert.False(t, found) - found, err = e.ScanStructContext(ctx, &TestCrudActionItem{}) - assert.EqualError(t, err, expectedErr.Error()) - assert.False(t, found) + var items []StructWithTags + cet.EqualError(e.ScanStructs(&items), expectedErr.Error()) + cet.EqualError(e.ScanStructsContext(ctx, &items), expectedErr.Error()) + found, err := e.ScanStruct(&StructWithTags{}) + cet.EqualError(err, expectedErr.Error()) + cet.False(found) + found, err = e.ScanStructContext(ctx, &StructWithTags{}) + cet.EqualError(err, expectedErr.Error()) + cet.False(found) var vals []string - assert.EqualError(t, e.ScanVals(&vals), expectedErr.Error()) - assert.EqualError(t, e.ScanValsContext(ctx, &vals), expectedErr.Error()) + cet.EqualError(e.ScanVals(&vals), expectedErr.Error()) + cet.EqualError(e.ScanValsContext(ctx, &vals), expectedErr.Error()) var val string found, err = e.ScanVal(&val) - assert.EqualError(t, err, expectedErr.Error()) - assert.False(t, found) + cet.EqualError(err, expectedErr.Error()) + cet.False(found) found, err = e.ScanValContext(ctx, &val) - assert.EqualError(t, err, expectedErr.Error()) - assert.False(t, found) + cet.EqualError(err, expectedErr.Error()) + cet.False(found) } func (cet *crudExecTest) TestScanStructs_withTaggedFields() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -102,18 +70,21 @@ func (cet *crudExecTest) TestScanStructs_withTaggedFields() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionItem - assert.NoError(t, e.ScanStructs(&items)) - assert.Equal(t, []TestCrudActionItem{ + var items []StructWithTags + cet.NoError(e.ScanStructs(&items)) + cet.Equal([]StructWithTags{ {Address: "111 Test Addr", Name: "Test1"}, {Address: "211 Test Addr", Name: "Test2"}, }, items) } func (cet *crudExecTest) TestScanStructs_withUntaggedFields() { - t := cet.T() + type StructWithNoTags struct { + Address string + Name string + } db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -122,18 +93,21 @@ func (cet *crudExecTest) TestScanStructs_withUntaggedFields() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionNoTagsItem - assert.NoError(t, e.ScanStructs(&items)) - assert.Equal(t, []TestCrudActionNoTagsItem{ + var items []StructWithNoTags + cet.NoError(e.ScanStructs(&items)) + cet.Equal([]StructWithNoTags{ {Address: "111 Test Addr", Name: "Test1"}, {Address: "211 Test Addr", Name: "Test2"}, }, items) } func (cet *crudExecTest) TestScanStructs_withPointerFields() { - t := cet.T() + type StructWithPointerFields struct { + Address *string + Name *string + } db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -142,18 +116,47 @@ func (cet *crudExecTest) TestScanStructs_withPointerFields() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionWithPointerField - assert.NoError(t, e.ScanStructs(&items)) - assert.Equal(t, []TestCrudActionWithPointerField{ + var items []StructWithPointerFields + cet.NoError(e.ScanStructs(&items)) + cet.Equal([]StructWithPointerFields{ {Address: &testAddr1, Name: &testName1}, {Address: &testAddr2, Name: &testName2}, }, items) } +func (cet *crudExecTest) TestScanStructs_withPrivateFields() { + type StructWithPrivateTags struct { + private string // nolint:structcheck,unused + Address string `db:"address"` + Name string `db:"name"` + } + + db, mock, err := sqlmock.New() + cet.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). + FromCSVString("111 Test Addr,Test1\n211 Test Addr,Test2")) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var items []StructWithPrivateTags + cet.NoError(e.ScanStructs(&items)) + cet.Equal([]StructWithPrivateTags{ + {Address: testAddr1, Name: testName1}, + {Address: testAddr2, Name: testName2}, + }, items) +} + func (cet *crudExecTest) TestScanStructs_pointers() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -162,18 +165,56 @@ func (cet *crudExecTest) TestScanStructs_pointers() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []*TestCrudActionItem - assert.NoError(t, e.ScanStructs(&items)) - assert.Equal(t, []*TestCrudActionItem{ + var items []*StructWithTags + cet.NoError(e.ScanStructs(&items)) + cet.Equal([]*StructWithTags{ {Address: "111 Test Addr", Name: "Test1"}, {Address: "211 Test Addr", Name: "Test2"}, }, items) } +func (cet *crudExecTest) TestScanStructs_withIgnoredEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedIgnoredStruct struct { + StructWithTags `db:"-"` + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + db, mock, err := sqlmock.New() + cet.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). + FromCSVString("111-111-1111,20\n222-222-2222,30")) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedIgnoredStruct + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]ComposedIgnoredStruct{ + {StructWithTags: StructWithTags{}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: StructWithTags{}, PhoneNumber: "222-222-2222", Age: 30}, + }, composed) +} + func (cet *crudExecTest) TestScanStructs_withEmbeddedStruct() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -182,18 +223,27 @@ func (cet *crudExecTest) TestScanStructs_withEmbeddedStruct() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []TestComposedCrudActionItem - assert.NoError(t, e.ScanStructs(&composed)) - assert.Equal(t, []TestComposedCrudActionItem{ - {TestCrudActionItem: TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []ComposedStruct + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]ComposedStruct{ + {StructWithTags: StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedStruct() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -202,18 +252,28 @@ func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedStruct() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []*TestComposedCrudActionItem - assert.NoError(t, e.ScanStructs(&composed)) - assert.Equal(t, []*TestComposedCrudActionItem{ - {TestCrudActionItem: TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []*ComposedStruct + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]*ComposedStruct{ + {StructWithTags: StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedStructDuplicateFields() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedStructWithDuplicateFields struct { + StructWithTags + Address string `db:"other_address"` + Name string `db:"other_name"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -222,26 +282,36 @@ func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedStructDuplicateFiel e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []*TestComposedDuplicateFieldsItem - assert.NoError(t, e.ScanStructs(&composed)) - assert.Equal(t, []*TestComposedDuplicateFieldsItem{ + var composed []*ComposedStructWithDuplicateFields + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]*ComposedStructWithDuplicateFields{ { - TestCrudActionItem: TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, - Address: "111 Test Addr Other", - Name: "Test1 Other", + StructWithTags: StructWithTags{Address: "111 Test Addr", Name: "Test1"}, + Address: "111 Test Addr Other", + Name: "Test1 Other", }, { - TestCrudActionItem: TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, - Address: "211 Test Addr Other", - Name: "Test2 Other", + StructWithTags: StructWithTags{Address: "211 Test Addr", Name: "Test2"}, + Address: "211 Test Addr Other", + Name: "Test2 Other", }, }, composed) } func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedPointerDuplicateFields() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithWithPointerWithDuplicateFields struct { + *StructWithTags + Address string `db:"other_address"` + Name string `db:"other_name"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -250,26 +320,66 @@ func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedPointerDuplicateFie e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []*TestComposedPointerDuplicateFieldsItem - assert.NoError(t, e.ScanStructs(&composed)) - assert.Equal(t, []*TestComposedPointerDuplicateFieldsItem{ + var composed []*ComposedWithWithPointerWithDuplicateFields + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]*ComposedWithWithPointerWithDuplicateFields{ { - TestCrudActionItem: &TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, - Address: "111 Test Addr Other", - Name: "Test1 Other", + StructWithTags: &StructWithTags{Address: "111 Test Addr", Name: "Test1"}, + Address: "111 Test Addr Other", + Name: "Test1 Other", }, { - TestCrudActionItem: &TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, - Address: "211 Test Addr Other", - Name: "Test2 Other", + StructWithTags: &StructWithTags{Address: "211 Test Addr", Name: "Test2"}, + Address: "211 Test Addr Other", + Name: "Test2 Other", }, }, composed) } +func (cet *crudExecTest) TestScanStructs_withIgnoredEmbeddedPointerStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedIgnoredPointerStruct struct { + *StructWithTags `db:"-"` + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + db, mock, err := sqlmock.New() + cet.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). + FromCSVString("111-111-1111,20\n222-222-2222,30")) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedIgnoredPointerStruct + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]ComposedIgnoredPointerStruct{ + {StructWithTags: &StructWithTags{}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: &StructWithTags{}, PhoneNumber: "222-222-2222", Age: 30}, + }, composed) +} + func (cet *crudExecTest) TestScanStructs_withEmbeddedStructPointer() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -278,18 +388,27 @@ func (cet *crudExecTest) TestScanStructs_withEmbeddedStructPointer() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []TestEmbeddedPtrCrudActionItem - assert.NoError(t, e.ScanStructs(&composed)) - assert.Equal(t, []TestEmbeddedPtrCrudActionItem{ - {TestCrudActionItem: &TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: &TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []ComposedWithPointerStruct + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: &StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedStructPointer() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -298,45 +417,57 @@ func (cet *crudExecTest) TestScanStructs_pointersWithEmbeddedStructPointer() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []*TestEmbeddedPtrCrudActionItem - assert.NoError(t, e.ScanStructs(&composed)) - assert.Equal(t, []*TestEmbeddedPtrCrudActionItem{ - {TestCrudActionItem: &TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: &TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []*ComposedWithPointerStruct + cet.NoError(e.ScanStructs(&composed)) + cet.Equal([]*ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: &StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructs_badValue() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + db, _, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionItem - assert.Equal(t, errUnsupportedScanStructsType, e.ScanStructs(items)) - assert.Equal(t, errUnsupportedScanStructsType, e.ScanStructs(&TestCrudActionItem{})) + var items []StructWithTags + cet.Equal(errUnsupportedScanStructsType, e.ScanStructs(items)) + cet.Equal(errUnsupportedScanStructsType, e.ScanStructs(&StructWithTags{})) } func (cet *crudExecTest) TestScanStructs_queryError() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionItem - assert.EqualError(t, e.ScanStructs(&items), "queryExecutor error") + var items []StructWithTags + cet.EqualError(e.ScanStructs(&items), "queryExecutor error") } func (cet *crudExecTest) TestScanStructsContext_withTaggedFields() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -345,19 +476,23 @@ func (cet *crudExecTest) TestScanStructsContext_withTaggedFields() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionItem - assert.NoError(t, e.ScanStructsContext(ctx, &items)) - assert.Equal(t, []TestCrudActionItem{ + var items []StructWithTags + cet.NoError(e.ScanStructsContext(ctx, &items)) + cet.Equal([]StructWithTags{ {Address: "111 Test Addr", Name: "Test1"}, {Address: "211 Test Addr", Name: "Test2"}, }, items) } func (cet *crudExecTest) TestScanStructsContext_withUntaggedFields() { - t := cet.T() + type StructWithNoTags struct { + Address string + Name string + } + ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -366,19 +501,22 @@ func (cet *crudExecTest) TestScanStructsContext_withUntaggedFields() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionNoTagsItem - assert.NoError(t, e.ScanStructsContext(ctx, &items)) - assert.Equal(t, []TestCrudActionNoTagsItem{ + var items []StructWithNoTags + cet.NoError(e.ScanStructsContext(ctx, &items)) + cet.Equal([]StructWithNoTags{ {Address: "111 Test Addr", Name: "Test1"}, {Address: "211 Test Addr", Name: "Test2"}, }, items) } func (cet *crudExecTest) TestScanStructsContext_withPointerFields() { - t := cet.T() + type StructWithPointerFields struct { + Address *string + Name *string + } ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -387,19 +525,23 @@ func (cet *crudExecTest) TestScanStructsContext_withPointerFields() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionWithPointerField - assert.NoError(t, e.ScanStructsContext(ctx, &items)) - assert.Equal(t, []TestCrudActionWithPointerField{ + var items []StructWithPointerFields + cet.NoError(e.ScanStructsContext(ctx, &items)) + cet.Equal([]StructWithPointerFields{ {Address: &testAddr1, Name: &testName1}, {Address: &testAddr2, Name: &testName2}, }, items) } func (cet *crudExecTest) TestScanStructsContext_pointers() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -408,19 +550,27 @@ func (cet *crudExecTest) TestScanStructsContext_pointers() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []*TestCrudActionItem - assert.NoError(t, e.ScanStructsContext(ctx, &items)) - assert.Equal(t, []*TestCrudActionItem{ + var items []*StructWithTags + cet.NoError(e.ScanStructsContext(ctx, &items)) + cet.Equal([]*StructWithTags{ {Address: "111 Test Addr", Name: "Test1"}, {Address: "211 Test Addr", Name: "Test2"}, }, items) } func (cet *crudExecTest) TestScanStructsContext_withEmbeddedStruct() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -429,19 +579,58 @@ func (cet *crudExecTest) TestScanStructsContext_withEmbeddedStruct() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []TestComposedCrudActionItem - assert.NoError(t, e.ScanStructsContext(ctx, &composed)) - assert.Equal(t, []TestComposedCrudActionItem{ - {TestCrudActionItem: TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []ComposedStruct + cet.NoError(e.ScanStructsContext(ctx, &composed)) + cet.Equal([]ComposedStruct{ + {StructWithTags: StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + }, composed) +} + +func (cet *crudExecTest) TestScanStructsContext_withIgnoredEmbeddedStruct() { + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedIgnoredStruct struct { + StructWithTags `db:"-"` + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + + ctx := context.Background() + db, mock, err := sqlmock.New() + cet.NoError(err) + + mock.ExpectQuery(`SELECT \* FROM "items"`). + WithArgs(). + WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). + FromCSVString("111-111-1111,20\n222-222-2222,30")) + + e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) + + var composed []ComposedIgnoredStruct + cet.NoError(e.ScanStructsContext(ctx, &composed)) + cet.Equal([]ComposedIgnoredStruct{ + {StructWithTags: StructWithTags{}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: StructWithTags{}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructsContext_pointersWithEmbeddedStruct() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -450,19 +639,29 @@ func (cet *crudExecTest) TestScanStructsContext_pointersWithEmbeddedStruct() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []*TestComposedCrudActionItem - assert.NoError(t, e.ScanStructsContext(ctx, &composed)) - assert.Equal(t, []*TestComposedCrudActionItem{ - {TestCrudActionItem: TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []*ComposedStruct + cet.NoError(e.ScanStructsContext(ctx, &composed)) + cet.Equal([]*ComposedStruct{ + {StructWithTags: StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructsContext_withEmbeddedStructPointer() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -471,19 +670,29 @@ func (cet *crudExecTest) TestScanStructsContext_withEmbeddedStructPointer() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []TestEmbeddedPtrCrudActionItem - assert.NoError(t, e.ScanStructsContext(ctx, &composed)) - assert.Equal(t, []TestEmbeddedPtrCrudActionItem{ - {TestCrudActionItem: &TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: &TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []ComposedWithPointerStruct + cet.NoError(e.ScanStructsContext(ctx, &composed)) + cet.Equal([]ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: &StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructsContext_pointersWithEmbeddedStructPointer() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). @@ -492,46 +701,74 @@ func (cet *crudExecTest) TestScanStructsContext_pointersWithEmbeddedStructPointe e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var composed []*TestEmbeddedPtrCrudActionItem - assert.NoError(t, e.ScanStructsContext(ctx, &composed)) - assert.Equal(t, []*TestEmbeddedPtrCrudActionItem{ - {TestCrudActionItem: &TestCrudActionItem{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, - {TestCrudActionItem: &TestCrudActionItem{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, + var composed []*ComposedWithPointerStruct + cet.NoError(e.ScanStructsContext(ctx, &composed)) + cet.Equal([]*ComposedWithPointerStruct{ + {StructWithTags: &StructWithTags{Address: "111 Test Addr", Name: "Test1"}, PhoneNumber: "111-111-1111", Age: 20}, + {StructWithTags: &StructWithTags{Address: "211 Test Addr", Name: "Test2"}, PhoneNumber: "222-222-2222", Age: 30}, }, composed) } func (cet *crudExecTest) TestScanStructsContext_badValue() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + ctx := context.Background() db, _, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionItem - assert.Equal(t, errUnsupportedScanStructsType, e.ScanStructsContext(ctx, items)) - assert.Equal(t, errUnsupportedScanStructsType, e.ScanStructsContext(ctx, &TestCrudActionItem{})) + var items []StructWithTags + cet.Equal(errUnsupportedScanStructsType, e.ScanStructsContext(ctx, items)) + cet.Equal(errUnsupportedScanStructsType, e.ScanStructsContext(ctx, &StructWithTags{})) } func (cet *crudExecTest) TestScanStructsContext_queryError() { - t := cet.T() + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + ctx := context.Background() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var items []TestCrudActionItem - assert.EqualError(t, e.ScanStructsContext(ctx, &items), "queryExecutor error") + var items []StructWithTags + cet.EqualError(e.ScanStructsContext(ctx, &items), "queryExecutor error") } func (cet *crudExecTest) TestScanStruct() { - t := cet.T() + type StructWithNoTags struct { + Address string + Name string + } + + type StructWithTags struct { + Address string `db:"address"` + Name string `db:"name"` + } + + type ComposedStruct struct { + StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + type ComposedWithPointerStruct struct { + *StructWithTags + PhoneNumber string `db:"phone_number"` + Age int64 `db:"age"` + } + db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) @@ -556,54 +793,53 @@ func (cet *crudExecTest) TestScanStruct() { e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) - var slicePtr []TestCrudActionItem - var item TestCrudActionItem + var slicePtr []StructWithTags + var item StructWithTags found, err := e.ScanStruct(item) - assert.Equal(t, errUnsupportedScanStructType, err) - assert.False(t, found) + cet.Equal(errUnsupportedScanStructType, err) + cet.False(found) found, err = e.ScanStruct(&slicePtr) - assert.Equal(t, errUnsupportedScanStructType, err) - assert.False(t, found) + cet.Equal(errUnsupportedScanStructType, err) + cet.False(found) found, err = e.ScanStruct(&item) - assert.EqualError(t, err, "queryExecutor error") - assert.False(t, found) + cet.EqualError(err, "queryExecutor error") + cet.False(found) found, err = e.ScanStruct(&item) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, item.Address, "111 Test Addr") - assert.Equal(t, item.Name, "Test1") + cet.NoError(err) + cet.True(found) + cet.Equal(item.Address, "111 Test Addr") + cet.Equal(item.Name, "Test1") - var composed TestComposedCrudActionItem + var composed ComposedStruct found, err = e.ScanStruct(&composed) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, composed.Address, "111 Test Addr") - assert.Equal(t, composed.Name, "Test1") - assert.Equal(t, composed.PhoneNumber, "111-111-1111") - assert.Equal(t, composed.Age, int64(20)) - - var embeddedPtr TestEmbeddedPtrCrudActionItem + cet.NoError(err) + cet.True(found) + cet.Equal(composed.Address, "111 Test Addr") + cet.Equal(composed.Name, "Test1") + cet.Equal(composed.PhoneNumber, "111-111-1111") + cet.Equal(composed.Age, int64(20)) + + var embeddedPtr ComposedWithPointerStruct found, err = e.ScanStruct(&embeddedPtr) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, embeddedPtr.Address, "111 Test Addr") - assert.Equal(t, embeddedPtr.Name, "Test1") - assert.Equal(t, embeddedPtr.PhoneNumber, "111-111-1111") - assert.Equal(t, embeddedPtr.Age, int64(20)) - - var noTag TestCrudActionNoTagsItem + cet.NoError(err) + cet.True(found) + cet.Equal(embeddedPtr.Address, "111 Test Addr") + cet.Equal(embeddedPtr.Name, "Test1") + cet.Equal(embeddedPtr.PhoneNumber, "111-111-1111") + cet.Equal(embeddedPtr.Age, int64(20)) + + var noTag StructWithNoTags found, err = e.ScanStruct(&noTag) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, noTag.Address, "111 Test Addr") - assert.Equal(t, noTag.Name, "Test1") + cet.NoError(err) + cet.True(found) + cet.Equal(noTag.Address, "111 Test Addr") + cet.Equal(noTag.Name, "Test1") } func (cet *crudExecTest) TestScanVals() { - t := cet.T() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT "id" FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) @@ -620,24 +856,23 @@ func (cet *crudExecTest) TestScanVals() { var id int64 var ids []int64 - assert.Equal(t, errUnsupportedScanValsType, e.ScanVals(ids)) - assert.Equal(t, errUnsupportedScanValsType, e.ScanVals(&id)) - assert.EqualError(t, e.ScanVals(&ids), "queryExecutor error") + cet.Equal(errUnsupportedScanValsType, e.ScanVals(ids)) + cet.Equal(errUnsupportedScanValsType, e.ScanVals(&id)) + cet.EqualError(e.ScanVals(&ids), "queryExecutor error") - assert.NoError(t, e.ScanVals(&ids)) - assert.Equal(t, ids, []int64{1, 2}) + cet.NoError(e.ScanVals(&ids)) + cet.Equal(ids, []int64{1, 2}) var pointers []*int64 - assert.NoError(t, e.ScanVals(&pointers)) - assert.Len(t, pointers, 2) - assert.Equal(t, *pointers[0], int64(1)) - assert.Equal(t, *pointers[1], int64(2)) + cet.NoError(e.ScanVals(&pointers)) + cet.Len(pointers, 2) + cet.Equal(*pointers[0], int64(1)) + cet.Equal(*pointers[1], int64(2)) } func (cet *crudExecTest) TestScanVal() { - t := cet.T() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT "id" FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) @@ -651,26 +886,25 @@ func (cet *crudExecTest) TestScanVal() { var id int64 var ids []int64 found, err := e.ScanVal(id) - assert.Equal(t, errScanValPointer, err) - assert.False(t, found) + cet.Equal(errScanValPointer, err) + cet.False(found) found, err = e.ScanVal(&ids) - assert.Equal(t, errScanValNonSlice, err) - assert.False(t, found) + cet.Equal(errScanValNonSlice, err) + cet.False(found) found, err = e.ScanVal(&id) - assert.EqualError(t, err, "queryExecutor error") - assert.False(t, found) + cet.EqualError(err, "queryExecutor error") + cet.False(found) var ptrID int64 found, err = e.ScanVal(&ptrID) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, ptrID, int64(1)) + cet.NoError(err) + cet.True(found) + cet.Equal(ptrID, int64(1)) } func (cet *crudExecTest) TestScanVal_withByteSlice() { - t := cet.T() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WithArgs(). @@ -680,19 +914,18 @@ func (cet *crudExecTest) TestScanVal_withByteSlice() { var bytes []byte found, err := e.ScanVal(bytes) - assert.Equal(t, errScanValPointer, err) - assert.False(t, found) + cet.Equal(errScanValPointer, err) + cet.False(found) found, err = e.ScanVal(&bytes) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, []byte("byte slice result"), bytes) + cet.NoError(err) + cet.True(found) + cet.Equal([]byte("byte slice result"), bytes) } func (cet *crudExecTest) TestScanVal_withRawBytes() { - t := cet.T() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WithArgs(). @@ -702,13 +935,13 @@ func (cet *crudExecTest) TestScanVal_withRawBytes() { var bytes sql.RawBytes found, err := e.ScanVal(bytes) - assert.Equal(t, errScanValPointer, err) - assert.False(t, found) + cet.Equal(errScanValPointer, err) + cet.False(found) found, err = e.ScanVal(&bytes) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, sql.RawBytes("byte slice result"), bytes) + cet.NoError(err) + cet.True(found) + cet.Equal(sql.RawBytes("byte slice result"), bytes) } type JSONBoolArray []bool @@ -718,9 +951,8 @@ func (b *JSONBoolArray) Scan(src interface{}) error { } func (cet *crudExecTest) TestScanVal_withValuerSlice() { - t := cet.T() db, mock, err := sqlmock.New() - assert.NoError(t, err) + cet.NoError(err) mock.ExpectQuery(`SELECT "bools" FROM "items"`). WithArgs(). @@ -730,13 +962,13 @@ func (cet *crudExecTest) TestScanVal_withValuerSlice() { var bools JSONBoolArray found, err := e.ScanVal(bools) - assert.Equal(t, errScanValPointer, err) - assert.False(t, found) + cet.Equal(errScanValPointer, err) + cet.False(found) found, err = e.ScanVal(&bools) - assert.NoError(t, err) - assert.True(t, found) - assert.Equal(t, JSONBoolArray{true, false, true}, bools) + cet.NoError(err) + cet.True(found) + cet.Equal(JSONBoolArray{true, false, true}, bools) } func TestCrudExecSuite(t *testing.T) { diff --git a/exp/exp.go b/exp/exp.go index dcc9f041..3bd69160 100644 --- a/exp/exp.go +++ b/exp/exp.go @@ -128,9 +128,7 @@ type ( ) type ( - // Alternative to writing map[string]interface{}. Can be used for Inserts, Updates or Deletes - Record map[string]interface{} - Vals []interface{} + Vals []interface{} // Parent of all expression types Expression interface { Clone() Expression diff --git a/exp/insert.go b/exp/insert.go index 6799bbf8..e5136171 100644 --- a/exp/insert.go +++ b/exp/insert.go @@ -85,6 +85,9 @@ func newInsert(rows ...interface{}) (insertExp InsertExpression, err error) { rowValue := reflect.Indirect(reflect.ValueOf(rows[0])) rowType := rowValue.Type() rowKind := rowValue.Kind() + if rowKind == reflect.Struct { + return createStructSliceInsert(rows...) + } vals := make([][]interface{}, len(rows)) var columns ColumnListExpression for i, row := range rows { @@ -119,15 +122,6 @@ func newInsert(rows ...interface{}) (insertExp InsertExpression, err error) { rowVals[j] = newRowValue.MapIndex(key).Interface() } vals[i] = rowVals - case reflect.Struct: - rowCols, rowVals, err := getFieldsValues(newRowValue) - if err != nil { - return nil, err - } - if columns == nil { - columns = NewColumnListExpression(rowCols...) - } - vals[i] = rowVals default: return nil, errors.New( "unsupported insert must be map, goqu.Record, or struct type got: %T", @@ -138,25 +132,31 @@ func newInsert(rows ...interface{}) (insertExp InsertExpression, err error) { return &insert{cols: columns, vals: vals}, nil } -func getFieldsValues(value reflect.Value) (rowCols, rowVals []interface{}, err error) { - if value.IsValid() { - cm, err := util.GetColumnMap(value.Interface()) - if err != nil { - return rowCols, rowVals, err +func createStructSliceInsert(rows ...interface{}) (insertExp InsertExpression, err error) { + rowValue := reflect.Indirect(reflect.ValueOf(rows[0])) + rowType := rowValue.Type() + recordRows := make([]interface{}, 0, len(rows)) + for _, row := range rows { + if rowType != reflect.Indirect(reflect.ValueOf(row)).Type() { + return nil, errors.New( + "rows must be all the same type expected %+v got %+v", + rowType, + reflect.Indirect(reflect.ValueOf(row)).Type(), + ) } - cols := cm.Cols() - for _, col := range cols { - f := cm[col] - if f.ShouldInsert { - v := value.FieldByIndex(f.FieldIndex) - rowCols = append(rowCols, col) - if f.DefaultIfEmpty && util.IsEmptyValue(v) { - rowVals = append(rowVals, Default()) - } else { - rowVals = append(rowVals, v.Interface()) - } - } + newRowValue := reflect.Indirect(reflect.ValueOf(row)) + record, err := getFieldsValuesFromStruct(newRowValue) + if err != nil { + return nil, err } + recordRows = append(recordRows, record) + } + return newInsert(recordRows...) +} + +func getFieldsValuesFromStruct(value reflect.Value) (row Record, err error) { + if value.IsValid() { + return NewRecordFromStruct(value.Interface(), true, false) } - return rowCols, rowVals, nil + return } diff --git a/exp/insert_test.go b/exp/insert_test.go index 748e74cc..d0b315ef 100644 --- a/exp/insert_test.go +++ b/exp/insert_test.go @@ -3,7 +3,6 @@ package exp import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) @@ -35,7 +34,6 @@ type insertExpressionTestSuite struct { } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentRecordTypes() { - t := iets.T() type testRecord struct { C string `db:"c"` } @@ -46,98 +44,156 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentReco testRecord{C: "v1"}, Record{"c": "v2"}, ) - assert.EqualError(t, err, "goqu: rows must be all the same type expected exp.testRecord got exp.Record") + iets.EqualError(err, "goqu: rows must be all the same type expected exp.testRecord got exp.Record") _, err = NewInsertExpression( testRecord{C: "v1"}, testRecord2{C: "v2"}, ) - assert.EqualError(t, err, "goqu: rows must be all the same type expected exp.testRecord got exp.testRecord2") + iets.EqualError(err, "goqu: rows must be all the same type expected exp.testRecord got exp.testRecord2") } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withInvalidValue() { - t := iets.T() _, err := NewInsertExpression(true) - assert.EqualError(t, err, "goqu: unsupported insert must be map, goqu.Record, or struct type got: bool") + iets.EqualError(err, "goqu: unsupported insert must be map, goqu.Record, or struct type got: bool") +} +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentTypes() { + _, err := NewInsertExpression(Record{"a": "a1"}, true) + iets.EqualError(err, "goqu: rows must be all the same type expected exp.Record got bool") } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withNoValues() { - t := iets.T() ie, err := NewInsertExpression() - assert.NoError(t, err) + iets.NoError(err) eie := new(insert) - assert.Equal(t, eie, ie) - assert.True(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.True(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } -func (iets *insertExpressionTestSuite) TestNewInsertExpression_appendableExpression() { - t := iets.T() +func (iets *insertExpressionTestSuite) TestNewInsertExpression_Vals() { + ie, err := NewInsertExpression() + iets.NoError(err) + vals := [][]interface{}{ + {"a", "b"}, + } + ie = ie.SetCols(NewColumnListExpression("a", "b")).SetVals(vals) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) + iets.Equal(vals, ie.Vals()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_Cols() { + ie, err := NewInsertExpression() + iets.NoError(err) + vals := [][]interface{}{ + {"a", "b"}, + } + ce := NewColumnListExpression("a", "b") + ie = ie.SetCols(ce).SetVals(vals) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) + iets.Equal(vals, ie.Vals()) + iets.Equal(ce, ie.Cols()) +} +func (iets *insertExpressionTestSuite) TestNewInsertExpression_From() { + ae := newTestAppendableExpression("select * from test", []interface{}{}) + ie, err := NewInsertExpression(ae) + iets.NoError(err) + iets.False(ie.IsEmpty()) + iets.True(ie.IsInsertFrom()) + iets.Equal(ae, ie.From()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_appendableExpression() { ae := newTestAppendableExpression("test ae", nil) ie, err := NewInsertExpression(ae) - assert.NoError(t, err) + iets.NoError(err) eie := &insert{from: ae} - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.True(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.True(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecords() { - t := iets.T() ie, err := NewInsertExpression(Record{"c": "a"}, Record{"c": "b"}) - assert.NoError(t, err) + iets.NoError(err) + eie := new(insert). + SetCols(NewColumnListExpression("c")). + SetVals([][]interface{}{{"a"}, {"b"}}) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecordsSlice() { + ie, err := NewInsertExpression([]Record{{"c": "a"}, {"c": "b"}}) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("c")). SetVals([][]interface{}{{"a"}, {"b"}}) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecordOfDifferentLength() { - t := iets.T() _, err := NewInsertExpression(Record{"c": "a"}, Record{"c": "b", "c2": "d"}) - assert.EqualError(t, err, "goqu: rows with different value length expected 1 got 2") + iets.EqualError(err, "goqu: rows with different value length expected 1 got 2") } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withRecordWithDifferentkeys() { - t := iets.T() _, err := NewInsertExpression(Record{"c1": "a"}, Record{"c2": "b"}) - assert.EqualError(t, err, `goqu: rows with different keys expected ["c1"] got ["c2"]`) + iets.EqualError(err, `goqu: rows with different keys expected ["c1"] got ["c2"]`) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withMap() { - t := iets.T() ie, err := NewInsertExpression( map[string]interface{}{"c": "a"}, map[string]interface{}{"c": "b"}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("c")). SetVals([][]interface{}{{"a"}, {"b"}}) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructs() { type testRecord struct { C string `db:"c"` } - t := iets.T() ie, err := NewInsertExpression( testRecord{C: "a"}, testRecord{C: "b"}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("c")). SetVals([][]interface{}{{"a"}, {"b"}}) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructSlice() { + type testRecord struct { + C string `db:"c"` + } + ie, err := NewInsertExpression([]testRecord{ + {C: "a"}, + {C: "b"}, + }) + iets.NoError(err) + eie := new(insert). + SetCols(NewColumnListExpression("c")). + SetVals([][]interface{}{{"a"}, {"b"}}) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithoutTags() { @@ -146,18 +202,17 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithou FieldB bool FieldC string } - t := iets.T() ie, err := NewInsertExpression( testRecord{FieldA: 1, FieldB: true, FieldC: "a"}, testRecord{FieldA: 2, FieldB: false, FieldC: "b"}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("fielda", "fieldb", "fieldc")). SetVals([][]interface{}{{int64(1), true, "a"}, {int64(2), false, "b"}}) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsIgnoredDbTag() { @@ -166,18 +221,17 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsIgnore FieldB bool FieldC string } - t := iets.T() ie, err := NewInsertExpression( testRecord{FieldA: 1, FieldB: true, FieldC: "a"}, testRecord{FieldA: 2, FieldB: false, FieldC: "b"}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("fieldb", "fieldc")). SetVals([][]interface{}{{true, "a"}, {false, "b"}}) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithGoquSkipInsert() { @@ -186,36 +240,34 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithGo FieldB bool `goqu:"skipupdate"` FieldC string `goqu:"skipinsert"` } - t := iets.T() ie, err := NewInsertExpression( testRecord{FieldA: 1, FieldB: true, FieldC: "a"}, testRecord{FieldA: 2, FieldB: false, FieldC: "b"}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("fielda", "fieldb")). SetVals([][]interface{}{{int64(1), true}, {int64(2), false}}) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructPointers() { type testRecord struct { C string `db:"c"` } - t := iets.T() ie, err := NewInsertExpression( &testRecord{C: "a"}, &testRecord{C: "b"}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("c")). SetVals([][]interface{}{{"a"}, {"b"}}) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEmbeddedStructs() { @@ -228,14 +280,13 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEm Address string `db:"address"` Name string `db:"name"` } - t := iets.T() ie, err := NewInsertExpression( item{Address: "111 Test Addr", Name: "Test1", Phone: Phone{Home: "123123", Primary: "456456"}}, item{Address: "211 Test Addr", Name: "Test2", Phone: Phone{Home: "123123", Primary: "456456"}}, item{Address: "311 Test Addr", Name: "Test3", Phone: Phone{Home: "123123", Primary: "456456"}}, item{Address: "411 Test Addr", Name: "Test4", Phone: Phone{Home: "123123", Primary: "456456"}}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("address", "home_phone", "name", "primary_phone")). SetVals([][]interface{}{ @@ -244,9 +295,9 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEm {"311 Test Addr", "123123", "Test3", "456456"}, {"411 Test Addr", "123123", "Test4", "456456"}, }) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) } func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEmbeddedStructPointers() { @@ -259,14 +310,13 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEm Address string `db:"address"` Name string `db:"name"` } - t := iets.T() ie, err := NewInsertExpression( item{Address: "111 Test Addr", Name: "Test1", Phone: &Phone{Home: "123123", Primary: "456456"}}, item{Address: "211 Test Addr", Name: "Test2", Phone: &Phone{Home: "123123", Primary: "456456"}}, item{Address: "311 Test Addr", Name: "Test3", Phone: &Phone{Home: "123123", Primary: "456456"}}, item{Address: "411 Test Addr", Name: "Test4", Phone: &Phone{Home: "123123", Primary: "456456"}}, ) - assert.NoError(t, err) + iets.NoError(err) eie := new(insert). SetCols(NewColumnListExpression("address", "home_phone", "name", "primary_phone")). SetVals([][]interface{}{ @@ -275,9 +325,81 @@ func (iets *insertExpressionTestSuite) TestNewInsertExpression_withStructsWithEm {"311 Test Addr", "123123", "Test3", "456456"}, {"411 Test Addr", "123123", "Test4", "456456"}, }) - assert.Equal(t, eie, ie) - assert.False(t, ie.IsEmpty()) - assert.False(t, ie.IsInsertFrom()) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withNilEmbeddedStructPointers() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1"}, + item{Address: "211 Test Addr", Name: "Test2"}, + item{Address: "311 Test Addr", Name: "Test3"}, + item{Address: "411 Test Addr", Name: "Test4"}, + ) + iets.NoError(err) + eie := new(insert). + SetCols(NewColumnListExpression("address", "name")). + SetVals([][]interface{}{ + {"111 Test Addr", "Test1"}, + {"211 Test Addr", "Test2"}, + {"311 Test Addr", "Test3"}, + {"411 Test Addr", "Test4"}, + }) + iets.Equal(eie, ie) + iets.False(ie.IsEmpty()) + iets.False(ie.IsInsertFrom()) +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentStructTypes() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + _, err := NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1"}, + Phone{Home: "123123", Primary: "456456"}, + item{Address: "311 Test Addr", Name: "Test3"}, + Phone{Home: "123123", Primary: "456456"}, + ) + iets.EqualError(err, "goqu: rows must be all the same type expected exp.item got exp.Phone") +} + +func (iets *insertExpressionTestSuite) TestNewInsertExpression_withDifferentColumnLengths() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type Phone2 struct { + Primary string `db:"primary_phone2"` + Home string `db:"home_phone2"` + } + type item struct { + *Phone + *Phone2 + Address string `db:"address"` + Name string `db:"name"` + } + _, err := NewInsertExpression( + item{Address: "111 Test Addr", Name: "Test1", Phone2: &Phone2{Home: "123123", Primary: "456456"}}, + item{Address: "311 Test Addr", Name: "Test3", Phone: &Phone{Home: "123123", Primary: "456456"}}, + ) + iets.EqualError(err, `goqu: rows with different keys expected `+ + `["address","home_phone2","name","primary_phone2"] got ["address","home_phone","name","primary_phone"]`) } func TestInsertExpressionSuite(t *testing.T) { diff --git a/exp/record.go b/exp/record.go new file mode 100644 index 00000000..d2bdbd40 --- /dev/null +++ b/exp/record.go @@ -0,0 +1,64 @@ +package exp + +import ( + "reflect" + "sort" + + "github.com/doug-martin/goqu/v8/internal/util" +) + +// Alternative to writing map[string]interface{}. Can be used for Inserts, Updates or Deletes +type Record map[string]interface{} + +func (r Record) Cols() []string { + cols := make([]string, 0, len(r)) + for col := range r { + cols = append(cols, col) + } + sort.Strings(cols) + return cols +} + +func NewRecordFromStruct(i interface{}, forInsert, forUpdate bool) (r Record, err error) { + value := reflect.ValueOf(i) + if value.IsValid() { + cm, err := util.GetColumnMap(value.Interface()) + if err != nil { + return nil, err + } + cols := cm.Cols() + r = make(map[string]interface{}) + for _, col := range cols { + f := cm[col] + switch { + case forInsert: + if f.ShouldInsert { + addFieldToRecord(r, value, f) + } + case forUpdate: + if f.ShouldUpdate { + addFieldToRecord(r, value, f) + } + default: + addFieldToRecord(r, value, f) + } + } + } + return +} + +func addFieldToRecord(r Record, val reflect.Value, f util.ColumnData) Record { + v, isAvailable := util.SafeGetFieldByIndex(val, f.FieldIndex) + if !isAvailable { + return r + } + switch { + case f.DefaultIfEmpty && util.IsEmptyValue(v): + r[f.ColumnName] = Default() + case v.IsValid(): + r[f.ColumnName] = v.Interface() + default: + r[f.ColumnName] = reflect.Zero(f.GoType).Interface() + } + return r +} diff --git a/exp/update.go b/exp/update.go index 25b3734e..84cada92 100644 --- a/exp/update.go +++ b/exp/update.go @@ -41,21 +41,13 @@ func NewUpdateExpressions(update interface{}) (updates []UpdateExpression, err e } func getUpdateExpressionsStruct(value reflect.Value) (updates []UpdateExpression, err error) { - cm, err := util.GetColumnMap(value.Interface()) + r, err := NewRecordFromStruct(value.Interface(), false, true) if err != nil { return updates, err } - cols := cm.Cols() + cols := r.Cols() for _, col := range cols { - f := cm[col] - if f.ShouldUpdate { - v := value.FieldByIndex(f.FieldIndex) - setV := v.Interface() - if f.DefaultIfEmpty && util.IsEmptyValue(v) { - setV = Default() - } - updates = append(updates, ParseIdentifier(col).Set(setV)) - } + updates = append(updates, ParseIdentifier(col).Set(r[col])) } return updates, nil } diff --git a/exp/update_test.go b/exp/update_test.go new file mode 100644 index 00000000..bd0e20aa --- /dev/null +++ b/exp/update_test.go @@ -0,0 +1,181 @@ +package exp + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +type updateExpressionTestSuite struct { + suite.Suite +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withInvalidValue() { + _, err := NewUpdateExpressions(true) + uets.EqualError(err, "goqu: unsupported update interface type bool") +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withRecords() { + ie, err := NewUpdateExpressions(Record{"c": "a", "b": "d"}) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("b"), val: "d"}, + update{col: ParseIdentifier("c"), val: "a"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withMap() { + ie, err := NewUpdateExpressions(map[string]interface{}{"c": "a", "b": "d"}) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("b"), val: "d"}, + update{col: ParseIdentifier("c"), val: "a"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructs() { + type testRecord struct { + C string `db:"c"` + B string `db:"b"` + } + ie, err := NewUpdateExpressions(testRecord{C: "a", B: "d"}) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("b"), val: "d"}, + update{col: ParseIdentifier("c"), val: "a"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithoutTags() { + type testRecord struct { + FieldA int64 + FieldB bool + FieldC string + } + ie, err := NewUpdateExpressions(testRecord{FieldA: 1, FieldB: true, FieldC: "a"}) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("fielda"), val: int64(1)}, + update{col: ParseIdentifier("fieldb"), val: true}, + update{col: ParseIdentifier("fieldc"), val: "a"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsIgnoredDbTag() { + type testRecord struct { + FieldA int64 `db:"-"` + FieldB bool + FieldC string + } + ie, err := NewUpdateExpressions(testRecord{FieldA: 1, FieldB: true, FieldC: "a"}) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("fieldb"), val: true}, + update{col: ParseIdentifier("fieldc"), val: "a"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithGoquSkipUpdate() { + type testRecord struct { + FieldA int64 + FieldB bool `goqu:"skipupdate"` + FieldC string `goqu:"skipinsert"` + } + ie, err := NewUpdateExpressions(testRecord{FieldA: 1, FieldB: true, FieldC: "a"}) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("fielda"), val: int64(1)}, + update{col: ParseIdentifier("fieldc"), val: "a"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructPointers() { + type testRecord struct { + C string `db:"c"` + B string `db:"b"` + } + ie, err := NewUpdateExpressions(&testRecord{C: "a", B: "d"}) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("b"), val: "d"}, + update{col: ParseIdentifier("c"), val: "a"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithEmbeddedStructs() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := NewUpdateExpressions( + item{Address: "111 Test Addr", Name: "Test1", Phone: Phone{Home: "123123", Primary: "456456"}}, + ) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("address"), val: "111 Test Addr"}, + update{col: ParseIdentifier("home_phone"), val: "123123"}, + update{col: ParseIdentifier("name"), val: "Test1"}, + update{col: ParseIdentifier("primary_phone"), val: "456456"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withStructsWithEmbeddedStructPointers() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := NewUpdateExpressions( + item{Address: "111 Test Addr", Name: "Test1", Phone: &Phone{Home: "123123", Primary: "456456"}}, + ) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("address"), val: "111 Test Addr"}, + update{col: ParseIdentifier("home_phone"), val: "123123"}, + update{col: ParseIdentifier("name"), val: "Test1"}, + update{col: ParseIdentifier("primary_phone"), val: "456456"}, + } + uets.Equal(eie, ie) +} + +func (uets *updateExpressionTestSuite) TestNewUpdateExpressions_withNilEmbeddedStructPointers() { + type Phone struct { + Primary string `db:"primary_phone"` + Home string `db:"home_phone"` + } + type item struct { + *Phone + Address string `db:"address"` + Name string `db:"name"` + } + ie, err := NewUpdateExpressions( + item{Address: "111 Test Addr", Name: "Test1"}, + ) + uets.NoError(err) + eie := []UpdateExpression{ + update{col: ParseIdentifier("address"), val: "111 Test Addr"}, + update{col: ParseIdentifier("name"), val: "Test1"}, + } + uets.Equal(eie, ie) +} + +func TestUpdateExpressionSuite(t *testing.T) { + suite.Run(t, new(updateExpressionTestSuite)) +} diff --git a/insert_dataset_example_test.go b/insert_dataset_example_test.go index 93237351..8ca71826 100644 --- a/insert_dataset_example_test.go +++ b/insert_dataset_example_test.go @@ -406,6 +406,72 @@ func ExampleInsertDataset_Rows_withGoquDefaultIfEmptyTag() { // INSERT INTO "items" ("address", "name") VALUES ('111 Test Addr', DEFAULT), ('112 Test Addr', 'Test2') [] } +func ExampleInsertDataset_Rows_withEmbeddedStruct() { + type Address struct { + Street string `db:"address_street"` + State string `db:"address_state"` + } + type User struct { + Address + FirstName string + LastName string + } + ds := goqu.Insert("user").Rows( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + User{Address: Address{Street: "211 Street", State: "NY"}, FirstName: "Jimmy", LastName: "Stewart"}, + User{Address: Address{Street: "311 Street", State: "NY"}, FirstName: "Jeff", LastName: "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("address_state", "address_street", "firstname", "lastname") VALUES ('NY', '111 Street', 'Greg', 'Farley'), ('NY', '211 Street', 'Jimmy', 'Stewart'), ('NY', '311 Street', 'Jeff', 'Jeffers') [] +} + +func ExampleInsertDataset_Rows_withIgnoredEmbedded() { + type Address struct { + Street string + State string + } + type User struct { + Address `db:"-"` + FirstName string + LastName string + } + ds := goqu.Insert("user").Rows( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + User{Address: Address{Street: "211 Street", State: "NY"}, FirstName: "Jimmy", LastName: "Stewart"}, + User{Address: Address{Street: "311 Street", State: "NY"}, FirstName: "Jeff", LastName: "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("firstname", "lastname") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + +func ExampleInsertDataset_Rows_withNilEmbeddedPointer() { + type Address struct { + Street string + State string + } + type User struct { + *Address + FirstName string + LastName string + } + ds := goqu.Insert("user").Rows( + User{FirstName: "Greg", LastName: "Farley"}, + User{FirstName: "Jimmy", LastName: "Stewart"}, + User{FirstName: "Jeff", LastName: "Jeffers"}, + ) + insertSQL, args, _ := ds.ToSQL() + fmt.Println(insertSQL, args) + + // Output: + // INSERT INTO "user" ("firstname", "lastname") VALUES ('Greg', 'Farley'), ('Jimmy', 'Stewart'), ('Jeff', 'Jeffers') [] +} + func ExampleInsertDataset_ClearOnConflict() { type item struct { ID uint32 `db:"id" goqu:"skipinsert"` diff --git a/internal/util/reflect.go b/internal/util/reflect.go index a2963d6a..c726de11 100644 --- a/internal/util/reflect.go +++ b/internal/util/reflect.go @@ -124,6 +124,20 @@ func GetTypeInfo(i interface{}, val reflect.Value) (reflect.Type, reflect.Kind, return t, valKind, isSliceOfPointers } +func SafeGetFieldByIndex(v reflect.Value, fieldIndex []int) (result reflect.Value, isAvailable bool) { + switch len(fieldIndex) { + case 0: + return v, true + case 1: + return v.FieldByIndex(fieldIndex), true + default: + if f := reflect.Indirect(v.Field(fieldIndex[0])); f.IsValid() { + return SafeGetFieldByIndex(f, fieldIndex[1:]) + } + } + return reflect.ValueOf(nil), false +} + type rowData = map[string]interface{} func AssignStructVals(i interface{}, results []rowData, cm ColumnMap) { @@ -195,12 +209,16 @@ func createColumnMap(t reflect.Type, fieldIndex []int) ColumnMap { for i := 0; i < n; i++ { f := t.Field(i) if f.Anonymous && (f.Type.Kind() == reflect.Struct || f.Type.Kind() == reflect.Ptr) { - if f.Type.Kind() == reflect.Ptr { - subColMaps = append(subColMaps, createColumnMap(f.Type.Elem(), append(fieldIndex, f.Index...))) - } else { - subColMaps = append(subColMaps, createColumnMap(f.Type, append(fieldIndex, f.Index...))) + goquTag := tag.New("db", f.Tag) + if !goquTag.Contains("-") { + if f.Type.Kind() == reflect.Ptr { + subColMaps = append(subColMaps, createColumnMap(f.Type.Elem(), append(fieldIndex, f.Index...))) + } else { + subColMaps = append(subColMaps, createColumnMap(f.Type, append(fieldIndex, f.Index...))) + } } - } else { + } else if f.PkgPath == "" { + // if PkgPath is empty then it is an exported field dbTag := tag.New("db", f.Tag) var columnName string if dbTag.IsEmpty() { diff --git a/internal/util/reflect_test.go b/internal/util/reflect_test.go index b1b722d7..dbb50355 100644 --- a/internal/util/reflect_test.go +++ b/internal/util/reflect_test.go @@ -972,6 +972,155 @@ func (rt *reflectTest) TestGetColumnMap_withStructWithEmbeddedStructPointer() { }, cm) } +func (rt *reflectTest) TestGetColumnMap_withIgnoredEmbeddedStruct() { + t := rt.T() + + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + EmbeddedStruct `db:"-"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := GetColumnMap(&ts) + assert.NoError(t, err) + assert.Equal(t, ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withIgnoredEmbeddedPointerStruct() { + t := rt.T() + + type EmbeddedStruct struct { + Str string + } + type TestStruct struct { + *EmbeddedStruct `db:"-"` + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := GetColumnMap(&ts) + assert.NoError(t, err) + assert.Equal(t, ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withPrivateFields() { + t := rt.T() + + type TestStruct struct { + str string // nolint:structcheck,unused + Int int64 + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := GetColumnMap(&ts) + assert.NoError(t, err) + assert.Equal(t, ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{3}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestGetColumnMap_withPrivateEmbeddedFields() { + t := rt.T() + + type TestEmbedded struct { + str string // nolint:structcheck,unused + Int int64 + } + + type TestStruct struct { + TestEmbedded + Bool bool + Valuer *sql.NullString + } + var ts TestStruct + cm, err := GetColumnMap(&ts) + assert.NoError(t, err) + assert.Equal(t, ColumnMap{ + "int": {ColumnName: "int", FieldIndex: []int{0, 1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(int64(1))}, + "bool": {ColumnName: "bool", FieldIndex: []int{1}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(true)}, + "valuer": {ColumnName: "valuer", FieldIndex: []int{2}, ShouldInsert: true, ShouldUpdate: true, GoType: reflect.TypeOf(&sql.NullString{})}, + }, cm) +} + +func (rt *reflectTest) TestSafeGetFieldByIndex() { + type TestEmbedded struct { + FieldA int + } + type TestEmbeddedPointerStruct struct { + *TestEmbedded + FieldB string + } + type TestEmbeddedStruct struct { + TestEmbedded + FieldB string + } + v := reflect.ValueOf(TestEmbeddedPointerStruct{}) + f, isAvailable := SafeGetFieldByIndex(v, []int{0, 0}) + rt.False(isAvailable) + rt.False(f.IsValid()) + f, isAvailable = SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) + + v = reflect.ValueOf(TestEmbeddedPointerStruct{TestEmbedded: &TestEmbedded{}}) + f, isAvailable = SafeGetFieldByIndex(v, []int{0, 0}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.Int, f.Type().Kind()) + f, isAvailable = SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) + + v = reflect.ValueOf(TestEmbeddedStruct{}) + f, isAvailable = SafeGetFieldByIndex(v, []int{0, 0}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.Int, f.Type().Kind()) + f, isAvailable = SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) + + v = reflect.ValueOf(TestEmbeddedStruct{TestEmbedded: TestEmbedded{}}) + f, isAvailable = SafeGetFieldByIndex(v, []int{0, 0}) + rt.True(isAvailable) + rt.True(f.IsValid()) + f, isAvailable = SafeGetFieldByIndex(v, []int{1}) + rt.True(isAvailable) + rt.True(f.IsValid()) + rt.Equal(reflect.String, f.Type().Kind()) + f, isAvailable = SafeGetFieldByIndex(v, []int{}) + rt.True(isAvailable) + rt.Equal(v, f) +} + func TestReflectSuite(t *testing.T) { suite.Run(t, new(reflectTest)) } diff --git a/issues_test.go b/issues_test.go index f89ee81a..564d9e19 100644 --- a/issues_test.go +++ b/issues_test.go @@ -1,7 +1,9 @@ package goqu_test import ( + "sync" "testing" + "time" "github.com/doug-martin/goqu/v8" "github.com/stretchr/testify/assert" @@ -34,6 +36,154 @@ func (gis *githubIssuesSuite) TestIssue49() { assert.Equal(t, `SELECT * FROM "table"`, sql) } +// Test for https://github.com/doug-martin/goqu/issues/118 +func (gis *githubIssuesSuite) TestIssue118_withEmbeddedStructWithoutExportedFields() { + // struct is in a custom package + type SimpleRole struct { + sync.RWMutex + permissions []string // nolint:structcheck,unused + } + + // ..... + + type Role struct { + *SimpleRole + + ID string `json:"id" db:"id" goqu:"skipinsert"` + Key string `json:"key" db:"key"` + Name string `json:"name" db:"name"` + CreatedAt time.Time `json:"-" db:"created_at" goqu:"skipinsert"` + } + + rUser := &Role{ + Key: `user`, + Name: `User role`, + } + + sql, arg, err := goqu.Insert(`rbac_roles`). + Returning(goqu.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal(`INSERT INTO "rbac_roles" ("key", "name") VALUES ('user', 'User role') RETURNING "id"`, sql) + + sql, arg, err = goqu.Update(`rbac_roles`). + Returning(goqu.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `UPDATE "rbac_roles" SET "created_at"='0001-01-01T00:00:00Z',"id"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) + + rUser = &Role{ + SimpleRole: &SimpleRole{}, + Key: `user`, + Name: `User role`, + } + + sql, arg, err = goqu.Insert(`rbac_roles`). + Returning(goqu.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal(`INSERT INTO "rbac_roles" ("key", "name") VALUES ('user', 'User role') RETURNING "id"`, sql) + + sql, arg, err = goqu.Update(`rbac_roles`). + Returning(goqu.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + gis.Equal( + `UPDATE "rbac_roles" SET `+ + `"created_at"='0001-01-01T00:00:00Z',"id"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) + +} + +// Test for https://github.com/doug-martin/goqu/issues/118 +func (gis *githubIssuesSuite) TestIssue118_withNilEmbeddedStructWithExportedFields() { + // struct is in a custom package + type SimpleRole struct { + sync.RWMutex + permissions []string // nolint:structcheck,unused + IDStr string + } + + // ..... + + type Role struct { + *SimpleRole + + ID string `json:"id" db:"id" goqu:"skipinsert"` + Key string `json:"key" db:"key"` + Name string `json:"name" db:"name"` + CreatedAt time.Time `json:"-" db:"created_at" goqu:"skipinsert"` + } + + rUser := &Role{ + Key: `user`, + Name: `User role`, + } + sql, arg, err := goqu.Insert(`rbac_roles`). + Returning(goqu.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal(`INSERT INTO "rbac_roles" ("key", "name") VALUES ('user', 'User role') RETURNING "id"`, sql) + + sql, arg, err = goqu.Update(`rbac_roles`). + Returning(goqu.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal( + `UPDATE "rbac_roles" SET "created_at"='0001-01-01T00:00:00Z',"id"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) + + rUser = &Role{ + SimpleRole: &SimpleRole{}, + Key: `user`, + Name: `User role`, + } + sql, arg, err = goqu.Insert(`rbac_roles`). + Returning(goqu.C(`id`)). + Rows(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal( + `INSERT INTO "rbac_roles" ("idstr", "key", "name") VALUES ('', 'user', 'User role') RETURNING "id"`, + sql, + ) + + sql, arg, err = goqu.Update(`rbac_roles`). + Returning(goqu.C(`id`)). + Set(rUser). + ToSQL() + gis.NoError(err) + gis.Empty(arg) + // it should not insert fields on nil embedded pointers + gis.Equal( + `UPDATE "rbac_roles" SET `+ + `"created_at"='0001-01-01T00:00:00Z',"id"='',"idstr"='',"key"='user',"name"='User role' RETURNING "id"`, + sql, + ) + +} + func TestGithubIssuesSuite(t *testing.T) { suite.Run(t, new(githubIssuesSuite)) } diff --git a/update_dataset_example_test.go b/update_dataset_example_test.go index d6c14914..6015ecc6 100644 --- a/update_dataset_example_test.go +++ b/update_dataset_example_test.go @@ -534,6 +534,66 @@ func ExampleUpdateDataset_Set_withNoTags() { // UPDATE "items" SET "address"='111 Test Addr',"name"='Test' [] } +func ExampleUpdateDataset_Set_withEmbeddedStruct() { + type Address struct { + Street string `db:"address_street"` + State string `db:"address_state"` + } + type User struct { + Address + FirstName string + LastName string + } + ds := goqu.Update("user").Set( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + ) + updateSQL, args, _ := ds.ToSQL() + fmt.Println(updateSQL, args) + + // Output: + // UPDATE "user" SET "address_state"='NY',"address_street"='111 Street',"firstname"='Greg',"lastname"='Farley' [] +} + +func ExampleUpdateDataset_Set_withIgnoredEmbedded() { + type Address struct { + Street string + State string + } + type User struct { + Address `db:"-"` + FirstName string + LastName string + } + ds := goqu.Update("user").Set( + User{Address: Address{Street: "111 Street", State: "NY"}, FirstName: "Greg", LastName: "Farley"}, + ) + updateSQL, args, _ := ds.ToSQL() + fmt.Println(updateSQL, args) + + // Output: + // UPDATE "user" SET "firstname"='Greg',"lastname"='Farley' [] +} + +func ExampleUpdateDataset_Set_withNilEmbeddedPointer() { + type Address struct { + Street string + State string + } + type User struct { + *Address + FirstName string + LastName string + } + ds := goqu.Update("user").Set( + User{FirstName: "Greg", LastName: "Farley"}, + ) + updateSQL, args, _ := ds.ToSQL() + fmt.Println(updateSQL, args) + + // Output: + // UPDATE "user" SET "firstname"='Greg',"lastname"='Farley' [] +} + func ExampleUpdateDataset_ToSQL_prepared() { type item struct { Address string `db:"address"`