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 <blake@blakeridgway.com>
This commit is contained in:
40
db/db.go
40
db/db.go
@@ -185,15 +185,17 @@ func (d *DB) AdjustKM(ctx context.Context, guildID, userID, username string, km
|
|||||||
// ── Stats Queries ─────────────────────────────────────────────────────────────
|
// ── Stats Queries ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func (d *DB) GetLeaderboard(ctx context.Context, guildID string, since time.Time, limit int) ([]*UserStats, error) {
|
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)
|
q := `SELECT user_id,
|
||||||
FROM distance_logs WHERE guild_id = $1`
|
(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}
|
args := []interface{}{guildID}
|
||||||
if !since.IsZero() {
|
if !since.IsZero() {
|
||||||
args = append(args, since)
|
args = append(args, since)
|
||||||
q += fmt.Sprintf(` AND logged_at >= $%d`, len(args))
|
q += fmt.Sprintf(` AND logged_at >= $%d`, len(args))
|
||||||
}
|
}
|
||||||
args = append(args, limit)
|
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...)
|
rows, err := d.conn.QueryContext(ctx, q, args...)
|
||||||
if err != nil {
|
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) {
|
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, '')
|
q := `SELECT user_id,
|
||||||
FROM distance_logs WHERE guild_id = $1 AND user_id = $2`
|
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}
|
args := []interface{}{guildID, userID}
|
||||||
if !since.IsZero() {
|
if !since.IsZero() {
|
||||||
args = append(args, since)
|
args = append(args, since)
|
||||||
q += fmt.Sprintf(` AND logged_at >= $%d`, len(args))
|
q += fmt.Sprintf(` AND logged_at >= $%d`, len(args))
|
||||||
}
|
}
|
||||||
q += ` GROUP BY user_id, username`
|
q += ` GROUP BY user_id`
|
||||||
|
|
||||||
s := &UserStats{}
|
s := &UserStats{}
|
||||||
err := d.conn.QueryRowContext(ctx, q, args...).Scan(
|
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) {
|
func (d *DB) GetStatsInRange(ctx context.Context, guildID string, from, to time.Time, limit int) ([]*UserStats, error) {
|
||||||
rows, err := d.conn.QueryContext(ctx, `
|
rows, err := d.conn.QueryContext(ctx, `
|
||||||
SELECT user_id, username, SUM(km), COUNT(*), MAX(logged_at)
|
SELECT user_id,
|
||||||
FROM distance_logs
|
(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
|
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
|
ORDER BY SUM(km) DESC
|
||||||
LIMIT $4
|
LIMIT $4
|
||||||
`, guildID, from, to, limit)
|
`, 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) {
|
func (d *DB) GetYearlyLeaderboard(ctx context.Context, guildID string, year, limit int) ([]*UserStats, error) {
|
||||||
rows, err := d.conn.QueryContext(ctx, `
|
rows, err := d.conn.QueryContext(ctx, `
|
||||||
SELECT user_id, username, SUM(km), COUNT(*), MAX(logged_at)
|
SELECT user_id,
|
||||||
FROM distance_logs
|
(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
|
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
|
ORDER BY SUM(km) DESC
|
||||||
LIMIT $3
|
LIMIT $3
|
||||||
`, guildID, year, limit)
|
`, guildID, year, limit)
|
||||||
@@ -332,10 +340,12 @@ func (d *DB) GetUserYearlyStats(ctx context.Context, guildID, userID string, yea
|
|||||||
s := &UserStats{UserID: userID}
|
s := &UserStats{UserID: userID}
|
||||||
var lastUpdated sql.NullTime
|
var lastUpdated sql.NullTime
|
||||||
err := d.conn.QueryRowContext(ctx, `
|
err := d.conn.QueryRowContext(ctx, `
|
||||||
SELECT user_id, username, COALESCE(SUM(km), 0), COUNT(*), MAX(logged_at)
|
SELECT user_id,
|
||||||
FROM distance_logs
|
(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
|
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)
|
`, guildID, userID, year).Scan(&s.UserID, &s.Username, &s.TotalKM, &s.LogCount, &lastUpdated)
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return s, nil
|
return s, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user