fix: 修复 *time.Time 字段扫描失败及 time.Time 精度丢失问题
- createColumnScans: *time.Time 等指针到结构体类型改用 *interface{} 扫描目标,
避免 database/sql 因无法将 []byte 转换为 *time.Time 而报 scan error
- ScanStruct: time.Time 值直接存入 record,跳过 toJSONRawMessage 格式化,
保留纳秒精度和时区 Location
- SafeSetVarValue: 新增 T→*T 赋值路径(适用于 time.Time→*time.Time 等场景),
以及从 *json.RawMessage 解析 *time.Time 的路径(处理 []byte datetime 字符串)
- 新增 parseTimeFromRaw 支持 MySQL datetime、date-only、ISO8601、RFC3339 等格式
- 新增测试:覆盖 time.Time/[]byte 驱动值、date-only 格式、NULL 指针等场景
This commit is contained in:
@@ -1426,6 +1426,93 @@ func (qes *queryExecutorSuite) TestScanVal_withStruct_sqlScanner() {
|
||||
qes.Equal(sql.NullString{String: "hello", Valid: true}, ns)
|
||||
}
|
||||
|
||||
// TestScanStructs_timeField_fromTime 验证 time.Time 和 *time.Time 字段直接从 time.Time 驱动值扫描,
|
||||
// 纳秒精度和时区 Location 必须原样保留。
|
||||
func (qes *queryExecutorSuite) TestScanStructs_timeField_fromTime() {
|
||||
type Row struct {
|
||||
T time.Time `db:"t"`
|
||||
PT *time.Time `db:"pt"`
|
||||
}
|
||||
db, mock, err := sqlmock.New()
|
||||
qes.NoError(err)
|
||||
now := time.Now() // 含纳秒、Local 时区
|
||||
mock.ExpectQuery(`SELECT \* FROM "items"`).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"t", "pt"}).
|
||||
AddRow(now, now).
|
||||
AddRow(now, nil),
|
||||
)
|
||||
|
||||
e := newQueryExecutor(db, nil, `SELECT * FROM "items"`)
|
||||
var items []Row
|
||||
qes.NoError(e.ScanStructs(&items))
|
||||
qes.Len(items, 2)
|
||||
|
||||
// 时间相等(Equal 只比较时刻,不比较 Location)
|
||||
qes.True(items[0].T.Equal(now))
|
||||
qes.NotNil(items[0].PT)
|
||||
qes.True(items[0].PT.Equal(now))
|
||||
// 纳秒精度保留
|
||||
qes.Equal(now.Nanosecond(), items[0].T.Nanosecond())
|
||||
qes.Equal(now.Nanosecond(), items[0].PT.Nanosecond())
|
||||
// NULL → nil 指针
|
||||
qes.Nil(items[1].PT)
|
||||
}
|
||||
|
||||
// TestScanStructs_timeField_fromBytes 验证 time.Time 和 *time.Time 字段从 []byte 字符串扫描,
|
||||
// 模拟 MySQL 驱动不配置 parseTime=true 时返回 []byte 的场景(原 bug:scan error on *time.Time)。
|
||||
func (qes *queryExecutorSuite) TestScanStructs_timeField_fromBytes() {
|
||||
type Row struct {
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
RetryAt *time.Time `db:"retry_at"`
|
||||
}
|
||||
db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{}))
|
||||
qes.NoError(err)
|
||||
|
||||
datetimeBytes := []byte("2024-03-15 10:30:45")
|
||||
mock.ExpectQuery(`SELECT \* FROM "items"`).
|
||||
WillReturnRows(mock.NewRows([]string{"created_at", "retry_at"}).
|
||||
AddRow(datetimeBytes, datetimeBytes).
|
||||
AddRow(datetimeBytes, nil),
|
||||
)
|
||||
|
||||
e := newQueryExecutor(db, nil, `SELECT * FROM "items"`)
|
||||
var items []Row
|
||||
qes.NoError(e.ScanStructs(&items))
|
||||
qes.Len(items, 2)
|
||||
|
||||
want := time.Date(2024, 3, 15, 10, 30, 45, 0, time.UTC)
|
||||
qes.True(items[0].CreatedAt.Equal(want))
|
||||
qes.NotNil(items[0].RetryAt)
|
||||
qes.True(items[0].RetryAt.Equal(want))
|
||||
qes.Nil(items[1].RetryAt)
|
||||
}
|
||||
|
||||
// TestScanStructs_timeField_fromDateOnlyBytes 验证仅含日期的 []byte(无时分秒)也能正确解析。
|
||||
func (qes *queryExecutorSuite) TestScanStructs_timeField_fromDateOnlyBytes() {
|
||||
type Row struct {
|
||||
Birthday time.Time `db:"birthday"`
|
||||
ExpiredAt *time.Time `db:"expired_at"`
|
||||
}
|
||||
db, mock, err := sqlmock.New(sqlmock.ValueConverterOption(anyValueConverter{}))
|
||||
qes.NoError(err)
|
||||
|
||||
dateBytes := []byte("2024-03-15")
|
||||
mock.ExpectQuery(`SELECT \* FROM "items"`).
|
||||
WillReturnRows(mock.NewRows([]string{"birthday", "expired_at"}).
|
||||
AddRow(dateBytes, dateBytes),
|
||||
)
|
||||
|
||||
e := newQueryExecutor(db, nil, `SELECT * FROM "items"`)
|
||||
var items []Row
|
||||
qes.NoError(e.ScanStructs(&items))
|
||||
qes.Len(items, 1)
|
||||
|
||||
want := time.Date(2024, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||
qes.True(items[0].Birthday.Equal(want))
|
||||
qes.NotNil(items[0].ExpiredAt)
|
||||
qes.True(items[0].ExpiredAt.Equal(want))
|
||||
}
|
||||
|
||||
func TestQueryExecutorSuite(t *testing.T) {
|
||||
suite.Run(t, new(queryExecutorSuite))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user