gastownhall/beads · warning

db: LabelSQLRepository.Delete: issueID must not be empty

Error message

db: LabelSQLRepository.Delete: issueID must not be empty

What it means

Input validation error thrown by LabelSQLRepository.Delete when the issueID argument is an empty string. The library refuses to issue a DELETE with an unconstrained or meaningless key, so the caller gets an immediate error instead of a silent no-op or accidental mass deletion.

Source

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

		}
		return nil
	}
	if err := r.events.Record(ctx, domain.Event{
		IssueID:  issueID,
		Type:     types.EventLabelAdded,
		Actor:    actor,
		NewValue: label,
	}, domain.RecordEventOpts{UseWispsTable: opts.UseWispsTable}); err != nil {
		return err
	}
	// A label is part of the bead snapshot; the idempotent no-op path above
	// returns without writing and journals nothing.
	return issueops.RecordEventInTx(ctx, r.runner, issueops.EventUpdate, issueID, actor)
}

func (r *labelSQLRepositoryImpl) Delete(ctx context.Context, issueID, label, actor string, opts domain.LabelOpts) error {
	if issueID == "" {
		return fmt.Errorf("db: LabelSQLRepository.Delete: issueID must not be empty")
	}
	if label == "" {
		return fmt.Errorf("db: LabelSQLRepository.Delete: label must not be empty")
	}
	table := pickLabelTable(opts.UseWispsTable)
	//nolint:gosec // G201: table is one of two hardcoded constants
	result, err := r.runner.ExecContext(ctx,
		fmt.Sprintf("DELETE FROM %s WHERE issue_id = ? AND label = ?", table),
		issueID, label,
	)
	if err != nil {
		return fmt.Errorf("db: LabelSQLRepository.Delete %s/%s: %w", issueID, label, err)
	}
	rows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("db: LabelSQLRepository.Delete %s/%s: rows affected: %w", issueID, label, err)
	}
	if rows == 0 {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure issueID is populated before calling Delete
  2. Add caller-side validation of issueID
  3. Check upstream parsing/deserialization that produced the empty ID

Example fix

// before
repo.Delete(ctx, issueID, label, actor, opts) // issueID may be ""
// after
if issueID == "" { return fmt.Errorf("issueID required") }
return repo.Delete(ctx, issueID, label, actor, opts)
Defensive patterns

Strategy: validation

Validate before calling

if err := validateIssueID(issueID); err != nil { return err }
// where
func validateIssueID(id string) error {
    if id == "" { return fmt.Errorf("issueID required") }
    return nil
}

Type guard

func hasIssueID(i Issue) bool { return i.ID != "" }

Try / catch

if err := repo.Delete(ctx, issueID, label, actor, opts); err != nil {
    if strings.Contains(err.Error(), "issueID must not be empty") { return fmt.Errorf("bug: empty issueID passed to Delete") }
    return err
}

Prevention

When it happens

Trigger: Calling Delete(ctx, "", label, actor, opts) — typically from an uninitialized variable, a zero-value struct field, or a caller that skipped its own input checks.

Common situations: Deserialization failure leaving IssueID empty; calling Delete before assigning fields; wiring bug passing wrong argument position into Delete.

Related errors


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