gastownhall/beads · error

scan remaining blocker: %w

Error message

scan remaining blocker: %w

What it means

Wraps a rows.Scan failure while reading (candidateID, blockerID) string pairs from a dependency-table result set in GetNewlyUnblockedByCloseInTx. This means the query returned rows whose columns could not be scanned into two strings — almost always a schema/type mismatch. The row iteration is aborted and the whole unblock computation fails.

Source

Thrown at internal/storage/issueops/dependency_queries.go:856

		remainingByCandidate := make(map[string][]string, len(batch))
		remainingBlockerSet := make(map[string]struct{})
		for _, depTable := range []string{"dependencies", "wisp_dependencies"} {
			//nolint:gosec // G201: depTable is hardcoded.
			depRows, err := tx.QueryContext(ctx, fmt.Sprintf(`
				SELECT issue_id, %s AS depends_on_id FROM %s
				WHERE issue_id IN (%s) AND type = 'blocks' AND %s != ?
			`, DepTargetExpr, depTable, placeholders, DepTargetExpr), append(batchArgs, closedIssueID)...)
			if err != nil {
				if optionalBlockedTable(depTable) && isTableNotExistError(err) {
					continue
				}
				return nil, fmt.Errorf("check remaining blockers from %s: %w", depTable, err)
			}
			for depRows.Next() {
				var candidateID, blockerID string
				if err := depRows.Scan(&candidateID, &blockerID); err != nil {
					_ = depRows.Close()
					return nil, fmt.Errorf("scan remaining blocker: %w", err)
				}
				remainingByCandidate[candidateID] = append(remainingByCandidate[candidateID], blockerID)
				remainingBlockerSet[blockerID] = struct{}{}
			}
			_ = depRows.Close()
			if err := depRows.Err(); err != nil {
				return nil, fmt.Errorf("remaining blocker rows from %s: %w", depTable, err)
			}
		}

		remainingBlockerIDs := make([]string, 0, len(remainingBlockerSet))
		for blockerID := range remainingBlockerSet {
			remainingBlockerIDs = append(remainingBlockerIDs, blockerID)
		}
		sort.Strings(remainingBlockerIDs)
		statusByID, err := loadStatusByIDInTx(ctx, tx, remainingBlockerIDs)
		if err != nil {
			return nil, fmt.Errorf("check remaining blocker status: %w", err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped driver error for the column/type that failed to scan
  2. Verify the dependency table columns are TEXT and NOT NULL as the schema expects
  3. Repair or recreate the table from the canonical schema/migrations
  4. Re-run the close/unblock operation after fixing the schema

Example fix

// before
var candidateID, blockerID string
rows.Scan(&candidateID, &blockerID) // panics/error on NULL
// after (schema side: ensure columns are TEXT NOT NULL)
SELECT COALESCE(issue_id,''), COALESCE(target_id,'') FROM ...
Defensive patterns

Strategy: try-catch

Validate before calling

// verify dep table column types are TEXT
rows, _ := db.Query("PRAGMA table_info(deps)")
// or run bd doctor schema checks before batch operations

Try / catch

if err := tx.CloseIssue(ctx, id); err != nil {
	if strings.Contains(err.Error(), "scan remaining blocker") {
		// schema drift: run migrations / repair before retry
	}
	return err
}

Prevention

When it happens

Trigger: The dependency table's columns have unexpected types (e.g. integer IDs instead of text, NULL values in non-nullable-expected columns, or extra/reordered columns) when GetNewlyUnblockedByCloseInTx iterates remaining blockers.

Common situations: Schema drift after an upgrade or hand-edited database; a custom dep table variant whose ID columns are not TEXT; corrupted rows containing NULLs.

Related errors


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