Skip to content

Commit f0b5069

Browse files
committed
問題情報がADTに上書きされないようにする
INSERT する前に存在確認を行うことで、UPDATE を無くして既存の問題の情報(contest_id 等)が ADT によって上書きされないようにした。
1 parent b57d591 commit f0b5069

2 files changed

Lines changed: 111 additions & 21 deletions

File tree

atcoder-problems-backend/src/crawler_utils.rs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -244,29 +244,29 @@ async fn upsert_problems(
244244
new_problems: Vec<Problem>,
245245
) -> Result<usize, DbErr> {
246246
for problem in &new_problems {
247-
// Insert into problems table
248-
let title = problem.title();
249-
let model = sql_entities::problems::ActiveModel {
250-
id: Set(problem.id.clone()),
251-
contest_id: Set(problem.contest_id.clone()),
252-
problem_index: Set(problem.problem_index.clone()),
253-
name: Set(problem.name.clone()),
254-
title: Set(title),
255-
};
256-
sql_entities::problems::Entity::insert(model)
257-
.on_conflict(
258-
OnConflict::column(sql_entities::problems::Column::Id)
259-
.update_columns([
260-
sql_entities::problems::Column::ContestId,
261-
sql_entities::problems::Column::ProblemIndex,
262-
sql_entities::problems::Column::Name,
263-
sql_entities::problems::Column::Title,
264-
])
265-
.to_owned(),
266-
)
267-
.exec(db)
247+
// Check if problem already exists; only insert if it doesn't.
248+
// This prevents overwriting existing problem metadata (e.g. original contest_id)
249+
// when the same problem_id appears in multiple contests (ADT reuses ABC problems).
250+
// In other words: this is an ADT-specific countermeasure to keep the first-seen
251+
// contest as the canonical one for the problem.
252+
let existing = sql_entities::problems::Entity::find_by_id(problem.id.clone())
253+
.one(db)
268254
.await?;
269255

256+
if existing.is_none() {
257+
let title = problem.title();
258+
let model = sql_entities::problems::ActiveModel {
259+
id: Set(problem.id.clone()),
260+
contest_id: Set(problem.contest_id.clone()),
261+
problem_index: Set(problem.problem_index.clone()),
262+
name: Set(problem.name.clone()),
263+
title: Set(title),
264+
};
265+
sql_entities::problems::Entity::insert(model)
266+
.exec(db)
267+
.await?;
268+
}
269+
270270
// Insert into contest_problem table
271271
let contest_problem = sql_entities::contest_problem::ActiveModel {
272272
contest_id: Set(problem.contest_id.clone()),

atcoder-problems-backend/tests/test_crawler_utils.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,3 +402,93 @@ async fn test_crawl_contests_fetches_filtered_archive_categories() {
402402
.any(|contest| contest.id == "adt_all_20260612_2")
403403
);
404404
}
405+
406+
#[tokio::test]
407+
async fn test_crawl_adt_problems() {
408+
let db = setup_db().await.unwrap();
409+
410+
// 1. ABCのコンテスト、問題がクロールされる
411+
sql_entities::contests::Entity::insert(sql_entities::contests::ActiveModel {
412+
id: Set("abc001".to_string()),
413+
start_epoch_second: Set(0),
414+
duration_second: Set(0),
415+
title: Set("AtCoder Beginner Contest 001".to_string()),
416+
rate_change: Set("?".to_string()),
417+
})
418+
.exec(&db)
419+
.await
420+
.unwrap();
421+
422+
let mut abc_fetcher = MockProblemFetcher::new();
423+
abc_fetcher
424+
.expect_fetch_problems()
425+
.withf(|contest_id| contest_id == "abc001")
426+
.times(1)
427+
.returning(|_| {
428+
Ok(vec![Problem {
429+
id: "abc001_a".to_string(),
430+
contest_id: "abc001".to_string(),
431+
problem_index: "A".to_string(),
432+
name: "Problem A".to_string(),
433+
}])
434+
});
435+
436+
atcoder_problems_backend::crawler_utils::crawl_problems(&abc_fetcher, &db)
437+
.await
438+
.unwrap();
439+
440+
// 2. 次に、ADTのコンテスト、問題がクロールされる
441+
sql_entities::contests::Entity::insert(sql_entities::contests::ActiveModel {
442+
id: Set("adt_all_20260612".to_string()),
443+
start_epoch_second: Set(0),
444+
duration_second: Set(0),
445+
title: Set("AtCoder Daily Training 2026/06/12 All".to_string()),
446+
rate_change: Set("-".to_string()),
447+
})
448+
.exec(&db)
449+
.await
450+
.unwrap();
451+
452+
let mut adt_fetcher = MockProblemFetcher::new();
453+
adt_fetcher
454+
.expect_fetch_problems()
455+
.withf(|contest_id| contest_id == "adt_all_20260612")
456+
.times(1)
457+
.returning(|_| {
458+
Ok(vec![Problem {
459+
id: "abc001_a".to_string(),
460+
contest_id: "adt_all_20260612".to_string(),
461+
problem_index: "C".to_string(),
462+
name: "Problem C".to_string(),
463+
}])
464+
});
465+
466+
atcoder_problems_backend::crawler_utils::crawl_problems(&adt_fetcher, &db)
467+
.await
468+
.unwrap();
469+
470+
// 3. abc001_aがabc001とadt_all_20260612の両方に紐づいていることを確認する
471+
let problems_after_adt = sql_entities::problems::Entity::find()
472+
.all(&db)
473+
.await
474+
.unwrap();
475+
assert_eq!(problems_after_adt.len(), 1);
476+
assert_eq!(problems_after_adt[0].id, "abc001_a");
477+
assert_eq!(problems_after_adt[0].contest_id, "abc001");
478+
479+
let contest_problems = sql_entities::contest_problem::Entity::find()
480+
.all(&db)
481+
.await
482+
.unwrap();
483+
assert_eq!(contest_problems.len(), 2);
484+
assert!(contest_problems.iter().any(|cp| cp.contest_id == "abc001"
485+
&& cp.problem_id == "abc001_a"
486+
&& cp.problem_index == "A"));
487+
assert!(
488+
contest_problems
489+
.iter()
490+
.any(|cp| cp.contest_id == "adt_all_20260612"
491+
&& cp.problem_id == "abc001_a"
492+
&& cp.problem_index == "C")
493+
);
494+
}

0 commit comments

Comments
 (0)