gastownhall/beads · error · storage.ErrAlreadyExists
%w: %s
Error message
%w: %s
What it means
InsertIssueIfNew wraps storage.ErrAlreadyExists with the issue ID when CreateOnly mode is enabled and a row with the same ID already exists in the target table. CreateOnly means 'insert only, never update', so an existing row is an error rather than an upsert. Note that when ConflictSkip (not CreateOnly) is set, the same situation silently returns instead of erroring.
Source
Thrown at internal/storage/issueops/create.go:684
// an empty notes field must not wipe local notes), but its aux data
// (labels/comments/deps, which never bump updated_at) still merges
// additively (bd-hj85c).
//
//nolint:gosec // G201: table is a hardcoded constant
func InsertIssueIfNew(ctx context.Context, tx DBTX, issueTable string, issue *types.Issue, opts storage.BatchCreateOptions) (isNew bool, staleRejected bool, err error) {
var existingCount int
if issue.ID != "" {
if err := tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE id = ?`, issueTable), issue.ID).Scan(&existingCount); err != nil {
return false, false, fmt.Errorf("failed to check issue existence for %s: %w", issue.ID, err)
}
}
if opts.ConflictSkip && existingCount > 0 {
return false, false, nil // issue already exists — skip, never overwrite
}
if opts.CreateOnly {
if err := insertIssueCreateOnly(ctx, tx, issueTable, issue); err != nil {
if isCreateOnlyDuplicateError(err) {
return false, false, fmt.Errorf("%w: %s", storage.ErrAlreadyExists, issue.ID)
}
return false, false, err
}
return true, false, nil
}
if opts.RejectStaleUpserts && existingCount > 0 {
var storedNewer int
if err := tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE id = ? AND updated_at > ?`, issueTable), issue.ID, issue.UpdatedAt).Scan(&storedNewer); err != nil {
return false, false, fmt.Errorf("failed to check issue staleness for %s: %w", issue.ID, err)
}
if storedNewer > 0 {
// The conditional ODKU would keep every stored column anyway;
// skipping the no-op insert makes the rejection observable.
return false, true, nil
}
}
if err := insertIssueIntoTable(ctx, tx, issueTable, issue, opts.RejectStaleUpserts); err != nil {
return false, false, fmt.Errorf("failed to insert issue %s: %w", issue.ID, err)View on GitHub (pinned to 71377f2769)
Solutions
- Check existence first (or use errors.Is(err, storage.ErrAlreadyExists)) and skip/merge instead of re-inserting.
- Set opts.ConflictSkip instead of CreateOnly if 'skip when present' is the intended behavior.
- Drop CreateOnly if an upsert (update existing) is acceptable.
- Assign a fresh ID if the new record is genuinely distinct from the existing one.
Example fix
// before
opts := issueops.CreateOpts{CreateOnly: true} // fails if ID exists
_, err := issueops.CreateIssueInTxWithResult(ctx, tx, issue, opts)
// after
if errors.Is(err, storage.ErrAlreadyExists) { return nil } // or:
opts := issueops.CreateOpts{ConflictSkip: true} // skip instead of error Defensive patterns
Strategy: try-catch
Validate before calling
var existing int
if err := tx.QueryRowContext(ctx,
"SELECT COUNT(*) FROM issues WHERE id = ?", issue.ID).Scan(&existing); err != nil {
return err
}
if existing > 0 && opts.CreateOnly {
return nil // already imported; skip
} Type guard
func isAlreadyExists(err error) bool {
return errors.Is(err, storage.ErrAlreadyExists)
} Try / catch
created, _, err := issueops.InsertIssueIfNew(ctx, tx, table, issue, opts)
if errors.Is(err, storage.ErrAlreadyExists) {
log.Infow("issue already present, skipping", "id", issue.ID)
return nil
}
if err != nil { return err } Prevention
- Use ConflictSkip when 'skip if present' is the intent; reserve CreateOnly for strict idempotent imports.
- Make imports idempotent: detect ErrAlreadyExists and continue.
- Avoid concurrent writers inserting the same explicit IDs.
- Track imported IDs client-side to skip replays cheaply.
When it happens
Trigger: Calling CreateIssueInTxWithResult or PromoteFromEphemeralInTx with opts.CreateOnly=true while an issue with the same ID already exists in the issues (or wisp) table; e.g. re-running an import in create-only mode, or a promote racing another writer that already inserted the ID.
Common situations: Replaying an import JSONL twice; concurrent agents creating the same auto-generated ID; retrying a partially committed transaction where the row already landed; upgrading tooling that switched from upsert to create-only semantics.
Related errors
- canonical issue not found: %s
- failed to add duplicate link: %w
- failed to close duplicate: %w
- %w: %w
- no store is open for this workspace
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/bfbccc8b03e8a8f2.
Report an issue: GitHub.