bd sync: 2025-11-26 11:14:59
This commit is contained in:
@@ -30,6 +30,9 @@ type MemoryStorage struct {
|
||||
metadata map[string]string // Metadata key-value pairs
|
||||
counters map[string]int // Prefix -> Last ID
|
||||
|
||||
// Indexes for O(1) lookups
|
||||
externalRefToID map[string]string // ExternalRef -> IssueID
|
||||
|
||||
// For tracking
|
||||
dirty map[string]bool // IssueIDs that have been modified
|
||||
|
||||
@@ -40,16 +43,17 @@ type MemoryStorage struct {
|
||||
// New creates a new in-memory storage backend
|
||||
func New(jsonlPath string) *MemoryStorage {
|
||||
return &MemoryStorage{
|
||||
issues: make(map[string]*types.Issue),
|
||||
dependencies: make(map[string][]*types.Dependency),
|
||||
labels: make(map[string][]string),
|
||||
events: make(map[string][]*types.Event),
|
||||
comments: make(map[string][]*types.Comment),
|
||||
config: make(map[string]string),
|
||||
metadata: make(map[string]string),
|
||||
counters: make(map[string]int),
|
||||
dirty: make(map[string]bool),
|
||||
jsonlPath: jsonlPath,
|
||||
issues: make(map[string]*types.Issue),
|
||||
dependencies: make(map[string][]*types.Dependency),
|
||||
labels: make(map[string][]string),
|
||||
events: make(map[string][]*types.Event),
|
||||
comments: make(map[string][]*types.Comment),
|
||||
config: make(map[string]string),
|
||||
metadata: make(map[string]string),
|
||||
counters: make(map[string]int),
|
||||
externalRefToID: make(map[string]string),
|
||||
dirty: make(map[string]bool),
|
||||
jsonlPath: jsonlPath,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +71,11 @@ func (m *MemoryStorage) LoadFromIssues(issues []*types.Issue) error {
|
||||
// Store the issue
|
||||
m.issues[issue.ID] = issue
|
||||
|
||||
// Index external ref for O(1) lookup
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||
m.externalRefToID[*issue.ExternalRef] = issue.ID
|
||||
}
|
||||
|
||||
// Store dependencies
|
||||
if len(issue.Dependencies) > 0 {
|
||||
m.dependencies[issue.ID] = issue.Dependencies
|
||||
@@ -184,6 +193,11 @@ func (m *MemoryStorage) CreateIssue(ctx context.Context, issue *types.Issue, act
|
||||
m.issues[issue.ID] = issue
|
||||
m.dirty[issue.ID] = true
|
||||
|
||||
// Index external ref for O(1) lookup
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||
m.externalRefToID[*issue.ExternalRef] = issue.ID
|
||||
}
|
||||
|
||||
// Record event
|
||||
event := &types.Event{
|
||||
IssueID: issue.ID,
|
||||
@@ -244,6 +258,11 @@ func (m *MemoryStorage) CreateIssues(ctx context.Context, issues []*types.Issue,
|
||||
m.issues[issue.ID] = issue
|
||||
m.dirty[issue.ID] = true
|
||||
|
||||
// Index external ref for O(1) lookup
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||
m.externalRefToID[*issue.ExternalRef] = issue.ID
|
||||
}
|
||||
|
||||
// Record event
|
||||
event := &types.Event{
|
||||
IssueID: issue.ID,
|
||||
@@ -288,28 +307,31 @@ func (m *MemoryStorage) GetIssueByExternalRef(ctx context.Context, externalRef s
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Linear search through all issues to find match by external_ref
|
||||
for _, issue := range m.issues {
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef == externalRef {
|
||||
// Return a copy to avoid mutations
|
||||
issueCopy := *issue
|
||||
|
||||
// Attach dependencies
|
||||
if deps, ok := m.dependencies[issue.ID]; ok {
|
||||
issueCopy.Dependencies = deps
|
||||
}
|
||||
|
||||
// Attach labels
|
||||
if labels, ok := m.labels[issue.ID]; ok {
|
||||
issueCopy.Labels = labels
|
||||
}
|
||||
|
||||
return &issueCopy, nil
|
||||
}
|
||||
// O(1) lookup using index
|
||||
issueID, exists := m.externalRefToID[externalRef]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Not found
|
||||
return nil, nil
|
||||
issue, exists := m.issues[issueID]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Return a copy to avoid mutations
|
||||
issueCopy := *issue
|
||||
|
||||
// Attach dependencies
|
||||
if deps, ok := m.dependencies[issue.ID]; ok {
|
||||
issueCopy.Dependencies = deps
|
||||
}
|
||||
|
||||
// Attach labels
|
||||
if labels, ok := m.labels[issue.ID]; ok {
|
||||
issueCopy.Labels = labels
|
||||
}
|
||||
|
||||
return &issueCopy, nil
|
||||
}
|
||||
|
||||
// UpdateIssue updates fields on an issue
|
||||
@@ -375,9 +397,23 @@ func (m *MemoryStorage) UpdateIssue(ctx context.Context, id string, updates map[
|
||||
issue.Assignee = ""
|
||||
}
|
||||
case "external_ref":
|
||||
// Update external ref index
|
||||
oldRef := issue.ExternalRef
|
||||
if v, ok := value.(string); ok {
|
||||
// Remove old index entry if exists
|
||||
if oldRef != nil && *oldRef != "" {
|
||||
delete(m.externalRefToID, *oldRef)
|
||||
}
|
||||
// Add new index entry
|
||||
if v != "" {
|
||||
m.externalRefToID[v] = id
|
||||
}
|
||||
issue.ExternalRef = &v
|
||||
} else if value == nil {
|
||||
// Remove old index entry if exists
|
||||
if oldRef != nil && *oldRef != "" {
|
||||
delete(m.externalRefToID, *oldRef)
|
||||
}
|
||||
issue.ExternalRef = nil
|
||||
}
|
||||
}
|
||||
@@ -417,10 +453,16 @@ func (m *MemoryStorage) DeleteIssue(ctx context.Context, id string) error {
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Check if issue exists
|
||||
if _, ok := m.issues[id]; !ok {
|
||||
issue, ok := m.issues[id]
|
||||
if !ok {
|
||||
return fmt.Errorf("issue not found: %s", id)
|
||||
}
|
||||
|
||||
// Remove external ref index entry
|
||||
if issue.ExternalRef != nil && *issue.ExternalRef != "" {
|
||||
delete(m.externalRefToID, *issue.ExternalRef)
|
||||
}
|
||||
|
||||
// Delete the issue
|
||||
delete(m.issues, id)
|
||||
|
||||
|
||||
@@ -879,3 +879,145 @@ func TestClose(t *testing.T) {
|
||||
t.Error("Store should be closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIssueByExternalRef(t *testing.T) {
|
||||
store := setupTestMemory(t)
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an issue with external ref
|
||||
extRef := "github#123"
|
||||
issue := &types.Issue{
|
||||
Title: "Test issue with external ref",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
ExternalRef: &extRef,
|
||||
}
|
||||
|
||||
if err := store.CreateIssue(ctx, issue, "test-user"); err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Lookup by external ref should find it
|
||||
found, err := store.GetIssueByExternalRef(ctx, "github#123")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatal("Expected to find issue by external ref")
|
||||
}
|
||||
if found.ID != issue.ID {
|
||||
t.Errorf("Expected issue ID %s, got %s", issue.ID, found.ID)
|
||||
}
|
||||
|
||||
// Lookup by non-existent ref should return nil
|
||||
notFound, err := store.GetIssueByExternalRef(ctx, "nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||
}
|
||||
if notFound != nil {
|
||||
t.Error("Expected nil for non-existent external ref")
|
||||
}
|
||||
|
||||
// Update external ref and verify index is updated
|
||||
newRef := "github#456"
|
||||
if err := store.UpdateIssue(ctx, issue.ID, map[string]interface{}{
|
||||
"external_ref": newRef,
|
||||
}, "test-user"); err != nil {
|
||||
t.Fatalf("UpdateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Old ref should not find anything
|
||||
oldRefResult, err := store.GetIssueByExternalRef(ctx, "github#123")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||
}
|
||||
if oldRefResult != nil {
|
||||
t.Error("Old external ref should not find issue after update")
|
||||
}
|
||||
|
||||
// New ref should find the issue
|
||||
newRefResult, err := store.GetIssueByExternalRef(ctx, "github#456")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||
}
|
||||
if newRefResult == nil {
|
||||
t.Fatal("New external ref should find issue")
|
||||
}
|
||||
if newRefResult.ID != issue.ID {
|
||||
t.Errorf("Expected issue ID %s, got %s", issue.ID, newRefResult.ID)
|
||||
}
|
||||
|
||||
// Delete issue and verify index is cleaned up
|
||||
if err := store.DeleteIssue(ctx, issue.ID); err != nil {
|
||||
t.Fatalf("DeleteIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// External ref should not find anything after delete
|
||||
deletedResult, err := store.GetIssueByExternalRef(ctx, "github#456")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||
}
|
||||
if deletedResult != nil {
|
||||
t.Error("External ref should not find issue after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIssueByExternalRefLoadFromIssues(t *testing.T) {
|
||||
store := New("")
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Load issues with external refs
|
||||
extRef1 := "jira#100"
|
||||
extRef2 := "jira#200"
|
||||
issues := []*types.Issue{
|
||||
{
|
||||
ID: "bd-1",
|
||||
Title: "Issue 1",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
ExternalRef: &extRef1,
|
||||
},
|
||||
{
|
||||
ID: "bd-2",
|
||||
Title: "Issue 2",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeBug,
|
||||
ExternalRef: &extRef2,
|
||||
},
|
||||
{
|
||||
ID: "bd-3",
|
||||
Title: "Issue 3 (no external ref)",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 3,
|
||||
IssueType: types.TypeFeature,
|
||||
},
|
||||
}
|
||||
|
||||
if err := store.LoadFromIssues(issues); err != nil {
|
||||
t.Fatalf("LoadFromIssues failed: %v", err)
|
||||
}
|
||||
|
||||
// Both external refs should be indexed
|
||||
found1, err := store.GetIssueByExternalRef(ctx, "jira#100")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||
}
|
||||
if found1 == nil || found1.ID != "bd-1" {
|
||||
t.Errorf("Expected to find bd-1 by external ref jira#100")
|
||||
}
|
||||
|
||||
found2, err := store.GetIssueByExternalRef(ctx, "jira#200")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
||||
}
|
||||
if found2 == nil || found2.ID != "bd-2" {
|
||||
t.Errorf("Expected to find bd-2 by external ref jira#200")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user