Fix N+1 query pattern in export operations (bd-rcmg)

**Problem**: Export operations called GetLabels() and GetIssueComments()
in a loop for each issue, creating N+1 query pattern. For 100 issues
this created 201 queries instead of 3-5.

**Solution**:
- Added GetCommentsForIssues() batch method to storage interface
- Implemented batch method in SQLite and memory storage backends
- Updated handleExport() and triggerExport() to use batch queries
- Added comprehensive tests for batch operations

**Impact**: Query count reduced from ~201 to ~3-5 for 100 issues.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-11-23 19:53:44 -08:00
parent 9d2dadbe87
commit 9c6b37500c
5 changed files with 239 additions and 31 deletions

View File

@@ -59,28 +59,32 @@ func (s *Server) handleExport(req *Request) Response {
issue.Dependencies = allDeps[issue.ID]
}
// Populate labels for all issues
for _, issue := range issues {
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get labels for %s: %v", issue.ID, err),
}
// Populate labels for all issues (avoid N+1)
issueIDs := make([]string, len(issues))
for i, issue := range issues {
issueIDs[i] = issue.ID
}
allLabels, err := store.GetLabelsForIssues(ctx, issueIDs)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get labels: %v", err),
}
issue.Labels = labels
}
for _, issue := range issues {
issue.Labels = allLabels[issue.ID]
}
// Populate comments for all issues
for _, issue := range issues {
comments, err := store.GetIssueComments(ctx, issue.ID)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get comments for %s: %v", issue.ID, err),
}
// Populate comments for all issues (avoid N+1)
allComments, err := store.GetCommentsForIssues(ctx, issueIDs)
if err != nil {
return Response{
Success: false,
Error: fmt.Sprintf("failed to get comments: %v", err),
}
issue.Comments = comments
}
for _, issue := range issues {
issue.Comments = allComments[issue.ID]
}
// Create temp file for atomic write
@@ -387,7 +391,7 @@ func (s *Server) triggerExport(ctx context.Context, store storage.Storage, dbPat
})
// CRITICAL: Populate all related data to prevent data loss
// This mirrors the logic in handleExport (lines 50-83)
// This mirrors the logic in handleExport
// Populate dependencies for all issues (avoid N+1 queries)
allDeps, err := store.GetAllDependencyRecords(ctx)
@@ -398,22 +402,26 @@ func (s *Server) triggerExport(ctx context.Context, store storage.Storage, dbPat
issue.Dependencies = allDeps[issue.ID]
}
// Populate labels for all issues
// Populate labels for all issues (avoid N+1 queries)
issueIDs := make([]string, len(allIssues))
for i, issue := range allIssues {
issueIDs[i] = issue.ID
}
allLabels, err := store.GetLabelsForIssues(ctx, issueIDs)
if err != nil {
return fmt.Errorf("failed to get labels: %w", err)
}
for _, issue := range allIssues {
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
return fmt.Errorf("failed to get labels for %s: %w", issue.ID, err)
}
issue.Labels = labels
issue.Labels = allLabels[issue.ID]
}
// Populate comments for all issues
// Populate comments for all issues (avoid N+1 queries)
allComments, err := store.GetCommentsForIssues(ctx, issueIDs)
if err != nil {
return fmt.Errorf("failed to get comments: %w", err)
}
for _, issue := range allIssues {
comments, err := store.GetIssueComments(ctx, issue.ID)
if err != nil {
return fmt.Errorf("failed to get comments for %s: %w", issue.ID, err)
}
issue.Comments = comments
issue.Comments = allComments[issue.ID]
}
// Write to JSONL file with atomic replace (temp file + rename)