gastownhall/beads · error

add comment to %s: %w

Error message

add comment to %s: %w

What it means

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.

Source

Thrown at internal/storage/issueops/derivedid.go:233

func InsertDerivedComment(ctx context.Context, tx DBTX, table, issueID, author, text, createdAt string) (id string, existed bool, err error) {
	err = tx.QueryRowContext(ctx, fmt.Sprintf(`
		SELECT id FROM %s
		WHERE issue_id = ? AND author = ? AND text = ? AND created_at = ?
		ORDER BY id LIMIT 1`, table),
		issueID, author, text, createdAt).Scan(&id)
	if err == nil {
		return id, true, nil
	}
	if err != sql.ErrNoRows {
		return "", false, fmt.Errorf("check comment existence in %s: %w", table, err)
	}
	digest := rowid.Digest([]sql.NullString{str(issueID), str(author), str(text), str(createdAt)})
	id = rowid.New(table, 0, digest)
	if _, err := tx.ExecContext(ctx, fmt.Sprintf(`
		INSERT INTO %s (id, issue_id, author, text, created_at)
		VALUES (?, ?, ?, ?, ?)`, table),
		id, issueID, author, text, createdAt); err != nil {
		return "", false, fmt.Errorf("add comment to %s: %w", table, err)
	}
	return id, false, nil
}

// InsertDerivedCompactionSnapshot inserts a compaction_snapshots row under
// its content-derived id, with the same ordinal discipline as events. Two
// clones compacting the same issue at the same tier in the same second
// produce byte-identical snapshots and therefore the same id.
func InsertDerivedCompactionSnapshot(ctx context.Context, tx DBTX, issueID string, level int, snapshotJSON []byte, createdAt string) error {
	if createdAt == "" {
		createdAt = NowAuxTime()
	}
	snap := string(snapshotJSON)
	digest := rowid.Digest([]sql.NullString{
		str(issueID), str(fmt.Sprintf("%d", level)), str(snap), str(createdAt),
	})
	taken := make(map[string]bool)
	rows, err := tx.QueryContext(ctx, `

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry in a fresh transaction — the next existence probe will find the winner's row and dedup instead of inserting.
  2. Serialize comment writes per issue (avoid racing processes against the same Dolt DB).
  3. Run migrations to restore the expected comments-table schema.
  4. Inspect the wrapped error: duplicate-key implies benign race; unknown-column implies schema drift.

Example fix

// before: naive concurrent comment add
id, created, err := InsertDerivedComment(ctx, tx, table, issueID, author, text, createdAt)
// after: retry once on failure; dedup probe wins on second pass
id, created, err := InsertDerivedComment(ctx, tx, table, issueID, author, text, createdAt)
if err != nil {
	tx2 := beginFresh()
	id, created, err = InsertDerivedComment(ctx, tx2, table, issueID, author, text, createdAt)
}
Defensive patterns

Strategy: retry

Validate before calling

cols, err := tableColumns(tx, table)
if err != nil { return err }
if len(cols) != 5 {
	return fmt.Errorf("%s expects 5 columns (id, issue_id, author, text, created_at), got %d", table, len(cols))
}
if err := ctx.Err(); err != nil { return err }

Type guard

func isDuplicateKeyErr(err error) bool {
	var me *mysql.MySQLError
	return errors.As(err, &me) && me.Number == 1062
}

Try / catch

id, created, err := InsertDerivedComment(ctx, tx, table, issueID, author, text, createdAt)
if err != nil {
	if isDuplicateKeyErr(err) || isTransient(err) {
		return insertCommentFreshTx(ctx, table, issueID, author, text, createdAt)
	}
	return err
}

Prevention

When it happens

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

Common situations: Concurrent `bd comment` invocations or parallel import workers inserting the same comment; version/schema mismatch; long imports whose transactions get reaped server-side.

Related errors


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