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

  1. Look at the wrapped cause for the real driver error.
  2. Retry the entire delete in a fresh transaction — the tx aborts atomically, so it is safe to retry.
  3. Ensure the caller passes a live, unexpired context to the delete call.
  4. 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

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


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