gastownhall/beads · error

classify child-count target in wisps: %w

Error message

classify child-count target in wisps: %w

What it means

Beads throws this when the second classification probe 'SELECT 1 FROM wisps WHERE id = ?' fails with something other than 'no rows'. A missing wisps table is tolerated (wisps are optional; the code falls back to depends_on_issue_id), but any other driver error aborts the close-policy classification.

Source

Thrown at internal/storage/issueops/close.go:283

// preferring a durable row when an id is present in both tables.
func dependencyTargetColumnForIDInTx(ctx context.Context, tx DBTX, id string) (string, error) {
	var found int
	err := tx.QueryRowContext(ctx, "SELECT 1 FROM issues WHERE id = ?", id).Scan(&found)
	if err == nil {
		return "depends_on_issue_id", nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return "", fmt.Errorf("classify child-count target in issues: %w", err)
	}
	err = tx.QueryRowContext(ctx, "SELECT 1 FROM wisps WHERE id = ?", id).Scan(&found)
	if err == nil {
		return "depends_on_wisp_id", nil
	}
	if optionalBlockedTable("wisps") && isTableNotExistError(err) {
		return "depends_on_issue_id", nil
	}
	if !errors.Is(err, sql.ErrNoRows) {
		return "", fmt.Errorf("classify child-count target in wisps: %w", err)
	}
	return "depends_on_issue_id", nil
}

// isClosedInTx reports whether the issue (or wisp) identified by id exists and
// is already closed. It probes the issues table then the optional wisps table,
// mirroring IsBlockedInTx's table order.
//
//nolint:gosec // G201: table is a hardcoded "issues" or "wisps".
func isClosedInTx(ctx context.Context, tx DBTX, id string) (closed bool, targetColumn string, found bool, err error) {
	for _, target := range []struct {
		table  string
		column string
	}{
		{table: "issues", column: "depends_on_issue_id"},
		{table: "wisps", column: "depends_on_wisp_id"},
	} {
		var status string

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the wrapped driver error after 'classify child-count target in wisps:'
  2. Retry the close if the failure was transient (connection, lock, timeout)
  3. Re-run wisp table migrations or drop/recreate the optional wisp schema if it is corrupt
  4. Confirm the DB user has SELECT on wisps if your deployment enables wisps
Defensive patterns

Strategy: try-catch

Validate before calling

// Optional: verify wisp schema is healthy before close operations
var one int
err := db.QueryRow("SELECT 1 FROM wisps LIMIT 1").Scan(&one)
wispBroken := err != nil && !dberrors.IsTableNotExist(err)
if wispBroken {
	return fmt.Errorf("wisps table exists but unreadable — run migrations: %w", err)
}

Try / catch

count, err := countOpenChildrenInTx(ctx, tx, id)
if err != nil {
	if dberrors.IsTableNotExist(err) {
		// wisps optional and missing is tolerated upstream; only surface other errors
		log.Printf("wisp probe failed for %s: %v", id, err)
	} else if dberrors.IsTransient(err) {
		return retryClose(ctx, id)
	} else {
		return err
	}
}

Prevention

When it happens

Trigger: countOpenChildrenInTx → dependencyTargetColumnForIDInTx when the id is absent from issues and the wisps probe fails: connection loss, lock timeout, permission error, corrupt/partial wisp schema (wisps table exists but unreadable), or context cancellation.

Common situations: Wisp tables half-created by an interrupted migration; database unavailable between the two probes; revoked privileges on wisps; context deadline during the wisps lookup.

Understand the failure class

Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.

Related errors


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