Skip to content

Commit 6a4a881

Browse files
authored
Merge pull request #3677 from AtCoder-NoviSteps/fix/skip-getanswers-for-anonymous
fix(task-results): skip getAnswers full-scan for anonymous users
2 parents 830ea8d + 10e8a04 commit 6a4a881

3 files changed

Lines changed: 72 additions & 11 deletions

File tree

src/lib/services/task_results.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { NOT_FOUND } from '$lib/constants/http-response-status-codes';
3232
const statusById = await getSubmissionStatusMapWithId();
3333
const statusByName = await getSubmissionStatusMapWithName();
3434

35-
export async function getTaskResults(userId: string): Promise<TaskResults> {
35+
export async function getTaskResults(userId: string | undefined): Promise<TaskResults> {
3636
// 問題と特定のユーザの回答状況を使ってデータを結合
3737
// 計算量: 問題数をN、特定のユーザの解答数をMとすると、O(N + M)になるはず。
3838
const mergedTasksMap = await getMergedTasksMap();
@@ -127,14 +127,16 @@ async function transferAnswers(
127127
// with_mapをtrueにすると、taskIdを使って各TaskResultにO(1)でアクセスできる。
128128
// Why : データ総量を抑えるため。
129129
export async function getTaskResultsOnlyResultExists(
130-
userId: string,
130+
userId: string | undefined,
131131
with_map: boolean = false,
132132
): Promise<TaskResults | Map<string, TaskResult>> {
133133
const taskResultsMap: Map<string, TaskResult> = new Map();
134134

135135
// TODO: answerの降順にしたい
136136
const tasks = await getTasks();
137-
const answers = await answer_crud.getAnswers(userId);
137+
// Skip the DB round-trip for anonymous users: getAnswers(undefined) drops the
138+
// WHERE filter and full-scans taskAnswer.
139+
const answers = userId !== undefined ? await answer_crud.getAnswers(userId) : new Map();
138140
const tasksHasAnswer = tasks.filter((task) => answers.has(task.task_id));
139141
const taskResultsWithAnswer = tasksHasAnswer.map((task: Task) => {
140142
const taskResult = createDefaultTaskResult(userId, task);
@@ -215,9 +217,11 @@ export async function getTaskResultsByTaskId(
215217
* @param userId - User ID for creating TaskResults
216218
* @returns Promise<TaskResults> - Array of TaskResult objects
217219
*/
218-
async function createTaskResults(tasks: Tasks, userId: string): Promise<TaskResults> {
219-
const answers = await answer_crud.getAnswers(userId);
220+
async function createTaskResults(tasks: Tasks, userId: string | undefined): Promise<TaskResults> {
220221
const isLoggedIn = userId !== undefined;
222+
// Skip the DB round-trip for anonymous users: getAnswers(undefined) drops the
223+
// WHERE filter and full-scans taskAnswer.
224+
const answers = isLoggedIn ? await answer_crud.getAnswers(userId) : new Map();
221225

222226
return tasks.map((task: Task) => {
223227
const answer = isLoggedIn ? answers.get(task.task_id) : null;
@@ -236,7 +240,7 @@ async function createTaskResults(tasks: Tasks, userId: string): Promise<TaskResu
236240
*/
237241
function mergeTaskAndAnswer(
238242
task: Task,
239-
userId: string,
243+
userId: string | undefined,
240244
answer: TaskAnswer | null | undefined,
241245
): TaskResult {
242246
const taskResult = createDefaultTaskResult(userId, task);
@@ -263,7 +267,7 @@ function mergeTaskAndAnswer(
263267
return taskResult;
264268
}
265269

266-
export function createDefaultTaskResult(userId: string, task: Task): TaskResult {
270+
export function createDefaultTaskResult(userId: string | undefined, task: Task): TaskResult {
267271
const taskResult: TaskResult = {
268272
contest_id: task.contest_id,
269273
task_id: task.task_id,
@@ -312,7 +316,7 @@ export async function updateTaskResult(taskId: string, submissionStatus: string,
312316

313317
export async function getTasksWithTagIds(
314318
tagIds_string: string,
315-
userId: string,
319+
userId: string | undefined,
316320
): Promise<TaskResults> {
317321
const tagIds = tagIds_string.split(',');
318322
const taskIdByTagIds = await db.taskTag.groupBy({

src/lib/types/task.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,14 @@ export type TaskGrades = TaskGrade[];
6161

6262
export const taskGradeValues = Object.values(TaskGrade);
6363

64+
/**
65+
* A user's submission result for a single task, extending {@link Task} with status metadata.
66+
*
67+
* Used for both authenticated and anonymous (logged-out) views.
68+
*/
6469
export interface TaskResult extends Task {
65-
user_id: string;
70+
/** Owner of this result; `undefined` for anonymous (logged-out) results. */
71+
user_id: string | undefined;
6672
status_name: string;
6773
status_id: string;
6874
submission_status_image_path: string;

src/test/lib/services/task_results.test.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212

1313
import { describe, test, expect, vi, beforeEach } from 'vitest';
1414

15-
import { getTaskResults } from '$lib/services/task_results';
16-
import type { TaskResult, TaskResults } from '$lib/types/task';
15+
import { getTasks } from '$lib/services/tasks';
16+
import { getAnswers } from '$lib/services/answers';
17+
import { getTaskResults, getTaskResultsOnlyResultExists } from '$lib/services/task_results';
18+
19+
import type { TaskResult, TaskResults, Tasks } from '$lib/types/task';
1720

1821
import {
1922
MOCK_TASKS_DATA,
@@ -381,6 +384,27 @@ describe('getTaskResults', () => {
381384
});
382385
});
383386

387+
describe('when anonymous (userId is undefined)', () => {
388+
beforeEach(async () => {
389+
mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS;
390+
vi.mocked(getAnswers).mockClear();
391+
taskResults = await getTaskResults(undefined);
392+
});
393+
394+
test('does not call getAnswers (skips the full-scan DB round-trip)', () => {
395+
expect(getAnswers).not.toHaveBeenCalled();
396+
});
397+
398+
test('returns default (未挑戦) results for all tasks', () => {
399+
expect(taskResults.length).toBeGreaterThan(0);
400+
taskResults.forEach((taskResult: TaskResult) => {
401+
expect(taskResult.is_ac).toBe(false);
402+
expect(taskResult.status_name).toBe('ns');
403+
expect(taskResult.submission_status_label_name).toBe('未挑戦');
404+
});
405+
});
406+
});
407+
384408
describe('when answers exist', () => {
385409
beforeEach(async () => {
386410
mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS;
@@ -411,6 +435,33 @@ describe('getTaskResults', () => {
411435
});
412436
});
413437

438+
describe('getTaskResultsOnlyResultExists', () => {
439+
describe('when anonymous (userId is undefined)', () => {
440+
beforeEach(() => {
441+
mockAnswersForTest = MOCK_ANSWERS_WITH_ANSWERS;
442+
vi.mocked(getAnswers).mockClear();
443+
vi.mocked(getTasks).mockResolvedValue([
444+
{
445+
id: '1',
446+
contest_id: 'abc101',
447+
task_id: 'arc099_a',
448+
contest_type: 'ABC',
449+
task_table_index: 'C',
450+
title: 'Minimization',
451+
grade: 'Q3',
452+
},
453+
] as unknown as Tasks);
454+
});
455+
456+
test('does not call getAnswers and returns an empty array', async () => {
457+
const taskResults = await getTaskResultsOnlyResultExists(undefined, false);
458+
459+
expect(getAnswers).not.toHaveBeenCalled();
460+
expect(taskResults).toEqual([]);
461+
});
462+
});
463+
});
464+
414465
describe('mergeTaskAndAnswer', () => {
415466
createMergedTaskResults(
416467
'when no answers exist',

0 commit comments

Comments
 (0)