{"record":{"id":"d850fc2bbc06b682","repo":"gastownhall/beads","slug":"legacy-sqlite-comments-d-and-d-share-current-imp","errorCode":null,"errorMessage":"legacy SQLite comments %d and %d share current import identity","messagePattern":"legacy SQLite comments (.+?) and (.+?) share current import identity","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":979,"sourceCode":"\t\t\treturn err\n\t\t}\n\t\tissue := byID[issueID]\n\t\tif issue == nil {\n\t\t\treturn fmt.Errorf(\"orphan comment for %s\", issueID)\n\t\t}\n\t\tif issue.Ephemeral && len(text) > currentTextBytes {\n\t\t\treturn fmt.Errorf(\"legacy SQLite ephemeral comment text is %d bytes (current TEXT maximum %d)\", len(text), currentTextBytes)\n\t\t}\n\t\tcreated, e := parseTime(at)\n\t\tif e != nil {\n\t\t\treturn e\n\t\t}\n\t\tif created.IsZero() {\n\t\t\treturn fmt.Errorf(\"comment created_at is zero for issue %s\", issueID)\n\t\t}\n\t\tidentity := commentIdentity{issueID: issueID, author: author, text: text, createdAt: created}\n\t\tif priorID, exists := seenComments[identity]; exists {\n\t\t\treturn fmt.Errorf(\"legacy SQLite comments %d and %d share current import identity\", priorID, id)\n\t\t}\n\t\tseenComments[identity] = id\n\t\tissue.Comments = append(issue.Comments, &types.Comment{ID: strconv.FormatInt(id, 10), IssueID: issueID, Author: author, Text: text, CreatedAt: created})\n\t}\n\treturn comments.Err()\n}\n\nfunc validateDependencyGraph(issues []*types.Issue) error {\n\tscheduling := make(map[string][]string)\n\thierarchy := make(map[string][]string)\n\tvar blocking []*types.Dependency\n\tfor _, issue := range issues {\n\t\tfor _, dep := range issue.Dependencies {\n\t\t\tif dep.IssueID == dep.DependsOnID {\n\t\t\t\treturn fmt.Errorf(\"dependency %s -> %s is a self-dependency\", dep.IssueID, dep.DependsOnID)\n\t\t\t}\n\t\t\tswitch dep.Type {\n\t\t\tcase types.DepBlocks, types.DepConditionalBlocks:","sourceCodeStart":961,"sourceCodeEnd":997,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L961-L997","documentation":"The reader builds an import identity for each comment from (issueID, author, text, createdAt) and tracks previously seen identities. Two legacy comments with identical identity would collide in the current model (which keys comments on these fields), so migration aborts naming both legacy row IDs.","triggerScenarios":"Legacy SQLite comments table containing two rows with the same issue_id, author, text, and created_at (differing only in their internal row id); the reader reports \"comments N and M share current import identity\".","commonSituations":"Duplicate inserts from re-run sync scripts; legacy DB restores that doubled comment rows; old tools that appended comments without dedup checks.","solutions":["Deduplicate the comments table keeping the lowest id: DELETE FROM comments WHERE id NOT IN (SELECT MIN(id) FROM comments GROUP BY issue_id, author, text, created_at)","If the duplicates should both survive, alter one copy's text or created_at so identities differ","Verify with SELECT issue_id, author, text, created_at, COUNT(*) FROM comments GROUP BY 1,2,3,4 HAVING COUNT(*) > 1 before re-running migration"],"exampleFix":"-- before\n-- comments 42 and 57 are identical (issue, author, text, created_at)\nDELETE FROM comments WHERE id = 57; -- keep 42\n-- after: unique comment identity per issue/author/text/time","handlingStrategy":"validation","validationCode":"const seen = new Set();\nfor (const c of legacyComments) {\n  const k = `${c.issue_id}|${c.author}|${c.text}|${c.created_at}`;\n  if (seen.has(k)) throw new Error(`duplicate comment identity: ${k}`);\n  seen.add(k);\n}","typeGuard":"function commentsHaveUniqueIdentity(comments) {\n  const keys = comments.map(c => `${c.issue_id}|${c.author}|${c.text}|${c.created_at}`);\n  return new Set(keys).size === keys.length;\n}","tryCatchPattern":"try { migrateLegacySQLite(dbPath) } catch (e) {\n  if (e.message.includes('share current import identity')) {\n    dedupeCommentsKeepLowestId(dbPath); // GROUP BY issue_id, author, text, created_at\n    retry();\n  } else throw e;\n}","preventionTips":["Deduplicate comments with GROUP BY issue_id, author, text, created_at before migrating","Guard legacy comment-insert scripts against re-runs","Restore legacy DBs from a single consistent backup, not overlapping copies"],"tags":["migration","sqlite","duplicate-data"],"backgroundTag":"duplicate-comment-identity","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}