gastownhall/beads · error

delete issue from %s: %w

Error message

delete issue from %s: %w

What it means

deleteIssueRowInTx wraps the SQL error from the DELETE statement itself, with the routed table name (issues vs the wisp table) interpolated — table names are hardcoded constants via WispTableRouting. This is a driver/SQL-level failure, not a 'row not found' case (that is a separate sentinel error).

Source

Thrown at internal/storage/issueops/delete.go:58

		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)
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("get rows affected: %w", err)
	}
	if rows == 0 {
		// Wrap the sentinel so callers can errors.Is(..., storage.ErrNotFound),
		// matching GetIssue/UpdateIssue. The storage conformance suite asserts
		// this parity across not-found paths.
		return fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
	}
	// Journal the delete in the same transaction. This worker backs single
	// deletes (DeleteIssueInTx) and the per-wisp branch of the bulk delete
	// (DeleteResolvedSetInTx); the bulk regular-issue branch journals its own
	// ids directly. The rows==0 return above is what keeps this
	// actually-deleted-only. The delete plumbing (storage.DeleteIssue and the
	// bulk/cascade resolvers) carries no actor, so the row records none.
	if err := RecordDeleteInTx(ctx, tx, id, ""); err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped SQL error for the concrete driver message (table, constraint, connection).
  2. Run schema migration if the routed table is missing (stale schema after upgrade).
  3. Check disk space and DB health if the message indicates I/O or corruption.
  4. Retry in a fresh transaction if the failure was a dropped connection; never reuse a poisoned tx.

Example fix

// before: ignoring schema drift after a version upgrade
err := storage.DeleteIssue(ctx, db, id) // fails: no such table: issues
// after: ensure migrations run before storage use
if err := bd.Migrate(db); err != nil { return err }
err = storage.DeleteIssue(ctx, db, id)
Defensive patterns

Strategy: validation

Validate before calling

// pre-flight: table exists and connection healthy
var one int
if err := db.QueryRowContext(ctx, "SELECT 1").Scan(&one); err != nil { return err }
if err := bd.MigrateIfStale(ctx, db); err != nil { return err }

Try / catch

if err := storage.DeleteIssue(ctx, db, id); err != nil {
	if errors.Is(err, storage.ErrNotFound) { return nil } // not this error; see not-found
	return fmt.Errorf("delete failed: %w", err) // surface the wrapped SQL cause
}

Prevention

When it happens

Trigger: Any call into DeleteIssue/DeleteIssues/DeleteResolvedSetInTx where the DELETE ... WHERE id = ? statement returns a driver error: bad connection, table missing, constraint violation from a foreign key not cleaned up, read-only replica.

Common situations: Database file corruption or a stale schema where the routed table doesn't exist; deleting an issue whose dependencies rows were created outside the tx by another process with FK enforcement; disk-full on the Dolt data directory.

Related errors


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