gastownhall/beads · error

legacy SQLite comments %d and %d share current import identi

Error message

legacy SQLite comments %d and %d share current import identity

What it means

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.

Source

Thrown at internal/migration/legacysqlite/reader.go:979

			return err
		}
		issue := byID[issueID]
		if issue == nil {
			return fmt.Errorf("orphan comment for %s", issueID)
		}
		if issue.Ephemeral && len(text) > currentTextBytes {
			return fmt.Errorf("legacy SQLite ephemeral comment text is %d bytes (current TEXT maximum %d)", len(text), currentTextBytes)
		}
		created, e := parseTime(at)
		if e != nil {
			return e
		}
		if created.IsZero() {
			return fmt.Errorf("comment created_at is zero for issue %s", issueID)
		}
		identity := commentIdentity{issueID: issueID, author: author, text: text, createdAt: created}
		if priorID, exists := seenComments[identity]; exists {
			return fmt.Errorf("legacy SQLite comments %d and %d share current import identity", priorID, id)
		}
		seenComments[identity] = id
		issue.Comments = append(issue.Comments, &types.Comment{ID: strconv.FormatInt(id, 10), IssueID: issueID, Author: author, Text: text, CreatedAt: created})
	}
	return comments.Err()
}

func validateDependencyGraph(issues []*types.Issue) error {
	scheduling := make(map[string][]string)
	hierarchy := make(map[string][]string)
	var blocking []*types.Dependency
	for _, issue := range issues {
		for _, dep := range issue.Dependencies {
			if dep.IssueID == dep.DependsOnID {
				return fmt.Errorf("dependency %s -> %s is a self-dependency", dep.IssueID, dep.DependsOnID)
			}
			switch dep.Type {
			case types.DepBlocks, types.DepConditionalBlocks:

View on GitHub (pinned to 71377f2769)

Solutions

  1. 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)
  2. If the duplicates should both survive, alter one copy's text or created_at so identities differ
  3. 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

Example fix

-- before
-- comments 42 and 57 are identical (issue, author, text, created_at)
DELETE FROM comments WHERE id = 57; -- keep 42
-- after: unique comment identity per issue/author/text/time
Defensive patterns

Strategy: validation

Validate before calling

const seen = new Set();
for (const c of legacyComments) {
  const k = `${c.issue_id}|${c.author}|${c.text}|${c.created_at}`;
  if (seen.has(k)) throw new Error(`duplicate comment identity: ${k}`);
  seen.add(k);
}

Type guard

function commentsHaveUniqueIdentity(comments) {
  const keys = comments.map(c => `${c.issue_id}|${c.author}|${c.text}|${c.created_at}`);
  return new Set(keys).size === keys.length;
}

Try / catch

try { migrateLegacySQLite(dbPath) } catch (e) {
  if (e.message.includes('share current import identity')) {
    dedupeCommentsKeepLowestId(dbPath); // GROUP BY issue_id, author, text, created_at
    retry();
  } else throw e;
}

Prevention

When it happens

Trigger: 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".

Common situations: Duplicate inserts from re-run sync scripts; legacy DB restores that doubled comment rows; old tools that appended comments without dedup checks.

Related errors


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