{"record":{"id":"eedcf15ab2dc4095","repo":"gastownhall/beads","slug":"db-eventssqlrepository-deleteallforids-from-s","errorCode":null,"errorMessage":"db: EventsSQLRepository.DeleteAllForIDs from %s: %w","messagePattern":"db: EventsSQLRepository\\.DeleteAllForIDs from (.+?): %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/events.go","lineNumber":63,"sourceCode":"\t\tif end > len(ids) {\n\t\t\tend = len(ids)\n\t\t}\n\t\tbatch := ids[start:end]\n\t\tplaceholders := make([]string, len(batch))\n\t\targs := make([]any, len(batch))\n\t\tfor i, id := range batch {\n\t\t\tplaceholders[i] = \"?\"\n\t\t\targs[i] = id\n\t\t}\n\t\t//nolint:gosec // G201: table is one of two hardcoded constants; ? placeholders only.\n\t\tres, err := r.runner.ExecContext(ctx,\n\t\t\tfmt.Sprintf(\"DELETE FROM %s WHERE issue_id IN (%s)\", table, strings.Join(placeholders, \",\")),\n\t\t\targs...)\n\t\tif err != nil {\n\t\t\tif opts.UseWispsTable && dberrors.IsTableNotExist(err) {\n\t\t\t\treturn total, nil\n\t\t\t}\n\t\t\treturn total, fmt.Errorf(\"db: EventsSQLRepository.DeleteAllForIDs from %s: %w\", table, err)\n\t\t}\n\t\tn, err := res.RowsAffected()\n\t\tif err != nil {\n\t\t\treturn total, fmt.Errorf(\"db: EventsSQLRepository.DeleteAllForIDs rows affected: %w\", err)\n\t\t}\n\t\ttotal += int(n)\n\t}\n\treturn total, nil\n}\n\nfunc (r *eventsSQLRepositoryImpl) CountAllForIDs(ctx context.Context, ids []string, opts domain.RecordEventOpts) (int, error) {\n\tif len(ids) == 0 {\n\t\treturn 0, nil\n\t}\n\ttable := \"events\"\n\tif opts.UseWispsTable {\n\t\ttable = \"wisp_events\"\n\t}","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/events.go#L45-L81","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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"],"exampleFix":"// before\ndeleted, err := repo.DeleteAllForIDs(ctx, allIDs, opts)\n// after\nvar total int\nfor _, chunk := range chunkIDs(allIDs, 500) {\n    n, err := repo.DeleteAllForIDs(ctx, chunk, opts)\n    total += n\n    if err != nil { return total, err }\n}","handlingStrategy":"retry","validationCode":"const maxPlaceholders = 500\nif len(ids) > maxPlaceholders { return chunkAndDelete(ids) }\nfor _, id := range ids { if id == \"\" { return fmt.Errorf(\"empty ID in delete batch\") } }","typeGuard":"func isLockTimeoutErr(err error) bool {\n    var mysqlErr *go_mysql.MySQLError\n    return errors.As(err, &mysqlErr) && (mysqlErr.Number == 1205 || mysqlErr.Number == 1213)\n}","tryCatchPattern":"deleted, err := repo.DeleteAllForIDs(ctx, ids, opts)\nif err != nil {\n    if isLockTimeoutErr(err) {\n        time.Sleep(backoff)\n        return retryDelete(ids, deleted) // idempotent: already-deleted rows are no-ops\n    }\n    return fmt.Errorf(\"delete events: %w (deleted %d so far)\", err, deleted)\n}","preventionTips":["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"],"tags":["database","events","delete","batch"],"backgroundTag":"batch-delete-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}