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() }