- Add migration for UNIQUE index on external_ref column (bd-897a) - Add validation for duplicate external_ref in batch imports (bd-7315) - Add query planner test to verify index usage (bd-f9a1) - Add concurrent import tests for external_ref (bd-3f6a) The migration detects existing duplicates and fails gracefully. Batch imports now reject duplicates with clear error messages. Tests verify the index is actually used by SQLite query planner. Amp-Thread-ID: https://ampcode.com/threads/T-45ca66ed-3912-46c4-963c-caa7724a9a2f Co-authored-by: Amp <amp@ampcode.com>
347 lines
9.3 KiB
Go
347 lines
9.3 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
func TestGetIssueByExternalRef(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create test issue with external_ref
|
|
externalRef := "JIRA-123"
|
|
issue := &types.Issue{
|
|
ID: "bd-test-1",
|
|
Title: "Test issue",
|
|
Description: "Test description",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeBug,
|
|
ExternalRef: &externalRef,
|
|
}
|
|
|
|
err := s.CreateIssue(ctx, issue, "test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
// Test: Find by external_ref
|
|
found, err := s.GetIssueByExternalRef(ctx, externalRef)
|
|
if err != nil {
|
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
|
}
|
|
|
|
if found == nil {
|
|
t.Fatal("Expected to find issue by external_ref, got nil")
|
|
}
|
|
|
|
if found.ID != issue.ID {
|
|
t.Errorf("Expected ID %s, got %s", issue.ID, found.ID)
|
|
}
|
|
|
|
if found.ExternalRef == nil || *found.ExternalRef != externalRef {
|
|
t.Errorf("Expected external_ref %s, got %v", externalRef, found.ExternalRef)
|
|
}
|
|
}
|
|
|
|
func TestGetIssueByExternalRefNotFound(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Test: Search for non-existent external_ref
|
|
found, err := s.GetIssueByExternalRef(ctx, "NONEXISTENT-999")
|
|
if err != nil {
|
|
t.Fatalf("GetIssueByExternalRef failed: %v", err)
|
|
}
|
|
|
|
if found != nil {
|
|
t.Errorf("Expected nil for non-existent external_ref, got %v", found)
|
|
}
|
|
}
|
|
|
|
func TestDetectCollisionsWithExternalRef(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create existing issue with external_ref
|
|
externalRef := "JIRA-456"
|
|
existing := &types.Issue{
|
|
ID: "bd-test-1",
|
|
Title: "Original title",
|
|
Description: "Original description",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeBug,
|
|
ExternalRef: &externalRef,
|
|
}
|
|
|
|
err := s.CreateIssue(ctx, existing, "test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create existing issue: %v", err)
|
|
}
|
|
|
|
// Incoming issue with same external_ref but different ID and content
|
|
incoming := &types.Issue{
|
|
ID: "bd-test-2", // Different ID
|
|
Title: "Updated title",
|
|
Description: "Updated description",
|
|
Status: types.StatusInProgress,
|
|
Priority: 2,
|
|
IssueType: types.TypeBug,
|
|
ExternalRef: &externalRef, // Same external_ref
|
|
UpdatedAt: time.Now().Add(1 * time.Hour), // Newer timestamp
|
|
}
|
|
|
|
// Test: Detect collision by external_ref
|
|
result, err := DetectCollisions(ctx, s, []*types.Issue{incoming})
|
|
if err != nil {
|
|
t.Fatalf("DetectCollisions failed: %v", err)
|
|
}
|
|
|
|
// Should detect as collision (update needed)
|
|
if len(result.Collisions) != 1 {
|
|
t.Fatalf("Expected 1 collision, got %d", len(result.Collisions))
|
|
}
|
|
|
|
collision := result.Collisions[0]
|
|
if collision.ExistingIssue.ID != existing.ID {
|
|
t.Errorf("Expected existing issue ID %s, got %s", existing.ID, collision.ExistingIssue.ID)
|
|
}
|
|
|
|
if collision.IncomingIssue.ID != incoming.ID {
|
|
t.Errorf("Expected incoming issue ID %s, got %s", incoming.ID, collision.IncomingIssue.ID)
|
|
}
|
|
|
|
// Should have conflicting fields
|
|
expectedConflicts := []string{"title", "description", "status", "priority"}
|
|
for _, field := range expectedConflicts {
|
|
found := false
|
|
for _, conflictField := range collision.ConflictingFields {
|
|
if conflictField == field {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("Expected conflict on field %s, but not found in %v", field, collision.ConflictingFields)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDetectCollisionsExternalRefPriorityOverID(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create existing issue with external_ref
|
|
externalRef := "GH-789"
|
|
existing := &types.Issue{
|
|
ID: "bd-test-1",
|
|
Title: "Original title",
|
|
Description: "Original description",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeFeature,
|
|
ExternalRef: &externalRef,
|
|
}
|
|
|
|
err := s.CreateIssue(ctx, existing, "test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create existing issue: %v", err)
|
|
}
|
|
|
|
// Create a second issue with a different ID and no external_ref
|
|
otherIssue := &types.Issue{
|
|
ID: "bd-test-2",
|
|
Title: "Other issue",
|
|
Description: "Other description",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
|
|
err = s.CreateIssue(ctx, otherIssue, "test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create other issue: %v", err)
|
|
}
|
|
|
|
// Incoming issue with:
|
|
// - Same external_ref as bd-test-1
|
|
// - Same ID as bd-test-2
|
|
// This tests that external_ref matching takes priority over ID matching
|
|
incoming := &types.Issue{
|
|
ID: "bd-test-2", // Matches otherIssue.ID
|
|
Title: "Updated from external system",
|
|
Description: "Updated description",
|
|
Status: types.StatusInProgress,
|
|
Priority: 2,
|
|
IssueType: types.TypeFeature,
|
|
ExternalRef: &externalRef, // Matches existing.ExternalRef
|
|
UpdatedAt: time.Now().Add(1 * time.Hour),
|
|
}
|
|
|
|
// Test: DetectCollisions should match by external_ref first
|
|
result, err := DetectCollisions(ctx, s, []*types.Issue{incoming})
|
|
if err != nil {
|
|
t.Fatalf("DetectCollisions failed: %v", err)
|
|
}
|
|
|
|
// Should match by external_ref, not ID
|
|
if len(result.Collisions) != 1 {
|
|
t.Fatalf("Expected 1 collision, got %d", len(result.Collisions))
|
|
}
|
|
|
|
collision := result.Collisions[0]
|
|
|
|
// The existing issue matched should be bd-test-1 (by external_ref), not bd-test-2 (by ID)
|
|
if collision.ExistingIssue.ID != existing.ID {
|
|
t.Errorf("Expected external_ref match with %s, but got %s", existing.ID, collision.ExistingIssue.ID)
|
|
}
|
|
|
|
if collision.ExistingIssue.ExternalRef == nil || *collision.ExistingIssue.ExternalRef != externalRef {
|
|
t.Errorf("Expected matched issue to have external_ref %s", externalRef)
|
|
}
|
|
}
|
|
|
|
func TestDetectCollisionsNoExternalRef(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Create existing issue without external_ref
|
|
existing := &types.Issue{
|
|
ID: "bd-test-1",
|
|
Title: "Local issue",
|
|
Description: "Local description",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
}
|
|
|
|
err := s.CreateIssue(ctx, existing, "test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create existing issue: %v", err)
|
|
}
|
|
|
|
// Incoming issue with same ID but no external_ref
|
|
incoming := &types.Issue{
|
|
ID: "bd-test-1",
|
|
Title: "Updated local issue",
|
|
Description: "Updated description",
|
|
Status: types.StatusInProgress,
|
|
Priority: 2,
|
|
IssueType: types.TypeTask,
|
|
UpdatedAt: time.Now().Add(1 * time.Hour),
|
|
}
|
|
|
|
// Test: Should still match by ID when no external_ref
|
|
result, err := DetectCollisions(ctx, s, []*types.Issue{incoming})
|
|
if err != nil {
|
|
t.Fatalf("DetectCollisions failed: %v", err)
|
|
}
|
|
|
|
if len(result.Collisions) != 1 {
|
|
t.Fatalf("Expected 1 collision, got %d", len(result.Collisions))
|
|
}
|
|
|
|
collision := result.Collisions[0]
|
|
if collision.ExistingIssue.ID != existing.ID {
|
|
t.Errorf("Expected ID match with %s, got %s", existing.ID, collision.ExistingIssue.ID)
|
|
}
|
|
}
|
|
|
|
func TestExternalRefIndex(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
// Verify that the external_ref index exists
|
|
var indexExists bool
|
|
err := s.db.QueryRowContext(ctx, `
|
|
SELECT EXISTS(
|
|
SELECT 1 FROM sqlite_master
|
|
WHERE type='index' AND name='idx_issues_external_ref'
|
|
)
|
|
`).Scan(&indexExists)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Failed to check for index: %v", err)
|
|
}
|
|
|
|
if !indexExists {
|
|
t.Error("Expected idx_issues_external_ref index to exist")
|
|
}
|
|
}
|
|
|
|
func TestExternalRefIndexUsage(t *testing.T) {
|
|
ctx := context.Background()
|
|
s, cleanup := setupTestDB(t)
|
|
defer cleanup()
|
|
|
|
externalRef := "JIRA-123"
|
|
issue := &types.Issue{
|
|
ID: "bd-test-1",
|
|
Title: "Test issue",
|
|
Status: types.StatusOpen,
|
|
Priority: 1,
|
|
IssueType: types.TypeTask,
|
|
ExternalRef: &externalRef,
|
|
}
|
|
|
|
err := s.CreateIssue(ctx, issue, "test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create issue: %v", err)
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
EXPLAIN QUERY PLAN
|
|
SELECT id, title, description, design, acceptance_criteria, notes, status, priority, issue_type, assignee,
|
|
created_at, updated_at, closed_at, external_ref,
|
|
compaction_level, compacted_at, compacted_at_commit, original_size
|
|
FROM issues
|
|
WHERE external_ref = ?
|
|
`, externalRef)
|
|
if err != nil {
|
|
t.Fatalf("Failed to get query plan: %v", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var planFound bool
|
|
var indexUsed bool
|
|
|
|
for rows.Next() {
|
|
var id, parent, notused int
|
|
var detail string
|
|
if err := rows.Scan(&id, &parent, ¬used, &detail); err != nil {
|
|
t.Fatalf("Failed to scan query plan row: %v", err)
|
|
}
|
|
planFound = true
|
|
|
|
if detail == "SEARCH TABLE issues USING INDEX idx_issues_external_ref (external_ref=?)" ||
|
|
detail == "SEARCH issues USING INDEX idx_issues_external_ref (external_ref=?)" ||
|
|
detail == "SEARCH TABLE issues USING INDEX idx_issues_external_ref_unique (external_ref=?)" ||
|
|
detail == "SEARCH issues USING INDEX idx_issues_external_ref_unique (external_ref=?)" {
|
|
indexUsed = true
|
|
}
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
t.Fatalf("Error reading query plan: %v", err)
|
|
}
|
|
|
|
if !planFound {
|
|
t.Error("Expected query plan output, got none")
|
|
}
|
|
|
|
if !indexUsed {
|
|
t.Error("Expected query planner to use idx_issues_external_ref index, but it didn't")
|
|
}
|
|
}
|