|
@@ -34,6 +34,9 @@ func zeroForVRTX(ft types.FieldType) any {
|
|
|
return int32(0)
|
|
return int32(0)
|
|
|
case types.FieldType_Uint64:
|
|
case types.FieldType_Uint64:
|
|
|
return uint64(0)
|
|
return uint64(0)
|
|
|
|
|
+ case types.FieldType_DateTime:
|
|
|
|
|
+ // 引擎 DateTime 列的零值: 零毫秒 (Unix 0)。非空列上的"没有时间"就落它。
|
|
|
|
|
+ return uint64(0)
|
|
|
case types.FieldType_Int8:
|
|
case types.FieldType_Int8:
|
|
|
return int8(0)
|
|
return int8(0)
|
|
|
case types.FieldType_Int16:
|
|
case types.FieldType_Int16:
|
|
@@ -126,6 +129,11 @@ func coerceForVRTX(ft types.FieldType, param byte, v any, col string, strict boo
|
|
|
}
|
|
}
|
|
|
return int32(uint32(n)), nil
|
|
return int32(uint32(n)), nil
|
|
|
|
|
|
|
|
|
|
+ case types.FieldType_DateTime:
|
|
|
|
|
+ // 引擎专用时间列 (Unix 毫秒): time.Time 直传、文本解析、整数按毫秒 ——
|
|
|
|
|
+ // 换算约定见 coerceDateTimeForVRTX。
|
|
|
|
|
+ return coerceDateTimeForVRTX(v, col, nullable)
|
|
|
|
|
+
|
|
|
case types.FieldType_Uint64:
|
|
case types.FieldType_Uint64:
|
|
|
// 有符号来源按"64 位位模式"原样搬: int64(-1) → uint64(0xFFFF...)。
|
|
// 有符号来源按"64 位位模式"原样搬: int64(-1) → uint64(0xFFFF...)。
|
|
|
// 导出回 SQL 时再用 int64(u) 写, 于是 MySQL 的 -1 ↔ VRTX 的 0xFFFF...
|
|
// 导出回 SQL 时再用 int64(u) 写, 于是 MySQL 的 -1 ↔ VRTX 的 0xFFFF...
|
|
@@ -358,7 +366,8 @@ func parseInt128(s string) (*big.Int, error) {
|
|
|
// - 字节类列 (定长串 / 变长) 若是合法 UTF-8 就绑成 string, 否则绑 []byte。
|
|
// - 字节类列 (定长串 / 变长) 若是合法 UTF-8 就绑成 string, 否则绑 []byte。
|
|
|
// 前者让 SQLite 把它存成 TEXT (BLOB 亲和性的列不会自动转换),
|
|
// 前者让 SQLite 把它存成 TEXT (BLOB 亲和性的列不会自动转换),
|
|
|
// 后者保证二进制列不被改坏。定长列导出时去掉尾部补零。
|
|
// 后者保证二进制列不被改坏。定长列导出时去掉尾部补零。
|
|
|
-// - 时间列 (TimeKind 非空) 一律还原成时间文本, 不写纳秒整数。
|
|
|
|
|
|
|
+// - 时间列一律还原成时间文本, 不写整数: FieldType_DateTime 列按【毫秒】解释,
|
|
|
|
|
+// 存量 Uint64+TimeKind 旧表按【纳秒】解释 (两条路的判别就是列的 FieldType)。
|
|
|
//
|
|
//
|
|
|
// mysql 来自目标方言 (Sink), 与 sqlTypeName 的第二个参数同源。
|
|
// mysql 来自目标方言 (Sink), 与 sqlTypeName 的第二个参数同源。
|
|
|
func coerceForSQL(c Column, v any, mysql bool) (any, error) {
|
|
func coerceForSQL(c Column, v any, mysql bool) (any, error) {
|
|
@@ -409,15 +418,33 @@ func coerceForSQL(c Column, v any, mysql bool) (any, error) {
|
|
|
// 同 uint16: 给无符号数值, 别把 4294967295 当成 -1 送给 MySQL 的 INT UNSIGNED。
|
|
// 同 uint16: 给无符号数值, 别把 4294967295 当成 -1 送给 MySQL 的 INT UNSIGNED。
|
|
|
return int64(uint32(n)), nil
|
|
return int64(uint32(n)), nil
|
|
|
|
|
|
|
|
|
|
+ case types.FieldType_DateTime:
|
|
|
|
|
+ // 时间列: 盘上 (引擎 DecodeValue) 是 uint64 毫秒, 回 SQL 得还原成时间文本 ——
|
|
|
|
|
+ // 否则 MySQL 的 DATETIME 列会收到一个 1.7e12 的整数 (驱动不会替我们转)。
|
|
|
|
|
+ // time.Time 先按绝对时间取毫秒, 其余形态按数值 (毫秒) 收。
|
|
|
|
|
+ var ms int64
|
|
|
|
|
+ switch x := v.(type) {
|
|
|
|
|
+ case time.Time:
|
|
|
|
|
+ ms = x.UnixMilli()
|
|
|
|
|
+ default:
|
|
|
|
|
+ u, err := asUint64(v)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("列 %s: %w", c.Name, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ ms = int64(u)
|
|
|
|
|
+ }
|
|
|
|
|
+ return formatSQLTime(ms, c.TimeKind), nil
|
|
|
|
|
+
|
|
|
case types.FieldType_Uint64:
|
|
case types.FieldType_Uint64:
|
|
|
u, err := asUint64(v)
|
|
u, err := asUint64(v)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("列 %s: %w", c.Name, err)
|
|
return nil, fmt.Errorf("列 %s: %w", c.Name, err)
|
|
|
}
|
|
}
|
|
|
if c.IsTime() {
|
|
if c.IsTime() {
|
|
|
- // 时间列: 盘上是 Unix 纳秒, 回 SQL 得还原成时间文本 —— 否则 MySQL 的
|
|
|
|
|
- // DATETIME 列会收到一个 1.7e18 的整数 (驱动不会替我们转)。
|
|
|
|
|
- return formatSQLTime(int64(u), c.TimeKind), nil
|
|
|
|
|
|
|
+ // 存量旧表: 这列是当年迁成 uint64 存 Unix【纳秒】的时间列 (meta 里带
|
|
|
|
|
+ // TimeKind 标记), 回 SQL 仍按纳秒还原成时间文本。新表是 FieldType_DateTime,
|
|
|
|
|
+ // 走上面那条毫秒路 —— 两条路并存, 判别依据就是列的 FieldType。
|
|
|
|
|
+ return formatSQLTimeNS(int64(u), c.TimeKind), nil
|
|
|
}
|
|
}
|
|
|
// 绑定的 Go 类型必须跟目标列的声明一致, 而两个方言的声明不同:
|
|
// 绑定的 Go 类型必须跟目标列的声明一致, 而两个方言的声明不同:
|
|
|
// - MySQL: sqlTypeName 声明成 BIGINT UNSIGNED (uint64 是无符号档),
|
|
// - MySQL: sqlTypeName 声明成 BIGINT UNSIGNED (uint64 是无符号档),
|
|
@@ -550,6 +577,9 @@ func zeroForSQL(c Column) any {
|
|
|
case types.FieldType_Uint8, types.FieldType_Uint16, types.FieldType_Uint32, types.FieldType_Uint64,
|
|
case types.FieldType_Uint8, types.FieldType_Uint16, types.FieldType_Uint32, types.FieldType_Uint64,
|
|
|
types.FieldType_Int8, types.FieldType_Int16, types.FieldType_Int32, types.FieldType_Int64:
|
|
types.FieldType_Int8, types.FieldType_Int16, types.FieldType_Int32, types.FieldType_Int64:
|
|
|
return int64(0)
|
|
return int64(0)
|
|
|
|
|
+ case types.FieldType_DateTime:
|
|
|
|
|
+ // 与 coerceForSQL 的 nil 分支同一约定: 零时间给 Unix 0 (1970-01-01), 不给数字 0。
|
|
|
|
|
+ return formatSQLTime(0, "datetime")
|
|
|
default:
|
|
default:
|
|
|
return ""
|
|
return ""
|
|
|
}
|
|
}
|
|
@@ -680,8 +710,14 @@ func asUint64(v any) (uint64, error) {
|
|
|
|
|
|
|
|
// ============================ 时间列 ============================
|
|
// ============================ 时间列 ============================
|
|
|
//
|
|
//
|
|
|
-// VRTX 的时间表示只有一种: 8 字节整数 (Unix 纳秒时间戳)。SQL 的时间列迁进来
|
|
|
|
|
-// 要换算成它, 迁回去要换算回时间文本 —— 这一节就是这两个方向。
|
|
|
|
|
|
|
+// 时间列在 VRTX 侧有两种盘面, 读路径按【列的 FieldType】分岔:
|
|
|
|
|
+//
|
|
|
|
|
+// - FieldType_DateTime (新): 引擎专用时间类型, 行内 8 字节 Unix【毫秒】;
|
|
|
|
|
+// - FieldType_Uint64 + TimeKind 标记 (旧): 更早的实现把时间列迁成 uint64 存
|
|
|
|
|
+// Unix【纳秒】, 存量表的数据就是这个盘面 —— 判别靠 TimeKind (新表是 DateTime,
|
|
|
|
|
+// 旧表才会出现 Uint64 列带着它)。
|
|
|
|
|
+//
|
|
|
|
|
+// SQL 时间列迁进来要换算成目标盘面, 迁出去要换算回时间文本 —— 这一节就是这两个方向。
|
|
|
|
|
|
|
|
// sqlTimeLayouts SQL 时间文本的常见写法。MySQL 的 DATETIME / TIMESTAMP 打出来
|
|
// sqlTimeLayouts SQL 时间文本的常见写法。MySQL 的 DATETIME / TIMESTAMP 打出来
|
|
|
// 就是第一个; SQLite 存的是 TEXT, 可能带 T 和时区; DATE 只有日期部分。
|
|
// 就是第一个; SQLite 存的是 TEXT, 可能带 T 和时区; DATE 只有日期部分。
|
|
@@ -693,32 +729,33 @@ var sqlTimeLayouts = []string{
|
|
|
"2006-01-02",
|
|
"2006-01-02",
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// parseSQLTime 把 SQL 时间文本解析成 Unix 纳秒。
|
|
|
|
|
|
|
+// parseSQLTime 把 SQL 时间文本解析成 time.Time。
|
|
|
//
|
|
//
|
|
|
// 一律按 UTC 解释 (除非文本自带偏移): MySQL 的 DATETIME 本身不带时区, 若用本地
|
|
// 一律按 UTC 解释 (除非文本自带偏移): MySQL 的 DATETIME 本身不带时区, 若用本地
|
|
|
// 时区去解释, 同一份数据在不同机器上会迁出不同的时间戳 —— 迁移工具不该引入
|
|
// 时区去解释, 同一份数据在不同机器上会迁出不同的时间戳 —— 迁移工具不该引入
|
|
|
// 这种漂移。往返一致性优先于"看着像本地时间"。
|
|
// 这种漂移。往返一致性优先于"看着像本地时间"。
|
|
|
-func parseSQLTime(s string) (int64, bool) {
|
|
|
|
|
|
|
+func parseSQLTime(s string) (time.Time, bool) {
|
|
|
for _, layout := range sqlTimeLayouts {
|
|
for _, layout := range sqlTimeLayouts {
|
|
|
if tm, err := time.ParseInLocation(layout, s, time.UTC); err == nil {
|
|
if tm, err := time.ParseInLocation(layout, s, time.UTC); err == nil {
|
|
|
- return tm.UnixNano(), true
|
|
|
|
|
|
|
+ return tm, true
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- return 0, false
|
|
|
|
|
|
|
+ return time.Time{}, false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// parseIntOrTime 先按整数读, 读不动再按时间文本读。
|
|
|
|
|
|
|
+// parseIntOrTime 先按整数读, 读不动再按时间文本读 (时间文本 → Unix 纳秒)。
|
|
|
//
|
|
//
|
|
|
-// 时间列映射成 uint64 后, 值可能是 time.Time (MySQL 驱动开了 parseTime),
|
|
|
|
|
-// 也可能是文本 (parseTime=false 的默认档, 以及 SQLite 的 DATETIME 列) ——
|
|
|
|
|
-// 后者必须在这里换算, 否则写进 uint64 列时会被当成"不是数字"直接报错。
|
|
|
|
|
|
|
+// 这是【旧盘面】的入口: 时间列映射成 uint64 后 (存量 Uint64+TimeKind 表), 值可能
|
|
|
|
|
+// 是 time.Time (MySQL 驱动开了 parseTime), 也可能是文本 (parseTime=false 的默认档,
|
|
|
|
|
+// 以及 SQLite 的 DATETIME 列) —— 后者必须在这里换算, 否则写进 uint64 列时会被
|
|
|
|
|
+// 当成"不是数字"直接报错。新的 FieldType_DateTime 列不走这里 (见 coerceDateTimeForVRTX)。
|
|
|
func parseIntOrTime(s string) (int64, error) {
|
|
func parseIntOrTime(s string) (int64, error) {
|
|
|
s = strings.TrimSpace(s)
|
|
s = strings.TrimSpace(s)
|
|
|
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
|
|
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
|
|
|
return n, nil
|
|
return n, nil
|
|
|
}
|
|
}
|
|
|
- if ns, ok := parseSQLTime(s); ok {
|
|
|
|
|
- return ns, nil
|
|
|
|
|
|
|
+ if tm, ok := parseSQLTime(s); ok {
|
|
|
|
|
+ return tm.UnixNano(), nil
|
|
|
}
|
|
}
|
|
|
return 0, fmt.Errorf("无法把 %q 当整数 (也不是能识别的时间文本)", s)
|
|
return 0, fmt.Errorf("无法把 %q 当整数 (也不是能识别的时间文本)", s)
|
|
|
}
|
|
}
|
|
@@ -729,17 +766,80 @@ func parseUintOrTime(s string) (uint64, error) {
|
|
|
if n, err := strconv.ParseUint(s, 10, 64); err == nil {
|
|
if n, err := strconv.ParseUint(s, 10, 64); err == nil {
|
|
|
return n, nil
|
|
return n, nil
|
|
|
}
|
|
}
|
|
|
- if ns, ok := parseSQLTime(s); ok {
|
|
|
|
|
- return uint64(ns), nil
|
|
|
|
|
|
|
+ if tm, ok := parseSQLTime(s); ok {
|
|
|
|
|
+ return uint64(tm.UnixNano()), nil
|
|
|
}
|
|
}
|
|
|
return 0, fmt.Errorf("无法把 %q 当整数 (也不是能识别的时间文本)", s)
|
|
return 0, fmt.Errorf("无法把 %q 当整数 (也不是能识别的时间文本)", s)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// formatSQLTime Unix 纳秒 → SQL 时间文本。
|
|
|
|
|
|
|
+// coerceDateTimeForVRTX SQL 侧时间值 → FieldType_DateTime 列的形态 (time.Time / 毫秒数)。
|
|
|
|
|
+//
|
|
|
|
|
+// 引擎 codec 认两种输入: time.Time (编码层自行截毫秒) 与毫秒数值。SQL 侧的驱动值
|
|
|
|
|
+// 是 time.Time / 文本 / 整数, 这里统一收编:
|
|
|
|
|
+//
|
|
|
|
|
+// - time.Time 原样直传 (与 gormvrtx / SQL driver 的写入路径同一约定);
|
|
|
|
|
+// - 文本按 UTC 解析成 time.Time (parseSQLTime, 不用本地时区 —— 同一批数据在
|
|
|
|
|
+// 不同时区的机器上必须迁出同一个时间戳);
|
|
|
|
|
+// - 整数按【毫秒】解释 —— DateTime 列的盘上单位就是毫秒 (Uint64+TimeKind 的
|
|
|
|
|
+// 旧库才是纳秒, 那条路在 coerceForSQL 的 Uint64 分支)。
|
|
|
|
|
+//
|
|
|
|
|
+// 零时间对齐 gormvrtx 语义: 可空列写 NULL (零 time.Time 与 nil 一样表示"没有时间"),
|
|
|
|
|
+// 非空列落零毫秒 (Unix 0)。
|
|
|
|
|
+func coerceDateTimeForVRTX(v any, col string, nullable bool) (any, error) {
|
|
|
|
|
+ switch x := v.(type) {
|
|
|
|
|
+ case time.Time:
|
|
|
|
|
+ if x.IsZero() {
|
|
|
|
|
+ if nullable {
|
|
|
|
|
+ return nil, nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return uint64(0), nil
|
|
|
|
|
+ }
|
|
|
|
|
+ return x, nil
|
|
|
|
|
+ case string:
|
|
|
|
|
+ return parseSQLTimeValue(x, col)
|
|
|
|
|
+ case []byte:
|
|
|
|
|
+ return parseSQLTimeValue(string(x), col)
|
|
|
|
|
+ default:
|
|
|
|
|
+ n, err := asInt64(v)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return nil, fmt.Errorf("列 %s: %w", col, err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if n < 0 {
|
|
|
|
|
+ return nil, fmt.Errorf("列 %s: 时间戳不能为负: %d", col, n)
|
|
|
|
|
+ }
|
|
|
|
|
+ return uint64(n), nil // 已是毫秒
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// parseSQLTimeValue 文本 → time.Time, 解析失败报出列名。
|
|
|
|
|
+func parseSQLTimeValue(s, col string) (any, error) {
|
|
|
|
|
+ tm, ok := parseSQLTime(strings.TrimSpace(s))
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ return nil, fmt.Errorf("列 %s: 无法把 %q 当时间文本", col, s)
|
|
|
|
|
+ }
|
|
|
|
|
+ return tm, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// formatSQLTime Unix 毫秒 → SQL 时间文本 (FieldType_DateTime 列的读路径)。
|
|
|
|
|
+//
|
|
|
|
|
+// DATE 只留日期; DATETIME / TIMESTAMP 留到毫秒 —— 这正是 DateTime 列的全部精度,
|
|
|
|
|
+// 转文本无损。
|
|
|
|
|
+func formatSQLTime(ms int64, kind string) string {
|
|
|
|
|
+ tm := time.UnixMilli(ms).UTC()
|
|
|
|
|
+ if kind == "date" {
|
|
|
|
|
+ return tm.Format("2006-01-02")
|
|
|
|
|
+ }
|
|
|
|
|
+ if ms%1000 == 0 {
|
|
|
|
|
+ return tm.Format("2006-01-02 15:04:05") // 整秒别拖一串 .000
|
|
|
|
|
+ }
|
|
|
|
|
+ return tm.Format("2006-01-02 15:04:05.000")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// formatSQLTimeNS Unix 纳秒 → SQL 时间文本 (存量 Uint64+TimeKind 旧表的读路径)。
|
|
|
//
|
|
//
|
|
|
-// DATE 只留日期; DATETIME / TIMESTAMP 留到微秒 (纳秒到微秒是 SQL 时间类型本身
|
|
|
|
|
-// 的限制, MySQL 最细 DATETIME(6), 不是工具丢的)。
|
|
|
|
|
-func formatSQLTime(ns int64, kind string) string {
|
|
|
|
|
|
|
+// 只能写到微秒 (MySQL DATETIME(6) 的上限) —— 纳秒进、微秒出是旧盘面自身的精度
|
|
|
|
|
+// 上限, 不是工具丢的。新表走 formatSQLTime, 毫秒无损。
|
|
|
|
|
+func formatSQLTimeNS(ns int64, kind string) string {
|
|
|
tm := time.Unix(0, ns).UTC()
|
|
tm := time.Unix(0, ns).UTC()
|
|
|
if kind == "date" {
|
|
if kind == "date" {
|
|
|
return tm.Format("2006-01-02")
|
|
return tm.Format("2006-01-02")
|