storage: add speedtests table + Save/List/Delete + retention
Adds a Store-method API for the RecordSpeedTest RPC (Task 8) and the
retention sweep (Task 14):
- SaveSpeedTest / ListRecentSpeedTests / DeleteSpeedTestsBefore
- SpeedTest struct (no json tags; that decision deferred to Task 10)
Schema lives in migrations/002_speedtests.sql to match the existing
embed.FS pattern (rather than inline in Go). The migrate() loop now
iterates every *.sql in lexical order, so future migrations drop in
without code changes. Both files use CREATE ... IF NOT EXISTS so the
existing 001 migration remains idempotent on already-initialised DBs.
CHECK constraints enforce node IN ('fl','pl1') and non-negative metric
values at the DB level. country has no DB CHECK — validation belongs
to the gRPC handler in Task 8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SpeedTest is a single client-side speedtest result reported via the
|
||||
// RecordSpeedTest RPC. Timestamp is unix seconds. Node is 'fl' or 'pl1'
|
||||
// (DB-level CHECK). Country is ISO-3166-1 alpha-2 (validated at the
|
||||
// gRPC handler, not at the DB).
|
||||
type SpeedTest struct {
|
||||
ID int64
|
||||
Timestamp int64
|
||||
Node string
|
||||
Country string
|
||||
DlMbps float64
|
||||
UlMbps float64
|
||||
PingMs float64
|
||||
JitterMs float64
|
||||
}
|
||||
|
||||
// SaveSpeedTest inserts a speedtest row and returns the new id.
|
||||
func (s *Store) SaveSpeedTest(ctx context.Context, st SpeedTest) (int64, error) {
|
||||
res, err := s.db.ExecContext(ctx, `
|
||||
INSERT INTO speedtests(timestamp, node, country, dl_mbps, ul_mbps, ping_ms, jitter_ms)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
||||
st.Timestamp, st.Node, st.Country, st.DlMbps, st.UlMbps, st.PingMs, st.JitterMs)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert speedtest: %w", err)
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// ListRecentSpeedTests returns up to limit speedtests ordered by id DESC
|
||||
// (newest first).
|
||||
func (s *Store) ListRecentSpeedTests(ctx context.Context, limit int) ([]SpeedTest, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT id, timestamp, node, country, dl_mbps, ul_mbps, ping_ms, jitter_ms
|
||||
FROM speedtests ORDER BY id DESC LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []SpeedTest{}
|
||||
for rows.Next() {
|
||||
var st SpeedTest
|
||||
if err := rows.Scan(&st.ID, &st.Timestamp, &st.Node, &st.Country,
|
||||
&st.DlMbps, &st.UlMbps, &st.PingMs, &st.JitterMs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// DeleteSpeedTestsBefore deletes rows with timestamp < ts and returns the
|
||||
// number of rows removed. Used by the retention sweep (Task 14).
|
||||
func (s *Store) DeleteSpeedTestsBefore(ctx context.Context, ts int64) (int64, error) {
|
||||
res, err := s.db.ExecContext(ctx, `DELETE FROM speedtests WHERE timestamp < ?`, ts)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
}
|
||||
Reference in New Issue
Block a user