gastownhall/beads · error

db: LabelSQLRepository.Insert %s/%s: rows affected: %w

Error message

db: LabelSQLRepository.Insert %s/%s: rows affected: %w

What it means

Thrown when result.RowsAffected() fails after the INSERT IGNORE in LabelSQLRepository.Insert. The driver failed to report how many rows the insert touched, so the repository cannot tell whether the label was actually added and cannot safely journal the EventLabelAdded event.

Source

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

	// 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()
	if err != nil {
		return fmt.Errorf("db: LabelSQLRepository.Insert %s/%s: rows affected: %w", issueID, label, err)
	}
	if rows == 0 {
		issueTable := "issues"
		if opts.UseWispsTable {
			issueTable = "wisps"
		}
		var count int
		//nolint:gosec // G201: issueTable is one of two hardcoded constants.
		if err := r.runner.QueryRowContext(ctx, fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE id = ?", issueTable), issueID).Scan(&count); err != nil {
			return fmt.Errorf("db: LabelSQLRepository.Insert %s/%s: verify issue: %w", issueID, label, err)
		}
		if count == 0 {
			return fmt.Errorf("db: LabelSQLRepository.Insert %s/%s: issue does not exist", issueID, label)
		}
		return nil
	}
	if err := r.events.Record(ctx, domain.Event{
		IssueID:  issueID,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the insert; transient connection drops often recover
  2. Check DB connectivity and reconnect the pool
  3. Verify the Dolt driver version is current
  4. If in tests, ensure the mock result implements RowsAffected

Example fix

// before
rows, err := result.RowsAffected()
if err != nil { return fmt.Errorf("... rows affected: %w", err) }
// after
db := sqlx.NewDb(conn, "mysql"); db.SetConnMaxLifetime(30 * time.Second) // avoid stale connections
rows, err := result.RowsAffected()
Defensive patterns

Strategy: retry

Validate before calling

if err := db.PingContext(ctx); err != nil { return fmt.Errorf("db unavailable: %w", err) }

Try / catch

err := repo.Insert(ctx, issueID, label, opts)
for i := 0; i < 3 && err != nil; i++ {
    time.Sleep(backoff(i))
    err = repo.Insert(ctx, issueID, label, opts)
}

Prevention

When it happens

Trigger: Calling Insert against a driver/connection that errors on RowsAffected (e.g. broken connection returned before metadata, or a driver wrapper that does not implement RowsAffected for INSERT IGNORE results).

Common situations: Stale pooled connection that died between ExecContext and RowsAffected; mismatched or buggy driver version; result object from a mock/test double lacking RowsAffected support.

Related errors


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