diff --git a/.jules/bolt.md b/.jules/bolt.md index bf784525..1465d52e 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -25,3 +25,7 @@ ## 2025-07-15 - Fast Bounding Box Pre-filter **Learning:** Calculating great circle distance (Haversine) for every issue against a target location is computationally expensive (O(N) with heavy math ops like sin, cos, atan2). In high-traffic aggregations, this can become a bottleneck. **Action:** Use a fast bounding box pre-filter (`get_bounding_box` with a 5% epsilon) to quickly discard issues that are definitely outside the search radius before running the expensive exact haversine distance calculation. + +## 2025-07-16 - N+1 Query in Escalation Loop +**Learning:** During periodic evaluation of overdue grievances (`evaluate_and_escalate_grievances`), checking `grievance.jurisdiction.level` without eager loading triggers a separate DB query for every overdue grievance. This is a classic N+1 bottleneck when scanning a large number of open issues. +**Action:** Always use `.options(joinedload(Model.relationship))` in queries that retrieve lists of items if those items' relationships will be accessed during iteration. diff --git a/backend/escalation_engine.py b/backend/escalation_engine.py index 67137b9b..2962811a 100644 --- a/backend/escalation_engine.py +++ b/backend/escalation_engine.py @@ -5,7 +5,7 @@ import datetime from typing import List, Dict, Any, Optional -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, joinedload from sqlalchemy import and_, or_ from backend.models import Grievance, Jurisdiction, EscalationAudit, GrievanceStatus, JurisdictionLevel, EscalationReason, SeverityLevel from backend.database import SessionLocal @@ -150,7 +150,10 @@ def _get_grievances_for_evaluation(self, db: Session) -> List[Grievance]: now = datetime.datetime.now(datetime.timezone.utc) # Get grievances that are active and past SLA deadline - return db.query(Grievance).filter( + # PERFORMANCE OPTIMIZATION: + # Added .options(joinedload(Grievance.jurisdiction)) to resolve an N+1 query problem. + # This prevents a separate database query for each jurisdiction during the evaluation loop. + return db.query(Grievance).options(joinedload(Grievance.jurisdiction)).filter( and_( Grievance.status.in_([GrievanceStatus.OPEN, GrievanceStatus.IN_PROGRESS, GrievanceStatus.ESCALATED]), Grievance.sla_deadline < now