Skip to content

Comments

✨ Create ContestTaskPair table (#2627)#2628

Merged
KATO-Hiro merged 33 commits intostagingfrom
#2627
Oct 15, 2025
Merged

✨ Create ContestTaskPair table (#2627)#2628
KATO-Hiro merged 33 commits intostagingfrom
#2627

Conversation

@KATO-Hiro
Copy link
Collaborator

@KATO-Hiro KATO-Hiro commented Sep 28, 2025

WHAT

close #2627

Summary by CodeRabbit

  • New Features

    • Support for mapping problems/tasks to multiple contests via a composite-key Contest-Task mapping with CRUD operations and admin import/mapping UI scaffold.
  • Documentation

    • New planning docs covering phased migration, import strategy, compatibility layer, risks/mitigations, rollout plan, and decision points.
  • Chores

    • Database schema and migration added with indexes; removal of an obsolete preview flag; new types and services to support the mapping.

今後の課題

  1. 移行方針: TaskMapAdapter による段階的移行
  2. アダプタ配置: src/lib/utils/task_results.ts(影響局所化・テスト容易性・削除容易性)
  3. id 属性: 将来性を考慮して追加
  4. Feature Flag: 環境変数制御、getTaskResults()のみ適用(段階1)
  5. テスト方針: test + expect to + TypeScript strict + assert追加
  6. 監視手法: console.log + Vercelログ(APM導入見送り)
  7. データ投入: seed.ts優先 → 管理者画面は、その次の段階で実装

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 28, 2025

Walkthrough

Adds planning docs, a new Prisma model and SQL migration for ContestTaskPair, ERD updates, TypeScript types, and a service module with CRUD operations to map tasks to multiple contests using a composite (contestId, taskId) uniqueness constraint.

Changes

Cohort / File(s) Summary
Planning documents
docs/dev-notes/2025-09-17/contest-task-mapping/initial_plan.md, docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md
New design and implementation plans describing a composite key approach (contestId:taskId), phased migration, adapters and compatibility layer, import/UI scaffold, types, Prisma model, service APIs, migration steps, testing, and monitoring notes.
Prisma schema, ERD & migration
prisma/schema.prisma, prisma/ERD.md, prisma/migrations/20250928072152_create_contest_task_pair/migration.sql
Adds ContestTaskPair model and documents contesttaskpair in ERD; SQL migration creates public.contesttaskpair table with id PK, contestId, taskTableIndex, taskId, timestamps, index on contestId, and unique index on (contestId, taskId); ERD relation labels adjusted.
Type definitions
src/lib/types/contest_task_pair.ts
Adds TS types and aliases: ContestTaskPair, ContestTaskPairs, ContestTaskPairRead, ContestTaskPairCreate, ContestTaskPairUpdate, ContestTaskPairKey (template string), and TaskResultMapByContestTaskPair.
Service layer
src/lib/services/contest_task_pairs.ts
New service module exporting getContestTaskPairs, getContestTaskPair, createContestTaskPair, updateContestTaskPair using Prisma with logging and explicit error handling for unique-violation (P2002) and not-found (P2025) scenarios.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Admin as Admin / Seeder
  participant App as App Service
  participant Prisma as Prisma Client
  participant DB as Postgres

  rect rgb(238,245,255)
    note right of App: Create (idempotent) flow
    Admin->>App: createContestTaskPair(contestId, taskTableIndex, taskId)
    App->>Prisma: findUnique(where: { contestId, taskId })
    Prisma->>DB: SELECT
    DB-->>Prisma: row?
    alt exists
      Prisma-->>App: existing row
      App-->>Admin: existing ContestTaskPair
    else not exists
      App->>Prisma: create({ contestId, taskTableIndex, taskId })
      Prisma->>DB: INSERT
      DB-->>Prisma: row
      Prisma-->>App: new ContestTaskPair
      App-->>Admin: created ContestTaskPair
    end
  end

  rect rgb(245,238,255)
    note right of App: Read (list) flow
    Admin->>App: getContestTaskPairs(contestId?)
    App->>Prisma: findMany(where: { contestId? })
    Prisma->>DB: SELECT ... WHERE contestId=?
    DB-->>Prisma: rows
    Prisma-->>App: ContestTaskPairs
    App-->>Admin: list
  end

  rect rgb(255,250,238)
    note right of App: Update flow
    Admin->>App: updateContestTaskPair(contestId, taskId, taskTableIndex)
    App->>Prisma: update(where: { contestId_taskId }, data: { taskTableIndex })
    Prisma->>DB: UPDATE
    DB-->>Prisma: updated row or not found
    alt updated
      Prisma-->>App: updated ContestTaskPair
      App-->>Admin: updated
    else not found
      Prisma-->>App: P2025 error
      App-->>Admin: Not Found error
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble keys of contest:task with glee,
I hop through rows where mappings will be.
Migrations stamped, types tied with care,
Indexes hum beneath my hare.
A carrot, a commit — mapping set! 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The pull request title accurately identifies the creation of the ContestTaskPair table but includes an emoji and an issue number, which add noise and reduce clarity. Please remove the emoji and issue number to produce a concise, clear title such as “Create ContestTaskPair table.”
Out of Scope Changes Check ⚠️ Warning This pull request includes development planning documents under docs/dev-notes that are unrelated to the table creation and attribute definition objectives in issue #2627. Please remove the planning documents from this pull request or move them to a separate documentation update to keep the change focused on the database schema implementation.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues Check ✅ Passed The pull request fully implements the database schema, migration scripts, Prisma model, type definitions, and CRUD service functions required to create and define the ContestTaskPair table as specified in issue #2627.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch #2627

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
prisma/schema.prisma (2)

111-123: Add supporting index for common access pattern by (contestId, taskTableIndex).

Fetching tasks within a contest ordered by letter/index is common; add a composite index. Consider a uniqueness guarantee if your domain requires one.

 model ContestTaskPair {
   // ...
   @@unique([contestId, taskId])
   @@index([contestId])
+  @@index([contestId, taskTableIndex])
+  // If the contest letter must be unique within a contest, enforce it:
+  // @@unique([contestId, taskTableIndex])
   @@map("contesttaskpair")
 }

113-113: Consider using database UUID type for id (optional).

If you’re okay with a slight inconsistency vs. existing tables, prefer @db.Uuid + @default(uuid()) so Postgres stores UUIDs natively.

-  id             String   @id @default(uuid())
+  id             String   @id @db.Uuid @default(uuid())
prisma/migrations/20250928072152_create_contest_task_pair/migration.sql (1)

13-17: Add helpful indexes for query patterns.

Add indexes to support reverse lookups and in‑contest ordering.

 CREATE INDEX "contesttaskpair_contestId_idx" ON "public"."contesttaskpair"("contestId");
+CREATE INDEX "contesttaskpair_taskId_idx" ON "public"."contesttaskpair"("taskId");
+CREATE INDEX "contesttaskpair_contestId_taskTableIndex_idx" ON "public"."contesttaskpair"("contestId","taskTableIndex");
-- Optionally enforce uniqueness if required by domain:
-- CREATE UNIQUE INDEX "contesttaskpair_contestId_taskTableIndex_key" ON "public"."contesttaskpair"("contestId","taskTableIndex");
prisma/ERD.md (1)

129-137: Show relation to Task in ERD (after schema change).

Once you add ContestTaskPair.task -> Task.task_id, re-run ERD generation so the diagram reflects the link.

docs/dev-notes/2025-09-17/contest-task-mapping/initial_plan.md (1)

176-189: Fix markdownlint MD040: specify language on code fences.

Add a language to the directory tree block (e.g., text) to satisfy linting.

-```
+```text
 src/routes/admin/import/
 ├── problems/           # 問題データインポート
 │   ├── +page.svelte
 │   └── types.ts
 ├── mapping/            # コンテスト-問題マッピング
 │   ├── +page.svelte
 │   ├── bulk/
 │   └── selective/
 └── shared/            # 共通コンポーネント
     ├── ImportStatus.svelte
     └── ValidationResults.svelte

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used**: CodeRabbit UI

**Review profile**: CHILL

**Plan**: Pro

**Cache: Disabled due to data retention organization setting**

**Knowledge base: Disabled due to data retention organization setting**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 07586d60698fefbae50a0e250b2eac2b10c73614 and 858761171777872228814448352945cf3c73c8d0.

</details>

<details>
<summary>📒 Files selected for processing (6)</summary>

* `docs/dev-notes/2025-09-17/contest-task-mapping/initial_plan.md` (1 hunks)
* `docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md` (1 hunks)
* `prisma/ERD.md` (2 hunks)
* `prisma/migrations/20250928072152_create_contest_task_pair/migration.sql` (1 hunks)
* `prisma/schema.prisma` (2 hunks)
* `src/lib/types/contest_task_pair.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

<details>
<summary>docs/dev-notes/2025-09-17/contest-task-mapping/initial_plan.md</summary>

176-176: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)</summary>

* GitHub Check: preview

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>prisma/schema.prisma (2)</summary><blockquote>

`8-9`: **Confirm binaryTargets match deploy runtime (OpenSSL 3.x).**

If your deploy images aren’t RHEL/OpenSSL 3.x, Prisma engines may fail to load. Verify the runtime and adjust if needed.

---

`35-39`: **Nice: camelCase for new/modified models.**

This follows the note about Lucia v3 and gradual migration. No action needed.

</blockquote></details>
<details>
<summary>src/lib/types/contest_task_pair.ts (1)</summary><blockquote>

`1-5`: **Types look good and align with Prisma model.**

Lightweight aliases; ready for use in services.


If you maintain barrel exports (e.g., `src/lib/types/index.ts`), add these types there for discoverability.

</blockquote></details>
<details>
<summary>docs/dev-notes/2025-09-17/contest-task-mapping/initial_plan.md (1)</summary><blockquote>

`121-141`: **Good phased plan and adapter sketch.**

The compatibility layer and key format are pragmatic for staged migration.

</blockquote></details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 8587611 and 26676ff.

📒 Files selected for processing (1)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
🧰 Additional context used
🪛 GitHub Actions: CI
docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md

[error] 1-1: Prettier formatting check failed. Run 'prettier --write' to fix code style issues.

🔇 Additional comments (1)
docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1)

302-356: Replace assertions with throwing guards in TaskMapAdapter.

Line 304 still uses console.assert, which only logs even when the condition fails. The test plan in this document expects adapter.get('') and adapter.get('task1', '') to throw, so copying this code will keep the bug we discussed earlier. Please swap the assertions for explicit guards that throw Errors when the inputs are invalid.

@@
-    console.assert(
-      typeof taskId === 'string' && taskId.length > 0,
-      'taskId must be non-empty string',
-    );
+    if (typeof taskId !== 'string' || taskId.length === 0) {
+      throw new Error('taskId must be a non-empty string');
+    }
@@
-    console.assert(
-      typeof contestId === 'string' && contestId.length > 0,
-      'contestId must be non-empty string',
-    );
-    console.assert(
-      typeof taskId === 'string' && taskId.length > 0,
-      'taskId must be non-empty string',
-    );
+    if (typeof contestId !== 'string' || contestId.length === 0) {
+      throw new Error('contestId must be a non-empty string');
+    }
+    if (typeof taskId !== 'string' || taskId.length === 0) {
+      throw new Error('taskId must be a non-empty string');
+    }

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (5)
docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (5)

308-318: Avoid performance.now portability issues in Node

performance may not be in lib.dom for TS; prefer Date.now() or import from node:perf_hooks.

Apply:

-    const start = performance.now();
+    const start = Date.now();
...
-            `TaskMapAdapter.get: enhanced hit (${(performance.now() - start).toFixed(2)}ms)`,
+            `TaskMapAdapter.get: enhanced hit (${(Date.now() - start).toFixed(2)}ms)`,
...
-        console.log(`TaskMapAdapter.get: legacy hit (${(performance.now() - start).toFixed(2)}ms)`);
+        console.log(`TaskMapAdapter.get: legacy hit (${(Date.now() - start).toFixed(2)}ms)`);
...
-              `TaskMapAdapter.get: enhanced scan hit (${(performance.now() - start).toFixed(2)}ms)`,
+              `TaskMapAdapter.get: enhanced scan hit (${(Date.now() - start).toFixed(2)}ms)`,

Or import { performance } from 'node:perf_hooks' and keep performance.now.

Also applies to: 326-327, 337-338


89-117: Race between existence check and create

Between findUnique and create, concurrent writers can still hit P2002. Prefer upsert (if updating taskTableIndex is acceptable) or catch P2002 explicitly.

Apply (catch P2002, preserving “create-only” semantics):

-    // 新規レコード作成
-    const contestTaskPair = await db.contestTaskPair.create({
-      data: {
-        contestId,
-        taskTableIndex,
-        taskId,
-      },
-    });
+    // 新規レコード作成(競合時は穏当に無視)
+    let contestTaskPair: ContestTaskPair | undefined;
+    try {
+      contestTaskPair = await db.contestTaskPair.create({
+        data: { contestId, taskTableIndex, taskId },
+      });
+    } catch (e: unknown) {
+      if (e && typeof e === 'object' && 'code' in e && (e as any).code === 'P2002') {
+        console.log(`ContestTaskPair already exists (race): contestId=${contestId}, taskId=${taskId}`);
+        return;
+      }
+      throw e;
+    }

Alternatively, use upsert to update taskTableIndex when the pair exists.


202-204: Unify type aliases for contest-task keyed maps

Names and shapes diverge: TaskResultMapByContestTaskPair vs TaskResultByContestTaskPair. Standardize on one generic alias and use consistently in snippets and imports.

Apply:

-// マップの型(明示的)
-type TaskResultMapByContestTaskPair = Map<ContestTaskPairKey, TaskResult>;
+// Generic map type keyed by ContestTaskPairKey
+export type MapByContestTaskPair<T> = Map<ContestTaskPairKey, T>;
-type TaskResultByContestTaskPair<T> = Map<ContestTaskPairKey, T>;
+export type TaskResultByContestTaskPair<T> = Map<ContestTaskPairKey, T>;

And ensure service usage/imports reference the exported alias:

-import { TaskMapAdapter } from '$lib/utils/task_results';
+import { TaskMapAdapter, TaskResultByContestTaskPair } from '$lib/utils/task_results';

Also applies to: 283-286, 440-446


409-418: Remove unused contestTaskPairMap or return/use it

contestTaskPairMap is built but unused. Drop it or include it in the returned object and use downstream.

Apply:

-  // contestId:taskId -> Task のマップ(TaskResult 作成で直接参照しやすい)
-  const contestTaskPairMap = new Map<string, Task>();
-  for (const t of tasks) {
-    const contests = contestTaskPairs.get(t.task_id) ?? [t.contest_id];
-    for (const c of contests) {
-      contestTaskPairMap.set(`${c}:${t.task_id}`, t);
-    }
-  }
-
-  return { tasks, contestTaskPairs, contestTaskPairMap };
+  return { tasks, contestTaskPairs };

145-149: Update only mutable fields in updateContestTaskPair

Passing contestId/taskId in data is redundant and risks accidental key changes. Update taskTableIndex only.

Apply:

   data: {
-    contestId,
-    taskTableIndex,
-    taskId,
+    taskTableIndex,
   },
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 26676ff and f7485e5.

📒 Files selected for processing (1)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: preview
🔇 Additional comments (3)
docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (3)

425-427: Ensure types are exported/imported consistently

TaskResultByContestTaskPair is used here but not imported in the snippet. Export it from utils and import alongside TaskMapAdapter.

Apply:

-import { TaskMapAdapter } from '$lib/utils/task_results';
+import { TaskMapAdapter, TaskResultByContestTaskPair } from '$lib/utils/task_results';

Please confirm the actual file exports/imports match this.

Also applies to: 440-446


373-381: Syntax error: missing closing brace in createContestTaskPairKey

Close the second if block before returning; otherwise this snippet won’t compile.

Apply:

 export function createContestTaskPairKey(contestId: string, taskId: string): ContestTaskPairKey {
   if (!contestId || contestId.trim() === '') {
     throw new Error('contestId must be a non-empty string');
   }
   if (!taskId || taskId.trim() === '') {
     throw new Error('taskId must be a non-empty string');
   }
+
   return `${contestId}:${taskId}`;
 }

302-307: Replace console.assert with throwing guards (aligns with tests)

console.assert won’t throw; tests expect errors on invalid inputs. Add explicit validation in get and makeKey (also validate contestId when provided).

Apply:

   get(taskId: string, contestId?: string): T | undefined {
-    console.assert(
-      typeof taskId === 'string' && taskId.length > 0,
-      'taskId must be non-empty string',
-    );
+    if (typeof taskId !== 'string' || taskId.trim().length === 0) {
+      throw new Error('taskId must be a non-empty string');
+    }
+    if (contestId !== undefined && (typeof contestId !== 'string' || contestId.trim().length === 0)) {
+      throw new Error('contestId must be a non-empty string when provided');
+    }
   private makeKey(contestId: string, taskId: string): ContestTaskPairKey {
-    console.assert(
-      typeof contestId === 'string' && contestId.length > 0,
-      'contestId must be non-empty string',
-    );
-    console.assert(
-      typeof taskId === 'string' && taskId.length > 0,
-      'taskId must be non-empty string',
-    );
+    if (typeof contestId !== 'string' || contestId.trim().length === 0) {
+      throw new Error('contestId must be a non-empty string');
+    }
+    if (typeof taskId !== 'string' || taskId.trim().length === 0) {
+      throw new Error('taskId must be a non-empty string');
+    }

Also applies to: 348-356, 241-246

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
src/lib/types/contest_task_pair.ts (2)

10-18: Refactor Read and Update types to match their semantic purposes.

ContestTaskPairRead and ContestTaskPairUpdate are currently aliases of ContestTaskPairCreate, but they should have distinct definitions:

  • Read should be an alias of ContestTaskPair (the full Prisma model with id, createdAt, updatedAt)
  • Update should typically omit auto-generated fields or be a Partial<ContestTaskPairCreate> if supporting partial updates

Apply this diff:

-export type ContestTaskPairRead = ContestTaskPairCreate;
+export type ContestTaskPairRead = ContestTaskPair;

-export type ContestTaskPairUpdate = ContestTaskPairCreate;
+export type ContestTaskPairUpdate = ContestTaskPairCreate;

The Update type might need further refinement depending on whether you support partial updates. If only full updates are supported, the current definition is acceptable but could be renamed to ContestTaskPairUpdateData for clarity.


20-23: Consider adding a key creation helper for type safety.

The ContestTaskPairKey template literal type provides basic compile-time safety, but runtime key creation should be centralized. According to the plan document, there's a createContestTaskPairKey helper in utils.

Consider re-exporting the key creation helper from this types file for discoverability:

// For mapping and identification
export type ContestTaskPairKey = `${string}:${string}`; // "contest_id:task_id"

export type TaskResultMapByContestTaskPair = Map<ContestTaskPairKey, TaskResult>;

// Helper function (re-export from utils if defined there)
export function createContestTaskPairKey(contestId: string, taskId: string): ContestTaskPairKey {
  if (!contestId || contestId.trim() === '') {
    throw new Error('contestId must be a non-empty string');
  }
  if (!taskId || taskId.trim() === '') {
    throw new Error('taskId must be a non-empty string');
  }
  return `${contestId}:${taskId}`;
}

This centralizes the key format and validation logic with its type definition.

src/lib/services/contest_task_pairs.ts (6)

22-36: Simplify by returning directly.

The function correctly uses the composite key, but the intermediate variable assignment is unnecessary.

Apply this diff:

 export async function getContestTaskPair(
   contestId: string,
   taskId: string,
 ): Promise<ContestTaskPair | null> {
-  const contestTaskPair = await db.contestTaskPair.findUnique({
+  return await db.contestTaskPair.findUnique({
     where: {
       contestId_taskId: {
         contestId,
         taskId,
       },
     },
   });
-
-  return contestTaskPair;
 }

60-60: Remove unused variable declaration.

The variable contestTaskPair is declared but immediately assigned from the create result. The declaration with undefined is unnecessary.

Apply this diff:

-    let contestTaskPair: ContestTaskPair | undefined;
-
-    contestTaskPair = await db.contestTaskPair.create({
+    const contestTaskPair = await db.contestTaskPair.create({
       data: {
         contestId,
         taskTableIndex,
         taskId,
       },
     });

52-79: Consider simplifying by removing pre-check and relying on P2002 handling.

The current implementation checks for an existing record before creating, but this introduces a time-of-check to time-of-use (TOCTOU) race condition window. Since you already handle the P2002 unique constraint violation error, you could simplify by attempting creation directly and catching the constraint error.

Apply this diff to remove the redundant pre-check:

 export async function createContestTaskPair(
   contestId: string,
   taskTableIndex: string,
   taskId: string,
 ): Promise<void> {
   try {
-    const existingRecord = await getContestTaskPair(contestId, taskId);
-
-    if (existingRecord) {
-      console.log(`ContestTaskPair already exists: contestId=${contestId}, taskId=${taskId}`);
-      return;
-    }
-
-    let contestTaskPair: ContestTaskPair | undefined;
-
-    contestTaskPair = await db.contestTaskPair.create({
+    const contestTaskPair = await db.contestTaskPair.create({
       data: {
         contestId,
         taskTableIndex,
         taskId,
       },
     });
 
     console.log('Created ContestTaskPair:', contestTaskPair);
   } catch (error) {
     if (error && typeof error === 'object' && 'code' in error && (error as any).code === 'P2002') {
-      console.log(`Found ContestTaskPair (race): contestId=${contestId}, taskId=${taskId}`);
+      console.log(`ContestTaskPair already exists: contestId=${contestId}, taskId=${taskId}`);
       return;
     }
 
     console.error('Failed to create ContestTaskPair:', error);
     throw error;
   }
 }

This approach is more idiomatic (optimistic concurrency) and eliminates the race condition.


72-72: Type-cast error check is fragile.

The error code check uses a type-unsafe cast (error as any).code. Consider using a type guard or Prisma's error types for better type safety.

Apply this diff:

+import { Prisma } from '@prisma/client';
+
 // ... in catch block
-    if (error && typeof error === 'object' && 'code' in error && (error as any).code === 'P2002') {
+    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
       console.log(`Found ContestTaskPair (race): contestId=${contestId}, taskId=${taskId}`);
       return;
     }

This leverages Prisma's built-in error types for proper type safety.


96-103: Pre-check before update is redundant.

Prisma's update method will throw a P2025 error if the record doesn't exist, making the pre-check unnecessary and introducing a TOCTOU race condition window.

Apply this diff to remove the redundant check:

 export async function updateContestTaskPair(
   contestId: string,
   taskTableIndex: string,
   taskId: string,
 ): Promise<void> {
   try {
-    const existingRecord = await getContestTaskPair(contestId, taskId);
-
-    if (!existingRecord) {
-      const errorMessage = `Not found ContestTaskPair: contestId=${contestId}, taskId=${taskId}`;
-      console.log(errorMessage);
-      throw new Error(errorMessage);
-    }
-
     const updatedContestTaskPair = await db.contestTaskPair.update({
       where: {
         contestId_taskId: {
           contestId,
           taskId,
         },
       },
       data: {
         taskTableIndex,
       },
     });
 
     console.log('Updated ContestTaskPair:', updatedContestTaskPair);
   } catch (error) {
+    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
+      console.error(`Not found ContestTaskPair: contestId=${contestId}, taskId=${taskId}`);
+    }
     console.error('Failed to update ContestTaskPair:', error);
     throw error;
   }
 }

Handle the P2025 error in the catch block instead.


47-122: Consider returning created/updated records from mutations.

Both createContestTaskPair and updateContestTaskPair currently return void, preventing callers from accessing the created or updated record. Consider changing the return type to Promise<ContestTaskPair> for better developer ergonomics.

This would allow callers to:

  • Confirm creation/update success
  • Access auto-generated fields (id, timestamps)
  • Use the returned record without making an additional fetch

Example for createContestTaskPair:

 export async function createContestTaskPair(
   contestId: string,
   taskTableIndex: string,
   taskId: string,
-): Promise<void> {
+): Promise<ContestTaskPair> {
   try {
     const contestTaskPair = await db.contestTaskPair.create({
       data: {
         contestId,
         taskTableIndex,
         taskId,
       },
     });
 
     console.log('Created ContestTaskPair:', contestTaskPair);
+    return contestTaskPair;
   } catch (error) {
     if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
       console.log(`ContestTaskPair already exists: contestId=${contestId}, taskId=${taskId}`);
-      return;
+      // Fetch and return existing record
+      const existing = await getContestTaskPair(contestId, taskId);
+      if (!existing) throw new Error('Unexpected: record exists but cannot be fetched');
+      return existing;
     }
 
     console.error('Failed to create ContestTaskPair:', error);
     throw error;
   }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 5daa1bf and 052d0f3.

📒 Files selected for processing (3)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
  • src/lib/services/contest_task_pairs.ts (1 hunks)
  • src/lib/types/contest_task_pair.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: preview
🔇 Additional comments (3)
src/lib/types/contest_task_pair.ts (1)

1-8: LGTM! Clean type aliases.

The import structure and basic type aliases are well-organized and leverage Prisma's generated types appropriately.

src/lib/services/contest_task_pairs.ts (1)

10-12: LGTM! Simple and correct retrieval.

The implementation correctly fetches all ContestTaskPair records. Consider adding pagination support in the future if the dataset grows large.

docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1)

1-244: Planning document aligns with implementation.

The planning document provides comprehensive context for the ContestTaskPair feature implementation. The actual implementation in src/lib/types/contest_task_pair.ts and src/lib/services/contest_task_pairs.ts follows the outlined structure.

Note that past review comments flagged issues in code snippets within this document (around lines 92-113 and elsewhere), and those have been addressed in commits 930f11c to 26676ff per the existing comments. The live implementation files are the authoritative source and take precedence over any draft code in this planning document.

The document serves its purpose as design documentation and historical context for the feature.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1)

49-161: Update code examples to align with lessons learned.

The CRUD implementation examples show:

  • createContestTaskPair and updateContestTaskPair returning Promise<void> (lines 91, 131)
  • Pre-checking for existing records before creating/updating (lines 94-99, 134-140)

However, your "教訓と一般化" section (lines 246-253) recommends the opposite:

  • Returning created/updated records to avoid additional queries
  • Avoiding pre-checks to prevent TOCTOU issues

The actual implementation in src/lib/services/contest_task_pairs.ts correctly follows the lessons learned, but readers of this plan document might be confused by the contradiction.

Consider updating the code examples to reflect the final approach or adding a note that these are initial examples superseded by the lessons learned section.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 052d0f3 and 7dabae3.

📒 Files selected for processing (3)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
  • src/lib/services/contest_task_pairs.ts (1 hunks)
  • src/lib/types/contest_task_pair.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/types/contest_task_pair.ts
🔇 Additional comments (6)
src/lib/services/contest_task_pairs.ts (4)

12-14: LGTM!

Simple and correct implementation for fetching all ContestTaskPair records.


24-36: LGTM!

Correctly uses the composite unique key contestId_taskId to retrieve a single record.


49-81: Solid race-condition handling.

The implementation correctly avoids TOCTOU issues by attempting the create operation directly and catching P2002 errors. The fallback fetch (lines 69-73) with the null check provides good defensive programming against edge cases where the record might have been deleted between the error and the fetch.


94-125: LGTM!

The update implementation correctly handles the P2025 error case when the record doesn't exist, providing a clear error message.

docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (2)

217-238: LGTM!

The execution checklist and decisions are clear and well-organized, providing good guidance for the implementation phases.


241-264: Excellent lessons learned section.

The generalized lessons provide valuable guidance, especially:

  • Lesson 1: Avoiding TOCTOU with Prisma error handling
  • Lesson 2: Returning records to reduce queries
  • Lesson 4: Leveraging Prisma error codes for specific handling

These insights will be valuable for future development.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 7dabae3 and 5ef8ffc.

📒 Files selected for processing (1)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: preview

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 5ef8ffc and b54c017.

📒 Files selected for processing (1)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: preview

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (7)

18-29: Prisma schema: consider Int for taskTableIndex, add FKs, and add per‑contest index/uniqueness.

  • taskTableIndex looks positional; Int is a better fit.
  • Add foreign keys to enforce referential integrity.
  • If each contest’s task index is unique, add a unique on (contestId, taskTableIndex). Also consider an index on taskId for reverse lookups.
 model ContestTaskPair {
-  id             String   @id @default(uuid())
-  contestId      String
-  taskTableIndex String
-  taskId         String
+  id             String   @id @default(uuid())
+  contestId      String
+  taskTableIndex Int
+  taskId         String
+  // Relations (adjust model/field names to your schema)
+  contest        Contest  @relation(fields: [contestId], references: [id], onDelete: Cascade)
+  task           Task     @relation(fields: [taskId], references: [id], onDelete: Cascade)
   createdAt      DateTime @default(now())
   updatedAt      DateTime @updatedAt
 
   @@unique([contestId, taskId]) // ペアの重複を防止
+  @@unique([contestId, taskTableIndex]) // 同一コンテスト内での並び重複を防止(必要なら)
   @@index([contestId])          // contestId 検索を高速化
+  @@index([taskId])             // taskId からの逆引きを高速化(必要なら)
   @@map("contesttaskpair")
 }

53-58: Imported Create/Update types are not used in function signatures.

Either use the parameter types in the service functions or remove these imports to avoid drift between docs and code.


82-84: Nit: avoid unnecessary await on return.

return db.contestTaskPair.findMany(); is equivalent and slightly cleaner.

-export async function getContestTaskPairs(): Promise<ContestTaskPairs> {
-  return await db.contestTaskPair.findMany();
-}
+export async function getContestTaskPairs(): Promise<ContestTaskPairs> {
+  return db.contestTaskPair.findMany();
+}

89-112: Create: prefer upsert/guard + trim/validate inputs for robustness.

  • Use upsert for idempotent creation under the composite unique constraint.
  • Trim and validate inputs; fail fast with clear errors.
 export async function createContestTaskPair(
   contestId: string,
-  taskTableIndex: string,
+  taskTableIndex: number,
   taskId: string,
 ): Promise<ContestTaskPair> {
   try {
-    return await db.contestTaskPair.create({
-      data: {
-        contestId,
-        taskTableIndex,
-        taskId,
-      },
-    });
+    const cId = contestId.trim();
+    const tId = taskId.trim();
+    if (!cId) throw new Error('contestId must be non-empty');
+    if (!tId) throw new Error('taskId must be non-empty');
+    if (!Number.isInteger(taskTableIndex) || taskTableIndex < 0) {
+      throw new Error('taskTableIndex must be a non-negative integer');
+    }
+    return await db.contestTaskPair.upsert({
+      where: { contestId_taskId: { contestId: cId, taskId: tId } },
+      create: { contestId: cId, taskTableIndex, taskId: tId },
+      update: {}, // idempotent
+    });
   } catch (error) {
     if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
       const errorMessage = `ContestTaskPair already exists: contestId=${contestId}, taskId=${taskId}`;
       console.error(errorMessage);
       throw new Error(errorMessage);
     }
 
     console.error('Failed to create ContestTaskPair:', error);
     throw error;
   }
 }

117-144: Update: validate inputs; consider no‑op updates and clearer where clause type.

  • Trim and validate identifiers; validate taskTableIndex if changed.
  • If no actual change, allow a no‑op or early return to avoid P2025 surprises in some flows.
 export async function updateContestTaskPair(
   contestId: string,
-  taskTableIndex: string,
+  taskTableIndex: number,
   taskId: string,
 ): Promise<ContestTaskPair> {
   try {
-    return await db.contestTaskPair.update({
+    const cId = contestId.trim();
+    const tId = taskId.trim();
+    if (!cId) throw new Error('contestId must be non-empty');
+    if (!tId) throw new Error('taskId must be non-empty');
+    if (!Number.isInteger(taskTableIndex) || taskTableIndex < 0) {
+      throw new Error('taskTableIndex must be a non-negative integer');
+    }
+    return await db.contestTaskPair.update({
       where: {
-        contestId_taskId: {
-          contestId,
-          taskId,
-        },
+        contestId_taskId: { contestId: cId, taskId: tId },
       },
       data: {
         taskTableIndex,
       },
     });
   } catch (error) {
     if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {
       const errorMessage = `Not found ContestTaskPair: contestId=${contestId}, taskId=${taskId}`;
       console.error(errorMessage);
       throw new Error(errorMessage);
     }
 
     console.error('Failed to update ContestTaskPair:', error);
     throw error;
   }
 }

168-178: Type shapes: make Update partial; align Create with inferred Prisma args.

Update typically allows changing only mutable fields; keep keys separate. If you change taskTableIndex to Int, reflect that here.

-export type ContestTaskPairCreate = {
-  contestId: string;
-  taskTableIndex: string;
-  taskId: string;
-};
+export type ContestTaskPairCreate = {
+  contestId: string;
+  taskTableIndex: number;
+  taskId: string;
+};
 
 export type ContestTaskPairRead = ContestTaskPairCreate;
 
-export type ContestTaskPairUpdate = ContestTaskPairCreate;
+export type ContestTaskPairUpdate = {
+  contestId: string;
+  taskId: string;
+  taskTableIndex?: number;
+};

183-193: Key helper: add validation and delimiter safety.

Ensure non-empty segments and prevent embedded colons from corrupting the key. Optionally encode segments.

-type ContestTaskPairKey = `${string}:${string}`; // "contest_id:task_id"
+type ContestTaskPairKey = `${string}:${string}`; // "contest_id:task_id"
 
 // ヘルパー関数
-function createContestTaskPairKey(contestId: string, taskId: string): ContestTaskPairKey {
-  return `${contestId}:${taskId}`;
-}
+function createContestTaskPairKey(contestId: string, taskId: string): ContestTaskPairKey {
+  const c = contestId?.trim();
+  const t = taskId?.trim();
+  if (!c) throw new Error('contestId must be a non-empty string');
+  if (!t) throw new Error('taskId must be a non-empty string');
+  if (c.includes(':') || t.includes(':')) {
+    throw new Error('IDs must not contain ":"');
+  }
+  return `${c}:${t}`;
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between b54c017 and ffc1fb8.

📒 Files selected for processing (1)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: preview

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (8)
docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (8)

26-29: Add an index on taskId for reverse lookups.

Frequent queries by taskId (e.g., "which contests include this task?") will benefit from an index.

 model ContestTaskPair {
@@
   @@unique([contestId, taskId]) // ペアの重複を防止
   @@index([contestId])          // contestId 検索を高速化
+  @@index([taskId])             // taskId からの逆引きを高速化
   @@map("contesttaskpair")
 }

82-84: Allow optional filtering by contestId (scales better).

Returning all rows can be heavy; support an optional filter now to avoid future churn.

-export async function getContestTaskPairs(): Promise<ContestTaskPairs> {
-  return await db.contestTaskPair.findMany();
-}
+export async function getContestTaskPairs(contestId?: string): Promise<ContestTaskPairs> {
+  return await db.contestTaskPair.findMany({
+    where: contestId ? { contestId } : undefined,
+  });
+}

89-93: Align function args with declared DTO types (or drop the unused imports).

You import ContestTaskPairCreate/Update but don’t use them. Prefer object DTOs for clarity and future extensibility.

-export async function createContestTaskPair(
-  contestId: string,
-  taskTableIndex: string,
-  taskId: string,
-): Promise<ContestTaskPair> {
+export async function createContestTaskPair(
+  params: ContestTaskPairCreate,
+): Promise<ContestTaskPair> {
   try {
     return await db.contestTaskPair.create({
       data: {
-        contestId,
-        taskTableIndex,
-        taskId,
+        contestId: params.contestId,
+        taskTableIndex: params.taskTableIndex,
+        taskId: params.taskId,
       },
     });

117-121: Same: use DTO for update signature.

-export async function updateContestTaskPair(
-  contestId: string,
-  taskTableIndex: string,
-  taskId: string,
-): Promise<ContestTaskPair> {
+export async function updateContestTaskPair(
+  params: ContestTaskPairUpdate,
+): Promise<ContestTaskPair> {
   try {
     return await db.contestTaskPair.update({
       where: {
         contestId_taskId: {
-          contestId,
-          taskId,
+          contestId: params.contestId,
+          taskId: params.taskId,
         },
       },
       data: {
-        taskTableIndex,
+        taskTableIndex: params.taskTableIndex,
       },
     });

103-107: Preserve original error as cause when rethrowing (diagnostics).

-      throw new Error(errorMessage);
+      throw new Error(errorMessage, { cause: error as Error });

135-139: Same for P2025 branch: keep the original error as cause.

-      throw new Error(errorMessage);
+      throw new Error(errorMessage, { cause: error as Error });

169-176: Refine Read type to just the lookup keys.

Read shouldn’t require taskTableIndex. Keeps call sites clean.

-export type ContestTaskPairRead = ContestTaskPairCreate;
+export type ContestTaskPairRead = {
+  contestId: string;
+  taskId: string;
+};

183-193: Export mapping types/helpers and validate inputs; clarify TaskResult import.

Current snippet won’t compile as-is (TaskResult undefined) and types aren’t exported.

-// 型定義
-type ContestTaskPairKey = `${string}:${string}`; // "contest_id:task_id"
+// 型定義
+export type ContestTaskPairKey = `${string}:${string}`; // "contest_id:task_id"
 
-// ヘルパー関数
-function createContestTaskPairKey(contestId: string, taskId: string): ContestTaskPairKey {
-  return `${contestId}:${taskId}`;
-}
+// ヘルパー関数
+export function createContestTaskPairKey(contestId: string, taskId: string): ContestTaskPairKey {
+  if (!contestId?.trim()) throw new Error('contestId must be a non-empty string');
+  if (!taskId?.trim()) throw new Error('taskId must be a non-empty string');
+  return `${contestId}:${taskId}`;
+}
 
 // マップの型(明示的)
-type TaskResultMapByContestTaskPair = Map<ContestTaskPairKey, TaskResult>;
+// import type { TaskResult } from '$lib/types/task_result'; // TODO: 正しいパスに置換
+export type TaskResultMapByContestTaskPair = Map<ContestTaskPairKey, TaskResult>;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between ffc1fb8 and e63c209.

📒 Files selected for processing (1)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/lib/services/contest_task_pairs.ts (1)

17-19: Consider pagination for future scalability.

The current implementation fetches all ContestTaskPair records without pagination. While this works fine initially, it may become a performance concern as the dataset grows.

For future consideration, you could add optional pagination parameters:

export async function getContestTaskPairs(
  skip?: number,
  take?: number
): Promise<ContestTaskPairs> {
  return await db.contestTaskPair.findMany({
    skip,
    take,
  });
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between e63c209 and 0c431f4.

📒 Files selected for processing (3)
  • docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (1 hunks)
  • src/lib/services/contest_task_pairs.ts (1 hunks)
  • src/lib/types/contest_task_pair.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/lib/types/contest_task_pair.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: preview
🔇 Additional comments (7)
src/lib/services/contest_task_pairs.ts (3)

29-41: LGTM! Clean composite key lookup.

The function correctly uses the composite unique constraint contestId_taskId and properly handles the null case. The implementation is straightforward and type-safe.


52-84: LGTM! Robust error handling for race conditions.

The P2002 handling correctly manages concurrent creation attempts by fetching and returning the existing record. The defensive check at lines 74-76 appropriately guards against edge cases where the record exists but can't be retrieved. This implementation aligns well with the PR's goal of avoiding TOCTOU issues through Prisma's error handling.


95-126: LGTM! Proper error handling for missing records.

The function correctly handles the P2025 error (record not found) by throwing a descriptive error with the original error as cause. This provides good debugging context while maintaining a clear error message for callers.

docs/dev-notes/2025-09-23/contest-task-pair-mapping/plan.md (4)

13-44: LGTM! Well-designed schema with clear rationale.

The Prisma schema is thoughtfully designed:

  • The composite unique constraint @@unique([contestId, taskId]) correctly prevents duplicate pairs
  • The @@index([contestId]) optimizes contest-based queries
  • The id field provides future extensibility as noted in the design rationale

The schema aligns perfectly with the PR objectives.


46-158: LGTM! Plan examples match actual implementation.

The CRUD examples in this planning document align perfectly with the actual implementation in src/lib/services/contest_task_pairs.ts. The past review concerns (missing Prisma import, incorrect error handling) have been properly addressed. The code demonstrates good practices:

  • DRY principle with shared getContestTaskPair()
  • Prisma error handling for P2002 and P2025
  • TOCTOU-safe approach relying on database constraints

168-215: LGTM! Clear and comprehensive type definitions.

The type definitions demonstrate good TypeScript practices:

  • Reusing Prisma-generated types ensures type safety and consistency
  • Separate CRUD parameter types (Create, Read, Update) provide clear contracts
  • The ContestTaskPairKey template literal type with helper function offers type-safe key generation
  • TaskResultMapByContestTaskPair explicitly documents the mapping relationship

217-264: LGTM! Comprehensive planning with clear next steps.

The execution plan clearly documents:

  • Completed items (schema, migration, CRUD, types) matching the PR scope
  • Design decisions aligned with PR objectives (naming, adapter location, feature flags, etc.)
  • Future tasks appropriately deferred to later phases (migration, adapter implementation, data seeding)

The lessons learned section provides valuable guidance for future development, emphasizing Prisma error handling patterns and type safety practices.

Copy link
Collaborator Author

@KATO-Hiro KATO-Hiro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@KATO-Hiro KATO-Hiro merged commit a2bf055 into staging Oct 15, 2025
3 checks passed
@KATO-Hiro KATO-Hiro deleted the #2627 branch October 15, 2025 12:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DBのテーブルを作成・属性を定義

1 participant