Files
db/internal/util/reflect.go
T
what e7fabd282f 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 指针等场景
2026-06-02 15:57:10 +08:00

337 lines
8.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package util
import (
"database/sql"
"encoding/json"
"reflect"
"strings"
"sync"
"time"
"git.fsdpf.net/go/db/internal/errors"
)
const (
skipUpdateTagName = "skipupdate"
skipInsertTagName = "skipinsert"
defaultIfEmptyTagName = "defaultifempty"
omitNilTagName = "omitnil"
omitEmptyTagName = "omitempty"
)
var scannerType = reflect.TypeOf((*sql.Scanner)(nil)).Elem()
var timeType = reflect.TypeOf(time.Time{})
var timeParseFmts = []string{
time.DateTime, // "2006-01-02 15:04:05"
time.DateOnly, // "2006-01-02"
"2006-01-02 15:04:05.999999999", // MySQL 带微秒
"2006-01-02T15:04:05", // ISO8601 无时区
time.RFC3339Nano,
time.RFC3339,
}
func parseTimeFromRaw(u *json.RawMessage) (time.Time, bool) {
s := strings.Trim(string(*u), `"`)
for _, f := range timeParseFmts {
if t, err := time.Parse(f, s); err == nil {
return t, true
}
}
return time.Time{}, false
}
func IsUint(k reflect.Kind) bool {
return (k == reflect.Uint) ||
(k == reflect.Uint8) ||
(k == reflect.Uint16) ||
(k == reflect.Uint32) ||
(k == reflect.Uint64)
}
func IsInt(k reflect.Kind) bool {
return (k == reflect.Int) ||
(k == reflect.Int8) ||
(k == reflect.Int16) ||
(k == reflect.Int32) ||
(k == reflect.Int64)
}
func IsFloat(k reflect.Kind) bool {
return (k == reflect.Float32) ||
(k == reflect.Float64)
}
func IsString(k reflect.Kind) bool {
return k == reflect.String
}
func IsBool(k reflect.Kind) bool {
return k == reflect.Bool
}
func IsSlice(k reflect.Kind) bool {
return k == reflect.Slice
}
func IsStruct(k reflect.Kind) bool {
return k == reflect.Struct
}
func IsMap(k reflect.Kind) bool {
return k == reflect.Map
}
func IsInvalid(k reflect.Kind) bool {
return k == reflect.Invalid
}
func IsPointer(k reflect.Kind) bool {
return k == reflect.Ptr
}
func IsNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Ptr, reflect.Interface, reflect.Slice, reflect.Map, reflect.Chan, reflect.Func:
return v.IsNil()
default:
return false
}
}
func IsEmptyValue(v reflect.Value) bool {
return !v.IsValid() || v.IsZero()
}
var (
structMapCache = make(map[interface{}]ColumnMap)
structMapCacheLock = sync.Mutex{}
)
var (
DefaultColumnRenameFunction = strings.ToLower
columnRenameFunction = DefaultColumnRenameFunction
ignoreUntaggedFields = false
)
func SetIgnoreUntaggedFields(ignore bool) {
// If the value here is changing, reset the struct map cache
if ignore != ignoreUntaggedFields {
ignoreUntaggedFields = ignore
structMapCacheLock.Lock()
defer structMapCacheLock.Unlock()
structMapCache = make(map[interface{}]ColumnMap)
}
}
func SetColumnRenameFunction(newFunction func(string) string) {
columnRenameFunction = newFunction
}
// GetSliceElementType returns the type for a slices elements.
func GetSliceElementType(val reflect.Value) reflect.Type {
elemType := val.Type().Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
return elemType
}
func GetMapElementType(val reflect.Value) reflect.Type {
elemType := val.Type().Elem()
if elemType.Kind() == reflect.Ptr {
elemType = elemType.Elem()
}
return elemType
}
// AppendSliceElement will append val to slice. Handles slice of pointers and
// not pointers. Val needs to be a pointer.
func AppendSliceElement(slice, val reflect.Value) {
if slice.Type().Elem().Kind() == reflect.Ptr {
slice.Set(reflect.Append(slice, val))
} else {
slice.Set(reflect.Append(slice, reflect.Indirect(val)))
}
}
func GetTypeInfo(i interface{}, val reflect.Value) (reflect.Type, reflect.Kind) {
var t reflect.Type
valKind := val.Kind()
if valKind == reflect.Slice {
if reflect.ValueOf(i).Kind() == reflect.Ptr {
t = reflect.TypeOf(i).Elem().Elem()
} else {
t = reflect.TypeOf(i).Elem()
}
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
valKind = t.Kind()
} else {
t = val.Type()
}
return t, valKind
}
func SafeGetFieldByIndex(v reflect.Value, fieldIndex []int) (result reflect.Value, isAvailable bool) {
switch len(fieldIndex) {
case 0:
return v, true
case 1:
return v.FieldByIndex(fieldIndex), true
default:
if f := reflect.Indirect(v.Field(fieldIndex[0])); f.IsValid() {
return SafeGetFieldByIndex(f, fieldIndex[1:])
}
}
return reflect.ValueOf(nil), false
}
func SafeSetFieldByIndex(v reflect.Value, fieldIndex []int, src interface{}) (result reflect.Value) {
v = reflect.Indirect(v)
switch len(fieldIndex) {
case 0:
return v
case 1:
f := v.FieldByIndex(fieldIndex)
if err := SafeSetVarValue(f, src); err != nil {
panic(err)
}
default:
f := v.Field(fieldIndex[0])
switch f.Kind() {
case reflect.Ptr:
s := f
if f.IsNil() || !f.IsValid() {
s = reflect.New(f.Type().Elem())
f.Set(s)
}
SafeSetFieldByIndex(reflect.Indirect(s), fieldIndex[1:], src)
case reflect.Struct:
SafeSetFieldByIndex(f, fieldIndex[1:], src)
default: // use the original value
}
}
return v
}
func SafeSetVarValue(v reflect.Value, src interface{}) error {
srcReflect := reflect.ValueOf(src)
// src 可能是 **TcreateColumnScans 的扫描目标)或裸值(测试/直接调用)
if srcReflect.Kind() != reflect.Ptr {
if srcReflect.IsValid() {
if v.Type().ConvertibleTo(srcReflect.Type()) {
v.Set(srcReflect.Convert(v.Type()))
} else if v.Kind() == reflect.Ptr && srcReflect.Type().ConvertibleTo(v.Type().Elem()) {
// src = T,v = *T:分配新指针并赋值(如 time.Time → *time.Time
p := reflect.New(v.Type().Elem())
p.Elem().Set(srcReflect.Convert(v.Type().Elem()))
v.Set(p)
}
}
return nil
}
srcVal := srcReflect.Elem() // **T → *T*T → T
if IsNil(srcVal) {
v.Set(reflect.Zero(v.Type()))
return nil
}
if v.Kind() == reflect.Ptr {
// v 是指针字段(如 *sql.NullString、*time.Time
if srcVal.Kind() == reflect.Ptr {
// src = **T, srcVal = *T → v = *T
if v.Type().ConvertibleTo(srcVal.Type()) {
v.Set(srcVal.Convert(v.Type()))
} else if u, ok := srcVal.Interface().(*json.RawMessage); ok && len(*u) >= 2 {
if v.Type().Elem() == timeType {
if t, ok := parseTimeFromRaw(u); ok {
p := reflect.New(timeType)
p.Elem().Set(reflect.ValueOf(t))
v.Set(p)
}
} else {
p := reflect.New(v.Type().Elem())
if err := json.Unmarshal(*u, p.Interface()); err == nil {
v.Set(p)
}
}
}
} else {
// src = *T, srcVal = T → allocate new *T and set
if v.Type().Elem().ConvertibleTo(srcVal.Type()) {
p := reflect.New(v.Type().Elem())
p.Elem().Set(srcVal.Convert(v.Type().Elem()))
v.Set(p)
}
}
return nil
}
// v 是非指针字段
if srcVal.Kind() == reflect.Ptr {
// srcVal = *T,取其值赋给 v
if v.Type().ConvertibleTo(srcVal.Type().Elem()) {
v.Set(srcVal.Elem().Convert(v.Type()))
} else if u, ok := srcVal.Interface().(*json.RawMessage); ok && len(*u) >= 2 {
if v.Type() == timeType {
if t, ok := parseTimeFromRaw(u); ok {
v.Set(reflect.ValueOf(t))
return nil
}
}
if err := json.Unmarshal(*u, v.Addr().Interface()); err != nil {
return err
}
}
} else {
// srcVal = T*T 扫描目标的 default 分支)
if v.Type().ConvertibleTo(srcVal.Type()) {
v.Set(srcVal.Convert(v.Type()))
}
}
return nil
}
type rowData = map[string]interface{}
// AssignStructVals will assign the data from rd to i.
func AssignStructVals(i interface{}, rd rowData, cm ColumnMap) {
val := reflect.Indirect(reflect.ValueOf(i))
for name, data := range cm {
src, ok := rd[name]
if ok {
SafeSetFieldByIndex(val, data.FieldIndex, src)
}
}
}
func GetColumnMap(i interface{}) (ColumnMap, error) {
val := reflect.Indirect(reflect.ValueOf(i))
t, valKind := GetTypeInfo(i, val)
if valKind != reflect.Struct {
return nil, errors.New("cannot scan into this type: %v", t) // #nosec
}
structMapCacheLock.Lock()
defer structMapCacheLock.Unlock()
if _, ok := structMapCache[t]; !ok {
structMapCache[t] = newColumnMap(t, []int{}, []string{})
}
return structMapCache[t], nil
}