gastownhall/beads · error

read status from %s: %w

Error message

read status from %s: %w

What it means

Beads throws this when reading the status of an id for the already-closed check fails with a non-'no rows' error on one of the probed tables (issues, then wisps). Missing optional wisp tables are skipped; missing rows just continue the probe; any other error is wrapped with the table name and aborts the close policy evaluation.

Source

Thrown at internal/storage/issueops/close.go:312

	for _, target := range []struct {
		table  string
		column string
	}{
		{table: "issues", column: "depends_on_issue_id"},
		{table: "wisps", column: "depends_on_wisp_id"},
	} {
		var status string
		err := tx.QueryRowContext(ctx, "SELECT status FROM "+target.table+" WHERE id = ?", id).Scan(&status)
		if err == nil {
			return types.Status(status) == types.StatusClosed, target.column, true, nil
		}
		if errors.Is(err, sql.ErrNoRows) {
			continue
		}
		if optionalBlockedTable(target.table) && isTableNotExistError(err) {
			continue
		}
		return false, "", false, fmt.Errorf("read status from %s: %w", target.table, err)
	}
	return false, "", false, nil
}

//nolint:gosec // G201: table names come from WispTableRouting (hardcoded constants)
func closeIssueInTx(ctx context.Context, tx DBTX, id string, reason, actor, session string, recordEvent bool) (*CloseResult, error) {
	isWisp := IsActiveWispInTx(ctx, tx, id)
	issueTable, _, eventTable, _ := WispTableRouting(isWisp)

	var affectedIssues, affectedWisps []string
	var aerr error
	if isWisp {
		affectedIssues, affectedWisps, aerr = AffectedByStatusChangeForWispInTx(ctx, tx, id)
	} else {
		affectedIssues, affectedWisps, aerr = AffectedByStatusChangeInTx(ctx, tx, id)
	}
	if aerr != nil {
		return nil, fmt.Errorf("affected by close for %s: %w", id, aerr)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped driver error after 'read status from <table>:' for the true cause
  2. Retry the close if transient (connection, lock timeout)
  3. Verify schema integrity and run migrations if the table exists but errors on read
  4. Adjust context timeouts; check for blocking transactions holding locks on the target table

Example fix

// before
ctx := context.Background()
closed, col, found, err := isClosedInTx(ctx, tx, id) // times out on huge table
// after
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
closed, col, found, err := isClosedInTx(ctx, tx, id)
Defensive patterns

Strategy: try-catch

Validate before calling

// Verify both probed tables are readable before close-policy checks
for _, t := range []string{"issues", "wisps"} {
	var one int
	err := db.QueryRow("SELECT 1 FROM " + t + " LIMIT 1").Scan(&one)
	if err != nil && !dberrors.IsTableNotExist(err) {
		return fmt.Errorf("%s unreadable before close: %w", t, err)
	}
}

Try / catch

err := CloseIssue(ctx, id, opts)
if err != nil && strings.Contains(err.Error(), "read status from") {
	switch {
	case dberrors.IsLockTimeout(err):
		return retryCloseWithBackoff(ctx, id)
	case dberrors.IsConnectionError(err) || errors.Is(err, context.DeadlineExceeded):
		return retryClose(ctx, id)
	default:
		return fmt.Errorf("permanent failure closing %s: %w", id, err)
	}
}

Prevention

When it happens

Trigger: CloseIssue or EnforceClosePolicyInTx → isClosedInTx when 'SELECT status FROM <table> WHERE id = ?' fails: connection failure, lock wait timeout, permission denied, unreadable/corrupt table, or context cancellation — on either the issues or wisps table.

Common situations: Connection drop mid-close-policy check; lock contention from concurrent closes or bulk operations; partial wisp migration leaving wisps present but damaged; context timeout on a slow database.

Understand the failure class

Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.

Related errors


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