1514 lines
42 KiB
Go
1514 lines
42 KiB
Go
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().UTC().Truncate(time.Second)
|
||
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 驱动值扫描。
|
||
// 注意:经过 toJSONRawMessage 格式化(精度到秒)再解析,纳秒和时区会丢失,结果为 UTC 秒精度。
|
||
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().UTC().Truncate(time.Second)
|
||
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)
|
||
|
||
qes.True(items[0].T.Equal(now))
|
||
qes.NotNil(items[0].PT)
|
||
qes.True(items[0].PT.Equal(now))
|
||
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))
|
||
}
|