gastownhall/beads · error

get labels for issues: scan: %w

Error message

get labels for issues: scan: %w

What it means

This error wraps a rows.Scan failure in getLabelsIntoFromTable while decoding an (issue_id, label) row pair into strings. It indicates a row whose columns are NULL or of unexpected types in the batched label fetch. The library closes rows and aborts the whole batch rather than returning partial label maps.

Source

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

		}
		batch := ids[start:end]
		placeholders := make([]string, len(batch))
		args := make([]any, len(batch))
		for i, id := range batch {
			placeholders[i] = "?"
			args[i] = id
		}
		rows, err := tx.QueryContext(ctx, fmt.Sprintf(
			`SELECT issue_id, label FROM %s WHERE issue_id IN (%s) ORDER BY issue_id, label`,
			labelTable, strings.Join(placeholders, ",")), args...)
		if err != nil {
			return fmt.Errorf("get labels for issues from %s: %w", labelTable, err)
		}
		for rows.Next() {
			var issueID, label string
			if err := rows.Scan(&issueID, &label); err != nil {
				_ = rows.Close()
				return fmt.Errorf("get labels for issues: scan: %w", err)
			}
			result[issueID] = append(result[issueID], label)
		}
		_ = rows.Close()
		if err := rows.Err(); err != nil {
			return fmt.Errorf("get labels for issues: rows: %w", err)
		}
	}
	return nil
}

// AddLabelInTx adds a label to an issue and records an event within an existing
// transaction. Automatically routes to wisp tables if the ID is an active wisp.
// Uses INSERT IGNORE for idempotency.
func AddLabelInTx(ctx context.Context, tx DBTX, labelTable, eventTable, issueID, label, actor string) error {
	// Reject an over-length label up front. The INSERT IGNORE below would
	// otherwise silently truncate it to the VARCHAR(255) column, storing a label
	// the caller never sent; a typed ErrFieldTooLong is the clean rejection.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Delete or repair rows with NULL issue_id/label: DELETE FROM labels WHERE issue_id IS NULL OR label IS NULL
  2. Add NOT NULL constraints to prevent recurrence
  3. If soft handling is needed, patch the scan to use sql.NullString and skip invalid rows
Defensive patterns

Strategy: validation

Validate before calling

var nulls int
_ = tx.QueryRow(`SELECT COUNT(*) FROM labels WHERE issue_id IS NULL OR label IS NULL`).Scan(&nulls)
if nulls > 0 { return fmt.Errorf("%d malformed label rows; repair before bulk hydration", nulls) }

Try / catch

m, err := issueops.GetLabelsForIssuesInTx(ctx, tx, ids)
if err != nil {
    if strings.Contains(err.Error(), "scan") {
        // locate and repair NULL rows, then retry
    }
    return err
}

Prevention

When it happens

Trigger: Rows in labels/wisp_labels with NULL issue_id or label values encountered during GetLabelsForIssuesInTx batch iteration.

Common situations: Corrupted or manually inserted rows; schema drift allowing NULLs; partial imports from migrations.

Related errors


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