From 44ccba2151d2c275da8036c8e5ce545e4a7497fd Mon Sep 17 00:00:00 2001 From: CC Worker Date: Mon, 8 Jun 2026 18:45:09 +0000 Subject: [PATCH] fix(exam): guarantee auto-map child rows reference an inserted question On papers where band detection yields few/no questions but opencv/gemma still emit response regions, those regions referenced a synthetic default_qid that was never inserted -> FK violation (exam_response_areas/exam_boundaries -> exam_questions). Ensure the fallback container question exists and reattach orphan child rows to it. Co-Authored-By: Claude Opus 4.8 --- routers/exam/templates.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/routers/exam/templates.py b/routers/exam/templates.py index 1090751..ef5cf75 100644 --- a/routers/exam/templates.py +++ b/routers/exam/templates.py @@ -540,6 +540,23 @@ def _map_first_pass_to_rows(template_id: str, first_pass: Dict[str, Any], pdf_by response_form = _response_form_from_region_type(region.get("region_type")) if response_form: response_areas.append({"id": _ai_id(template_id, "region", page_index, idx), "template_id": template_id, "question_id": first_part_by_page.get(page_index, default_qid), "page": page_index + 1, "bounds": bounds, "kind": "response", "response_form": response_form, "source": "ai", "confirmed": False, "confidence": _safe_confidence(region.get("confidence")), "derivation": region.get("detection_method") or "opencv-response-region"}) + # Integrity guard: every response_area/boundary question_id must reference an inserted question + # (FK exam_response_areas/exam_boundaries -> exam_questions). On papers where band detection yields + # few/no questions but opencv/gemma still emit regions, those regions point at the synthetic + # default_qid which was never inserted. Ensure that fallback container question exists and reattach + # any orphan child rows to it, so persistence can't violate the FK. + qid_set = {q["id"] for q in questions} + orphans = [r for r in (response_areas + boundaries) if r.get("question_id") not in qid_set] + if orphans: + if default_qid not in qid_set: + questions.insert(0, {"id": default_qid, "template_id": template_id, "label": "Unassigned", + "order": 0, "max_marks": 0, "is_container": True, "source": "ai", + "confirmed": False, "confidence": 0.5, + "derivation": "auto-map-fallback-container"}) + qid_set.add(default_qid) + for r in orphans: + r["question_id"] = default_qid + return {"questions": questions, "response_areas": response_areas, "boundaries": boundaries, "layout": layout}