gastownhall/beads · error
journal dependency removals for %s: %w
Error message
journal dependency removals for %s: %w
What it means
DeleteIssueInTx wraps a failure from RecordDependencyRemovalsForIssuesInTx, which journals the dependency edges that are about to disappear so consumers can replay/audit removals. This runs inside the same transaction before the issue row is deleted, so a failure here aborts the delete with no data loss.
Source
Thrown at internal/storage/issueops/delete.go:40
//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func DeleteIssueInTx(ctx context.Context, tx *sql.Tx, id string) error {
isWisp := IsActiveWispInTx(ctx, tx, id)
var deletedIssues, deletedWisps []string
if isWisp {
deletedWisps = []string{id}
} else {
deletedIssues = []string{id}
}
affectedIssues, affectedWisps, aerr := AffectedByDeletionInTx(ctx, tx, deletedIssues, deletedWisps)
if aerr != nil {
return fmt.Errorf("affected by delete for %s: %w", id, aerr)
}
// Edges are journaled before the rows go, while their source snapshots can
// still be read.
if err := RecordDependencyRemovalsForIssuesInTx(ctx, tx, []string{id}); err != nil {
return fmt.Errorf("journal dependency removals for %s: %w", id, err)
}
if err := deleteIssueRowInTx(ctx, tx, id, isWisp); err != nil {
return err
}
if err := RecomputeIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {
return fmt.Errorf("recompute is_blocked after delete for %s: %w", id, err)
}
return nil
}
//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func deleteIssueRowInTx(ctx context.Context, tx *sql.Tx, id string, isWisp bool) error {
issueTable, _, _, _ := WispTableRouting(isWisp)
result, err := tx.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s WHERE id = ?", issueTable), id)
if err != nil {
return fmt.Errorf("delete issue from %s: %w", issueTable, err)View on GitHub (pinned to 71377f2769)
Solutions
- Look at the wrapped cause for the real driver error.
- Retry the entire delete in a fresh transaction — the tx aborts atomically, so it is safe to retry.
- Ensure the caller passes a live, unexpired context to the delete call.
- Check the journal/dependencies tables exist and are writable in the current schema version (run bd migrate if schema is stale).
Defensive patterns
Strategy: retry
Validate before calling
if err := ctx.Err(); err != nil { return fmt.Errorf("context expired before delete: %w", err) } Try / catch
if err := storage.DeleteIssue(ctx, db, id); err != nil {
var e *transientDBError
if errors.As(err, &e) { /* retry whole delete in a new tx — the tx rolled back atomically */ }
return err
} Prevention
- The delete is transactional — safe to retry the entire operation on transient failures.
- Keep schema current (bd migrate) so journal tables exist and are writable.
- Avoid sharing one context/tx across many long operations.
When it happens
Trigger: Calling DeleteIssue/DeleteIssueInTx when the journal insert for dependency removals fails: tx already aborted, context canceled, connection lost, or the journal table write hits a driver/SQL error.
Common situations: Network blip between journaling and delete; context deadline exceeded on a slow server; the transaction was already poisoned by an earlier failed statement on the same tx handle.
Related errors
- failed to close issue: %w
- affected by delete for %s: %w
- recompute is_blocked after delete for %s: %w
- partition delete ids: %w
- ErrTransaction
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/3897291d6eab50f7.
Report an issue: GitHub.