package store import ( "context" "database/sql" "errors" "fmt" "time" ) // RepoStatsHistoryPoint is one (day, host) point for the trend chart. // Both metric pointers may be nil — a row exists as soon as either // metric was reported on that day. type RepoStatsHistoryPoint struct { Day time.Time // 00:00:00 UTC TotalSizeBytes *int64 SnapshotCount *int64 } // UpsertHostRepoStatsHistory records the metrics carried by a // repo.stats patch into the daily history table. Only the non-nil // fields of patch.TotalSizeBytes / patch.SnapshotCount are written; // existing values in the row are preserved via COALESCE so a // prune-only or check-only patch does not null out a backup-time // size we already captured earlier the same day. func (s *Store) UpsertHostRepoStatsHistory( ctx context.Context, hostID, day string, patch HostRepoStats, recordedAt time.Time, ) error { _, err := s.db.ExecContext(ctx, ` INSERT INTO host_repo_stats_history (host_id, day, total_size_bytes, snapshot_count, recorded_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(host_id, day) DO UPDATE SET total_size_bytes = COALESCE(excluded.total_size_bytes, host_repo_stats_history.total_size_bytes), snapshot_count = COALESCE(excluded.snapshot_count, host_repo_stats_history.snapshot_count), recorded_at = excluded.recorded_at`, hostID, day, nullableInt64(patch.TotalSizeBytes), nullableInt64(patch.SnapshotCount), recordedAt.UTC().Format(time.RFC3339Nano), ) if err != nil { return fmt.Errorf("store: upsert host_repo_stats_history: %w", err) } return nil } // ListHostRepoStatsHistory returns all points for hostID with day // >= since (UTC), ordered ascending. Pass time.Time{} to fetch the // full history. func (s *Store) ListHostRepoStatsHistory( ctx context.Context, hostID string, since time.Time, ) ([]RepoStatsHistoryPoint, error) { sinceStr := "" if !since.IsZero() { sinceStr = since.UTC().Format("2006-01-02") } rows, err := s.db.QueryContext(ctx, ` SELECT day, total_size_bytes, snapshot_count FROM host_repo_stats_history WHERE host_id = ? AND day >= ? ORDER BY day ASC`, hostID, sinceStr) if err != nil { return nil, fmt.Errorf("store: list host_repo_stats_history: %w", err) } defer func() { _ = rows.Close() }() var out []RepoStatsHistoryPoint for rows.Next() { var ( dayStr string total sql.NullInt64 snapCnt sql.NullInt64 ) if err := rows.Scan(&dayStr, &total, &snapCnt); err != nil { return nil, fmt.Errorf("store: scan history row: %w", err) } d, perr := time.Parse("2006-01-02", dayStr) if perr != nil { return nil, fmt.Errorf("store: parse history day %q: %w", dayStr, perr) } p := RepoStatsHistoryPoint{Day: d} if total.Valid { v := total.Int64 p.TotalSizeBytes = &v } if snapCnt.Valid { v := snapCnt.Int64 p.SnapshotCount = &v } out = append(out, p) } if err := rows.Err(); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("store: iterate history rows: %w", err) } return out, nil }