| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118 |
- package fixture
- import (
- "context"
- "database/sql"
- "fmt"
- "time"
- )
- // 本文件是**迁移运行标记**:把「这次映射是什么时刻跑的」落到目标库里,供另一个进程的
- // verify 精确取用。
- //
- // 为什么必须落库:
- // create_time 这类**业务时间**列在补了 DEFAULT CURRENT_TIMESTAMP(3) 之后,
- // 如果映射忘了传值,DB 会用「插入时刻」静默填上——不报错、不空、格式正确,
- // 只有「与迁移执行时刻比对」这一条断言能抓住它。
- // 而 map 与 verify 是两个独立进程,时间戳无法通过内存传递,所以写进 t_audit_log。
- //
- // 取值精度:毫秒(DATETIME(3)),与列精度一致,避免"差 1 毫秒"的伪差异。
- // RunMarkerAction 是标记行的 action 值(t_audit_log.action)。
- const RunMarkerAction = "ac15_fixture_run"
- // WriteRunMarker 写入一条运行标记,返回该标记的时间戳(= 本次迁移的起点)。
- //
- // 用 t_audit_log 而不是新建表:它是 core DDL 里既有的审计表(14 列、0 行、已对齐默认值),
- // 语义上也正是"谁在什么时候做了什么"。清理时按 action 删除即可。
- func WriteRunMarker(ctx context.Context, db *sql.DB, phase string) (time.Time, error) {
- now := time.Now().Truncate(time.Millisecond)
- const q = `
- INSERT INTO t_audit_log (actor_user_id, action, entity_type, entity_id, detail, trace_id, created_at)
- VALUES (NULL, ?, 'fixture', ?, ?, 'ac15-fixture', ?)`
- detail := fmt.Sprintf(`{"phase":%q,"tool":"ac15fixture","note":"migration run marker"}`, phase)
- if _, err := db.ExecContext(ctx, q, RunMarkerAction, phase, detail,
- now.Format("2006-01-02 15:04:05.000")); err != nil {
- return time.Time{}, fmt.Errorf("写运行标记失败:%w", err)
- }
- return now, nil
- }
- // ReadRunMarker 读最近一条运行标记的时间戳。
- func ReadRunMarker(ctx context.Context, db *sql.DB) (time.Time, bool, error) {
- var s sql.NullString
- err := db.QueryRowContext(ctx,
- `SELECT DATE_FORMAT(created_at,'%Y-%m-%d %H:%i:%s.%f') FROM t_audit_log
- WHERE action = ? ORDER BY id DESC LIMIT 1`, RunMarkerAction).Scan(&s)
- if err == sql.ErrNoRows || (err == nil && !s.Valid) {
- return time.Time{}, false, nil
- }
- if err != nil {
- return time.Time{}, false, err
- }
- t, perr := time.ParseInLocation("2006-01-02 15:04:05.000000", s.String, time.Local)
- if perr != nil {
- return time.Time{}, false, fmt.Errorf("解析运行标记时间失败(%q):%w", s.String, perr)
- }
- return t, true, nil
- }
- // ClearRunMarkers 删除全部运行标记(清理阶段调用)。
- func ClearRunMarkers(ctx context.Context, db *sql.DB) (int64, error) {
- res, err := db.ExecContext(ctx, `DELETE FROM t_audit_log WHERE action = ?`, RunMarkerAction)
- if err != nil {
- return 0, err
- }
- return res.RowsAffected()
- }
- // countRowsAfter 统计某表 create_time >= marker 的行数。
- //
- // 这就是「漏传业务时间」的探针:DB 默认值只会给出「插入时刻」,必然 >= marker;
- // 而 legacy 的历史业务时间一定 < marker(本次迁移发生在历史数据之后)。
- func countRowsAfter(ctx context.Context, db *sql.DB, table, column string, marker time.Time) (int, error) {
- var n int
- q := fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE %s >= ?`, table, column)
- if err := db.QueryRowContext(ctx, q, marker.Format("2006-01-02 15:04:05.000")).Scan(&n); err != nil {
- return 0, fmt.Errorf("统计 %s.%s 失败:%w", table, column, err)
- }
- return n, nil
- }
- // createTimeAssertions 对一批「迁移过来的写类表」生成 create_time 断言。
- //
- // tables 里的列名统一叫 create_time(本项目的业务时间列名);
- // marker 取不到时不判 pass(视为失败并明确说明),避免"没标记就当通过"。
- func createTimeAssertions(ctx context.Context, db *sql.DB, tables []string) []Assertion {
- marker, ok, err := ReadRunMarker(ctx, db)
- if err != nil {
- return []Assertion{{
- Name: "create_time 未落入 DB 默认值(业务时间必须来自 legacy)",
- Pass: false, Detail: fmt.Sprintf("读运行标记失败:%v", err),
- }}
- }
- if !ok {
- return []Assertion{{
- Name: "create_time 未落入 DB 默认值(业务时间必须来自 legacy)",
- Pass: false,
- Detail: "未找到运行标记(t_audit_log.action='ac15_fixture_run')—— 无法判定,按失败处理(先跑 map 再跑 verify)",
- }}
- }
- out := make([]Assertion, 0, len(tables))
- for _, t := range tables {
- n, err := countRowsAfter(ctx, db, t, "create_time", marker)
- name := t + ".create_time 未落入 DB 默认值(>= 迁移执行时刻的行数 = 0)"
- if err != nil {
- out = append(out, Assertion{Name: name, Pass: false, Detail: err.Error()})
- continue
- }
- out = append(out, Assertion{
- Name: name,
- Pass: n == 0,
- Detail: fmt.Sprintf("迁移执行时刻=%s;create_time >= 该时刻的行数 = %d(期望 0)"+
- "—— 补了 DEFAULT CURRENT_TIMESTAMP(3) 之后,漏传业务时间会静默落成插入时刻,只有这条能抓",
- marker.Format("2006-01-02 15:04:05"), n),
- })
- }
- return out
- }
|