gastownhall/beads · error

get labels: scan: %w

Error message

get labels: scan: %w

What it means

This error wraps a rows.Scan failure while reading a label string in GetLabelsInTx. Scan fails when the column value cannot be converted into the destination — typically a NULL label or unexpected type. The library throws it to distinguish row-decoding problems from query-execution problems.

Source

Thrown at internal/storage/issueops/labels.go:30

// Automatically routes to wisp_labels if the ID is an active wisp.
// Returns labels sorted alphabetically.
func GetLabelsInTx(ctx context.Context, tx DBTX, table, issueID string) ([]string, error) {
	if table == "" {
		isWisp := IsActiveWispInTx(ctx, tx, issueID)
		_, table, _, _ = WispTableRouting(isWisp)
	}
	//nolint:gosec // G201: table is from WispTableRouting ("labels" or "wisp_labels")
	rows, err := tx.QueryContext(ctx, fmt.Sprintf(`SELECT label FROM %s WHERE issue_id = ? ORDER BY label`, table), issueID)
	if err != nil {
		return nil, fmt.Errorf("get labels: %w", err)
	}
	defer rows.Close()

	var labels []string
	for rows.Next() {
		var label string
		if err := rows.Scan(&label); err != nil {
			return nil, fmt.Errorf("get labels: scan: %w", err)
		}
		labels = append(labels, label)
	}
	return labels, rows.Err()
}

// GetLabelsForIssuesInTx fetches labels for multiple issues in a single transaction.
// Routes each ID to labels or wisp_labels based on wisp status.
// Uses a single batched wisp-partition query plus batched IN clauses per label
// table, so the number of round-trips is O(1 + N/queryBatchSize) rather than
// O(N). This matters on remote backends (Dolt) where per-ID round-trips would
// otherwise blow past the context deadline — see GH#3414.
//
// Callers hydrating multiple batches inside one tx may pass a precomputed
// active-wisp set scoped to issueIDs to avoid rebuilding it.
func GetLabelsForIssuesInTx(ctx context.Context, tx DBTX, issueIDs []string, wispSetOpt ...map[string]struct{}) (map[string][]string, error) {
	if len(issueIDs) == 0 {
		return make(map[string][]string), nil

View on GitHub (pinned to 71377f2769)

Solutions

  1. Clean up rows with NULL label values: DELETE FROM labels WHERE label IS NULL
  2. Add a NOT NULL constraint to the label column if schema drift allowed NULLs
  3. Use COALESCE in queries or scan into sql.NullString if tolerant reads are desired

Example fix

// before
var label string
if err := rows.Scan(&label); err != nil {
    return nil, fmt.Errorf("get labels: scan: %w", err)
}
// after (data-cleanup path)
// DELETE FROM labels WHERE label IS NULL;
-- or defensively at call site, tolerate NULLs via a custom scan wrapper
Defensive patterns

Strategy: validation

Validate before calling

var nulls int
_ = tx.QueryRow(`SELECT COUNT(*) FROM labels WHERE label IS NULL`).Scan(&nulls)
if nulls > 0 { return fmt.Errorf("%d label rows have NULL label values; clean them up", nulls) }

Try / catch

labels, err := issueops.GetLabelsInTx(ctx, tx, table, issueID)
if err != nil {
    var scanErr *fmt.ScanError // or inspect message
    if strings.Contains(err.Error(), "scan") {
        // data corruption path: repair rows before retry
    }
    return err
}

Prevention

When it happens

Trigger: A row in labels/wisp_labels whose label column is NULL or holds a non-string type, encountered during iteration in GetLabelsInTx.

Common situations: Manual inserts into the labels table with NULL label values; schema drift where label column type changed; data imported from another tool with malformed rows.

Related errors


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