store: history table helpers (upsert/list, COALESCE preserves prior values)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user