Files
MyGoNavi/internal/db/query_value.go
Syngnat 0632c5242c 🐛 fix(oceanbase/data-grid): 修复 Oracle 时间字段显示编辑与结果视图异常
- 修复 OceanBase Oracle DATE 与 TIMESTAMP 的解码、展示和编辑精度丢失问题
- 修复查询结果与数据视图的行号显示、分页页数和日期列展示口径
- 打通 Oracle 与 OceanBase 会话执行链路的扫描方言透传
- 补齐 DBQuery、DataGrid temporal 和 OceanBase 结果链路回归测试
2026-06-17 09:49:15 +08:00

655 lines
17 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 db
import (
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"reflect"
"strconv"
"strings"
"time"
"unicode"
"unicode/utf8"
mssql "github.com/microsoft/go-mssqldb"
)
const (
jsMaxSafeInteger int64 = 9007199254740991
jsMinSafeInteger int64 = -9007199254740991
jsMaxSafeUint uint64 = 9007199254740991
oceanBaseOracleScanDialect = "oceanbase-oracle"
)
var (
jsMaxSafeBigInt = big.NewInt(jsMaxSafeInteger)
jsMinSafeBigInt = big.NewInt(jsMinSafeInteger)
)
// normalizeQueryValue normalizes driver-returned values for UI/JSON transport.
// 当前主要处理 []byte如果是可读文本则转为 string否则转为十六进制字符串避免前端出现“空白值”。
func normalizeQueryValue(v interface{}) interface{} {
return normalizeQueryValueWithDBType(v, "")
}
func normalizeQueryValueWithDBType(v interface{}, databaseTypeName string) interface{} {
return normalizeQueryValueWithDBTypeAndDialect(v, databaseTypeName, "")
}
func normalizeQueryValueWithDBTypeAndDialect(v interface{}, databaseTypeName, dialect string) interface{} {
if tm, ok := v.(time.Time); ok {
return normalizeTemporalValueForDisplay(tm, databaseTypeName, dialect)
}
if s, ok := v.(string); ok {
if tm, normalizedType, ok := decodeOceanBaseOracleTemporalString(s, databaseTypeName, dialect); ok {
return normalizeTemporalValueForDisplay(tm, normalizedType, dialect)
}
}
if b, ok := v.([]byte); ok {
if tm, normalizedType, ok := decodeOceanBaseOracleTemporalBytes(b, databaseTypeName, dialect); ok {
return normalizeTemporalValueForDisplay(tm, normalizedType, dialect)
}
return bytesToDisplayValue(b, databaseTypeName)
}
return normalizeCompositeQueryValue(v)
}
func normalizeTemporalValueForDisplay(value time.Time, databaseTypeName, dialect string) interface{} {
if value.IsZero() {
if zeroValue, ok := zeroTemporalDisplayValue(databaseTypeName); ok {
return zeroValue
}
}
if shouldDisplayTemporalValueAsDateOnly(databaseTypeName, dialect) || shouldDisplayOceanBaseOracleDateAsDateOnly(value, databaseTypeName, dialect) {
return value.Format("2006-01-02")
}
return value.Format(time.RFC3339Nano)
}
func isDateOnlyDatabaseTypeName(databaseTypeName string) bool {
typeName := strings.ToUpper(strings.TrimSpace(databaseTypeName))
return typeName == "DATE" || typeName == "NEWDATE"
}
func shouldDisplayTemporalValueAsDateOnly(databaseTypeName, dialect string) bool {
if !isDateOnlyDatabaseTypeName(databaseTypeName) {
return false
}
switch strings.ToLower(strings.TrimSpace(dialect)) {
case "mysql", "mariadb", "goldendb", "greatdb", "gdb", "diros", "doris", "starrocks", "sphinx":
return true
default:
return false
}
}
func shouldDisplayOceanBaseOracleDateAsDateOnly(value time.Time, databaseTypeName, dialect string) bool {
if !isDateOnlyDatabaseTypeName(databaseTypeName) {
return false
}
if strings.ToLower(strings.TrimSpace(dialect)) != oceanBaseOracleScanDialect {
return false
}
return value.Hour() == 0 && value.Minute() == 0 && value.Second() == 0 && value.Nanosecond() == 0
}
func decodeOceanBaseOracleTemporalString(value string, databaseTypeName, dialect string) (time.Time, string, bool) {
return decodeOceanBaseOracleTemporalBytes([]byte(value), databaseTypeName, dialect)
}
func decodeOceanBaseOracleTemporalBytes(value []byte, databaseTypeName, dialect string) (time.Time, string, bool) {
if strings.ToLower(strings.TrimSpace(dialect)) != oceanBaseOracleScanDialect {
return time.Time{}, "", false
}
if !shouldAttemptOceanBaseOracleTemporalDecode(databaseTypeName, value) {
return time.Time{}, "", false
}
return parseOceanBaseOracleTemporal(value, databaseTypeName)
}
func shouldAttemptOceanBaseOracleTemporalDecode(databaseTypeName string, value []byte) bool {
if isOceanBaseOracleTemporalDatabaseTypeName(databaseTypeName) {
return true
}
if !hasOceanBaseOracleTemporalEncodedLength(value) {
return false
}
typeName := strings.ToUpper(strings.TrimSpace(databaseTypeName))
if typeName == "" {
return true
}
if isLikelyOceanBaseOracleTemporalCarrierType(typeName) {
return true
}
return false
}
func hasOceanBaseOracleTemporalEncodedLength(value []byte) bool {
switch len(value) {
case 5, 7, 8, 11, 12, 13:
return true
default:
return false
}
}
func isLikelyOceanBaseOracleTemporalCarrierType(typeName string) bool {
if typeName == "" {
return false
}
switch {
case strings.Contains(typeName, "CHAR"),
strings.Contains(typeName, "TEXT"),
strings.Contains(typeName, "STRING"),
strings.Contains(typeName, "BINARY"),
strings.Contains(typeName, "VARBINARY"),
strings.Contains(typeName, "RAW"),
strings.Contains(typeName, "BLOB"),
strings.Contains(typeName, "LOB"):
return true
default:
return false
}
}
func isOceanBaseOracleTemporalDatabaseTypeName(databaseTypeName string) bool {
typeName := strings.ToUpper(strings.TrimSpace(databaseTypeName))
if typeName == "DATE" || typeName == "TYPE_CA" {
return true
}
return strings.HasPrefix(typeName, "TIMESTAMP")
}
func parseOceanBaseOracleTemporal(value []byte, databaseTypeName string) (time.Time, string, bool) {
if tm, normalizedType, ok := parseOracleBinaryTemporal(value, databaseTypeName); ok {
return tm, normalizedType, true
}
if tm, normalizedType, ok := parseOceanBaseOracleTypeCATemporal(value, databaseTypeName); ok {
return tm, normalizedType, true
}
return parseMySQLLengthEncodedTemporal(value, databaseTypeName)
}
func parseOceanBaseOracleTypeCATemporal(value []byte, databaseTypeName string) (time.Time, string, bool) {
if len(value) != 12 {
return time.Time{}, "", false
}
yearHigh := int(value[0])
yearLow := int(value[1])
month := int(value[2])
day := int(value[3])
hour := int(value[4])
minute := int(value[5])
second := int(value[6])
nsec := int(binary.LittleEndian.Uint32(value[7:11]))
scale := int(value[11])
if yearHigh < 0 || yearHigh > 99 || yearLow < 0 || yearLow > 99 {
return time.Time{}, "", false
}
if month < 1 || month > 12 || day < 1 || day > 31 || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
return time.Time{}, "", false
}
if scale < 0 || scale > 9 || nsec < 0 || nsec >= 1_000_000_000 {
return time.Time{}, "", false
}
if !matchesTemporalScale(nsec, scale) {
return time.Time{}, "", false
}
year := yearHigh*100 + yearLow
parsed := time.Date(year, time.Month(month), day, hour, minute, second, nsec, time.UTC)
if parsed.Year() != year || int(parsed.Month()) != month || parsed.Day() != day ||
parsed.Hour() != hour || parsed.Minute() != minute || parsed.Second() != second || parsed.Nanosecond() != nsec {
return time.Time{}, "", false
}
return parsed, normalizeOracleTemporalDatabaseTypeName(databaseTypeName), true
}
func matchesTemporalScale(nsec, scale int) bool {
if scale >= 9 {
return true
}
step := 1
for i := 0; i < 9-scale; i++ {
step *= 10
}
return nsec%step == 0
}
func parseOracleBinaryTemporal(value []byte, databaseTypeName string) (time.Time, string, bool) {
switch len(value) {
case 7:
tm, ok := parseOracleBinaryDateTime(value[:7])
return tm, "DATE", ok
case 11:
tm, ok := parseOracleBinaryTimestamp(value)
return tm, normalizeOracleTemporalDatabaseTypeName(databaseTypeName), ok
case 13:
tm, ok := parseOracleBinaryTimestampWithTimezone(value)
return tm, normalizeOracleTemporalDatabaseTypeName(databaseTypeName), ok
default:
return time.Time{}, "", false
}
}
func parseMySQLLengthEncodedTemporal(value []byte, databaseTypeName string) (time.Time, string, bool) {
if len(value) == 0 {
return time.Time{}, "", false
}
payloadLength := int(value[0])
if payloadLength != len(value)-1 {
return time.Time{}, "", false
}
switch payloadLength {
case 4:
tm, ok := parseMySQLBinaryDateTimePayload(value[1:], false)
return tm, "DATE", ok
case 7:
tm, ok := parseMySQLBinaryDateTimePayload(value[1:], false)
return tm, normalizeOracleTemporalDatabaseTypeName(databaseTypeName), ok
case 11:
tm, ok := parseMySQLBinaryDateTimePayload(value[1:], true)
return tm, normalizeOracleTemporalDatabaseTypeName(databaseTypeName), ok
default:
return time.Time{}, "", false
}
}
func parseMySQLBinaryDateTimePayload(value []byte, withFraction bool) (time.Time, bool) {
expectedLength := 7
if !withFraction {
switch len(value) {
case 4:
year := int(binary.LittleEndian.Uint16(value[0:2]))
month := int(value[2])
day := int(value[3])
if year < 0 || month < 1 || month > 12 || day < 1 || day > 31 {
return time.Time{}, false
}
parsed := time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
if parsed.Year() != year || int(parsed.Month()) != month || parsed.Day() != day {
return time.Time{}, false
}
return parsed, true
case expectedLength:
default:
return time.Time{}, false
}
} else if len(value) != 11 {
return time.Time{}, false
}
year := int(binary.LittleEndian.Uint16(value[0:2]))
month := int(value[2])
day := int(value[3])
hour := int(value[4])
minute := int(value[5])
second := int(value[6])
nsec := 0
if withFraction {
usec := binary.LittleEndian.Uint32(value[7:11])
if usec >= 1_000_000 {
return time.Time{}, false
}
nsec = int(usec) * 1000
}
if year < 0 || month < 1 || month > 12 || day < 1 || day > 31 || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
return time.Time{}, false
}
parsed := time.Date(year, time.Month(month), day, hour, minute, second, nsec, time.UTC)
if parsed.Year() != year || int(parsed.Month()) != month || parsed.Day() != day ||
parsed.Hour() != hour || parsed.Minute() != minute || parsed.Second() != second || parsed.Nanosecond() != nsec {
return time.Time{}, false
}
return parsed, true
}
func normalizeOracleTemporalDatabaseTypeName(databaseTypeName string) string {
typeName := strings.ToUpper(strings.TrimSpace(databaseTypeName))
switch typeName {
case "TYPE_CA":
return "TIMESTAMP"
default:
if typeName == "" {
return "TIMESTAMP"
}
return typeName
}
}
func parseOracleBinaryTimestamp(value []byte) (time.Time, bool) {
if len(value) != 11 {
return time.Time{}, false
}
baseTime, ok := parseOracleBinaryDateTime(value[:7])
if !ok {
return time.Time{}, false
}
nsec := binary.BigEndian.Uint32(value[7:11])
if nsec >= 1_000_000_000 {
return time.Time{}, false
}
return time.Date(
baseTime.Year(),
baseTime.Month(),
baseTime.Day(),
baseTime.Hour(),
baseTime.Minute(),
baseTime.Second(),
int(nsec),
time.UTC,
), true
}
func parseOracleBinaryTimestampWithTimezone(value []byte) (time.Time, bool) {
if len(value) != 13 {
return time.Time{}, false
}
baseTime, ok := parseOracleBinaryTimestamp(value[:11])
if !ok {
return time.Time{}, false
}
tzHour := int(value[11]) - 20
tzMinute := int(value[12]) - 60
if tzHour < -12 || tzHour > 14 || tzMinute < 0 || tzMinute >= 60 {
return time.Time{}, false
}
location := time.FixedZone("", tzHour*3600+tzMinute*60)
return time.Date(
baseTime.Year(),
baseTime.Month(),
baseTime.Day(),
baseTime.Hour(),
baseTime.Minute(),
baseTime.Second(),
baseTime.Nanosecond(),
location,
), true
}
func parseOracleBinaryDateTime(value []byte) (time.Time, bool) {
if len(value) != 7 {
return time.Time{}, false
}
year := (int(value[0]) - 100) * 100
year += int(value[1]) - 100
month := int(value[2])
day := int(value[3])
hour := int(value[4]) - 1
minute := int(value[5]) - 1
second := int(value[6]) - 1
if year < 0 || month < 1 || month > 12 || day < 1 || day > 31 || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59 {
return time.Time{}, false
}
parsed := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.UTC)
if parsed.Year() != year || int(parsed.Month()) != month || parsed.Day() != day ||
parsed.Hour() != hour || parsed.Minute() != minute || parsed.Second() != second {
return time.Time{}, false
}
return parsed, true
}
func zeroTemporalDisplayValue(databaseTypeName string) (string, bool) {
typeName := strings.ToUpper(strings.TrimSpace(databaseTypeName))
if typeName == "" {
return "0000-00-00 00:00:00", true
}
switch {
case strings.Contains(typeName, "TIMESTAMP") || strings.Contains(typeName, "DATETIME"):
return "0000-00-00 00:00:00", true
case typeName == "DATE" || typeName == "NEWDATE":
return "0000-00-00", true
case strings.Contains(typeName, "TIME"):
return "00:00:00", true
case strings.Contains(typeName, "YEAR"):
return "0000", true
default:
return "", false
}
}
func normalizeCompositeQueryValue(v interface{}) interface{} {
if v == nil {
return nil
}
switch typed := v.(type) {
case []interface{}:
items := make([]interface{}, len(typed))
for i, item := range typed {
items[i] = normalizeQueryValue(item)
}
return items
case map[string]interface{}:
out := make(map[string]interface{}, len(typed))
for key, value := range typed {
out[key] = normalizeQueryValue(value)
}
return out
case json.Number:
return normalizeJSONNumberForJS(typed)
}
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Pointer:
if rv.IsNil() {
return nil
}
return normalizeQueryValue(rv.Elem().Interface())
case reflect.Map:
if rv.IsNil() {
return nil
}
out := make(map[string]interface{}, rv.Len())
iter := rv.MapRange()
for iter.Next() {
out[mapKeyToString(iter.Key().Interface())] = normalizeQueryValue(iter.Value().Interface())
}
return out
case reflect.Slice, reflect.Array:
// []byte 在上层已单独处理,这里保留对其它切片/数组的递归规整。
if rv.Kind() == reflect.Slice && rv.IsNil() {
return nil
}
size := rv.Len()
items := make([]interface{}, size)
for i := 0; i < size; i++ {
items[i] = normalizeQueryValue(rv.Index(i).Interface())
}
return items
case reflect.Struct:
// 部分驱动(如 Kingbase会返回复杂结构体值直接透传会导致前端渲染和比较开销激增。
// 统一降级为可读字符串,避免对象深层序列化触发 UI 卡顿。
if tm, ok := v.(time.Time); ok {
return normalizeTemporalValueForDisplay(tm, "", "")
}
if stringer, ok := v.(fmt.Stringer); ok {
return stringer.String()
}
return fmt.Sprintf("%v", v)
default:
return normalizeUnsafeIntegerForJS(rv, v)
}
}
func normalizeJSONNumberForJS(n json.Number) interface{} {
text := strings.TrimSpace(n.String())
if text == "" {
return ""
}
if integer, ok := parseJSONInteger(text); ok {
if integer.Cmp(jsMaxSafeBigInt) > 0 || integer.Cmp(jsMinSafeBigInt) < 0 {
return text
}
return integer.Int64()
}
if f, err := n.Float64(); err == nil {
return f
}
return text
}
func parseJSONInteger(text string) (*big.Int, bool) {
if text == "" {
return nil, false
}
start := 0
if text[0] == '+' || text[0] == '-' {
if len(text) == 1 {
return nil, false
}
start = 1
}
for i := start; i < len(text); i++ {
if text[i] < '0' || text[i] > '9' {
return nil, false
}
}
value, ok := new(big.Int).SetString(text, 10)
if !ok {
return nil, false
}
return value, true
}
func mapKeyToString(key interface{}) string {
if key == nil {
return "null"
}
if s, ok := key.(string); ok {
return s
}
return fmt.Sprintf("%v", key)
}
func bytesToDisplayValue(b []byte, databaseTypeName string) interface{} {
if b == nil {
return nil
}
if len(b) == 0 {
return ""
}
dbType := strings.ToUpper(strings.TrimSpace(databaseTypeName))
if isSQLServerUniqueIdentifierType(dbType) {
var guid mssql.UniqueIdentifier
if err := guid.Scan(b); err == nil {
return guid.String()
}
}
if isBitLikeDBType(dbType) {
if u, ok := bytesToUint64(b); ok {
// JS number precision is limited; keep large bitmasks as string.
if u <= jsMaxSafeUint {
return int64(u)
}
return fmt.Sprintf("%d", u)
}
}
if utf8.Valid(b) {
s := string(b)
if isMostlyPrintable(s) {
return s
}
}
// Fallback: some drivers return BIT(1) as []byte{0} / []byte{1} without type info.
if dbType == "" && len(b) == 1 && (b[0] == 0 || b[0] == 1) {
return int64(b[0])
}
return bytesToReadableString(b)
}
func bytesToReadableString(b []byte) interface{} {
if b == nil {
return nil
}
if len(b) == 0 {
return ""
}
return "0x" + hex.EncodeToString(b)
}
func isSQLServerUniqueIdentifierType(typeName string) bool {
return typeName == "UNIQUEIDENTIFIER"
}
func isBitLikeDBType(typeName string) bool {
if typeName == "" {
return false
}
switch typeName {
case "BIT", "VARBIT":
return true
default:
}
return strings.HasPrefix(typeName, "BIT")
}
func bytesToUint64(b []byte) (uint64, bool) {
if len(b) == 0 || len(b) > 8 {
return 0, false
}
var u uint64
for _, v := range b {
u = (u << 8) | uint64(v)
}
return u, true
}
func normalizeUnsafeIntegerForJS(rv reflect.Value, original interface{}) interface{} {
switch rv.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n := rv.Int()
if n > jsMaxSafeInteger || n < jsMinSafeInteger {
return strconv.FormatInt(n, 10)
}
return original
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
u := rv.Uint()
if u > jsMaxSafeUint {
return strconv.FormatUint(u, 10)
}
return original
default:
return original
}
}
func isMostlyPrintable(s string) bool {
if s == "" {
return true
}
total := 0
printable := 0
for _, r := range s {
total++
switch r {
case '\n', '\r', '\t':
printable++
continue
default:
}
if unicode.IsPrint(r) {
printable++
}
}
// 允许少量不可见字符,避免把正常文本误判为二进制。
return printable*100 >= total*90
}