106 lines
3.5 KiB
Go
106 lines
3.5 KiB
Go
// 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)
|
|
}
|
|
}
|