gastownhall/beads · error

wake expired defers: scan %s row: %w

Error message

wake expired defers: scan %s row: %w

What it means

Thrown when rows.Scan fails while reading an id from the expired-defers result set. Scan errors here mean the driver returned a row whose single column could not be decoded into a string (NULL id, driver type mismatch, or rows already invalidated). The rows cursor is closed before returning to avoid leaking the result set.

Source

Thrown at internal/storage/issueops/wake_defers.go:93

	// Snapshot first so each genuinely-woken row gets its own event. The
	// UPDATE below repeats the whole predicate, so a row rescued between the
	// SELECT and its UPDATE (re-deferred further out, claimed, closed) matches
	// nothing and is skipped rather than clobbered.
	//nolint:gosec // G201: table is a hardcoded constant from the caller above.
	rows, err := tx.QueryContext(ctx, fmt.Sprintf(`
		SELECT id FROM %s
		WHERE status = 'deferred' AND defer_until IS NOT NULL
		  AND defer_until <= UTC_TIMESTAMP()
	`, table))
	if err != nil {
		return nil, fmt.Errorf("wake expired defers: scan %s: %w", table, err)
	}
	var expired []string
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			_ = rows.Close()
			return nil, fmt.Errorf("wake expired defers: scan %s row: %w", table, err)
		}
		expired = append(expired, id)
	}
	if err := rows.Err(); err != nil {
		_ = rows.Close()
		return nil, fmt.Errorf("wake expired defers: iterate %s: %w", table, err)
	}
	if err := rows.Close(); err != nil {
		return nil, fmt.Errorf("wake expired defers: close %s rows: %w", table, err)
	}
	if len(expired) == 0 {
		return nil, nil
	}

	var woken []string
	now := time.Now().UTC()
	for _, id := range expired {
		// row_lock is rewritten so a concurrent claim/update conflicts at

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped driver error and the offending row; fix or delete the row with the malformed id.
  2. Ensure the id column is NOT NULL with a string-compatible type (VARCHAR/CHAR).
  3. Update the database driver to a version with correct type-conversion handling.
  4. Re-run bd doctor / storage integrity checks on the affected table.

Example fix

// before: nullable id allows NULL rows
id VARCHAR(255) NULL
// after
id VARCHAR(255) NOT NULL PRIMARY KEY
Defensive patterns

Strategy: validation

Validate before calling

rows, err := db.Query("SELECT COUNT(*) FROM issues WHERE id IS NULL")
if err != nil { return err }
defer rows.Close()
var n int
rows.Scan(&n)
if n > 0 { return fmt.Errorf("%d issues rows have NULL id; repair before waking defers", n) }

Try / catch

if err != nil {
    if strings.Contains(err.Error(), "converting NULL to string") {
        return fmt.Errorf("corrupt row with NULL id in %s: repair table", table)
    }
    return err
}

Prevention

When it happens

Trigger: A row in the table has a NULL or non-string id value, or the driver returns an incompatible column type for id, during the WakeExpiredDefersInTx scan loop.

Common situations: Corrupted or manually-edited rows where id is NULL; using a driver that returns []byte or custom types without proper conversion support (e.g. Dolt/MySQL drivers with odd column charset declarations).

Related errors


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