{"record":{"id":"b80380b7fbfd48fe","repo":"gastownhall/beads","slug":"add-comment-to-s-w","errorCode":null,"errorMessage":"add comment to %s: %w","messagePattern":"add comment to (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/derivedid.go","lineNumber":233,"sourceCode":"func InsertDerivedComment(ctx context.Context, tx DBTX, table, issueID, author, text, createdAt string) (id string, existed bool, err error) {\n\terr = tx.QueryRowContext(ctx, fmt.Sprintf(`\n\t\tSELECT id FROM %s\n\t\tWHERE issue_id = ? AND author = ? AND text = ? AND created_at = ?\n\t\tORDER BY id LIMIT 1`, table),\n\t\tissueID, author, text, createdAt).Scan(&id)\n\tif err == nil {\n\t\treturn id, true, nil\n\t}\n\tif err != sql.ErrNoRows {\n\t\treturn \"\", false, fmt.Errorf(\"check comment existence in %s: %w\", table, err)\n\t}\n\tdigest := rowid.Digest([]sql.NullString{str(issueID), str(author), str(text), str(createdAt)})\n\tid = rowid.New(table, 0, digest)\n\tif _, err := tx.ExecContext(ctx, fmt.Sprintf(`\n\t\tINSERT INTO %s (id, issue_id, author, text, created_at)\n\t\tVALUES (?, ?, ?, ?, ?)`, table),\n\t\tid, issueID, author, text, createdAt); err != nil {\n\t\treturn \"\", false, fmt.Errorf(\"add comment to %s: %w\", table, err)\n\t}\n\treturn id, false, nil\n}\n\n// InsertDerivedCompactionSnapshot inserts a compaction_snapshots row under\n// its content-derived id, with the same ordinal discipline as events. Two\n// clones compacting the same issue at the same tier in the same second\n// produce byte-identical snapshots and therefore the same id.\nfunc InsertDerivedCompactionSnapshot(ctx context.Context, tx DBTX, issueID string, level int, snapshotJSON []byte, createdAt string) error {\n\tif createdAt == \"\" {\n\t\tcreatedAt = NowAuxTime()\n\t}\n\tsnap := string(snapshotJSON)\n\tdigest := rowid.Digest([]sql.NullString{\n\t\tstr(issueID), str(fmt.Sprintf(\"%d\", level)), str(snap), str(createdAt),\n\t})\n\ttaken := make(map[string]bool)\n\trows, err := tx.QueryContext(ctx, `","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/derivedid.go#L215-L251","documentation":"Wraps the INSERT of a new comment row (under its content-derived id) in InsertDerivedComment after the existence probe found no duplicate. Failure means the row couldn't be written: duplicate derived id, missing table/columns, constraint violation, or the transaction already aborted. Callers addIssueCommentInTx and PersistComments propagate it, failing the comment add.","triggerScenarios":"Two concurrent writers compute the same content-derived comment id; comments table schema drifted (missing author/text/created_at columns); transaction killed before the INSERT.","commonSituations":"Concurrent `bd comment` invocations or parallel import workers inserting the same comment; version/schema mismatch; long imports whose transactions get reaped server-side.","solutions":["Retry in a fresh transaction — the next existence probe will find the winner's row and dedup instead of inserting.","Serialize comment writes per issue (avoid racing processes against the same Dolt DB).","Run migrations to restore the expected comments-table schema.","Inspect the wrapped error: duplicate-key implies benign race; unknown-column implies schema drift."],"exampleFix":"// before: naive concurrent comment add\nid, created, err := InsertDerivedComment(ctx, tx, table, issueID, author, text, createdAt)\n// after: retry once on failure; dedup probe wins on second pass\nid, created, err := InsertDerivedComment(ctx, tx, table, issueID, author, text, createdAt)\nif err != nil {\n\ttx2 := beginFresh()\n\tid, created, err = InsertDerivedComment(ctx, tx2, table, issueID, author, text, createdAt)\n}","handlingStrategy":"retry","validationCode":"cols, err := tableColumns(tx, table)\nif err != nil { return err }\nif len(cols) != 5 {\n\treturn fmt.Errorf(\"%s expects 5 columns (id, issue_id, author, text, created_at), got %d\", table, len(cols))\n}\nif err := ctx.Err(); err != nil { return err }","typeGuard":"func isDuplicateKeyErr(err error) bool {\n\tvar me *mysql.MySQLError\n\treturn errors.As(err, &me) && me.Number == 1062\n}","tryCatchPattern":"id, created, err := InsertDerivedComment(ctx, tx, table, issueID, author, text, createdAt)\nif err != nil {\n\tif isDuplicateKeyErr(err) || isTransient(err) {\n\t\treturn insertCommentFreshTx(ctx, table, issueID, author, text, createdAt)\n\t}\n\treturn err\n}","preventionTips":["Serialize comment writes per issue across processes.","Retry once in a fresh transaction; the dedup probe resolves races.","Keep import transactions short-lived to avoid server-side kills.","Verify schema post-restore before running comment imports."],"tags":["sql","insert-error","storage","comments"],"backgroundTag":"sql-insert-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}