diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 00000000..e28a3287 --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,5 @@ +## 2024-06-06 - N+1 Issue Query Optimization + +**Learning:** `_build_issues_batch` in `db_issues.py` performs 6 separate batched queries to resolve an issue's labels, blocks, blocked_by, children, and open_blockers_by_id, but the open_blockers_by_id is just `len(blocked_by)`. We can remove the 6th batched query entirely to reduce round trips and DB load. + +**Action:** Remove the `open_blockers_by_id` query from `_build_issues_batch` and replace it with `len(blocked_by_id.get(iid, []))` when constructing the Issue object. diff --git a/src/filigree/db_issues.py b/src/filigree/db_issues.py index f59be287..66b1fe0d 100644 --- a/src/filigree/db_issues.py +++ b/src/filigree/db_issues.py @@ -686,19 +686,6 @@ def _build_issues_batch(self, issue_ids: list[str]) -> list[Issue]: for r in self.conn.execute(f"SELECT id, parent_id FROM issues WHERE parent_id IN ({placeholders})", chunk).fetchall(): children_by_id[r["parent_id"]].append(r["id"]) - # 6. Batch compute open blocker counts — same blocker semantics as step 4. - open_blockers_by_id: dict[str, int] = dict.fromkeys(issue_ids, 0) - for chunk in self._chunk_issue_ids_for_sqlite(issue_ids, extra_params=len(blocker_done_params)): - placeholders = ",".join("?" * len(chunk)) - for r in self.conn.execute( - f"SELECT d.issue_id, COUNT(*) as cnt FROM dependencies d " - f"JOIN issues blocker ON d.depends_on_id = blocker.id " - f"WHERE d.issue_id IN ({placeholders}) AND NOT ({blocker_done_sql}) " - f"GROUP BY d.issue_id", - [*chunk, *blocker_done_params], - ).fetchall(): - open_blockers_by_id[r["issue_id"]] = r["cnt"] - # Build Issue objects preserving input order result: list[Issue] = [] for iid in issue_ids: @@ -732,7 +719,9 @@ def _build_issues_batch(self, issue_ids: list[str]) -> list[Issue]: # state name shared across types in different categories # is classified correctly. self._resolve_status_category(row["type"], row["status"]) == "open" - and open_blockers_by_id.get(iid, 0) == 0 + # ⚡ Bolt Optimization: Removed redundant DB COUNT(*) query for open blockers. + # blocked_by_id already contains the exact same filtered list of open blockers. + and len(blocked_by_id.get(iid, [])) == 0 and not (row["assignee"] or "") ), children=children_by_id.get(iid, []),