gastownhall/beads · error

db: LabelSQLRepository.Insert: label must not be empty

Error message

db: LabelSQLRepository.Insert: label must not be empty

What it means

Input validation error from LabelSQLRepository.Insert: the label argument is empty. The repository rejects empty labels before INSERT IGNORE, because the database would otherwise silently store an empty/blank label row. This mirrors the issueops.AddLabelInTx guard so both write stacks behave identically.

Source

Thrown at internal/storage/domain/db/label.go:40

	runner Runner
	events domain.EventsSQLRepository
}

var _ domain.LabelSQLRepository = (*labelSQLRepositoryImpl)(nil)

func pickLabelTable(useWisps bool) string {
	if useWisps {
		return "wisp_labels"
	}
	return "labels"
}

func (r *labelSQLRepositoryImpl) Insert(ctx context.Context, issueID, label, actor string, opts domain.LabelOpts) error {
	if issueID == "" {
		return fmt.Errorf("db: LabelSQLRepository.Insert: issueID must not be empty")
	}
	if label == "" {
		return fmt.Errorf("db: LabelSQLRepository.Insert: label must not be empty")
	}
	// Reject an over-length label before the INSERT IGNORE, which would otherwise
	// silently truncate it to the VARCHAR(255) column. This is the proxied-server
	// (uow) analog of issueops.AddLabelInTx's guard, so both write stacks return a
	// typed ErrFieldTooLong instead of storing a label the caller never sent.
	if err := types.CheckFieldLen("label", label); err != nil {
		return err
	}
	table := pickLabelTable(opts.UseWispsTable)
	//nolint:gosec // G201: table is one of two hardcoded constants
	result, err := r.runner.ExecContext(ctx,
		fmt.Sprintf("INSERT IGNORE INTO %s (issue_id, label) VALUES (?, ?)", table),
		issueID, label,
	)
	if err != nil {
		return fmt.Errorf("db: LabelSQLRepository.Insert %s/%s: %w", issueID, label, err)
	}
	rows, err := result.RowsAffected()

View on GitHub (pinned to 71377f2769)

Solutions

  1. Validate the label is non-empty (after trimming) before calling Insert
  2. Reject empty label at the API/CLI layer with a clear message
  3. Check upstream parsing for fields dropped or trimmed to empty
  4. Also mind CheckFieldLen: labels over 255 chars are rejected as typed ErrFieldTooLong

Example fix

// before
label := strings.TrimSpace(input.Label)
repo.Insert(ctx, id, label, actor, opts)
// after
label := strings.TrimSpace(input.Label)
if label == "" {
    return fmt.Errorf("label must not be empty")
}
repo.Insert(ctx, id, label, actor, opts)
Defensive patterns

Strategy: validation

Validate before calling

label := strings.TrimSpace(userLabel)
if label == "" || len(label) > 255 {
    return fmt.Errorf("label must be 1-255 chars")
}

Type guard

func validLabel(s string) bool {
    t := strings.TrimSpace(s)
    return t != "" && len(t) <= 255
}

Try / catch

if err := repo.Insert(ctx, issueID, label, actor, opts); err != nil {
    var tooLong *types.ErrFieldTooLong
    if strings.Contains(err.Error(), "label must not be empty") || errors.As(err, &tooLong) {
        return fmt.Errorf("invalid label %q: %w", label, err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling Insert(ctx, issueID, "", actor, opts) — e.g. a label string trimmed to nothing, a missing field in a JSON payload, or whitespace-only input.

Common situations: Client sending "label": "" in an API request, strings.TrimLeft wiping a whitespace label, or a template/config defaulting the label to empty.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


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