Skip to content

perf: replace N+1 hydration with SQL aggregates on active repos and users pages#253

Merged
coopernetes merged 1 commit into
mainfrom
perf/issue-247-aggregate-queries
May 13, 2026
Merged

perf: replace N+1 hydration with SQL aggregates on active repos and users pages#253
coopernetes merged 1 commit into
mainfrom
perf/issue-247-aggregate-queries

Conversation

@coopernetes
Copy link
Copy Markdown
Member

Summary

  • Active Repos page: replaces pushStore.find(limit=5000) + per-record hydration with a single GROUP BY aggregate (PushStore.summarizeByRepo()). At 130 records × 3 child queries × 8ms RTT this was ~3s; now one query.
  • Users list: replaces the per-user countPushesByStatus() loop (1 + N×3 queries) with a single SELECT resolved_user, status, COUNT(*) GROUP BY via PushStore.countPushStatusByUser(). GET /api/users/{username} also updated via countByStatus(query).
  • Push page status counts: adds GET /api/push/counts returning Map<String, Long> from one GROUP BY status query. PushList.tsx previously fired 7 separate fully-hydrated requests (one per status) just to get .length; now one lightweight call. Counts also update when repo/user filters change.

The WHERE clause builder in JdbcPushStore was extracted into a shared private method reused by find() and countByStatus(). Default interface implementations on PushStore keep InMemoryPushStore and MongoPushStore correct without changes.

Test plan

  • Existing PushControllerTest, RepoControllerTest updated and passing
  • New PushControllerTest$Counts tests cover the /api/push/counts endpoint
  • Full build + coverage threshold passes locally
  • Manual: Active Repos, Users, and Pushes pages load noticeably faster against a remote DB

closes #247

🤖 Generated with Claude Code

…sers pages

Eliminates three N+1 query patterns identified in #247:

1. Active Repos page: replace pushStore.find(limit=5000) + per-record
   hydration with a single GROUP BY aggregate via
   PushStore.summarizeByRepo(). JdbcPushStore implements this as one SQL
   query; default interface implementation falls back to find() for
   InMemory/Mongo.

2. Users list: replace per-user countPushesByStatus() loop (1 + N×3
   queries) with a single aggregate via PushStore.countPushStatusByUser().
   UserController.list() loads all push status counts in one query and
   distributes them to UserSummary objects without touching push records.
   toDetail() now uses PushStore.countByStatus(PushQuery) instead of
   fetching and hydrating all push records for the user.

3. Push page status counts: add GET /api/push/counts endpoint that
   returns a Map<String, Long> from a single GROUP BY query. Accepts the
   same filter params as the list endpoint (repo, user, search) so tab
   counts reflect the current filter set. Updates PushList.tsx to call
   this endpoint instead of firing one request per status value.

Also extracts the WHERE clause builder in JdbcPushStore into a shared
private method reused by find(), countByStatus(), and the existing query
path.

closes #247

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coopernetes coopernetes merged commit 27c031e into main May 13, 2026
16 checks passed
@coopernetes coopernetes deleted the perf/issue-247-aggregate-queries branch May 13, 2026 04:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

perf: replace N+1 hydration with SQL aggregates on Active Repos and Users dashboard pages

1 participant