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

  1. Check the wrapped driver error; run migrations if the table is missing on the non-wisp path
  2. Chunk the ID list to stay under SQL placeholder limits
  3. Retry on lock-wait/timeouts after the blocking transaction completes
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/eedcf15ab2dc4095. Report an issue: GitHub.