gastownhall/beads · error

db: ChildCounterSQLRepository.NextChildID: scan: %w

Error message

db: ChildCounterSQLRepository.NextChildID: scan: %w

What it means

NextChildID scans each child-issue ID returned by the query over `issues` (or `wisps`) to find the highest numeric suffix under the parent. This error wraps any database/sql rows.Scan failure while reading one ID column. It means a row could not be decoded into a Go string, which is a driver/row-decode failure rather than a missing-parent problem.

Source

Thrown at internal/storage/domain/db/child_counter.go:65

	case errors.Is(err, sql.ErrNoRows):
		lastChild = 0
	default:
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: read counter for %s: %w", parentID, err)
	}

	rows, err := r.runner.QueryContext(ctx, fmt.Sprintf(`
		SELECT id FROM %s
		WHERE id LIKE CONCAT(?, '.%%')
		  AND id NOT LIKE CONCAT(?, '.%%.%%')
	`, issueTable), parentID, parentID) //nolint:gosec // G201: issueTable is one of two hardcoded constants
	if err != nil {
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: scan existing children of %s: %w", parentID, err)
	}
	defer rows.Close()
	for rows.Next() {
		var id string
		if err := rows.Scan(&id); err != nil {
			return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: scan: %w", err)
		}
		if n, ok := parseChildSuffix(id); ok && n > lastChild {
			lastChild = n
		}
	}
	if err := rows.Err(); err != nil {
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: rows: %w", err)
	}

	next := lastChild + 1
	//nolint:gosec // G201: counterTable is one of two hardcoded constants
	if _, err := r.runner.ExecContext(ctx, fmt.Sprintf(`
		INSERT INTO %s (parent_id, last_child) VALUES (?, ?)
		ON DUPLICATE KEY UPDATE last_child = ?
	`, counterTable), parentID, next, next); err != nil {
		return "", fmt.Errorf("db: ChildCounterSQLRepository.NextChildID: upsert counter for %s: %w", parentID, err)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Check the `issues`/`wisps` table for rows with NULL or non-text `id` values and repair them (id is expected to be a NOT NULL text primary key).
  2. Verify the database driver version matches what beads expects (Dolt-compatible driver); upgrade/downgrade the driver.
  3. Retry the call; if transient connection errors appear, check server connectivity and timeouts.
  4. Inspect the wrapped cause with errors.Unwrap to see the underlying driver error.

Example fix

// before: rows contain NULL id, Scan into string fails
var id string
rows.Scan(&id)

// after: tolerate NULL ids defensively (or repair the schema to NOT NULL)
var id sql.NullString
if err := rows.Scan(&id); err != nil { return "", err }
if id.Valid { /* process id.String */ }
Defensive patterns

Strategy: try-catch

Validate before calling

// Go has no pre-call validation for row decoding; ensure schema integrity instead:
// SELECT COUNT(*) FROM issues WHERE id IS NULL  -- must be 0

Type guard

func isScanError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "ChildCounterSQLRepository.NextChildID: scan:")
}

Try / catch

childID, err := repo.NextChildID(ctx, parentID, opts)
if err != nil {
    if isScanError(err) {
        // inspect/repair corrupted id row, then retry
        return fmt.Errorf("corrupt child id row for %s: %w", parentID, err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling ChildCounterSQLRepository.NextChildID(ctx, parentID, opts) when an `id` value in the result set cannot be scanned into a string (e.g. NULL id in the table, driver type conversion failure, connection dropped mid-iteration).

Common situations: Corrupted or manually edited rows where `issues.id` is NULL; driver mismatch after switching Dolt/MySQL driver versions with stricter Scan conversion; network interruption while iterating rows.

Related errors


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