gastownhall/beads · error
get labels for issues: scan
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.
Solutions
- Delete or repair rows with NULL issue_id/label: DELETE FROM labels WHERE issue_id IS NULL OR label IS NULL
- Add NOT NULL constraints to prevent recurrence
- 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
- Add NOT NULL constraints to issue_id and label columns
- Sanitize data imported from external tools
- Avoid manual SQL inserts into storage tables
- Schedule integrity checks for NULL values in aux tables
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
- get labels: scan
- db: GetAllConfig: scan
- db: GetCustomTypes: scan custom_types
- add label: record event
- add label /
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/39049630470b54c2.
Report an issue: GitHub.
Appendix: 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)