gastownhall/beads · error

delete leases: %w

Error message

delete leases: %w

What it means

After deleting the issue rows, DeleteResolvedSetInTx removes any leases referencing the deleted issues; failure of `DELETE FROM leases WHERE issue_id IN (...)` is wrapped with this message. It exists to keep lease rows from orphaning after issues disappear.

Source

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

			end = len(set.RegularIDs)
		}
		batch := set.RegularIDs[i:end]
		batchInClause, batchArgs := buildSQLInClause(batch)

		deleteResult, err := tx.ExecContext(ctx,
			fmt.Sprintf(`DELETE FROM issues WHERE id IN (%s)`, batchInClause),
			batchArgs...)
		if err != nil {
			return nil, fmt.Errorf("delete issues: %w", err)
		}
		rowsAffected, _ := deleteResult.RowsAffected()
		totalRegularsDeleted += int(rowsAffected)

		// Deleted issues hold no leases.
		if _, err := tx.ExecContext(ctx,
			fmt.Sprintf(`DELETE FROM leases WHERE issue_id IN (%s)`, batchInClause),
			batchArgs...); err != nil {
			return nil, fmt.Errorf("delete leases: %w", err)
		}
	}
	result.DeletedCount = totalRegularsDeleted + len(set.WispIDs)

	// Journal every regular issue this bulk/cascade delete removed. Wisps went
	// through deleteIssueRowInTx above, which journals each itself; set.All is
	// cascade-expanded, so this records cascade deletes too. The delete
	// plumbing carries no actor, so the rows record none.
	for _, id := range journaledDeletes {
		if err := RecordDeleteInTx(ctx, tx, id, ""); err != nil {
			return nil, err
		}
	}

	if err := RecomputeIsBlockedInTx(ctx, tx, affectedIssues, affectedWisps); err != nil {
		return nil, fmt.Errorf("recompute is_blocked after batch delete: %w", err)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the wrapped error; if it is 'no such table: leases', migrate the database schema
  2. Retry after the competing writer releases its lock / raise busy timeout
  3. Verify connection stability for remote Dolt deployments

Example fix

// before
if _, err := tx.ExecContext(ctx, fmt.Sprintf(`DELETE FROM leases WHERE issue_id IN (%s)`, batchInClause), batchArgs...); err != nil {
	return nil, fmt.Errorf("delete leases: %w", err)
}
// after — migrate first when the table is absent
// bd doctor / apply migrations before retrying the delete
Defensive patterns

Strategy: try-catch

Validate before calling

// confirm the leases table exists before deleting
var n int
if err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE name='leases'").Scan(&n); err != nil || n == 0 {
	return fmt.Errorf("leases table missing; run migrations")
}

Try / catch

if _, err := DeleteIssuesInTx(ctx, tx, ids, opts); err != nil {
	if strings.Contains(err.Error(), "delete leases: ") && strings.Contains(err.Error(), "no such table") {
		migrate(db) // then retry
	}
	return err
}

Prevention

When it happens

Trigger: DeleteIssuesInTx/DeleteInTx where the issues DELETE succeeded but the follow-up leases DELETE fails — leases table missing (older schema), lock contention, or connection failure mid-transaction.

Common situations: Running a new binary against an old database where the leases table does not exist; concurrent agent holding leases causing lock contention on SQLite/Dolt.

Related errors


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