Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 3b261ef

Browse files
authored
Preload the {coverage,bundle}Status for commits (#1270)
1 parent ab4cb5b commit 3b261ef

File tree

3 files changed

+77
-39
lines changed

3 files changed

+77
-39
lines changed

graphql_api/actions/commits.py

Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
from collections import defaultdict
12
from typing import Optional
23

3-
from django.db.models import Prefetch, Q, QuerySet
4+
from django.db.models import Count, Prefetch, Q, QuerySet
45
from django.db.models.functions import Lower, Substr
6+
from graphql import GraphQLResolveInfo
57

68
from core.models import Commit, Pull, Repository
79
from graphql_api.types.enums import CommitStatus
@@ -23,35 +25,51 @@ def pull_commits(pull: Pull) -> QuerySet[Commit]:
2325
return Commit.objects.filter(id__in=subquery).defer("_report")
2426

2527

28+
def load_commit_statuses(
29+
commit_ids: list[int],
30+
) -> dict[int, dict[CommitReport.ReportType, str]]:
31+
qs = (
32+
CommitReport.objects.filter(commit__in=commit_ids)
33+
.values_list("commit_id", "report_type", "sessions__state")
34+
.annotate(sessions_count=Count("sessions"))
35+
)
36+
37+
grouped: dict[tuple[int, CommitReport.ReportType], dict[str, int]] = defaultdict(
38+
dict
39+
)
40+
for id, report_type, state, count in qs:
41+
# The above query generates a `LEFT OUTER JOIN` with a proper `GROUP BY`.
42+
# However, it is also yielding rows with a `NULL` state in case a report does not have any uploads.
43+
if not report_type or not state:
44+
continue
45+
grouped[(id, report_type)][state] = count
46+
47+
results: dict[int, dict[CommitReport.ReportType, str]] = {
48+
id: {} for id in commit_ids
49+
}
50+
for (id, report_type), states in grouped.items():
51+
status = CommitStatus.COMPLETED.value
52+
if states.get("error", 0) > 0:
53+
status = CommitStatus.ERROR.value
54+
elif states.get("uploaded", 0) > 0:
55+
status = CommitStatus.PENDING.value
56+
57+
results[id][report_type] = status
58+
59+
return results
60+
61+
2662
def commit_status(
27-
commit: Commit, report_type: CommitReport.ReportType
28-
) -> Optional[CommitStatus]:
29-
report = CommitReport.objects.filter(report_type=report_type, commit=commit).first()
30-
if not report:
31-
return None
32-
33-
sessions = report.sessions.all()
34-
if not sessions:
35-
return None
36-
37-
# Only care about these 3 states, ignoring fully and partially overwritten
38-
upload_states = [
39-
s.state for s in sessions if s.state in ["processed", "uploaded", "error"]
40-
]
41-
42-
has_error, has_pending = False, False
43-
for state in upload_states:
44-
if state == "error":
45-
has_error = True
46-
if state == "uploaded":
47-
has_pending = True
48-
49-
# Prioritize returning error over pending
50-
if has_error:
51-
return CommitStatus.ERROR.value
52-
if has_pending:
53-
return CommitStatus.PENDING.value
54-
return CommitStatus.COMPLETED.value
63+
info: GraphQLResolveInfo, commit: Commit, report_type: CommitReport.ReportType
64+
) -> str | None:
65+
commit_statuses = info.context.setdefault("commit_statuses", {})
66+
commit_status = commit_statuses.get(commit.id)
67+
if commit_status is None:
68+
updated_statuses = load_commit_statuses([commit.id])
69+
commit_statuses.update(updated_statuses)
70+
commit_status = updated_statuses[commit.id]
71+
72+
return commit_status.get(report_type)
5573

5674

5775
def repo_commits(
@@ -100,11 +118,17 @@ def repo_commits(
100118
coverage_status = filters.get("coverage_status")
101119

102120
if coverage_status:
121+
# FIXME(swatinem):
122+
# This filter here is insane, it resolves *all* the results in the unbounded queryset,
123+
# just to check the status, and to then add it as another restricting filter.
124+
# I’m pretty sure this will completely break the server if anyone actually uses this filter, lol.
125+
commit_ids = [commit.id for commit in queryset]
126+
commit_statuses = load_commit_statuses(commit_ids)
127+
103128
to_be_included = [
104-
commit.id
105-
for commit in queryset
106-
if commit_status(commit, CommitReport.ReportType.COVERAGE)
107-
in coverage_status
129+
id
130+
for id, statuses in commit_statuses.items()
131+
if statuses.get(CommitReport.ReportType.COVERAGE) in coverage_status
108132
]
109133
queryset = queryset.filter(id__in=to_be_included)
110134

graphql_api/types/commit/commit.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
MissingHeadReport,
3737
)
3838
from graphql_api.types.enums import (
39-
CommitStatus,
4039
OrderingDirection,
4140
PathContentDisplayType,
4241
)
@@ -304,15 +303,15 @@ async def resolve_total_uploads(commit, info):
304303
@commit_bindable.field("bundleStatus")
305304
@sync_to_async
306305
@sentry_sdk.trace
307-
def resolve_bundle_status(commit: Commit, info) -> Optional[CommitStatus]:
308-
return commit_status(commit, CommitReport.ReportType.BUNDLE_ANALYSIS)
306+
def resolve_bundle_status(commit: Commit, info: GraphQLResolveInfo) -> str | None:
307+
return commit_status(info, commit, CommitReport.ReportType.BUNDLE_ANALYSIS)
309308

310309

311310
@commit_bindable.field("coverageStatus")
312311
@sync_to_async
313312
@sentry_sdk.trace
314-
def resolve_coverage_status(commit: Commit, info) -> Optional[CommitStatus]:
315-
return commit_status(commit, CommitReport.ReportType.COVERAGE)
313+
def resolve_coverage_status(commit: Commit, info: GraphQLResolveInfo) -> str | None:
314+
return commit_status(info, commit, CommitReport.ReportType.COVERAGE)
316315

317316

318317
@commit_bindable.field("coverageAnalytics")

graphql_api/types/repository/repository.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@
1313

1414
from codecov_auth.models import SERVICE_GITHUB, SERVICE_GITHUB_ENTERPRISE, Owner
1515
from core.models import Branch, Commit, Pull, Repository
16-
from graphql_api.actions.commits import repo_commits
16+
from graphql_api.actions.commits import load_commit_statuses, repo_commits
1717
from graphql_api.dataloader.commit import CommitLoader
1818
from graphql_api.dataloader.owner import OwnerLoader
1919
from graphql_api.helpers.connection import queryset_to_connection
20+
from graphql_api.helpers.requested_fields import selected_fields
2021
from graphql_api.types.coverage_analytics.coverage_analytics import (
2122
CoverageAnalyticsProps,
2223
)
@@ -123,6 +124,11 @@ async def resolve_pulls(
123124
)
124125

125126

127+
# the `requested_fields` here are prefixed with `edges.node`, as this is a `Connection`
128+
# and using `commits { edges { node { ... } } }` is the way this is queried.
129+
STATUS_FIELDS = {"edges.node.coverageStatus", "edges.node.bundleStatus"}
130+
131+
126132
@repository_bindable.field("commits")
127133
async def resolve_commits(
128134
repository: Repository,
@@ -144,6 +150,15 @@ async def resolve_commits(
144150
loader = CommitLoader.loader(info, repository.repoid)
145151
loader.cache(commit)
146152

153+
requested_fields = selected_fields(info)
154+
should_load_statuses = not requested_fields.isdisjoint(STATUS_FIELDS)
155+
156+
if should_load_statuses:
157+
commit_ids = [edge["node"].id for edge in connection.edges]
158+
info.context["commit_statuses"] = await sync_to_async(load_commit_statuses)(
159+
commit_ids
160+
)
161+
147162
return connection
148163

149164

0 commit comments

Comments
 (0)