{"record":{"id":"bfbccc8b03e8a8f2","repo":"gastownhall/beads","slug":"w-s-bfbccc","errorCode":null,"errorMessage":"%w: %s","messagePattern":"%w: %s","errorType":"exception","errorClass":"storage.ErrAlreadyExists","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/create.go","lineNumber":684,"sourceCode":"// an empty notes field must not wipe local notes), but its aux data\n// (labels/comments/deps, which never bump updated_at) still merges\n// additively (bd-hj85c).\n//\n//nolint:gosec // G201: table is a hardcoded constant\nfunc InsertIssueIfNew(ctx context.Context, tx DBTX, issueTable string, issue *types.Issue, opts storage.BatchCreateOptions) (isNew bool, staleRejected bool, err error) {\n\tvar existingCount int\n\tif issue.ID != \"\" {\n\t\tif err := tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE id = ?`, issueTable), issue.ID).Scan(&existingCount); err != nil {\n\t\t\treturn false, false, fmt.Errorf(\"failed to check issue existence for %s: %w\", issue.ID, err)\n\t\t}\n\t}\n\tif opts.ConflictSkip && existingCount > 0 {\n\t\treturn false, false, nil // issue already exists — skip, never overwrite\n\t}\n\tif opts.CreateOnly {\n\t\tif err := insertIssueCreateOnly(ctx, tx, issueTable, issue); err != nil {\n\t\t\tif isCreateOnlyDuplicateError(err) {\n\t\t\t\treturn false, false, fmt.Errorf(\"%w: %s\", storage.ErrAlreadyExists, issue.ID)\n\t\t\t}\n\t\t\treturn false, false, err\n\t\t}\n\t\treturn true, false, nil\n\t}\n\tif opts.RejectStaleUpserts && existingCount > 0 {\n\t\tvar storedNewer int\n\t\tif err := tx.QueryRowContext(ctx, fmt.Sprintf(`SELECT COUNT(*) FROM %s WHERE id = ? AND updated_at > ?`, issueTable), issue.ID, issue.UpdatedAt).Scan(&storedNewer); err != nil {\n\t\t\treturn false, false, fmt.Errorf(\"failed to check issue staleness for %s: %w\", issue.ID, err)\n\t\t}\n\t\tif storedNewer > 0 {\n\t\t\t// The conditional ODKU would keep every stored column anyway;\n\t\t\t// skipping the no-op insert makes the rejection observable.\n\t\t\treturn false, true, nil\n\t\t}\n\t}\n\tif err := insertIssueIntoTable(ctx, tx, issueTable, issue, opts.RejectStaleUpserts); err != nil {\n\t\treturn false, false, fmt.Errorf(\"failed to insert issue %s: %w\", issue.ID, err)","sourceCodeStart":666,"sourceCodeEnd":702,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/create.go#L666-L702","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nopts := issueops.CreateOpts{CreateOnly: true} // fails if ID exists\n_, err := issueops.CreateIssueInTxWithResult(ctx, tx, issue, opts)\n// after\nif errors.Is(err, storage.ErrAlreadyExists) { return nil } // or:\nopts := issueops.CreateOpts{ConflictSkip: true} // skip instead of error","handlingStrategy":"try-catch","validationCode":"var existing int\nif err := tx.QueryRowContext(ctx,\n    \"SELECT COUNT(*) FROM issues WHERE id = ?\", issue.ID).Scan(&existing); err != nil {\n    return err\n}\nif existing > 0 && opts.CreateOnly {\n    return nil // already imported; skip\n}","typeGuard":"func isAlreadyExists(err error) bool {\n    return errors.Is(err, storage.ErrAlreadyExists)\n}","tryCatchPattern":"created, _, err := issueops.InsertIssueIfNew(ctx, tx, table, issue, opts)\nif errors.Is(err, storage.ErrAlreadyExists) {\n    log.Infow(\"issue already present, skipping\", \"id\", issue.ID)\n    return nil\n}\nif err != nil { return err }","preventionTips":["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."],"tags":["storage","duplicate","create-only"],"backgroundTag":"already-exists","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}