gastownhall/beads · error

journal: read is_blocked for %s: %w

Error message

journal: read is_blocked for %s: %w

What it means

getJournalIssueInTx reads the persisted is_blocked flag with a direct SELECT on the candidate issue table; if that query fails (scan error, table missing, connection issue) the error is wrapped as 'journal: read is_blocked'. This is a journal-only augmentation of the normal issue snapshot.

Source

Thrown at internal/storage/issueops/journal.go:406

		labelTable string
	}{
		{"issues", "labels"},
		{"wisps", "wisp_labels"},
	} {
		issue, err := getIssueFromTableInTx(ctx, tx, candidate.issueTable, candidate.labelTable, issueID)
		if errors.Is(err, storage.ErrNotFound) {
			continue
		}
		if err != nil {
			if optionalBlockedTable(candidate.issueTable) && isTableNotExistError(err) {
				continue
			}
			return nil, err
		}
		var blocked int
		//nolint:gosec // candidate.issueTable is one of the two hardcoded values above.
		if err := tx.QueryRowContext(ctx, fmt.Sprintf("SELECT is_blocked FROM %s WHERE id = ?", candidate.issueTable), issueID).Scan(&blocked); err != nil {
			return nil, fmt.Errorf("journal: read is_blocked for %s: %w", issueID, err)
		}
		issue.IsBlocked = blocked != 0
		return issue, nil
	}
	return nil, fmt.Errorf("%w: issue %s", storage.ErrNotFound, issueID)
}

// insertEventRow performs the actual INSERT. It is the ONE seam both write
// plumbings funnel through, so the seq mechanism cannot drift between them. A
// nil issue is stored as SQL NULL (deletes); a nil dep is stored as SQL NULL
// (non-dependency ops). ts is the insert time, stamped inside the committing
// transaction. actor is stored as-is — "" for the genuinely unattributable
// paths — in the NOT NULL DEFAULT ” actor column.
func insertEventRow(ctx context.Context, tx DBTX, op EventOp, issueID string, issue *types.Issue, dep *EventDep, comment *EventComment, actor string) error {
	var issueJSON any
	if issue != nil {
		b, err := json.Marshal(issue)
		if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run schema migrations (bd migrate / bd doctor) so is_blocked exists on the issue tables
  2. Check DB connectivity and retry the operation
  3. Inspect the wrapped %w error (sql.ErrNoRows means the issue vanished concurrently)
  4. Restore from backup if the issues table is corrupted

Example fix

// before
ops.RecordEventInTx(ctx, tx, op, id, nil, nil) // fails: old schema lacks is_blocked
// after
if err := migrate.Run(ctx, db); err != nil { return err } // bring schema current
ops.RecordEventInTx(ctx, tx, op, id, nil, nil)
Defensive patterns

Strategy: validation

Validate before calling

// ensure schema current before writes
if err := migrate.Run(ctx, db); err != nil { return err }
rows, _ := db.Query("SHOW COLUMNS FROM issues LIKE 'is_blocked'")
if !rows.Next() { return errors.New("is_blocked column missing; migrate first") }

Try / catch

if err := doJournalWrite(ctx, tx); err != nil {
    if strings.Contains(err.Error(), "read is_blocked") {
        return fmt.Errorf("schema/connection problem: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: The is_blocked SELECT fails: issues/deleted_issues table missing or renamed, row deleted between snapshot and probe, driver-level SQL error (lock timeout, connection drop), or Scan type mismatch.

Common situations: Schema drift after upgrading beads versions where is_blocked column was added; running against an older/pre-migration database; transient Dolt/driver connection failures mid-transaction.

Related errors


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