package exec import ( "context" "database/sql" "database/sql/driver" "encoding/json" "fmt" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" "github.com/stretchr/testify/suite" ) // anyValueConverter 允许任意类型作为 driver.Value 透传,模拟 DuckDB 等返回 map/struct 的驱动 type anyValueConverter struct{} func (anyValueConverter) ConvertValue(v interface{}) (driver.Value, error) { return v, nil } var ( testAddr1 = "111 Test Addr" testAddr2 = "211 Test Addr" testName1 = "Test1" testName2 = "Test2" testPhone1 = "111-111-1111" testPhone2 = "222-222-2222" testAge1 int64 = 10 testAge2 int64 = 20 testByteSliceContent = "byte slice result" otherAddr1 = "111 Test Addr Other" otherAddr2 = "211 Test Addr Other" otherName1 = "Test1 Other" otherName2 = "Test2 Other" ) type queryExecutorSuite struct { suite.Suite } func (qes *queryExecutorSuite) TestWithError() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } ctx := context.Background() db, _, err := sqlmock.New() qes.NoError(err) expectedErr := fmt.Errorf("crud exec error") e := newQueryExecutor(db, expectedErr, `SELECT * FROM "items"`) var items []StructWithTags qes.EqualError(e.ScanStructs(&items), expectedErr.Error()) qes.EqualError(e.ScanStructsContext(ctx, &items), expectedErr.Error()) found, err := e.ScanStruct(&StructWithTags{}) qes.EqualError(err, expectedErr.Error()) qes.False(found) found, err = e.ScanStructContext(ctx, &StructWithTags{}) qes.EqualError(err, expectedErr.Error()) qes.False(found) var vals []string qes.EqualError(e.ScanVals(&vals), expectedErr.Error()) qes.EqualError(e.ScanValsContext(ctx, &vals), expectedErr.Error()) var val string found, err = e.ScanVal(&val) qes.EqualError(err, expectedErr.Error()) qes.False(found) found, err = e.ScanValContext(ctx, &val) qes.EqualError(err, expectedErr.Error()) qes.False(found) } func (qes *queryExecutorSuite) TestToSQL() { db, _, err := sqlmock.New() qes.NoError(err) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) query, args, err := e.ToSQL() qes.NoError(err) qes.Equal(`SELECT * FROM "items"`, query) qes.Empty(args) } func (qes *queryExecutorSuite) TestScanStructs_withTaggedFields() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithTags qes.NoError(e.ScanStructs(&items)) qes.Equal([]StructWithTags{ {Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}, }, items) } func (qes *queryExecutorSuite) TestScanStructs_withUntaggedFields() { type StructWithNoTags struct { Address string Name string } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2)) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithNoTags qes.NoError(e.ScanStructs(&items)) qes.Equal([]StructWithNoTags{ {Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}, }, items) } func (qes *queryExecutorSuite) TestScanStructs_withPointerFields() { type StructWithPointerFields struct { Str *string Time *time.Time Bool *bool Int *int64 Float *float64 } db, mock, err := sqlmock.New() qes.NoError(err) now := time.Now() str1, str2 := "str1", "str2" t := true var i1, i2 int64 = 1, 2 f1, f2 := 1.1, 2.1 mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"str", "time", "bool", "int", "float"}). AddRow(str1, now, true, i1, f1). AddRow(str2, now, true, i2, f2). AddRow(nil, nil, nil, nil, nil), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithPointerFields qes.NoError(e.ScanStructs(&items)) qes.Equal([]StructWithPointerFields{ {Str: &str1, Time: &now, Bool: &t, Int: &i1, Float: &f1}, {Str: &str2, Time: &now, Bool: &t, Int: &i2, Float: &f2}, {}, }, items) } func (qes *queryExecutorSuite) TestScanStructs_withPrivateFields() { type StructWithPrivateTags struct { private string //nolint:structcheck,unused // need for test Address string `db:"address"` Name string `db:"name"` } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithPrivateTags qes.NoError(e.ScanStructs(&items)) qes.Equal([]StructWithPrivateTags{ {Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}, }, items) } func (qes *queryExecutorSuite) TestScanStructs_pointers() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []*StructWithTags qes.NoError(e.ScanStructs(&items)) qes.Equal([]*StructWithTags{ {Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}, }, items) } func (qes *queryExecutorSuite) 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). AddRow(testPhone1, testAge1).AddRow(testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []ComposedIgnoredStruct qes.NoError(e.ScanStructs(&composed)) qes.Equal([]ComposedIgnoredStruct{ {StructWithTags: StructWithTags{}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: StructWithTags{}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructs_withEmbeddedStruct() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []ComposedStruct qes.NoError(e.ScanStructs(&composed)) qes.Equal([]ComposedStruct{ {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedStruct() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []*ComposedStruct qes.NoError(e.ScanStructs(&composed)) qes.Equal([]*ComposedStruct{ {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedStructDuplicateFields() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "other_address", "other_name"}). AddRow(testAddr1, testName1, otherAddr1, otherName1). AddRow(testAddr2, testName2, otherAddr2, otherName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []*ComposedStructWithDuplicateFields qes.NoError(e.ScanStructs(&composed)) qes.Equal([]*ComposedStructWithDuplicateFields{ { StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, Address: otherAddr1, Name: otherName1, }, { StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, Address: otherAddr2, Name: otherName2, }, }, composed) } func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedPointerDuplicateFields() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "other_address", "other_name"}). AddRow(testAddr1, testName1, otherAddr1, otherName1). AddRow(testAddr2, testName2, otherAddr2, otherName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []*ComposedWithWithPointerWithDuplicateFields qes.NoError(e.ScanStructs(&composed)) qes.Equal([]*ComposedWithWithPointerWithDuplicateFields{ { StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, Address: otherAddr1, Name: otherName1, }, { StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, Address: otherAddr2, Name: otherName2, }, }, composed) } func (qes *queryExecutorSuite) 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). AddRow(testPhone1, testAge1). AddRow(testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []ComposedIgnoredPointerStruct qes.NoError(e.ScanStructs(&composed)) qes.Equal([]ComposedIgnoredPointerStruct{ {PhoneNumber: testPhone1, Age: testAge1}, {PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructs_withEmbeddedStructPointer() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []ComposedWithPointerStruct qes.NoError(e.ScanStructs(&composed)) qes.Equal([]ComposedWithPointerStruct{ {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructs_pointersWithEmbeddedStructPointer() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []*ComposedWithPointerStruct qes.NoError(e.ScanStructs(&composed)) qes.Equal([]*ComposedWithPointerStruct{ {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructs_badValue() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } tests := []struct { name string items interface{} }{ { name: "non-pointer items", items: []StructWithTags{}, }, { name: "non-slice items", items: &StructWithTags{}, }, } for i := range tests { test := tests[i] qes.Run(test.name, func() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1).AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) qes.Equal(errUnsupportedScanStructsType, e.ScanStructs(test.items)) }) } } func (qes *queryExecutorSuite) TestScanStructs_queryError() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithTags qes.EqualError(e.ScanStructs(&items), "queryExecutor error") } func (qes *queryExecutorSuite) TestScanStructsContext_withTaggedFields() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } ctx := context.Background() db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithTags qes.NoError(e.ScanStructsContext(ctx, &items)) qes.Equal([]StructWithTags{ {Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}, }, items) } func (qes *queryExecutorSuite) TestScanStructsContext_withUntaggedFields() { type StructWithNoTags struct { Address string Name string } ctx := context.Background() db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithNoTags qes.NoError(e.ScanStructsContext(ctx, &items)) qes.Equal([]StructWithNoTags{ {Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}, }, items) } func (qes *queryExecutorSuite) TestScanStructsContext_withPointerFields() { type StructWithPointerFields struct { Address *string Name *string } ctx := context.Background() db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithPointerFields qes.NoError(e.ScanStructsContext(ctx, &items)) qes.Equal([]StructWithPointerFields{ {Address: &testAddr1, Name: &testName1}, {Address: &testAddr2, Name: &testName2}, }, items) } func (qes *queryExecutorSuite) TestScanStructsContext_pointers() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } ctx := context.Background() db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1). AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []*StructWithTags qes.NoError(e.ScanStructsContext(ctx, &items)) qes.Equal([]*StructWithTags{ {Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}, }, items) } func (qes *queryExecutorSuite) TestScanStructsContext_withEmbeddedStruct() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []ComposedStruct qes.NoError(e.ScanStructsContext(ctx, &composed)) qes.Equal([]ComposedStruct{ {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"phone_number", "age"}). AddRow(testPhone1, testAge1). AddRow(testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []ComposedIgnoredStruct qes.NoError(e.ScanStructsContext(ctx, &composed)) qes.Equal([]ComposedIgnoredStruct{ {StructWithTags: StructWithTags{}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: StructWithTags{}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructsContext_pointersWithEmbeddedStruct() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []*ComposedStruct qes.NoError(e.ScanStructsContext(ctx, &composed)) qes.Equal([]*ComposedStruct{ {StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructsContext_withEmbeddedStructPointer() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []ComposedWithPointerStruct qes.NoError(e.ScanStructsContext(ctx, &composed)) qes.Equal([]ComposedWithPointerStruct{ {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructsContext_pointersWithEmbeddedStructPointer() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1). AddRow(testAddr2, testName2, testPhone2, testAge2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var composed []*ComposedWithPointerStruct qes.NoError(e.ScanStructsContext(ctx, &composed)) qes.Equal([]*ComposedWithPointerStruct{ {StructWithTags: &StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1}, {StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2}, }, composed) } func (qes *queryExecutorSuite) TestScanStructsContext_badValue() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } tests := []struct { name string items interface{} }{ { name: "non-pointer items", items: []StructWithTags{}, }, { name: "non-slice items", items: &StructWithTags{}, }, } for i := range tests { test := tests[i] qes.Run(test.name, func() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1).AddRow(testAddr2, testName2), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) qes.Equal(errUnsupportedScanStructsType, e.ScanStructsContext(context.Background(), test.items)) }) } } func (qes *queryExecutorSuite) TestScanStructsContext_queryError() { type StructWithTags struct { Address string `db:"address"` Name string `db:"name"` } ctx := context.Background() db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []StructWithTags qes.EqualError(e.ScanStructsContext(ctx, &items), "queryExecutor error") } func (qes *queryExecutorSuite) TestScanStruct() { 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() qes.NoError(err) mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(nil, nil), ) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"})) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}). AddRow(testAddr1, testName1), ) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1), ) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name", "phone_number", "age"}). AddRow(testAddr1, testName1, testPhone1, testAge1), ) mock.ExpectQuery(`SELECT \* FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).AddRow(testAddr1, testName1)) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var slicePtr []StructWithTags var item StructWithTags found, err := e.ScanStruct(item) qes.Equal(errUnsupportedScanStructType, err) qes.False(found) found, err = e.ScanStruct(&slicePtr) qes.Equal(errUnsupportedScanStructType, err) qes.False(found) found, err = e.ScanStruct(&item) qes.EqualError(err, "queryExecutor error") qes.False(found) // NULL 值扫描进 string 字段:通过 **string 中间层正常处理,结果为空字符串 found, err = e.ScanStruct(&item) qes.NoError(err) qes.True(found) qes.Equal(StructWithTags{Address: "", Name: ""}, item) found, err = e.ScanStruct(&item) qes.NoError(err) qes.False(found) found, err = e.ScanStruct(&item) qes.NoError(err) qes.True(found) qes.Equal(StructWithTags{ Address: testAddr1, Name: testName1, }, item) var composed ComposedStruct found, err = e.ScanStruct(&composed) qes.NoError(err) qes.True(found) qes.Equal(ComposedStruct{ StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1, }, composed) var embeddedPtr ComposedWithPointerStruct found, err = e.ScanStruct(&embeddedPtr) qes.NoError(err) qes.True(found) qes.Equal(ComposedWithPointerStruct{ StructWithTags: &StructWithTags{ Address: testAddr1, Name: testName1, }, PhoneNumber: testPhone1, Age: testAge1, }, embeddedPtr) var noTag StructWithNoTags found, err = e.ScanStruct(&noTag) qes.NoError(err) qes.True(found) qes.Equal(StructWithNoTags{ Address: testAddr1, Name: testName1, }, noTag) } func (qes *queryExecutorSuite) TestScanStruct_taggedStructs() { 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"` } type StructWithTaggedStructs struct { NoTags StructWithNoTags `db:"notags"` Tags StructWithTags `db:"tags"` Composed ComposedStruct `db:"composedstruct"` ComposedPointer ComposedWithPointerStruct `db:"composedptrstruct"` } db, mock, err := sqlmock.New() qes.NoError(err) cols := []string{ "notags.address", "notags.name", "tags.address", "tags.name", "composedstruct.address", "composedstruct.name", "composedstruct.phone_number", "composedstruct.age", "composedptrstruct.address", "composedptrstruct.name", "composedptrstruct.phone_number", "composedptrstruct.age", } q := `SELECT` + strings.Join(cols, ", ") + ` FROM "items"` mock.ExpectQuery(q). WithArgs(). WillReturnRows(sqlmock.NewRows(cols).AddRow( testAddr1, testName1, testAddr2, testName2, testAddr1, testName1, testPhone1, testAge1, testAddr2, testName2, testPhone2, testAge2, )) e := newQueryExecutor(db, nil, q) var item StructWithTaggedStructs found, err := e.ScanStruct(&item) qes.NoError(err) qes.True(found) qes.Equal(StructWithTaggedStructs{ NoTags: StructWithNoTags{Address: testAddr1, Name: testName1}, Tags: StructWithTags{Address: testAddr2, Name: testName2}, Composed: ComposedStruct{ StructWithTags: StructWithTags{Address: testAddr1, Name: testName1}, PhoneNumber: testPhone1, Age: testAge1, }, ComposedPointer: ComposedWithPointerStruct{ StructWithTags: &StructWithTags{Address: testAddr2, Name: testName2}, PhoneNumber: testPhone2, Age: testAge2, }, }, item) } func (qes *queryExecutorSuite) TestScanVals() { db, mock, err := sqlmock.New() qes.NoError(err) var id1, id2 int64 = 1, 2 mock.ExpectQuery(`SELECT "id" FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).RowError(0, fmt.Errorf("row error"))) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).AddRow("a")) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).AddRow(id2)) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1).AddRow(id2)) e := newQueryExecutor(db, nil, `SELECT "id" FROM "items"`) var ids []int64 qes.EqualError(e.ScanVals(&ids), "queryExecutor error") qes.EqualError(e.ScanVals(&ids), "row error") qes.Error(e.ScanVals(&ids)) ids = ids[0:0] qes.NoError(e.ScanVals(&ids)) qes.Equal(ids, []int64{id1, id2}) var pointers []*int64 qes.NoError(e.ScanVals(&pointers)) qes.Len(pointers, 2) qes.Equal(&id1, pointers[0]) qes.Equal(&id2, pointers[1]) } func (qes *queryExecutorSuite) TestScanValsError() { var id int64 tests := []struct { name string items interface{} }{ { name: "non-pointer items", items: []int64{}, }, { name: "non-slice items", items: &id, }, } for i := range tests { test := tests[i] qes.Run(test.name, func() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1).AddRow(2)) e := newQueryExecutor(db, nil, `SELECT "id" FROM "items"`) qes.Equal(errUnsupportedScanValsType, e.ScanVals(test.items)) }) } } func (qes *queryExecutorSuite) TestScanVal() { db, mock, err := sqlmock.New() qes.NoError(err) id1 := int64(1) mock.ExpectQuery(`SELECT "id" FROM "items"`). WillReturnError(fmt.Errorf("queryExecutor error")) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).RowError(0, fmt.Errorf("row error")).AddRow(id1)) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("c")) mock.ExpectQuery(`SELECT "id" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(id1)) e := newQueryExecutor(db, nil, `SELECT "id" FROM "items"`) var id int64 var ids []int64 found, err := e.ScanVal(id) qes.Equal(errScanValPointer, err) qes.False(found) found, err = e.ScanVal(&ids) qes.Equal(errScanValNonSlice, err) qes.False(found) found, err = e.ScanVal(&id) qes.EqualError(err, "queryExecutor error") qes.False(found) found, err = e.ScanVal(&id) qes.EqualError(err, "row error") qes.False(found) found, err = e.ScanVal(&id) qes.Error(err) qes.False(found) var ptrID *int64 found, err = e.ScanVal(&ptrID) qes.NoError(err) qes.True(found) qes.Equal(&id1, ptrID) } func (qes *queryExecutorSuite) TestScanVal_withByteSlice() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow(testByteSliceContent)) e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) var bytes []byte found, err := e.ScanVal(bytes) qes.Equal(errScanValPointer, err) qes.False(found) found, err = e.ScanVal(&bytes) qes.NoError(err) qes.True(found) qes.Equal([]byte(testByteSliceContent), bytes) } func (qes *queryExecutorSuite) TestScanVal_withRawBytes() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow(testByteSliceContent)) e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) var bytes sql.RawBytes found, err := e.ScanVal(bytes) qes.Equal(errScanValPointer, err) qes.False(found) found, err = e.ScanVal(&bytes) qes.NoError(err) qes.True(found) qes.Equal(sql.RawBytes(testByteSliceContent), bytes) } type JSONBoolArray []bool func (b *JSONBoolArray) Scan(src interface{}) error { return json.Unmarshal(src.([]byte), b) } func (qes *queryExecutorSuite) TestScanVal_withValuerSlice() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "bools" FROM "items"`). WithArgs(). WillReturnRows(sqlmock.NewRows([]string{"bools"}).FromCSVString(`"[true, false, true]"`)) e := newQueryExecutor(db, nil, `SELECT "bools" FROM "items"`) var bools JSONBoolArray found, err := e.ScanVal(bools) qes.Equal(errScanValPointer, err) qes.False(found) found, err = e.ScanVal(&bools) qes.NoError(err) qes.True(found) qes.Equal(JSONBoolArray{true, false, true}, bools) } func (qes *queryExecutorSuite) TestScanVal_withByteSlice_notFound() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WillReturnRows(sqlmock.NewRows([]string{"name"})) e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) var b []byte found, err := e.ScanVal(&b) qes.NoError(err) qes.False(found) qes.Nil(b) } func (qes *queryExecutorSuite) TestScanVal_withByteSlice_queryError() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WillReturnError(fmt.Errorf("query error")) e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) var b []byte found, err := e.ScanVal(&b) qes.EqualError(err, "query error") qes.False(found) } func (qes *queryExecutorSuite) TestScanVal_withByteSlice_complexJSON() { // 模拟 DuckDB 等驱动将 JSON 列直接以 map 形式返回 // 必须使用 mock.NewRows 而非 sqlmock.NewRows,才会使用自定义 converter db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{})) qes.NoError(err) mock.ExpectQuery(`SELECT "output" FROM "items"`). WillReturnRows(mock.NewRows([]string{"output"}). AddRow(map[string]interface{}{"key": "value", "num": float64(42)})) e := newQueryExecutor(db, nil, `SELECT "output" FROM "items"`) var b []byte found, err := e.ScanVal(&b) qes.NoError(err) qes.True(found) var result map[string]interface{} qes.NoError(json.Unmarshal(b, &result)) qes.Equal("value", result["key"]) qes.Equal(float64(42), result["num"]) } func (qes *queryExecutorSuite) TestScanVal_withRawBytes_notFound() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WillReturnRows(sqlmock.NewRows([]string{"name"})) e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) var rb sql.RawBytes found, err := e.ScanVal(&rb) qes.NoError(err) qes.False(found) qes.Nil(rb) } func (qes *queryExecutorSuite) TestScanVal_withRawBytes_queryError() { db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WillReturnError(fmt.Errorf("query error")) e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) var rb sql.RawBytes found, err := e.ScanVal(&rb) qes.EqualError(err, "query error") qes.False(found) } func (qes *queryExecutorSuite) TestScanVal_withRawBytes_binaryDriver() { // 驱动直接返回 []byte(如 BLOB 列),结果需独立拷贝 db, mock, err := sqlmock.New() qes.NoError(err) content := []byte("binary \x00 content") mock.ExpectQuery(`SELECT "data" FROM "items"`). WillReturnRows(sqlmock.NewRows([]string{"data"}).AddRow(content)) e := newQueryExecutor(db, nil, `SELECT "data" FROM "items"`) var rb sql.RawBytes found, err := e.ScanVal(&rb) qes.NoError(err) qes.True(found) qes.Equal(sql.RawBytes(content), rb) } func (qes *queryExecutorSuite) TestScanVal_withStruct() { // 模拟 DuckDB 将 JSON/STRUCT 列以 map[string]interface{} 返回 type DocItem struct { Title string `json:"title"` Score int `json:"score"` } db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{})) qes.NoError(err) mock.ExpectQuery(`SELECT "doc" FROM "items"`). WillReturnRows(mock.NewRows([]string{"doc"}). AddRow(map[string]interface{}{"title": "hello", "score": float64(99)})) e := newQueryExecutor(db, nil, `SELECT "doc" FROM "items"`) var doc DocItem found, err := e.ScanVal(&doc) qes.NoError(err) qes.True(found) qes.Equal(DocItem{Title: "hello", Score: 99}, doc) } func (qes *queryExecutorSuite) TestScanVal_withStruct_null() { // 驱动返回 NULL,结构体保持零值 type DocItem struct { Title string `json:"title"` } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "doc" FROM "items"`). WillReturnRows(sqlmock.NewRows([]string{"doc"}).AddRow(nil)) e := newQueryExecutor(db, nil, `SELECT "doc" FROM "items"`) var doc DocItem found, err := e.ScanVal(&doc) qes.NoError(err) qes.True(found) qes.Equal(DocItem{}, doc) } func (qes *queryExecutorSuite) TestScanVal_withStruct_notFound() { type DocItem struct { Title string `json:"title"` } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "doc" FROM "items"`). WillReturnRows(sqlmock.NewRows([]string{"doc"})) e := newQueryExecutor(db, nil, `SELECT "doc" FROM "items"`) var doc DocItem found, err := e.ScanVal(&doc) qes.NoError(err) qes.False(found) } func (qes *queryExecutorSuite) TestScanVal_withStruct_queryError() { type DocItem struct { Title string `json:"title"` } db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "doc" FROM "items"`). WillReturnError(fmt.Errorf("query error")) e := newQueryExecutor(db, nil, `SELECT "doc" FROM "items"`) var doc DocItem found, err := e.ScanVal(&doc) qes.EqualError(err, "query error") qes.False(found) } func (qes *queryExecutorSuite) TestScanVal_withStruct_sqlScanner() { // 实现了 sql.Scanner 的结构体走原有直接扫描路径 db, mock, err := sqlmock.New() qes.NoError(err) mock.ExpectQuery(`SELECT "name" FROM "items"`). WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("hello")) e := newQueryExecutor(db, nil, `SELECT "name" FROM "items"`) var ns sql.NullString found, err := e.ScanVal(&ns) qes.NoError(err) qes.True(found) qes.Equal(sql.NullString{String: "hello", Valid: true}, ns) } // TestScanStructs_timeField_fromTime 验证 time.Time 和 *time.Time 字段直接从 time.Time 驱动值扫描, // 纳秒精度和时区 Location 必须原样保留。 func (qes *queryExecutorSuite) TestScanStructs_timeField_fromTime() { type Row struct { T time.Time `db:"t"` PT *time.Time `db:"pt"` } db, mock, err := sqlmock.New() qes.NoError(err) now := time.Now() // 含纳秒、Local 时区 mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnRows(sqlmock.NewRows([]string{"t", "pt"}). AddRow(now, now). AddRow(now, nil), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []Row qes.NoError(e.ScanStructs(&items)) qes.Len(items, 2) // 时间相等(Equal 只比较时刻,不比较 Location) qes.True(items[0].T.Equal(now)) qes.NotNil(items[0].PT) qes.True(items[0].PT.Equal(now)) // 纳秒精度保留 qes.Equal(now.Nanosecond(), items[0].T.Nanosecond()) qes.Equal(now.Nanosecond(), items[0].PT.Nanosecond()) // NULL → nil 指针 qes.Nil(items[1].PT) } // TestScanStructs_timeField_fromBytes 验证 time.Time 和 *time.Time 字段从 []byte 字符串扫描, // 模拟 MySQL 驱动不配置 parseTime=true 时返回 []byte 的场景(原 bug:scan error on *time.Time)。 func (qes *queryExecutorSuite) TestScanStructs_timeField_fromBytes() { type Row struct { CreatedAt time.Time `db:"created_at"` RetryAt *time.Time `db:"retry_at"` } db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{})) qes.NoError(err) datetimeBytes := []byte("2024-03-15 10:30:45") mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnRows(mock.NewRows([]string{"created_at", "retry_at"}). AddRow(datetimeBytes, datetimeBytes). AddRow(datetimeBytes, nil), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []Row qes.NoError(e.ScanStructs(&items)) qes.Len(items, 2) want := time.Date(2024, 3, 15, 10, 30, 45, 0, time.UTC) qes.True(items[0].CreatedAt.Equal(want)) qes.NotNil(items[0].RetryAt) qes.True(items[0].RetryAt.Equal(want)) qes.Nil(items[1].RetryAt) } // TestScanStructs_timeField_fromDateOnlyBytes 验证仅含日期的 []byte(无时分秒)也能正确解析。 func (qes *queryExecutorSuite) TestScanStructs_timeField_fromDateOnlyBytes() { type Row struct { Birthday time.Time `db:"birthday"` ExpiredAt *time.Time `db:"expired_at"` } db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{})) qes.NoError(err) dateBytes := []byte("2024-03-15") mock.ExpectQuery(`SELECT \* FROM "items"`). WillReturnRows(mock.NewRows([]string{"birthday", "expired_at"}). AddRow(dateBytes, dateBytes), ) e := newQueryExecutor(db, nil, `SELECT * FROM "items"`) var items []Row qes.NoError(e.ScanStructs(&items)) qes.Len(items, 1) want := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC) qes.True(items[0].Birthday.Equal(want)) qes.NotNil(items[0].ExpiredAt) qes.True(items[0].ExpiredAt.Equal(want)) } func TestQueryExecutorSuite(t *testing.T) { suite.Run(t, new(queryExecutorSuite)) }