run_marker.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. package fixture
  2. import (
  3. "context"
  4. "database/sql"
  5. "fmt"
  6. "time"
  7. )
  8. // 本文件是**迁移运行标记**:把「这次映射是什么时刻跑的」落到目标库里,供另一个进程的
  9. // verify 精确取用。
  10. //
  11. // 为什么必须落库:
  12. // create_time 这类**业务时间**列在补了 DEFAULT CURRENT_TIMESTAMP(3) 之后,
  13. // 如果映射忘了传值,DB 会用「插入时刻」静默填上——不报错、不空、格式正确,
  14. // 只有「与迁移执行时刻比对」这一条断言能抓住它。
  15. // 而 map 与 verify 是两个独立进程,时间戳无法通过内存传递,所以写进 t_audit_log。
  16. //
  17. // 取值精度:毫秒(DATETIME(3)),与列精度一致,避免"差 1 毫秒"的伪差异。
  18. // RunMarkerAction 是标记行的 action 值(t_audit_log.action)。
  19. const RunMarkerAction = "ac15_fixture_run"
  20. // WriteRunMarker 写入一条运行标记,返回该标记的时间戳(= 本次迁移的起点)。
  21. //
  22. // 用 t_audit_log 而不是新建表:它是 core DDL 里既有的审计表(14 列、0 行、已对齐默认值),
  23. // 语义上也正是"谁在什么时候做了什么"。清理时按 action 删除即可。
  24. func WriteRunMarker(ctx context.Context, db *sql.DB, phase string) (time.Time, error) {
  25. now := time.Now().Truncate(time.Millisecond)
  26. const q = `
  27. INSERT INTO t_audit_log (actor_user_id, action, entity_type, entity_id, detail, trace_id, created_at)
  28. VALUES (NULL, ?, 'fixture', ?, ?, 'ac15-fixture', ?)`
  29. detail := fmt.Sprintf(`{"phase":%q,"tool":"ac15fixture","note":"migration run marker"}`, phase)
  30. if _, err := db.ExecContext(ctx, q, RunMarkerAction, phase, detail,
  31. now.Format("2006-01-02 15:04:05.000")); err != nil {
  32. return time.Time{}, fmt.Errorf("写运行标记失败:%w", err)
  33. }
  34. return now, nil
  35. }
  36. // ReadRunMarker 读最近一条运行标记的时间戳。
  37. func ReadRunMarker(ctx context.Context, db *sql.DB) (time.Time, bool, error) {
  38. var s sql.NullString
  39. err := db.QueryRowContext(ctx,
  40. `SELECT DATE_FORMAT(created_at,'%Y-%m-%d %H:%i:%s.%f') FROM t_audit_log
  41. WHERE action = ? ORDER BY id DESC LIMIT 1`, RunMarkerAction).Scan(&s)
  42. if err == sql.ErrNoRows || (err == nil && !s.Valid) {
  43. return time.Time{}, false, nil
  44. }
  45. if err != nil {
  46. return time.Time{}, false, err
  47. }
  48. t, perr := time.ParseInLocation("2006-01-02 15:04:05.000000", s.String, time.Local)
  49. if perr != nil {
  50. return time.Time{}, false, fmt.Errorf("解析运行标记时间失败(%q):%w", s.String, perr)
  51. }
  52. return t, true, nil
  53. }
  54. // ClearRunMarkers 删除全部运行标记(清理阶段调用)。
  55. func ClearRunMarkers(ctx context.Context, db *sql.DB) (int64, error) {
  56. res, err := db.ExecContext(ctx, `DELETE FROM t_audit_log WHERE action = ?`, RunMarkerAction)
  57. if err != nil {
  58. return 0, err
  59. }
  60. return res.RowsAffected()
  61. }
  62. // countRowsAfter 统计某表 create_time >= marker 的行数。
  63. //
  64. // 这就是「漏传业务时间」的探针:DB 默认值只会给出「插入时刻」,必然 >= marker;
  65. // 而 legacy 的历史业务时间一定 < marker(本次迁移发生在历史数据之后)。
  66. func countRowsAfter(ctx context.Context, db *sql.DB, table, column string, marker time.Time) (int, error) {
  67. var n int
  68. q := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE %s >= ?`, table, column)
  69. if err := db.QueryRowContext(ctx, q, marker.Format("2006-01-02 15:04:05.000")).Scan(&n); err != nil {
  70. return 0, fmt.Errorf("统计 %s.%s 失败:%w", table, column, err)
  71. }
  72. return n, nil
  73. }
  74. // createTimeAssertions 对一批「迁移过来的写类表」生成 create_time 断言。
  75. //
  76. // tables 里的列名统一叫 create_time(本项目的业务时间列名);
  77. // marker 取不到时不判 pass(视为失败并明确说明),避免"没标记就当通过"。
  78. func createTimeAssertions(ctx context.Context, db *sql.DB, tables []string) []Assertion {
  79. marker, ok, err := ReadRunMarker(ctx, db)
  80. if err != nil {
  81. return []Assertion{{
  82. Name: "create_time 未落入 DB 默认值(业务时间必须来自 legacy)",
  83. Pass: false, Detail: fmt.Sprintf("读运行标记失败:%v", err),
  84. }}
  85. }
  86. if !ok {
  87. return []Assertion{{
  88. Name: "create_time 未落入 DB 默认值(业务时间必须来自 legacy)",
  89. Pass: false,
  90. Detail: "未找到运行标记(t_audit_log.action='ac15_fixture_run')—— 无法判定,按失败处理(先跑 map 再跑 verify)",
  91. }}
  92. }
  93. out := make([]Assertion, 0, len(tables))
  94. for _, t := range tables {
  95. n, err := countRowsAfter(ctx, db, t, "create_time", marker)
  96. name := t + ".create_time 未落入 DB 默认值(>= 迁移执行时刻的行数 = 0)"
  97. if err != nil {
  98. out = append(out, Assertion{Name: name, Pass: false, Detail: err.Error()})
  99. continue
  100. }
  101. out = append(out, Assertion{
  102. Name: name,
  103. Pass: n == 0,
  104. Detail: fmt.Sprintf("迁移执行时刻=%s;create_time >= 该时刻的行数 = %d(期望 0)"+
  105. "—— 补了 DEFAULT CURRENT_TIMESTAMP(3) 之后,漏传业务时间会静默落成插入时刻,只有这条能抓",
  106. marker.Format("2006-01-02 15:04:05"), n),
  107. })
  108. }
  109. return out
  110. }