gastownhall/beads · error

record event in %s: %w

Error message

record event in %s: %w

What it means

Wraps the INSERT that records the derived event row under its content-derived id in InsertDerivedEventReturningID. ExecContext failure here means the row could not be written — duplicate id (content collision with different disposition), constraint violation, missing table, or transaction already aborted. Callers AddCommentEventInTx and InsertDerivedEvent propagate it.

Source

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

		var id string
		if err := rows.Scan(&id); err != nil {
			_ = rows.Close()
			return "", fmt.Errorf("scan same-content events in %s: %w", table, err)
		}
		taken[id] = true
	}
	_ = rows.Close()
	if err := rows.Err(); err != nil {
		return "", fmt.Errorf("scan same-content events in %s: %w", table, err)
	}

	id := firstFreeDerivedID(table, digest, taken)
	if _, err := tx.ExecContext(ctx, fmt.Sprintf(`
		INSERT INTO %s (id, issue_id, event_type, actor, old_value, new_value, comment, created_at)
		VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, table),
		id,
		e.IssueID, string(e.EventType), e.Actor, e.OldValue, e.NewValue, e.Comment, e.CreatedAt); err != nil {
		return "", fmt.Errorf("record event in %s: %w", table, err)
	}
	return id, nil
}

// NextLiveCommentTime returns the created_at to stamp on a comment being added
// live (as opposed to imported), given the wall-clock instant the caller
// observed. The result is always truncated to whole seconds — the created_at
// column's DATETIME(0) precision — and is advanced to one second past the
// issue's newest existing comment when that comment is at or after `now`.
//
// Why: comments read back in (created_at ASC, id ASC) order, created_at holds
// whole seconds, and since bd-ri8bd a comment's id is a content digest rather
// than a time-ordered UUIDv7. Two comments added to one issue inside the same
// wall-clock second therefore tie on the primary sort key and then order by
// hash — arbitrarily with respect to the order they were written. Keeping
// (issue_id, created_at) unique on the live path is what restores insertion
// order for the reader without putting ordering information into the id, which
// content-derivation cannot carry.

View on GitHub (pinned to 71377f2769)

Solutions

  1. Retry the operation in a fresh transaction — if the conflicting id now exists, the dedup SELECT will see it and pick the next free ordinal.
  2. Serialize concurrent writers on the same issue (bd's locking) instead of racing inserts from multiple processes.
  3. Verify the table exists with the expected 8 columns; run migrations if not.
  4. Inspect the wrapped driver error to distinguish PK-duplicate from connection/abort causes.

Example fix

// before: racing inserts collide on derived id
if _, err := tx.ExecContext(ctx, insertSQL, id, ...); err != nil {
	return "", fmt.Errorf("record event in %s: %w", table, err)
}
// after: re-read taken set and pick next free id on duplicate-key
if _, err := tx.ExecContext(ctx, insertSQL, id, ...); err != nil {
	if isDuplicateKey(err) {
		taken[id] = true
		id = firstFreeDerivedID(table, digest, taken)
		_, err = tx.ExecContext(ctx, insertSQL, id, ...)
	}
	if err != nil { return "", fmt.Errorf("record event in %s: %w", table, err) }
}
Defensive patterns

Strategy: retry

Validate before calling

cols, err := tableColumns(tx, table)
if err != nil { return err }
if len(cols) != 8 {
	return fmt.Errorf("%s expects 8 columns, found %d; migrate", 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, err := InsertDerivedEventReturningID(ctx, tx, table, e)
if err != nil {
	if isDuplicateKeyErr(err) {
		// benign race: retry in a fresh tx; dedup SELECT will see the row
		return insertInFreshTx(ctx, table, e)
	}
	return err
}

Prevention

When it happens

Trigger: Two events compute the same derived id via firstFreeDerivedID but race within conflicting transactions; the events table lacks expected columns; the transaction was already rolled back by an earlier error; UNIQUE/PK violation on id.

Common situations: Concurrent writers inserting identical comments through separate processes against the same Dolt DB; schema drift; long-running transactions that get killed server-side before the INSERT.

Related errors


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