{"record":{"id":"e8af424a32252518","repo":"gastownhall/beads","slug":"journal-prune-below-d-w","errorCode":null,"errorMessage":"journal: prune below %d: %w","messagePattern":"journal: prune below (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/journal_prune.go","lineNumber":422,"sourceCode":"\t}\n\treturn readRowsCeil, readDaysFloor\n}\n\n// PruneEventsInTx deletes journal rows with seq below before, honoring the\n// retain-days and retain-rows floors (0 disables a floor), and returns the\n// number of rows deleted. It runs inside the caller's transaction.\nfunc PruneEventsInTx(ctx context.Context, tx DBTX, before int64, retainDays, retainRows int, now time.Time) (int64, error) {\n\treadRowsCeil, readDaysFloor := eventsPruneFloorReadersInTx(ctx, tx, retainRows)\n\twhere, args, skip, err := ComputeEventsPruneWhere(before, retainDays, retainRows, now, readRowsCeil, readDaysFloor)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif skip {\n\t\treturn 0, nil\n\t}\n\tres, err := tx.ExecContext(ctx, \"DELETE FROM bd_events_journal WHERE \"+where, args...)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"journal: prune below %d: %w\", before, err)\n\t}\n\treturn res.RowsAffected()\n}\n","sourceCodeStart":404,"sourceCodeEnd":426,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/journal_prune.go#L404-L426","documentation":"PruneEventsInTx executes DELETE FROM bd_events_journal WHERE <computed bound> to drop journal rows below the computed seq. This error wraps the DELETE failing, and includes the `before` seq bound in the message. The prune itself was correctly computed (skip was false); only the deletion failed.","triggerScenarios":"PruneEventsInTx failing at ExecContext: foreign-key or trigger on bd_events_journal rejecting deletes, lock contention with concurrent journal readers/writers, read-only connection, lost connection mid-DELETE, or a corrupted `where` clause from a malformed bound.","commonSituations":"Long-running DELETE timing out under lock contention on an active journal; running with a read-only DB user; server-enforced max execution time exceeded on very large deletes.","solutions":["Retry the prune — lock contention with readers is the most common cause and is transient.","Ensure the DB user has DELETE privilege and the connection is not read-only.","Prune in smaller batches (lower retention steps) so each DELETE stays under timeout limits.","Schedule pruning outside peak write windows to reduce lock waits."],"exampleFix":"// before\ndefer store.PruneEventsInTx(ctx, tx, bound) // giant single DELETE, times out\n// after\nfor bound > 0 { // prune incrementally\n  step := bound; if step > 10000 { step = 10000 }\n  if _, err := store.PruneEventsInTx(ctx, tx, step); err != nil { return err }\n  bound -= step\n}","handlingStrategy":"retry","validationCode":"var ro int\n_ = db.QueryRow(\"SELECT @@read_only\").Scan(&ro)\nif ro == 1 { return errors.New(\"database is read-only; pruning will fail\") }","typeGuard":null,"tryCatchPattern":"n, err := store.PruneEventsInTx(ctx, tx, before)\nif err != nil && strings.Contains(err.Error(), \"journal: prune below\") {\n    if isLockWaitOrTransient(err) { return retryPrune(before) }\n    return fmt.Errorf(\"prune to %d failed; check DELETE grants and FK/trigger config: %w\", before, err)\n}","preventionTips":["Grant DELETE on bd_events_journal to the app user.","Prune in off-peak windows and in bounded batches.","Avoid triggers/FKs on the journal table that block deletes.","Set server lock-wait timeouts high enough for large deletes."],"tags":["storage","database","journal","pruning","delete"],"backgroundTag":"journal-prune-delete-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}