Fix bd-49: Sync counters after deletions to prevent desync

- Call SyncAllCounters() after DeleteIssue and DeleteIssues
- Change SyncAllCounters to use excluded.last_id (allows counter to decrease)
- Delete orphaned counter rows when no issues remain for a prefix
- Add comprehensive tests in counter_sync_test.go

Fixes the issue where deleting issues left counters at high values, causing
new issues to skip IDs. Now counters accurately reflect the max existing ID.

Closes bd-49

Amp-Thread-ID: https://ampcode.com/threads/T-c3bdb8b9-d67b-4de5-901e-7ea76fc9e399
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-10-21 23:33:01 -07:00
parent 2aeaa283d4
commit 582bd6e183
2 changed files with 260 additions and 2 deletions

View File

@@ -463,8 +463,24 @@ func (s *SQLiteStorage) getNextIDForPrefix(ctx context.Context, prefix string) (
// SyncAllCounters synchronizes all ID counters based on existing issues in the database
// This scans all issues and updates counters to prevent ID collisions with auto-generated IDs
// Note: This unconditionally overwrites counter values, allowing them to decrease after deletions
func (s *SQLiteStorage) SyncAllCounters(ctx context.Context) error {
// First, delete counters for prefixes that have no issues
_, err := s.db.ExecContext(ctx, `
DELETE FROM issue_counters
WHERE prefix NOT IN (
SELECT DISTINCT substr(id, 1, instr(id, '-') - 1)
FROM issues
WHERE instr(id, '-') > 0
AND substr(id, instr(id, '-') + 1) GLOB '[0-9]*'
)
`)
if err != nil {
return fmt.Errorf("failed to delete orphaned counters: %w", err)
}
// Then, upsert counters for prefixes that have issues
_, err = s.db.ExecContext(ctx, `
INSERT INTO issue_counters (prefix, last_id)
SELECT
substr(id, 1, instr(id, '-') - 1) as prefix,
@@ -474,7 +490,7 @@ func (s *SQLiteStorage) SyncAllCounters(ctx context.Context) error {
AND substr(id, instr(id, '-') + 1) GLOB '[0-9]*'
GROUP BY prefix
ON CONFLICT(prefix) DO UPDATE SET
last_id = MAX(last_id, excluded.last_id)
last_id = excluded.last_id
`)
if err != nil {
return fmt.Errorf("failed to sync counters: %w", err)
@@ -1400,7 +1416,12 @@ func (s *SQLiteStorage) DeleteIssue(ctx context.Context, id string) error {
return fmt.Errorf("issue not found: %s", id)
}
return tx.Commit()
if err := tx.Commit(); err != nil {
return err
}
// Sync counters after deletion to keep them accurate
return s.SyncAllCounters(ctx)
}
// DeleteIssuesResult contains statistics about a batch deletion operation
@@ -1612,6 +1633,11 @@ func (s *SQLiteStorage) DeleteIssues(ctx context.Context, ids []string, cascade
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
// Sync counters after deletion to keep them accurate
if err := s.SyncAllCounters(ctx); err != nil {
return nil, fmt.Errorf("failed to sync counters after deletion: %w", err)
}
return result, nil
}