// Apply a reviewed, backed-up stale-baseline plan and rebuild its projections atomically. package main import ( "context" "database/sql" "encoding/json" "flag" "fmt" _ "github.com/go-sql-driver/mysql" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/stats" "math" "os" "sort" "strconv" "time" ) type change struct { Before struct { VIN string `json:"vin"` Protocol string `json:"protocol"` Date string `json:"stat_date"` Source string `json:"source_key"` First string `json:"first_total_mileage_km"` FirstTime string `json:"first_event_time"` Total string `json:"latest_total_mileage_km"` LastTime string `json:"latest_event_time"` Daily string `json:"daily_mileage_km"` } `json:"before"` Baseline struct { Total string `json:"total"` Time string `json:"time"` } `json:"baseline"` Daily string `json:"daily"` Status string `json:"status"` Reason string `json:"reason"` } func main() { input := flag.String("input", "", "reviewed JSON plan") apply := flag.Bool("apply", false, "apply plan and projections in one transaction") manifest := flag.String("backup-manifest", "", "verified backup manifest required for apply") flag.Parse() data, err := os.ReadFile(*input) must(err) var changes []change must(json.Unmarshal(data, &changes)) if !*apply { fmt.Printf("planned=%d dry_run=true\n", len(changes)) return } backup, err := os.ReadFile(*manifest) must(err) if !json.Valid(backup) { panic("invalid backup manifest") } db, err := sql.Open("mysql", os.Getenv("MYSQL_DSN")) must(err) defer db.Close() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) defer cancel() tx, err := db.BeginTx(ctx, nil) must(err) defer tx.Rollback() affected := map[string]change{} for _, c := range changes { b := c.Before total, stamp, found, err := stats.LookupLatestSourceBaselineBefore(ctx, tx, b.VIN, b.Date, envelope.Protocol(b.Protocol), b.Source) must(err) expected, err := strconv.ParseFloat(c.Baseline.Total, 64) must(err) if !found || math.Abs(total-expected) > 0.0001 || stamp.Format("2006-01-02 15:04:05") != c.Baseline.Time { panic(fmt.Sprintf("baseline changed: %s %s", b.VIN, b.Date)) } result, err := tx.ExecContext(ctx, `UPDATE vehicle_daily_mileage_source SET first_total_mileage_km=?,first_event_time=?,daily_mileage_km=?,quality_status=?,quality_reason=? WHERE vin=? AND stat_date=? AND protocol=? AND source_key=? AND first_total_mileage_km <=> ? AND first_event_time <=> ? AND latest_total_mileage_km=? AND latest_event_time=? AND daily_mileage_km=? AND quality_status='OK'`, c.Baseline.Total, c.Baseline.Time, c.Daily, c.Status, c.Reason, b.VIN, b.Date, b.Protocol, b.Source, b.First, b.FirstTime, b.Total, b.LastTime, b.Daily) must(err) n, err := result.RowsAffected() must(err) if n != 1 { panic(fmt.Sprintf("concurrent change: %s %s %s", b.VIN, b.Date, b.Source)) } affected[b.Date+"|"+b.VIN+"|"+b.Protocol] = c } keys := make([]string, 0, len(affected)) for key := range affected { keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { b := affected[key].Before must(stats.ProjectDailyMileage(ctx, tx, b.VIN, b.Date, envelope.Protocol(b.Protocol))) } must(tx.Commit()) must(json.NewEncoder(os.Stdout).Encode(map[string]any{"updatedSources": len(changes), "reprojectedDays": len(keys), "committed": true})) } func must(err error) { if err != nil { panic(err) } }