From a8d066616d71c8d442a255602849927d2fc9e6c7 Mon Sep 17 00:00:00 2001 From: jmgasper Date: Tue, 23 Jun 2026 18:25:01 +1000 Subject: [PATCH] PM-4151: use latest MM winner scores What was broken The previous PM-4151 fixes made submitter and valid submitter exports choose the latest usable Marathon Match scored submission, but the winners export still aggregated all historical submission scores with MAX(). That could expose an older high provisional or final score for a winner after a later scored run existed. Root cause The winners SQL kept a separate Marathon Match score CTE that grouped by member and selected MAX(provisional_score) and MAX(final_score_raw). That bypassed the latest-submission selection pattern added to the other challenge export queries. What was changed Updated the winners export to select the latest usable Marathon Match scored submission by submission timestamp and submission id before rounding/exporting provisionalScore and finalScore. Updated the Reports Portal description for Challenge Winners to document the latest non-failed provisionalScore behavior. Any added/updated tests Updated src/reports/challenges/challenge-export-sql.spec.ts to assert all challenge user exports use DISTINCT ON latest scored submissions and do not aggregate Marathon Match provisional/final scores with MAX(). --- sql/reports/challenges/winners.sql | 30 ++++++++++++------- .../challenges/challenge-export-sql.spec.ts | 16 ++++++++++ src/reports/report-directory.data.ts | 2 +- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/sql/reports/challenges/winners.sql b/sql/reports/challenges/winners.sql index b45e236..85ac684 100644 --- a/sql/reports/challenges/winners.sql +++ b/sql/reports/challenges/winners.sql @@ -10,7 +10,9 @@ WITH challenge_context AS ( ), submission_metrics AS ( SELECT + s.id AS submission_id, s."memberId", + COALESCE(s."submittedDate", s."createdAt") AS submission_timestamp, COALESCE( final_review."aggregateScore", s."finalScore"::double precision, @@ -96,26 +98,32 @@ standard_member_scores AS ( FROM submission_metrics AS sm GROUP BY sm."memberId" ), -mm_member_scores AS ( - SELECT +mm_latest_submission_scores AS ( + SELECT DISTINCT ON (sm."memberId") sm."memberId", - MAX(sm.provisional_score) AS provisional_score_raw, - MAX(sm.final_score_raw) AS final_score_raw + sm.provisional_score AS provisional_score_raw, + sm.final_score_raw FROM submission_metrics AS sm - GROUP BY sm."memberId" + WHERE + sm.provisional_score IS NOT NULL + OR sm.final_score_raw IS NOT NULL + ORDER BY + sm."memberId", + sm.submission_timestamp DESC NULLS LAST, + sm.submission_id DESC ), mm_winner_scores AS ( SELECT - mms."memberId", + mlss."memberId", CASE - WHEN mms.provisional_score_raw IS NULL THEN NULL - ELSE ROUND(mms.provisional_score_raw::numeric, 2) + WHEN mlss.provisional_score_raw IS NULL THEN NULL + ELSE ROUND(mlss.provisional_score_raw::numeric, 2) END AS "provisionalScore", CASE - WHEN mms.final_score_raw IS NULL THEN NULL - ELSE ROUND(mms.final_score_raw::numeric, 2) + WHEN mlss.final_score_raw IS NULL THEN NULL + ELSE ROUND(mlss.final_score_raw::numeric, 2) END AS "finalScore" - FROM mm_member_scores AS mms + FROM mm_latest_submission_scores AS mlss ) SELECT COALESCE( diff --git a/src/reports/challenges/challenge-export-sql.spec.ts b/src/reports/challenges/challenge-export-sql.spec.ts index ebc7157..2023771 100644 --- a/src/reports/challenges/challenge-export-sql.spec.ts +++ b/src/reports/challenges/challenge-export-sql.spec.ts @@ -74,6 +74,22 @@ describe("Challenge export SQL", () => { ); }); + it.each(challengeUserSqlPaths)( + "uses the latest usable Marathon Match scored submission in %s", + (sqlPath) => { + const sql = sqlLoader.load(sqlPath); + + expect(sql).toMatch(/SELECT DISTINCT ON \(\w+\."memberId"\)/); + expect(sql).toMatch( + /ORDER BY\s+\w+\."memberId",\s+\w+\.submission_timestamp DESC NULLS LAST,\s+\w+\.submission_id DESC/, + ); + expect(sql).not.toMatch( + /MAX\(\w+\.provisional_score\) AS provisional_score_raw/, + ); + expect(sql).not.toMatch(/MAX\(\w+\.final_score_raw\) AS final_score_raw/); + }, + ); + it("only returns Marathon Match winner finalRank for completed challenges", () => { const sql = sqlLoader.load("reports/challenges/winners.sql"); diff --git a/src/reports/report-directory.data.ts b/src/reports/report-directory.data.ts index cb470c4..3c110a2 100644 --- a/src/reports/report-directory.data.ts +++ b/src/reports/report-directory.data.ts @@ -428,7 +428,7 @@ const REGISTERED_REPORTS_DIRECTORY: RegisteredReportsDirectory = { challengeReport( "Challenge Winners", "/challenges/:challengeId/winners", - "Return the challenge winners report with placement winners only. Marathon Match exports include non-failed provisionalScore and completed challenge finalScore/finalRank values.", + "Return the challenge winners report with placement winners only. Marathon Match exports use the latest non-failed provisionalScore and include completed challenge finalScore/finalRank values.", AppScopes.Challenge.Winners, [challengeIdParam], ),