gastownhall/beads · error · storage.ErrAlreadyExists

%w: %s

Error message

%w: %s

What it means

The sentinel duplicate error: the ID was found in the issues or wisps table, so EnsureIssueIDAvailableInTx returns fmt.Errorf("%w: %s", storage.ErrAlreadyExists, id). Callers match it with errors.Is(err, storage.ErrAlreadyExists). It is the guard's normal refusal for occupied IDs, raised after coordination serializes the create.

Source

Thrown at internal/storage/issueops/create_only_guard.go:30

// EnsureIssueIDAvailableInTx serializes same-shard creates and rejects occupied IDs.
func EnsureIssueIDAvailableInTx(ctx context.Context, tx DBTX, id string) error {
	if tx == nil {
		return fmt.Errorf("ensure issue ID available: transaction is nil")
	}
	if id == "" {
		return fmt.Errorf("ensure issue ID available: ID is empty")
	}
	key := issueCreateCoordinationKey(id)
	if _, err := tx.ExecContext(ctx, "REPLACE INTO local_metadata (`key`, value) VALUES (?, ?)", key, strconv.FormatInt(FreshRowLock(), 10)); err != nil {
		return fmt.Errorf("coordinate issue create: %w", err)
	}
	for _, table := range []string{"issues", "wisps"} {
		var count int
		if err := tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table+" WHERE id = ?", id).Scan(&count); err != nil {
			return fmt.Errorf("check %s for issue %q: %w", table, id, err)
		}
		if count > 0 {
			return fmt.Errorf("%w: %s", storage.ErrAlreadyExists, id)
		}
	}
	return nil
}

func issueCreateCoordinationKey(id string) string {
	sum := sha256.Sum256([]byte(id))
	shard := uint16(sum[0])<<4 | uint16(sum[1])>>4
	return fmt.Sprintf("issue-create/v1/%03x", shard)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Match with errors.Is(err, storage.ErrAlreadyExists) and switch to update-or-skip logic for that ID
  2. Generate a fresh unique ID for the new issue and retry
  3. Check existence up front (or prefix IDs with a per-run unique token) before batch import

Example fix

// before
err := CreateIssueInTxWithResult(ctx, tx, issue) // panics on dup at call site
// after
if err := CreateIssueInTxWithResult(ctx, tx, issue); errors.Is(err, storage.ErrAlreadyExists) {
    return skipOrCreateFreshID(issue)
}
Defensive patterns

Strategy: type-guard

Validate before calling

var count int
row := db.QueryRow("SELECT (SELECT COUNT(*) FROM issues WHERE id=?) + (SELECT COUNT(*) FROM wisps WHERE id=?)", id, id)
row.Scan(&count)
// count > 0 means the create will be refused

Type guard

func isAlreadyExists(err error) bool { return errors.Is(err, storage.ErrAlreadyExists) }

Try / catch

if err := CreateIssueInTxWithResult(ctx, tx, issue); err != nil {
    if isAlreadyExists(err) { /* skip, update, or mint a new ID */ }
    return err
}

Prevention

When it happens

Trigger: CreateIssueInTxWithResult called with an ID that already exists in either issues or wisps — including a wisp holding the same ID as a would-be regular issue (the guard checks both tables).

Common situations: Re-importing an exported file without skip-existing logic; ID collisions between regular issues and wisps; concurrent workers generating the same ID.

Related errors


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