gastownhall/beads · error

scan dependency source: %w

Error message

scan dependency source: %w

What it means

This error wraps a rows.Scan failure while reading a dependency source row (id plus the three nullable target columns) in rekeyDependencySourceInTx. Scan fails when the stored value's type does not match the scan destination (e.g. non-string id, corrupt NULL in a NOT NULL column) or the column count mismatches the schema.

Source

Thrown at internal/storage/issueops/dependencies.go:652

// in case a caller reaches here before the cascade) and re-asserts issue_id = newID
// so the row converges either way. Only rows whose id is actually stale are touched.
func rekeyDependencySourceInTx(ctx context.Context, tx *sql.Tx, oldID, newID string) error {
	queryRows, err := tx.QueryContext(ctx, `
		SELECT id, depends_on_issue_id, depends_on_wisp_id, depends_on_external
		FROM dependencies
		WHERE issue_id = ? OR issue_id = ?
	`, newID, oldID)
	if err != nil {
		return fmt.Errorf("query dependency sources: %w", err)
	}
	type rekey struct{ oldRowID, newRowID string }
	var rekeys []rekey
	for queryRows.Next() {
		var id string
		var issueTarget, wispTarget, external sql.NullString
		if err := queryRows.Scan(&id, &issueTarget, &wispTarget, &external); err != nil {
			_ = queryRows.Close()
			return fmt.Errorf("scan dependency source: %w", err)
		}
		target, ok := resolveDependencyTarget(issueTarget, wispTarget, external)
		if !ok {
			continue // ck_dep_one_target guarantees one target; skip defensively
		}
		if want := depid.New(newID, target); want != id {
			rekeys = append(rekeys, rekey{oldRowID: id, newRowID: want})
		}
	}
	_ = queryRows.Close()
	if err := queryRows.Err(); err != nil {
		return fmt.Errorf("iterate dependency sources: %w", err)
	}
	for _, rk := range rekeys {
		if _, err := tx.ExecContext(ctx,
			"UPDATE dependencies SET id = ?, issue_id = ? WHERE id = ?",
			rk.newRowID, newID, rk.oldRowID); err != nil {
			return fmt.Errorf("rekey dependency source id %s -> %s: %w", rk.oldRowID, rk.newRowID, err)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped scan error to see which column/type mismatched
  2. Run bd doctor to detect and repair schema/data inconsistencies
  3. Dump and inspect the offending dependencies row: SELECT * FROM dependencies WHERE issue_id IN (oldID, newID)
  4. Restore the row from a backup or delete/recreate the malformed dependency edge

Example fix

// before: corrupt row breaks rename
default_params: id column holds an integer where a string is expected -> scan dependency source: sql: Scan error
// after: repair the row
UPDATE dependencies SET id = CAST(id AS CHAR) WHERE id REGEXP '^[0-9]+$';  -- or restore from backup
Defensive patterns

Strategy: validation

Validate before calling

// Detect rows that cannot be scanned into the expected types
var bad int
err := db.Get(&bad, `SELECT COUNT(*) FROM dependencies
  WHERE id IS NULL OR id = ''
     OR (depends_on_issue_id IS NULL AND depends_on_wisp_id IS NULL AND depends_on_external IS NULL)`)
if err != nil { return err }
if bad > 0 { return fmt.Errorf("%d malformed dependency rows; run bd doctor before renaming issues", bad) }

Prevention

When it happens

Trigger: During UpdateIssueIDInTx, a row in dependencies matching issue_id = oldID OR issue_id = newID has a value that cannot be scanned into string or sql.NullString — typically corrupt or schema-divergent data.

Common situations: Database written by an incompatible older/newer beads version; manual SQL edits that changed column types; Dolt replication divergence producing malformed rows.

Related errors


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