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 虚拟连接
This commit is contained in:
@@ -3,6 +3,7 @@ package exec
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -13,6 +14,11 @@ import (
|
||||
"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"
|
||||
@@ -929,9 +935,11 @@ func (qes *queryExecutorSuite) TestScanStruct() {
|
||||
qes.EqualError(err, "queryExecutor error")
|
||||
qes.False(found)
|
||||
|
||||
// NULL 值扫描进 string 字段:通过 **string 中间层正常处理,结果为空字符串
|
||||
found, err = e.ScanStruct(&item)
|
||||
qes.Error(err)
|
||||
qes.False(found)
|
||||
qes.NoError(err)
|
||||
qes.True(found)
|
||||
qes.Equal(StructWithTags{Address: "", Name: ""}, item)
|
||||
|
||||
found, err = e.ScanStruct(&item)
|
||||
qes.NoError(err)
|
||||
@@ -1242,6 +1250,182 @@ func (qes *queryExecutorSuite) TestScanVal_withValuerSlice() {
|
||||
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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user