From 205a7bf3d577ce395a4da56e7470954255a78449 Mon Sep 17 00:00:00 2001 From: hyungi Date: Tue, 16 Jun 2026 14:36:24 +0900 Subject: [PATCH] fix(study): attempt (quiz_session_id, study_question_id) partial UNIQUE (R9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit submit_attempt FOR UPDATE(3ba9537) 1차 방어에 더해 DB 레벨 belt-and-suspenders — 모바일 더블탭/재시도가 어떤 경로로든 이중 attempt INSERT 에 도달해도 차단. prod 실측 중복 0 (GROUP BY HAVING count>1 = 0)이라 안전 — dedup DELETE 멱등 precaution + partial UNIQUE (quiz_session_id IS NOT NULL). 세션 외 직접입력(NULL)은 비대상. 검증: migration_smoke PASS(post-baseline 361 적용). ★FOR UPDATE 가 정상경로선 막으므로 이 제약은 거의 트리거 안 됨 — 트리거 시 IntegrityError→500(should-never-happen 가시화); graceful 409 변환이 필요하면 submit_attempt 에 try/except 추가 가능(별도). Co-Authored-By: Claude Opus 4.8 (1M context) --- migrations/361_attempt_session_question_unique.sql | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 migrations/361_attempt_session_question_unique.sql diff --git a/migrations/361_attempt_session_question_unique.sql b/migrations/361_attempt_session_question_unique.sql new file mode 100644 index 0000000..5670886 --- /dev/null +++ b/migrations/361_attempt_session_question_unique.sql @@ -0,0 +1,14 @@ +-- 361: quiz 세션 내 같은 문제 이중 attempt 방지 partial UNIQUE (R9). +-- submit_attempt 의 FOR UPDATE 행잠금이 1차 방어이고, 이 제약은 DB 레벨 belt-and-suspenders +-- (모바일 더블탭/재시도가 어떤 경로로든 이중 INSERT 에 도달해도 차단). prod 실측 중복 0 건 +-- (SELECT ... GROUP BY HAVING count>1 = 0) — dedup DELETE 는 멱등 precaution, UNIQUE 는 안전. +-- quiz_session_id IS NULL(세션 외 직접 입력)은 대상 아님 → partial index. +DELETE FROM study_question_attempts a USING study_question_attempts b +WHERE a.quiz_session_id IS NOT NULL + AND a.quiz_session_id = b.quiz_session_id + AND a.study_question_id = b.study_question_id + AND a.id > b.id; + +CREATE UNIQUE INDEX IF NOT EXISTS uq_attempt_session_question +ON study_question_attempts (quiz_session_id, study_question_id) +WHERE quiz_session_id IS NOT NULL;