gastownhall/beads · error
db: EventsSQLRepository.DeleteAllForIDs from %s: %w
Error message
db: EventsSQLRepository.DeleteAllForIDs from %s: %w
What it means
Wraps DELETE failures in DeleteAllForIDs when removing event rows for issue IDs from events or wisp_events. Wisp-table 'not exist' errors are tolerated (returns early), so this error means a genuine delete failure against an existing table, and the partial total accumulated so far is returned alongside the error.
Source
Thrown at internal/storage/domain/db/events.go:63
if end > len(ids) {
end = len(ids)
}
batch := ids[start:end]
placeholders := make([]string, len(batch))
args := make([]any, len(batch))
for i, id := range batch {
placeholders[i] = "?"
args[i] = id
}
//nolint:gosec // G201: table is one of two hardcoded constants; ? placeholders only.
res, err := r.runner.ExecContext(ctx,
fmt.Sprintf("DELETE FROM %s WHERE issue_id IN (%s)", table, strings.Join(placeholders, ",")),
args...)
if err != nil {
if opts.UseWispsTable && dberrors.IsTableNotExist(err) {
return total, nil
}
return total, fmt.Errorf("db: EventsSQLRepository.DeleteAllForIDs from %s: %w", table, err)
}
n, err := res.RowsAffected()
if err != nil {
return total, fmt.Errorf("db: EventsSQLRepository.DeleteAllForIDs rows affected: %w", err)
}
total += int(n)
}
return total, nil
}
func (r *eventsSQLRepositoryImpl) CountAllForIDs(ctx context.Context, ids []string, opts domain.RecordEventOpts) (int, error) {
if len(ids) == 0 {
return 0, nil
}
table := "events"
if opts.UseWispsTable {
table = "wisp_events"
}View on GitHub (pinned to 71377f2769)
Solutions
- Check the wrapped driver error; run migrations if the table is missing on the non-wisp path
- Chunk the ID list to stay under SQL placeholder limits
- Retry on lock-wait/timeouts after the blocking transaction completes
- Note the returned partial total — some batches may have already deleted; make retries idempotent
Example fix
// before
deleted, err := repo.DeleteAllForIDs(ctx, allIDs, opts)
// after
var total int
for _, chunk := range chunkIDs(allIDs, 500) {
n, err := repo.DeleteAllForIDs(ctx, chunk, opts)
total += n
if err != nil { return total, err }
} Defensive patterns
Strategy: retry
Validate before calling
const maxPlaceholders = 500
if len(ids) > maxPlaceholders { return chunkAndDelete(ids) }
for _, id := range ids { if id == "" { return fmt.Errorf("empty ID in delete batch") } } Type guard
func isLockTimeoutErr(err error) bool {
var mysqlErr *go_mysql.MySQLError
return errors.As(err, &mysqlErr) && (mysqlErr.Number == 1205 || mysqlErr.Number == 1213)
} Try / catch
deleted, err := repo.DeleteAllForIDs(ctx, ids, opts)
if err != nil {
if isLockTimeoutErr(err) {
time.Sleep(backoff)
return retryDelete(ids, deleted) // idempotent: already-deleted rows are no-ops
}
return fmt.Errorf("delete events: %w (deleted %d so far)", err, deleted)
} Prevention
- Chunk ID batches under SQL placeholder limits
- Make deletes idempotent since partial totals are possible
- Schedule bulk deletes during low-write windows
- Ensure migrations exist for the non-wisp events table
When it happens
Trigger: Calling DeleteAllForIDs when the DELETE ... WHERE issue_id IN (...) statement fails: too many placeholders, connection loss mid-batch, lock wait timeout from concurrent writers, or missing table with UseWispsTable=false.
Common situations: Bulk issue deletion against a table locked by a long transaction; deleting tens of thousands of IDs exceeding placeholder limits; events table absent because migrations never ran (non-wisp path).
Related errors
- delete: drop events: %w
- no database connection available (%s)
- failed to remove federation peer: %w
- failed to batch delete wisps: %w
- db: DependencySQLRepository.Insert: record dependency_added
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/eedcf15ab2dc4095.
Report an issue: GitHub.