- 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 虚拟连接
244 lines
5.8 KiB
Go
244 lines
5.8 KiB
Go
package exec
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
"github.com/stretchr/testify/suite"
|
|
)
|
|
|
|
type scannerSuite struct {
|
|
suite.Suite
|
|
}
|
|
|
|
func TestScanner(t *testing.T) {
|
|
suite.Run(t, &scannerSuite{})
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanStructs() {
|
|
type StructWithTags struct {
|
|
Address string `db:"address"`
|
|
Name string `db:"name"`
|
|
}
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT \* FROM "items"`).
|
|
WithArgs().
|
|
WillReturnRows(sqlmock.NewRows([]string{"address", "name"}).
|
|
AddRow(testAddr1, testName1).
|
|
AddRow(testAddr2, testName2),
|
|
)
|
|
rows, err := db.Query(`SELECT * FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
|
|
result := make([]StructWithTags, 0)
|
|
err = sc.ScanStructs(result)
|
|
s.Require().EqualError(err, errUnsupportedScanStructsType.Error())
|
|
|
|
err = sc.ScanStructs(&result)
|
|
s.Require().NoError(err)
|
|
s.Require().ElementsMatch(
|
|
[]StructWithTags{{Address: testAddr1, Name: testName1}, {Address: testAddr2, Name: testName2}},
|
|
result,
|
|
)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVals() {
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT "id" FROM "items"`).
|
|
WithArgs().
|
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1).AddRow(2))
|
|
|
|
rows, err := db.Query(`SELECT "id" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
|
|
result := make([]int, 0)
|
|
err = sc.ScanVals(result)
|
|
s.Require().EqualError(err, errUnsupportedScanValsType.Error())
|
|
|
|
err = sc.ScanVals(&result)
|
|
s.Require().NoError(err)
|
|
s.Require().ElementsMatch([]int{1, 2}, result)
|
|
}
|
|
|
|
func (s *scannerSuite) TestGetRecords() {
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery("SELECT \\* FROM `items`").
|
|
WithArgs().
|
|
WillReturnRows(
|
|
sqlmock.NewRows([]string{"address", "name"}).
|
|
AddRow("111 Test Addr", "Test1").
|
|
AddRow("111 Test Addr", "Test1"),
|
|
)
|
|
|
|
rows, err := db.Query("SELECT * FROM `items`")
|
|
s.Require().NoError(err)
|
|
|
|
result, err := NewScanner(rows).GetRecords()
|
|
s.Require().NoError(err)
|
|
|
|
s.Equal([]map[string]any{
|
|
{"address": "111 Test Addr", "name": "Test1"},
|
|
{"address": "111 Test Addr", "name": "Test1"},
|
|
}, result)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVal() {
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT "id" FROM "items"`).
|
|
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(42))
|
|
|
|
rows, err := db.Query(`SELECT "id" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
s.True(sc.Next())
|
|
|
|
var id int64
|
|
err = sc.ScanVal(&id)
|
|
s.Require().NoError(err)
|
|
s.Equal(int64(42), id)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVal_withRawBytes() {
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT "data" FROM "items"`).
|
|
WillReturnRows(sqlmock.NewRows([]string{"data"}).AddRow([]byte(testByteSliceContent)))
|
|
|
|
rows, err := db.Query(`SELECT "data" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
s.True(sc.Next())
|
|
|
|
var rb sql.RawBytes
|
|
err = sc.ScanVal(&rb)
|
|
s.Require().NoError(err)
|
|
|
|
_ = sc.Close()
|
|
// 关闭后缓冲区应已拷贝,值仍然有效
|
|
s.Equal(sql.RawBytes(testByteSliceContent), rb)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVal_withByteSlice() {
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT "data" FROM "items"`).
|
|
WillReturnRows(sqlmock.NewRows([]string{"data"}).AddRow(testByteSliceContent))
|
|
|
|
rows, err := db.Query(`SELECT "data" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
s.True(sc.Next())
|
|
|
|
var b []byte
|
|
err = sc.ScanVal(&b)
|
|
s.Require().NoError(err)
|
|
s.Equal([]byte(testByteSliceContent), b)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVal_withByteSlice_complexJSON() {
|
|
db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{}))
|
|
s.Require().NoError(err)
|
|
|
|
payload := map[string]interface{}{"key": "val", "num": float64(1)}
|
|
mock.ExpectQuery(`SELECT "data" FROM "items"`).
|
|
WillReturnRows(mock.NewRows([]string{"data"}).AddRow(payload))
|
|
|
|
rows, err := db.Query(`SELECT "data" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
s.True(sc.Next())
|
|
|
|
var b []byte
|
|
err = sc.ScanVal(&b)
|
|
s.Require().NoError(err)
|
|
|
|
var got map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(b, &got))
|
|
s.Equal(payload, got)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVal_withStruct() {
|
|
type DocItem struct {
|
|
Title string `json:"title"`
|
|
Score int `json:"score"`
|
|
}
|
|
db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{}))
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT "doc" FROM "items"`).
|
|
WillReturnRows(mock.NewRows([]string{"doc"}).
|
|
AddRow(map[string]interface{}{"title": "hello", "score": float64(99)}))
|
|
|
|
rows, err := db.Query(`SELECT "doc" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
s.True(sc.Next())
|
|
|
|
var doc DocItem
|
|
err = sc.ScanVal(&doc)
|
|
s.Require().NoError(err)
|
|
s.Equal(DocItem{Title: "hello", Score: 99}, doc)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVal_withStruct_null() {
|
|
type DocItem struct {
|
|
Title string `json:"title"`
|
|
}
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT "doc" FROM "items"`).
|
|
WillReturnRows(sqlmock.NewRows([]string{"doc"}).AddRow(nil))
|
|
|
|
rows, err := db.Query(`SELECT "doc" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
s.True(sc.Next())
|
|
|
|
var doc DocItem
|
|
err = sc.ScanVal(&doc)
|
|
s.Require().NoError(err)
|
|
s.Equal(DocItem{}, doc)
|
|
}
|
|
|
|
func (s *scannerSuite) TestScanVal_withStruct_sqlScanner() {
|
|
db, mock, err := sqlmock.New()
|
|
s.Require().NoError(err)
|
|
|
|
mock.ExpectQuery(`SELECT "name" FROM "items"`).
|
|
WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("hello"))
|
|
|
|
rows, err := db.Query(`SELECT "name" FROM "items"`)
|
|
s.Require().NoError(err)
|
|
|
|
sc := NewScanner(rows)
|
|
s.True(sc.Next())
|
|
|
|
var ns sql.NullString
|
|
err = sc.ScanVal(&ns)
|
|
s.Require().NoError(err)
|
|
s.Equal(sql.NullString{String: "hello", Valid: true}, ns)
|
|
}
|