gastownhall/beads · error

count open children: unsupported target column %q

Error message

count open children: unsupported target column %q

What it means

Beads throws this when a close-policy child count is requested against a parent-dependency column other than the two supported ones: depends_on_issue_id (durable issues) or depends_on_wisp_id (wisps). The column is interpolated into the COUNT query, so it is strictly validated against these two hardcoded identifiers before use. This is an internal invariant guard, not a user-facing validation error.

Source

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

		return 0, err
	}
	if !found {
		return 0, fmt.Errorf("%w: issue %s", storage.ErrNotFound, id)
	}
	return enforceClosePolicyForTargetInTx(ctx, tx, id, targetColumn, force, closed)
}

func countOpenChildrenInTx(ctx context.Context, tx DBTX, id string) (int, error) {
	targetColumn, err := dependencyTargetColumnForIDInTx(ctx, tx, id)
	if err != nil {
		return 0, err
	}
	return countOpenChildrenForTargetInTx(ctx, tx, id, targetColumn)
}

func countOpenChildrenForTargetInTx(ctx context.Context, tx DBTX, id, targetColumn string) (int, error) {
	if targetColumn != "depends_on_issue_id" && targetColumn != "depends_on_wisp_id" {
		return 0, fmt.Errorf("count open children: unsupported target column %q", targetColumn)
	}
	var durableCount int
	//nolint:gosec // G201: targetColumn is validated above against two hardcoded identifiers.
	durableQuery := fmt.Sprintf(`
		SELECT COUNT(DISTINCT dependency.issue_id)
		FROM dependencies AS dependency
		JOIN issues AS child ON child.id = dependency.issue_id
		WHERE dependency.%s = ?
		  AND dependency.type = 'parent-child'
		  AND child.status != 'closed'
	`, targetColumn)
	if err := tx.QueryRowContext(ctx, durableQuery, id).Scan(&durableCount); err != nil {
		return 0, fmt.Errorf("count open durable children for %s: %w", id, err)
	}

	var wispCount int
	//nolint:gosec // G201: targetColumn is validated above against two hardcoded identifiers.
	wispQuery := fmt.Sprintf(`

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure the value passed as targetColumn comes only from dependencyTargetColumnForIDInTx or the hardcoded literals "depends_on_issue_id"/"depends_on_wisp_id"
  2. Check for empty-string columns caused by an early-return or error path in the classifier being swallowed upstream
  3. If you added a new dependency column, extend the guard in internal/storage/issueops/close.go:194 to whitelist it (and audit the fmt.Sprintf query for injection safety)

Example fix

// before
err := countOpenChildrenForTargetInTx(ctx, tx, id, "parent_id") // panics into guard
// after
col := "depends_on_issue_id"
if isWispID(id) { col = "depends_on_wisp_id" }
err := countOpenChildrenForTargetInTx(ctx, tx, id, col)
Defensive patterns

Strategy: validation

Validate before calling

func validTargetColumn(col string) bool {
	return col == "depends_on_issue_id" || col == "depends_on_wisp_id"
}
if !validTargetColumn(targetColumn) {
	return fmt.Errorf("refusing to count children with column %q", targetColumn)
}

Type guard

func isDependencyTargetColumn(s string) bool {
	return s == "depends_on_issue_id" || s == "depends_on_wisp_id"
}

Prevention

When it happens

Trigger: Calling countOpenChildrenForTargetInTx (or countOpenChildrenInTx / enforceClosePolicyForTargetInTx above it) with a targetColumn string other than 'depends_on_issue_id' or 'depends_on_wisp_id' — i.e. a caller-produced column name from a classifier that returned an unexpected value.

Common situations: A fork or local patch added a third dependency table/column and routed it through the close policy; a classifier function was modified to return an empty string or a renamed column; tests injecting a bogus column value to assert the guard fires.

Related errors


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