From d858d04e84353f9f1a42f70a4a9a2eee167c0307 Mon Sep 17 00:00:00 2001 From: Blake Ridgway Date: Thu, 28 May 2026 14:16:02 -0500 Subject: [PATCH] Fix duplicate leaderboard entries with correlated subquery dedup Replace GROUP BY user_id,username with GROUP BY user_id and a correlated subquery that picks the latest username per user. This fixes the case where a user changes their Discord display name and appears as two separate leaderboard entries. Affected queries: GetLeaderboard, GetUserStats, GetStatsInRange, GetYearlyLeaderboard, GetUserYearlyStats. Also keeps the write-time username sync in AddLog for long-term data cleanup. Signed-off-by: Blake Ridgway --- db/db.go | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/db/db.go b/db/db.go index 9ff0cfb..12ca748 100644 --- a/db/db.go +++ b/db/db.go @@ -185,15 +185,17 @@ func (d *DB) AdjustKM(ctx context.Context, guildID, userID, username string, km // ── Stats Queries ───────────────────────────────────────────────────────────── func (d *DB) GetLeaderboard(ctx context.Context, guildID string, since time.Time, limit int) ([]*UserStats, error) { - q := `SELECT user_id, username, SUM(km), COUNT(*), MAX(logged_at) - FROM distance_logs WHERE guild_id = $1` + q := `SELECT user_id, + (SELECT username FROM distance_logs sub WHERE sub.user_id = dl.user_id AND sub.guild_id = dl.guild_id ORDER BY logged_at DESC LIMIT 1), + SUM(km), COUNT(*), MAX(logged_at) + FROM distance_logs dl WHERE guild_id = $1` args := []interface{}{guildID} if !since.IsZero() { args = append(args, since) q += fmt.Sprintf(` AND logged_at >= $%d`, len(args)) } args = append(args, limit) - q += fmt.Sprintf(` GROUP BY user_id, username ORDER BY SUM(km) DESC LIMIT $%d`, len(args)) + q += fmt.Sprintf(` GROUP BY user_id ORDER BY SUM(km) DESC LIMIT $%d`, len(args)) rows, err := d.conn.QueryContext(ctx, q, args...) if err != nil { @@ -227,14 +229,16 @@ func (d *DB) GetTotalKM(ctx context.Context, guildID string, since time.Time) (f } func (d *DB) GetUserStats(ctx context.Context, guildID, userID string, since time.Time) (*UserStats, error) { - q := `SELECT user_id, username, COALESCE(SUM(km), 0), COUNT(*), COALESCE(MAX(logged_at)::text, '') - FROM distance_logs WHERE guild_id = $1 AND user_id = $2` + q := `SELECT user_id, + COALESCE((SELECT username FROM distance_logs sub WHERE sub.user_id = dl.user_id AND sub.guild_id = dl.guild_id ORDER BY logged_at DESC LIMIT 1), ''), + COALESCE(SUM(km), 0), COUNT(*), COALESCE(MAX(logged_at)::text, '') + FROM distance_logs dl WHERE guild_id = $1 AND user_id = $2` args := []interface{}{guildID, userID} if !since.IsZero() { args = append(args, since) q += fmt.Sprintf(` AND logged_at >= $%d`, len(args)) } - q += ` GROUP BY user_id, username` + q += ` GROUP BY user_id` s := &UserStats{} err := d.conn.QueryRowContext(ctx, q, args...).Scan( @@ -247,10 +251,12 @@ func (d *DB) GetUserStats(ctx context.Context, guildID, userID string, since tim func (d *DB) GetStatsInRange(ctx context.Context, guildID string, from, to time.Time, limit int) ([]*UserStats, error) { rows, err := d.conn.QueryContext(ctx, ` - SELECT user_id, username, SUM(km), COUNT(*), MAX(logged_at) - FROM distance_logs + SELECT user_id, + (SELECT username FROM distance_logs sub WHERE sub.user_id = dl.user_id AND sub.guild_id = dl.guild_id ORDER BY logged_at DESC LIMIT 1), + SUM(km), COUNT(*), MAX(logged_at) + FROM distance_logs dl WHERE guild_id = $1 AND logged_at >= $2 AND logged_at <= $3 - GROUP BY user_id, username + GROUP BY user_id ORDER BY SUM(km) DESC LIMIT $4 `, guildID, from, to, limit) @@ -303,10 +309,12 @@ type YearTotal struct { func (d *DB) GetYearlyLeaderboard(ctx context.Context, guildID string, year, limit int) ([]*UserStats, error) { rows, err := d.conn.QueryContext(ctx, ` - SELECT user_id, username, SUM(km), COUNT(*), MAX(logged_at) - FROM distance_logs + SELECT user_id, + (SELECT username FROM distance_logs sub WHERE sub.user_id = dl.user_id AND sub.guild_id = dl.guild_id ORDER BY logged_at DESC LIMIT 1), + SUM(km), COUNT(*), MAX(logged_at) + FROM distance_logs dl WHERE guild_id = $1 AND EXTRACT(YEAR FROM logged_at) = $2 - GROUP BY user_id, username + GROUP BY user_id ORDER BY SUM(km) DESC LIMIT $3 `, guildID, year, limit) @@ -332,10 +340,12 @@ func (d *DB) GetUserYearlyStats(ctx context.Context, guildID, userID string, yea s := &UserStats{UserID: userID} var lastUpdated sql.NullTime err := d.conn.QueryRowContext(ctx, ` - SELECT user_id, username, COALESCE(SUM(km), 0), COUNT(*), MAX(logged_at) - FROM distance_logs + SELECT user_id, + (SELECT username FROM distance_logs sub WHERE sub.user_id = dl.user_id AND sub.guild_id = dl.guild_id ORDER BY logged_at DESC LIMIT 1), + COALESCE(SUM(km), 0), COUNT(*), MAX(logged_at) + FROM distance_logs dl WHERE guild_id = $1 AND user_id = $2 AND EXTRACT(YEAR FROM logged_at) = $3 - GROUP BY user_id, username + GROUP BY user_id `, guildID, userID, year).Scan(&s.UserID, &s.Username, &s.TotalKM, &s.LogCount, &lastUpdated) if err == sql.ErrNoRows { return s, nil