Files
db/exec/query_executor_internal_test.go
T
what 21b80bdea4 feat: 完善扫描器、exec 及 schema 相关功能
- exec/scanner: 用 *interface{} 替换 **json.RawMessage 扫描目标,兼容 DuckDB 返回 map[string]interface{} 的场景;新增 toJSONRawMessage 转换函数
- exec/scanner: ScanVal 支持结构体指针,通过 JSON 中间层转换(DuckDB STRUCT 列)
- exec/scanner: 将 *sql.RawBytes 和 *[]byte 的处理从 ScanValContext 移入 scanner.ScanVal
- exec/query_executor: 简化 ScanValContext,移除私有 scan 方法
- exec: 补充 scanner 级别 ScanVal 测试用例
- internal/util/reflect: 重写 SafeSetVarValue,修复非指针 src 及 nil 指针字段的 panic
- internal/util/column_map: 恢复非匿名带标签结构体字段的展开逻辑
- schema: 新增 vector 列类型支持
- engine: 补充 DuckDB 相关配置
- dialect/sqlite3/vtab: 完善虚拟表适配器
- 各方言测试改用 sqlmock 虚拟连接
2026-05-20 17:52:28 +08:00

1432 lines
40 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()
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)
}
func TestQueryExecutorSuite(t *testing.T) {
suite.Run(t, new(queryExecutorSuite))
}