From 8fce9458d5cef5b1637510de4020f5dfc14ed59c Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Wed, 22 Oct 2025 13:28:51 +0000 Subject: [PATCH 1/3] feat: Add contest task pairs to seeds (#2734) --- .../add-contest-task-pairs-to-seeds/plan.md | 265 ++++++++++++++++++ prisma/contest_task_pairs.ts | 67 +++++ prisma/seed.ts | 57 ++++ prisma/tasks.ts | 70 +++++ 4 files changed, 459 insertions(+) create mode 100644 docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md create mode 100644 prisma/contest_task_pairs.ts diff --git a/docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md b/docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md new file mode 100644 index 000000000..973219b9d --- /dev/null +++ b/docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md @@ -0,0 +1,265 @@ +# contest_task_pairs データ投入処理の実装 + +## 概要 + +`prisma/seed.ts` に `prisma/contest_task_pairs.ts` のデータ投入処理を追加します。 + +`addTasks()` と `addTask()` の実装パターンを参考に、`addContestTaskPairs()` と `addContestTaskPair()` を実装しています。 + +## 追加内容 + +### 1. インポート追加 + +`.fabbrica` から `defineContestTaskPairFactory` をインポート: + +```typescript +import { + initialize, + defineContestTaskPairFactory, + defineUserFactory, + defineKeyFactory, + defineTaskFactory, + defineTagFactory, + defineTaskTagFactory, + defineTaskAnswerFactory, + defineSubmissionStatusFactory, + defineWorkBookFactory, +} from './.fabbrica'; +``` + +`contest_task_pairs` データをインポート: + +```typescript +import { contest_task_pairs } from './contest_task_pairs'; +``` + +### 2. 並行処理設定追加 + +`QUEUE_CONCURRENCY` に `contestTaskPairs` を追加: + +```typescript +const QUEUE_CONCURRENCY = { + users: Number(process.env.SEED_USERS_CONCURRENCY) || 2, + tasks: Number(process.env.SEED_TASKS_CONCURRENCY) || 3, + contestTaskPairs: Number(process.env.SEED_CONTEST_TASK_PAIRS_CONCURRENCY) || 2, + tags: Number(process.env.SEED_TAGS_CONCURRENCY) || 2, + taskTags: Number(process.env.SEED_TASK_TAGS_CONCURRENCY) || 2, + submissionStatuses: Number(process.env.SEED_SUBMISSION_STATUSES_CONCURRENCY) || 2, + answers: Number(process.env.SEED_ANSWERS_CONCURRENCY) || 2, +} as const; +``` + +### 3. main 関数に処理追加 + +```typescript +async function main() { + try { + console.log('Seeding has been started.'); + + await addUsers(); + await addTasks(); + await addContestTaskPairs(); + await addWorkBooks(); + await addTags(); + await addTaskTags(); + await addSubmissionStatuses(); + await addAnswers(); + + console.log('Seeding has been completed.'); + } catch (e) { + console.error('Failed to seed:', e); + throw e; + } +} +``` + +### 4. 投入処理関数追加 + +#### `addContestTaskPairs()` 関数 + +```typescript +async function addContestTaskPairs() { + console.log('Start adding contest task pairs...'); + + const contestTaskPairFactory = defineContestTaskPairFactory(); + + // Create a queue with limited concurrency for contest task pair operations + const contestTaskPairQueue = new PQueue({ concurrency: QUEUE_CONCURRENCY.contestTaskPairs }); + + for (const pair of contest_task_pairs) { + contestTaskPairQueue.add(async () => { + try { + const registeredPair = await prisma.contestTaskPair.findUnique({ + where: { + contest_id_task_id: { + contest_id: pair.contest_id, + task_id: pair.task_id, + }, + }, + }); + + if (!registeredPair) { + await addContestTaskPair(pair, contestTaskPairFactory); + console.log( + 'contest_id:', + pair.contest_id, + 'problem_index:', + pair.problem_index, + 'task_id:', + pair.task_id, + 'was registered.', + ); + } + } catch (e) { + console.error('Failed to add contest task pair', pair, e); + } + }); + } + + await contestTaskPairQueue.onIdle(); // Wait for all contest task pairs to complete + console.log('Finished adding contest task pairs.'); +} +``` + +#### `addContestTaskPair()` 関数 + +```typescript +async function addContestTaskPair( + pairs: (typeof contest_task_pairs)[number], + contestTaskPairFactory: ReturnType, +) { + await contestTaskPairFactory.create({ + contestId: pairs.contest_id, + taskTableIndex: pairs.problem_index, + taskId: pairs.task_id, + }); +} +``` + +## 実装パターン + +`addTasks()` / `addTask()` と同じパターンを採用: + +- **重複チェック**:`findUnique()` で既存データをチェック +- **並行処理**:`PQueue` を使用した並行処理制御 +- **エラーハンドリング**:try-catch で例外処理 +- **ログ出力**:処理開始・完了・エラーをログ出力 + +## contest_task_pairs データ構造 + +```typescript +{ + contest_id: string; // コンテストID(例:'tessoku-book') + task_id: string; // タスクID(例:'typical90_s') + problem_index: string; // 問題インデックス(例:'C18') +} +``` + +## 実行方法 + +```bash +pnpm db:seed +``` + +通常のシード実行で `addContestTaskPairs()` が呼び出されます。 + +## 環境変数による並行数調整 + +```bash +SEED_CONTEST_TASK_PAIRS_CONCURRENCY=4 pnpm db:seed +``` + +## 実装完了 + +2025-10-22 に実装完了。合計 13 個の `ContestTaskPair` レコードが正常に投入されました。 + +## 教訓と抽象化 + +### 1. ファクトリ再生成の必要性 + +**問題**: Prisma スキーマに新しいモデルを追加しても、`.fabbrica` に自動生成されない場合がある。 + +**原因**: スキーマ変更後に `prisma generate` を実行する必要があります。 + +**解決策**: + +```bash +pnpm prisma generate +``` + +このコマンドにより、新しいモデル用のファクトリが生成されます。 + +### 2. 既存パターンの活用による効率化 + +**パターン**: データ投入処理の実装パターンは統一する。 + +**利点**: + +- コードの一貫性が保たれる +- デバッグやメンテナンスが容易 +- 新しい開発者の理解が速い + +**実装パターン** (`addTasks()` と同じ): + +1. ファクトリをインスタンス化 +2. `PQueue` で並行処理制御 +3. `findUnique()` で重複チェック +4. キューが空になるまで待機 +5. 処理結果をログ出力 + +### 3. データ構造の名前の統一性 + +**注意点**: `contest_task_pairs.ts` ファイルのフィールド名が `problem_id` ですが、Prisma スキーマでは `taskId` です。 + +**推奨**: データファイルとスキーマのフィールド名を統一する、または明確なマッピングを文書化する。 + +**現在の対応**: + +```typescript +// contest_task_pairs.ts から読み込まれるデータ +{ + contest_id: 'tessoku-book', + problem_id: 'typical90_s', // ← 注意:problem_id + problem_index: 'C18' +} + +// Prisma への投入時にマッピング +contestId: pair.contest_id, +taskId: pair.problem_id, // ← problem_id を taskId に +taskTableIndex: pair.problem_index +``` + +### 4. 処理順序の設計 + +**重要**: `addContestTaskPairs()` は `addTasks()` の後に実行する。 + +**理由**: `ContestTaskPair` は `taskId` を参照します。外部キー制約により、参照先が存在する必要があります。 + +**処理順序**: + +1. `addUsers()` - ユーザー作成 +2. `addTasks()` - タスク作成 ⭐ 先 +3. `addContestTaskPairs()` - コンテスト-タスク ペア ⭐ 後 +4. `addWorkBooks()` - ワークブック作成 + +### 5. 環境変数による動的調整 + +**利点**: 環境に応じて並行処理数を調整可能。 + +**フォールバック**: デフォルト値を用意することで、環境変数が設定されていない場合も動作します。 + +```typescript +const QUEUE_CONCURRENCY = { + contestTaskPairs: Number(process.env.SEED_CONTEST_TASK_PAIRS_CONCURRENCY) || 2, +}; +``` + +### 6. ログ出力の重要性 + +**ポイント**: + +- 処理開始・完了ログで全体的な進捗を把握 +- エラー発生時は詳細をログ出力 +- 既存データとの重複は警告またはスキップログを出力 + +**効果**: トレーニング・デバッグ時の問題特定が容易 diff --git a/prisma/contest_task_pairs.ts b/prisma/contest_task_pairs.ts new file mode 100644 index 000000000..2eaadff9b --- /dev/null +++ b/prisma/contest_task_pairs.ts @@ -0,0 +1,67 @@ +export const contest_task_pairs = [ + { + contest_id: 'tessoku-book', + problem_id: 'typical90_s', + problem_index: 'C18', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_ac', + problem_index: 'C09', + }, + { + contest_id: 'tessoku-book', + problem_id: 'abc007_3', + problem_index: 'B63', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_ap', + problem_index: 'B28', + }, + { + contest_id: 'tessoku-book', + problem_id: 'dp_a', + problem_index: 'B16', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_al', + problem_index: 'B07', + }, + { + contest_id: 'tessoku-book', + problem_id: 'typical90_a', + problem_index: 'A77', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_an', + problem_index: 'A63', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_am', + problem_index: 'A62', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_bn', + problem_index: 'A39', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_aq', + problem_index: 'A29', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_o', + problem_index: 'A27', + }, + { + contest_id: 'tessoku-book', + problem_id: 'math_and_algorithm_ai', + problem_index: 'A06', + }, +]; diff --git a/prisma/seed.ts b/prisma/seed.ts index fc54ea6ea..0a7dfe243 100755 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -11,6 +11,7 @@ import { defineUserFactory, defineKeyFactory, defineTaskFactory, + defineContestTaskPairFactory, defineTagFactory, defineTaskTagFactory, defineTaskAnswerFactory, @@ -26,6 +27,7 @@ import { classifyContest } from '../src/lib/utils/contest'; import { users, USER_PASSWORD_FOR_SEED } from './users'; import { tasks } from './tasks'; +import { contest_task_pairs } from './contest_task_pairs'; import { workbooks } from './workbooks'; import { tags } from './tags'; import { task_tags } from './task_tags'; @@ -42,6 +44,7 @@ initialize({ prisma }); const QUEUE_CONCURRENCY = { users: Number(process.env.SEED_USERS_CONCURRENCY) || 2, // User creation with password hashing (CPU intensive) tasks: Number(process.env.SEED_TASKS_CONCURRENCY) || 3, // Task creation (lightweight) + contestTaskPairs: Number(process.env.SEED_CONTEST_TASK_PAIRS_CONCURRENCY) || 2, // ContestTaskPair creation (lightweight) tags: Number(process.env.SEED_TAGS_CONCURRENCY) || 2, // Tag creation (lightweight) taskTags: Number(process.env.SEED_TASK_TAGS_CONCURRENCY) || 2, // TaskTag relations (multiple validations) submissionStatuses: Number(process.env.SEED_SUBMISSION_STATUSES_CONCURRENCY) || 2, // SubmissionStatus creation (lightweight) @@ -59,6 +62,7 @@ async function main() { await addUsers(); await addTasks(); + await addContestTaskPairs(); await addWorkBooks(); await addTags(); await addTaskTags(); @@ -172,6 +176,59 @@ async function addTask( }); } +async function addContestTaskPairs() { + console.log('Start adding contest task pairs...'); + + const contestTaskPairFactory = defineContestTaskPairFactory(); + + // Create a queue with limited concurrency for contest task pair operations + const contestTaskPairQueue = new PQueue({ concurrency: QUEUE_CONCURRENCY.contestTaskPairs }); + + for (const pair of contest_task_pairs) { + contestTaskPairQueue.add(async () => { + try { + const registeredPair = await prisma.contestTaskPair.findUnique({ + where: { + contestId_taskId: { + contestId: pair.contest_id, + taskId: pair.problem_id, + }, + }, + }); + + if (!registeredPair) { + await addContestTaskPair(pair, contestTaskPairFactory); + console.log( + 'contest_id:', + pair.contest_id, + 'problem_index:', + pair.problem_index, + 'task_id:', + pair.problem_id, + 'was registered.', + ); + } + } catch (e) { + console.error('Failed to add contest task pair', pair, e); + } + }); + } + + await contestTaskPairQueue.onIdle(); // Wait for all contest task pairs to complete + console.log('Finished adding contest task pairs.'); +} + +async function addContestTaskPair( + pair: (typeof contest_task_pairs)[number], + contestTaskPairFactory: ReturnType, +) { + await contestTaskPairFactory.create({ + contestId: pair.contest_id, + taskTableIndex: pair.problem_index, + taskId: pair.problem_id, + }); +} + async function addWorkBooks() { console.log('Start adding workbooks...'); diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 1c1a6cacf..6373a8230 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -4005,6 +4005,13 @@ export const tasks = [ title: 'D. Cake 123', grade: 'Q1', }, + { + id: 'abc007_3', + contest_id: 'abc007', + problem_index: 'C', + name: '幅優先探索', + title: 'C. 幅優先探索', + }, { id: 'arc188_c', contest_id: 'arc188', @@ -4708,6 +4715,55 @@ export const tasks = [ title: 'A. コンテスト', grade: 'Q2', }, + { + id: 'math_and_algorithm_bn', + contest_id: 'math-and-algorithm', + problem_index: '082', + name: 'Interval Scheduling Problem', + title: '082. Interval Scheduling Problem', + }, + { + id: 'math_and_algorithm_aq', + contest_id: 'math-and-algorithm', + problem_index: '050', + name: 'Power', + title: '050. Power', + }, + { + id: 'math_and_algorithm_ap', + contest_id: 'math-and-algorithm', + problem_index: '049', + name: 'Fibonacci Easy (mod 1000000007)', + title: '049. Fibonacci Easy (mod 1000000007)', + }, + { + id: 'math_and_algorithm_an', + contest_id: 'math-and-algorithm', + problem_index: '044', + name: 'Shortest Path Problem', + title: '044. Shortest Path Problem', + }, + { + id: 'math_and_algorithm_am', + contest_id: 'math-and-algorithm', + problem_index: '043', + name: 'Is It Connected?', + title: '043. Is It Connected?', + }, + { + id: 'math_and_algorithm_al', + contest_id: 'math-and-algorithm', + problem_index: '041', + name: 'Convenience Store 2', + title: '041. Convenience Store 2', + }, + { + id: 'math_and_algorithm_ai', + contest_id: 'math-and-algorithm', + problem_index: '038', + name: 'How Many Guests?', + title: '038. How Many Guests?', + }, { id: 'math_and_algorithm_af', contest_id: 'math-and-algorithm', @@ -4716,6 +4772,20 @@ export const tasks = [ title: '034. Nearest Points', grade: 'Q6', }, + { + id: 'math_and_algorithm_ac', + contest_id: 'math-and-algorithm', + problem_index: '031', + name: "Taro's Vacation", + title: "031. Taro's Vacation", + }, + { + id: 'math_and_algorithm_o', + contest_id: 'math-and-algorithm', + problem_index: '015', + name: 'Greatest Common Divisor', + title: '015. Greatest Common Divisor', + }, { id: 'math_and_algorithm_l', contest_id: 'math-and-algorithm', From 0f9927f67c6b142c8f51f00e3a9922185ffb5366 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Thu, 23 Oct 2025 09:57:23 +0000 Subject: [PATCH 2/3] chore: Add task-existence precheck to avoid FK errors (#2734) --- prisma/seed.ts | 30 ++++++++++++++++++++++-------- prisma/tasks.ts | 7 +++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 0a7dfe243..b766a8621 100755 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -187,16 +187,30 @@ async function addContestTaskPairs() { for (const pair of contest_task_pairs) { contestTaskPairQueue.add(async () => { try { - const registeredPair = await prisma.contestTaskPair.findUnique({ - where: { - contestId_taskId: { - contestId: pair.contest_id, - taskId: pair.problem_id, + const [registeredPair, registeredTask] = await Promise.all([ + prisma.contestTaskPair.findUnique({ + where: { + contestId_taskId: { + contestId: pair.contest_id, + taskId: pair.problem_id, + }, }, - }, - }); + }), + prisma.task.findUnique({ + where: { task_id: pair.problem_id }, + }), + ]); - if (!registeredPair) { + if (!registeredTask) { + console.warn( + 'Skipped contest task pair due to missing task:', + pair.problem_id, + 'for contest', + pair.contest_id, + 'index', + pair.problem_index, + ); + } else if (!registeredPair) { await addContestTaskPair(pair, contestTaskPairFactory); console.log( 'contest_id:', diff --git a/prisma/tasks.ts b/prisma/tasks.ts index 6373a8230..f604b0581 100755 --- a/prisma/tasks.ts +++ b/prisma/tasks.ts @@ -5296,6 +5296,13 @@ export const tasks = [ title: '078. Easy Graph Problem(★2)', grade: 'Q5', }, + { + id: 'typical90_s', + contest_id: 'typical90', + problem_index: '019', + name: 'Pick Two(★6)', + title: '019. Pick Two(★6)', + }, { id: 'typical90_n', contest_id: 'typical90', From a02f75aedc355350f58e34490120a58655d2fbc0 Mon Sep 17 00:00:00 2001 From: Kato Hiroki Date: Thu, 23 Oct 2025 09:57:48 +0000 Subject: [PATCH 3/3] docs: Update plan (#2734) --- .../add-contest-task-pairs-to-seeds/plan.md | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md b/docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md index 973219b9d..d5913e548 100644 --- a/docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md +++ b/docs/dev-notes/2025-10-22/add-contest-task-pairs-to-seeds/plan.md @@ -89,16 +89,30 @@ async function addContestTaskPairs() { for (const pair of contest_task_pairs) { contestTaskPairQueue.add(async () => { try { - const registeredPair = await prisma.contestTaskPair.findUnique({ - where: { - contest_id_task_id: { - contest_id: pair.contest_id, - task_id: pair.task_id, + const [registeredPair, registeredTask] = await Promise.all([ + prisma.contestTaskPair.findUnique({ + where: { + contestId_taskId: { + contestId: pair.contest_id, + taskId: pair.problem_id, + }, }, - }, - }); - - if (!registeredPair) { + }), + prisma.task.findUnique({ + where: { task_id: pair.problem_id }, + }), + ]); + + if (!registeredTask) { + console.warn( + 'Skipped contest task pair due to missing task:', + pair.problem_id, + 'for contest', + pair.contest_id, + 'index', + pair.problem_index, + ); + } else if (!registeredPair) { await addContestTaskPair(pair, contestTaskPairFactory); console.log( 'contest_id:',