Skip to content

Commit 0b23fef

Browse files
committed
MM leaderboard - provisional data fix
1 parent 811bb61 commit 0b23fef

2 files changed

Lines changed: 201 additions & 56 deletions

File tree

sql/reports/topcoder/leaderboard-mm.sql

Lines changed: 141 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,55 @@ WITH challenge_context AS (
88
ON ct.id = c."typeId"
99
WHERE c.id = ANY($1::text[])
1010
),
11+
-- Automated review types that must never contribute a leaderboard score.
12+
scan_review_types AS (
13+
SELECT rt.id
14+
FROM reviews."reviewType" AS rt
15+
WHERE LOWER(rt.name) IN ('av scan', 'sonarqube review', 'virus scan')
16+
),
1117
submission_metrics AS (
1218
SELECT
1319
cc.challenge_id,
20+
cc.challenge_name,
21+
cc.is_marathon_match,
1422
s.id AS submission_id,
1523
s."memberId" AS user_id,
1624
COALESCE(
1725
NULLIF(TRIM(u.handle), ''),
1826
NULLIF(TRIM(mem.handle), ''),
1927
fallback.member_handle
2028
) AS handle,
21-
COALESCE(final_review."aggregateScore", s."finalScore"::double precision) AS standard_score,
22-
provisional_review.provisional_score,
23-
COALESCE(final_review."aggregateScore", s."finalScore"::double precision) AS final_score_raw,
24-
COALESCE(s."submittedDate", s."createdAt") AS submitted_date
29+
COALESCE(NULLIF(TRIM(mem."firstName"), ''), NULLIF(TRIM(u.handle), ''), NULLIF(TRIM(mem.handle), '')) AS name,
30+
COALESCE(
31+
NULLIF(TRIM(mem."competitionCountryCode"), ''),
32+
NULLIF(TRIM(mem."homeCountryCode"), '')
33+
) AS country_code,
34+
mem."photoURL" AS photo_url,
35+
mmr.rating AS rating,
36+
mmr."ratingColor" AS rating_color,
37+
-- Final score comes ONLY from a non-provisional reviewSummation. submission."finalScore"
38+
-- is a denormalized column that is written during provisional MM reviews too, so using it
39+
-- here would surface a "final" score for matches that are still running.
40+
final_review."aggregateScore" AS final_score,
41+
final_review.is_strict_final,
42+
-- Provisional score: latest non-scan review row first, provisional summation second.
43+
-- NULLIF(..., 0) reproduces the old `||` chain, where a 0 review score fell through to
44+
-- the provisional summation before ending up back at 0.
45+
COALESCE(
46+
NULLIF(latest_review.review_score, 0),
47+
provisional_review.provisional_score,
48+
latest_review.review_score
49+
) AS provisional_score,
50+
COALESCE(s."submittedDate", s."createdAt") AS submitted_date,
51+
COALESCE(s."updatedAt", s."submittedDate", s."createdAt") AS updated_date
2552
FROM challenge_context AS cc
2653
JOIN reviews."submission" AS s
2754
ON s."challengeId" = cc.challenge_id
2855
AND s."memberId" IS NOT NULL
2956
LEFT JOIN LATERAL (
30-
SELECT rs."aggregateScore"
57+
SELECT
58+
rs."aggregateScore",
59+
rs."isFinal" IS TRUE AS is_strict_final
3160
FROM reviews."reviewSummation" AS rs
3261
WHERE rs."submissionId" = s.id
3362
AND COALESCE(rs."isFinal", TRUE) = TRUE
@@ -43,8 +72,25 @@ submission_metrics AS (
4372
ORDER BY COALESCE(rs."reviewedDate", rs."createdAt") DESC NULLS LAST, rs.id DESC
4473
LIMIT 1
4574
) AS provisional_review ON TRUE
75+
LEFT JOIN LATERAL (
76+
SELECT COALESCE(r."finalScore", r."initialScore") AS review_score
77+
FROM reviews.review AS r
78+
WHERE r."submissionId" = s.id
79+
AND (r."typeId" IS NULL OR r."typeId" NOT IN (SELECT id FROM scan_review_types))
80+
AND COALESCE(r."finalScore", r."initialScore") IS NOT NULL
81+
ORDER BY COALESCE(r."reviewDate", r."updatedAt", r."createdAt") DESC NULLS LAST, r.id DESC
82+
LIMIT 1
83+
) AS latest_review ON TRUE
4684
LEFT JOIN members."member" AS mem
4785
ON mem."userId" = s."memberId"::bigint
86+
LEFT JOIN LATERAL (
87+
SELECT DISTINCT ON (mmr."userId")
88+
mmr.rating,
89+
mmr."ratingColor"
90+
FROM members."memberMaxRating" AS mmr
91+
WHERE mmr."userId" = s."memberId"::bigint
92+
ORDER BY mmr."userId", mmr.rating DESC
93+
) AS mmr ON TRUE
4894
LEFT JOIN identity."user" AS u
4995
ON s."memberId" ~ '^[0-9]+$'
5096
AND u.user_id = s."memberId"::numeric
@@ -54,44 +100,111 @@ submission_metrics AS (
54100
WHERE r."challengeId" = cc.challenge_id
55101
AND r."memberId" = s."memberId"
56102
) AS fallback ON TRUE
57-
WHERE COALESCE(final_review."aggregateScore", s."finalScore"::double precision, s."initialScore"::double precision) IS NOT NULL
103+
),
104+
scored_submissions AS (
105+
SELECT sm.*
106+
FROM submission_metrics AS sm
107+
WHERE sm.handle IS NOT NULL
108+
AND (sm.final_score IS NOT NULL OR sm.provisional_score IS NOT NULL)
58109
),
59110
unique_member_submissions AS (
60111
SELECT DISTINCT ON (challenge_id, user_id)
61-
sm.*
62-
FROM submission_metrics AS sm
112+
ss.*
113+
FROM scored_submissions AS ss
63114
ORDER BY
64-
sm.challenge_id,
65-
sm.user_id,
66-
sm.submitted_date DESC NULLS LAST,
67-
sm.submission_id DESC
115+
ss.challenge_id,
116+
ss.user_id,
117+
ss.submitted_date DESC NULLS LAST,
118+
ss.updated_date DESC NULLS LAST,
119+
ss.submission_id DESC
120+
),
121+
-- A match is "complete" only when at least one submission carries an isFinal summation.
122+
-- Until then the leaderboard is ranked on the provisional/standard score.
123+
challenge_progress AS (
124+
SELECT
125+
challenge_id,
126+
BOOL_OR(is_strict_final AND final_score IS NOT NULL) AS has_final_results
127+
FROM unique_member_submissions
128+
GROUP BY challenge_id
68129
),
69130
ranked_submissions AS (
70131
SELECT
71132
ums.challenge_id,
72-
ums.user_id AS "userId",
73-
ums.submission_id AS "submissionId",
74-
ums.handle AS handle,
133+
ums.challenge_name,
134+
ums.is_marathon_match,
135+
ums.user_id,
136+
ums.submission_id,
137+
ums.handle,
138+
ums.name,
139+
ums.country_code,
140+
ums.photo_url,
141+
ums.rating,
142+
ums.rating_color,
143+
ums.final_score,
144+
ums.provisional_score,
145+
COALESCE(ums.final_score, ums.provisional_score) AS standard_score,
146+
ums.submitted_date,
75147
ROW_NUMBER() OVER (
76148
PARTITION BY ums.challenge_id
77-
ORDER BY ums.final_score_raw DESC NULLS LAST, ums.submitted_date ASC NULLS LAST, ums.user_id ASC
149+
ORDER BY
150+
CASE
151+
WHEN cp.has_final_results THEN ums.final_score
152+
ELSE COALESCE(ums.final_score, ums.provisional_score)
153+
END DESC NULLS LAST,
154+
ums.submitted_date ASC NULLS LAST,
155+
ums.user_id ASC
78156
) AS placement,
79-
ums.provisional_score AS "provisionalScore",
80-
ums.final_score_raw AS "finalScore",
81-
ums.standard_score AS score
157+
ROW_NUMBER() OVER (
158+
PARTITION BY ums.challenge_id
159+
ORDER BY
160+
ums.provisional_score DESC NULLS LAST,
161+
ums.submitted_date ASC NULLS LAST,
162+
ums.user_id ASC
163+
) AS provisional_rank
82164
FROM unique_member_submissions AS ums
165+
JOIN challenge_progress AS cp
166+
ON cp.challenge_id = ums.challenge_id
83167
)
84168
SELECT
85-
cc.challenge_id AS "challengeId",
86-
cc.challenge_name AS "challengeName",
87-
rs."userId",
88-
rs."submissionId",
169+
rs.challenge_id AS "challengeId",
170+
rs.challenge_name AS "challengeName",
171+
rs.is_marathon_match AS "isMarathonMatch",
172+
rs.user_id AS "userId",
173+
rs.submission_id AS "submissionId",
89174
rs.handle,
175+
rs.name,
176+
COALESCE(
177+
comp_code.name,
178+
comp_id.name,
179+
NULLIF(TRIM(rs.country_code), '')
180+
) AS country,
181+
NULLIF(TRIM(rs.country_code), '') AS "countryCode",
182+
rs.photo_url AS "photoURL",
183+
rs.rating AS rating,
184+
rs.rating_color AS "ratingColor",
90185
rs.placement,
91-
rs."provisionalScore",
92-
rs."finalScore",
93-
rs.score
186+
rs.provisional_rank AS "provisionalRank",
187+
-- Match the legacy formatting: 2 decimals, with anything in (0, 0.01) pinned to 0.01
188+
-- so a non-zero score never renders as 0.
189+
CASE
190+
WHEN rs.provisional_score IS NULL THEN NULL
191+
WHEN rs.provisional_score > 0 AND rs.provisional_score < 0.01 THEN 0.01
192+
ELSE ROUND(rs.provisional_score::numeric, 2)::double precision
193+
END AS "provisionalScore",
194+
CASE
195+
WHEN rs.final_score IS NULL THEN NULL
196+
WHEN rs.final_score > 0 AND rs.final_score < 0.01 THEN 0.01
197+
ELSE ROUND(rs.final_score::numeric, 2)::double precision
198+
END AS "finalScore",
199+
CASE
200+
WHEN rs.standard_score IS NULL THEN NULL
201+
WHEN rs.standard_score > 0 AND rs.standard_score < 0.01 THEN 0.01
202+
ELSE ROUND(rs.standard_score::numeric, 2)::double precision
203+
END AS score,
204+
rs.submitted_date AS "submittedDate"
94205
FROM ranked_submissions AS rs
95-
JOIN challenge_context AS cc
96-
ON cc.challenge_id = rs.challenge_id
206+
LEFT JOIN lookups."Country" AS comp_code
207+
ON UPPER(comp_code."countryCode") = UPPER(rs.country_code)
208+
LEFT JOIN lookups."Country" AS comp_id
209+
ON UPPER(comp_id.id) = UPPER(rs.country_code)
97210
ORDER BY rs.challenge_id, rs.placement;

src/reports/topcoder/topcoder-reports.service.ts

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,48 @@ type LeaderboardGenericRow = {
123123
type LeaderboardMmRow = {
124124
challengeId: string;
125125
challengeName: string;
126+
isMarathonMatch: boolean;
126127
userId: string;
128+
submissionId: string;
127129
handle: string;
130+
name: string | null;
131+
country: string | null;
132+
countryCode: string | null;
133+
photoURL: string | null;
134+
rating: string | number | null;
135+
ratingColor: string | null;
128136
placement: number;
137+
provisionalRank: number;
129138
provisionalScore: number | null;
130139
finalScore: number | null;
131140
score: number | null;
141+
submittedDate: string | null;
142+
};
143+
144+
// Mirrors the web LeaderboardEntry shape. `finalScore` is intentionally optional and
145+
// omitted (not null) when a match has no final results, because the leaderboard table
146+
// decides between "Final" and "Provisional" columns via `finalScore !== undefined`.
147+
type LeaderboardMmEntry = {
148+
challengeId: string;
149+
challengeName: string;
150+
userId: string;
151+
handle: string;
152+
placement: number;
153+
provisionalRank: number;
154+
provisionalScore?: number;
155+
finalScore?: number;
156+
score?: number;
157+
};
158+
159+
type LeaderboardMmMemberInfo = {
160+
userId: string;
161+
handle: string;
162+
name: string;
163+
country: string;
164+
countryCode: string;
165+
photoURL?: string;
166+
rating?: number;
167+
ratingColor?: string;
132168
};
133169

134170
type EngagementDataBaseRow = {
@@ -1219,45 +1255,41 @@ export class TopcoderReportsService implements OnModuleDestroy {
12191255
]);
12201256

12211257
const placementData = rows.reduce(
1222-
(acc: Record<string, LeaderboardMmRow[]>, row) => {
1258+
(acc: Record<string, LeaderboardMmEntry[]>, row) => {
12231259
acc[row.challengeId] = acc[row.challengeId] ?? [];
12241260
acc[row.challengeId].push({
12251261
challengeId: row.challengeId,
12261262
challengeName: row.challengeName,
12271263
userId: row.userId,
12281264
handle: row.handle,
1229-
placement: row.placement,
1230-
provisionalScore: row.provisionalScore,
1231-
finalScore: row.finalScore,
1232-
score: row.score,
1265+
placement: Number(row.placement),
1266+
provisionalRank: Number(row.provisionalRank),
1267+
...(row.provisionalScore !== null && {
1268+
provisionalScore: Number(row.provisionalScore),
1269+
}),
1270+
...(row.finalScore !== null && {
1271+
finalScore: Number(row.finalScore),
1272+
}),
1273+
...(row.score !== null && { score: Number(row.score) }),
12331274
});
12341275
return acc;
12351276
},
1236-
{} as Record<string, LeaderboardMmRow[]>,
1277+
{} as Record<string, LeaderboardMmEntry[]>,
12371278
);
12381279

1239-
const memberHandles = new Set<string>();
1240-
Object.values(placementData).forEach((entries) => {
1241-
entries.forEach((entry) => memberHandles.add(entry.handle));
1242-
});
1243-
1244-
const membersDetails: Record<
1245-
string,
1246-
{
1247-
userId: string;
1248-
handle: string;
1249-
name: string;
1250-
country: string;
1251-
countryCode: string;
1252-
}
1253-
> = {};
1254-
memberHandles.forEach((handle) => {
1255-
membersDetails[handle] = {
1256-
userId: handle,
1257-
handle,
1258-
name: handle,
1259-
country: "",
1260-
countryCode: "",
1280+
const membersDetails: Record<string, LeaderboardMmMemberInfo> = {};
1281+
rows.forEach((row) => {
1282+
const rating = row.rating === null ? undefined : Number(row.rating);
1283+
membersDetails[row.handle] = {
1284+
...membersDetails[row.handle],
1285+
userId: row.userId,
1286+
handle: row.handle,
1287+
name: row.name ?? row.handle,
1288+
country: row.country ?? "",
1289+
countryCode: row.countryCode ?? "",
1290+
...(row.photoURL && { photoURL: row.photoURL }),
1291+
...(rating !== undefined && Number.isFinite(rating) && { rating }),
1292+
...(row.ratingColor && { ratingColor: row.ratingColor }),
12611293
};
12621294
});
12631295

0 commit comments

Comments
 (0)