gastownhall/beads · critical

delete issues: %w

Error message

delete issues: %w

What it means

The core bulk DELETE (`DELETE FROM issues WHERE source_repo = ?`) failing is wrapped as "delete issues: %w". This is the main destructive statement; its failure rolls back nothing by itself (caller's tx governs), but signals the issues rows were not removed.

Source

Thrown at internal/storage/issueops/bulk_ops.go:215

		return 0, fmt.Errorf("affected by source-repo delete: %w", aerr)
	}

	// Deleted issues hold no leases: clear them while the id set is still
	// joinable (before the issues rows go away).
	if _, err := tx.ExecContext(ctx,
		`DELETE FROM leases WHERE issue_id IN (SELECT id FROM issues WHERE source_repo = ?)`, sourceRepo); err != nil {
		return 0, fmt.Errorf("delete leases: %w", err)
	}

	// Edges are journaled before the rows go, while their source snapshots can
	// still be read.
	if err := RecordDependencyRemovalsForIssuesInTx(ctx, tx, issueIDs); err != nil {
		return 0, fmt.Errorf("journal dependency removals for source-repo delete: %w", err)
	}

	result, err := tx.ExecContext(ctx, `DELETE FROM issues WHERE source_repo = ?`, sourceRepo)
	if err != nil {
		return 0, fmt.Errorf("delete issues: %w", err)
	}

	rowsAffected, err := result.RowsAffected()
	if err != nil {
		return 0, fmt.Errorf("rows affected: %w", err)
	}

	// Journal each deleted issue in the same transaction. issueIDs is the exact
	// set removed by the DELETE above (both were scoped to source_repo), so
	// there are no phantom records here. The source-repo bulk delete plumbing
	// carries no actor, so the rows record none.
	for _, id := range issueIDs {
		if err := RecordDeleteInTx(ctx, tx, id, ""); err != nil {
			return int(rowsAffected), err
		}
	}

	if err := RecomputeIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped error — a FOREIGN KEY constraint failed means a child table still references the issues
  2. Migrate or add CASCADE handling for any new referencing tables
  3. Retry when the database lock clears; serialize bulk deletes against other writers
  4. Point the operation at a writable primary connection, not a read-only replica
Defensive patterns

Strategy: try-catch

Validate before calling

// detect tables referencing issues without cascade before deleting
rows, _ := db.Query("SELECT name FROM sqlite_master WHERE type='table'") // inspect schema for FK references

Type guard

if strings.Contains(err.Error(), "FOREIGN KEY constraint failed") { /* a child table still references issues */ }

Try / catch

err := DeleteIssuesBySourceRepoInTx(ctx, tx, repo)
if err != nil {
    if strings.Contains(err.Error(), "FOREIGN KEY") {
        // migrate schema to add ON DELETE CASCADE for the referencing table
    }
    // the tx is aborted: rollback and retry after fixing
    tx.Rollback()
    return err
}

Prevention

When it happens

Trigger: tx.ExecContext on the DELETE fails: foreign-key constraint from a table still referencing issues (no ON DELETE CASCADE), database locked by a concurrent writer, read-only connection, or context canceled.

Common situations: Schema variant lacking CASCADE on dependent tables (e.g. a plugin-added table referencing issues); another bd process mid-write; replica/read-only connection mistakenly used for the delete.

Related errors


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