gastownhall/beads · error

journal: scan row: %w

Error message

journal: scan row: %w

What it means

While iterating journal rows, rows.Scan into EventsJournalRow (Seq, TS, Op, IssueID, Actor, and three nullable JSON columns) failed. This wraps column-count or type-conversion mismatches between the live table schema and what the scanner expects. TS is normalized afterwards, and NULL JSON columns are collapsed to empty strings.

Source

Thrown at internal/storage/issueops/journal_prune.go:276

	if limit > 0 {
		q += " LIMIT " + strconv.Itoa(limit)
	}
	rows, err := tx.QueryContext(ctx, q, since)
	if err != nil {
		return nil, fmt.Errorf("journal: read since %d: %w", since, err)
	}
	defer rows.Close()

	var out []storage.EventsJournalRow
	for rows.Next() {
		var (
			r         storage.EventsJournalRow
			issueJS   sql.NullString
			depJS     sql.NullString
			commentJS sql.NullString
		)
		if err := rows.Scan(&r.Seq, &r.TS, &r.Op, &r.IssueID, &r.Actor, &issueJS, &depJS, &commentJS); err != nil {
			return nil, fmt.Errorf("journal: scan row: %w", err)
		}
		r.TS = normalizeEventsTimestamp(r.TS)
		r.IssueJSON = issueJS.String
		r.DepJSON = depJS.String
		r.CommentJSON = commentJS.String
		out = append(out, r)
	}
	return out, rows.Err()
}

// normalizeEventsTimestamp emits the journal boundary's stable RFC3339Nano UTC
// contract. Dolt/MySQL stringify DATETIME with a space rather than a `T`, and a
// driver may or may not append an offset; a consumer parsing the record needs
// one parsable UTC timestamp regardless of which shape the backend produced.
func normalizeEventsTimestamp(raw string) string {
	for _, layout := range []string{
		time.RFC3339Nano,
		"2006-01-02 15:04:05.999999999Z07:00",

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run `bd doctor` / migrations to reconcile bd_events_journal schema with the expected projection.
  2. Compare the table's columns against the SELECT in readEventsRowsInTx; fix drift with an explicit ALTER or restore from a matching-version backup.
  3. Find the offending row (scan page-by-page around the failure) and NULL/repair out-of-range seq or op values.
  4. Upgrade/downgrade the Dolt/MySQL driver to a version matching the app's expectations.

Example fix

// before
ALTER TABLE bd_events_journal DROP COLUMN actor; -- schema drift
// after
ALTER TABLE bd_events_journal ADD COLUMN actor VARCHAR(255) NOT NULL DEFAULT ''; -- restore expected column
Defensive patterns

Strategy: validation

Validate before calling

cols, _ := db.Query("SHOW COLUMNS FROM bd_events_journal")
want := map[string]bool{"seq": true, "ts": true, "op": true, "issue_id": true, "actor": true, "issue_json": true, "dep_json": true, "comment_json": true}
for cols.Next() { var c string; var t, null, key, extra interface{}; _ = cols.Scan(&c, &t, &null, &key, &extra); if !want[c] { return fmt.Errorf("schema drift: unexpected column %s", c) }; delete(want, c) }
if len(want) > 0 { return fmt.Errorf("schema drift: missing columns %v", want) }

Try / catch

if err != nil && strings.Contains(err.Error(), "journal: scan row") {
    return fmt.Errorf("bd_events_journal schema drifted; run migrations or restore matching schema: %w", err)
}

Prevention

When it happens

Trigger: ReadEventsInTx / ReadEventsPageInTx hitting a row whose shape differs from the SELECT projection: schema drift (extra/missing/reordered columns after a manual ALTER or partial migration), a non-integer or NULL value in a NOT NULL-expected column (seq, op), or a driver returning an unexpected type for ts.

Common situations: Manually altered bd_events_journal; restoring a table from an older export with fewer columns; a driver version returning DATETIME/timestamp in a type the scanner rejects.

Related errors


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