Optimize bd list: replace N+1 label queries with bulk fetch

Problem:
In direct mode, bd list was making a separate GetLabels() call for
each issue when displaying labels. With 538 issues, this resulted in
538 separate database queries.

While investigating the reported 5+ second slowness, discovered this
N+1 query issue that would impact performance with many issues.

Solution:
1. Added GetLabelsForIssues(issueIDs []string) to Storage interface
2. Implemented bulk fetch in SQLite (already existed, now exposed)
3. Implemented bulk fetch in MemoryStorage
4. Updated list.go to fetch all labels in single query

Changes:
- internal/storage/storage.go: Add GetLabelsForIssues to interface
- internal/storage/memory/memory.go: Implement GetLabelsForIssues
- cmd/bd/list.go: Use bulk fetching in all output modes

Impact:
Eliminates N queries for labels, replacing with 1 bulk query.
This optimization applies to direct mode only (daemon mode already
uses bulk operations via RPC).

Note: The reported 5s slowness was actually caused by daemon auto-start
timeout. Use --no-daemon flag or run 'bd migrate --update-repo-id' to
resolve the legacy database issue causing daemon startup failures.

🤖 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-20 20:25:17 -05:00
parent 0916abf8fe
commit 968d9e2ea1
3 changed files with 30 additions and 10 deletions

View File

@@ -339,18 +339,19 @@ var listCmd = &cobra.Command{
}
if jsonOutput {
// Populate labels for JSON output
for _, issue := range issues {
issue.Labels, _ = store.GetLabels(ctx, issue.ID)
}
// Get dependency counts in bulk (single query instead of N queries)
// Get labels and dependency counts in bulk (single query instead of N queries)
issueIDs := make([]string, len(issues))
for i, issue := range issues {
issueIDs[i] = issue.ID
}
labelsMap, _ := store.GetLabelsForIssues(ctx, issueIDs)
depCounts, _ := store.GetDependencyCounts(ctx, issueIDs)
// Populate labels for JSON output
for _, issue := range issues {
issue.Labels = labelsMap[issue.ID]
}
// Build response with counts
issuesWithCounts := make([]*types.IssueWithCounts, len(issues))
for i, issue := range issues {
@@ -368,12 +369,18 @@ var listCmd = &cobra.Command{
return
}
// Load labels in bulk for display
issueIDs := make([]string, len(issues))
for i, issue := range issues {
issueIDs[i] = issue.ID
}
labelsMap, _ := store.GetLabelsForIssues(ctx, issueIDs)
if longFormat {
// Long format: multi-line with details
fmt.Printf("\nFound %d issues:\n\n", len(issues))
for _, issue := range issues {
// Load labels for display
labels, _ := store.GetLabels(ctx, issue.ID)
labels := labelsMap[issue.ID]
fmt.Printf("%s [P%d] [%s] %s\n", issue.ID, issue.Priority, issue.IssueType, issue.Status)
fmt.Printf(" %s\n", issue.Title)
@@ -388,8 +395,7 @@ var listCmd = &cobra.Command{
} else {
// Compact format: one line per issue
for _, issue := range issues {
// Load labels for display
labels, _ := store.GetLabels(ctx, issue.ID)
labels := labelsMap[issue.ID]
labelsStr := ""
if len(labels) > 0 {