gastownhall/beads · error · storage.ErrValidation

%w: count edges id %d is empty

Error message

%w: count edges id %d is empty

What it means

ValidateEdgeCountRequest rejects an EdgeCountRequest whose IDs slice contains an empty string at position %d. An empty ID entry names no issue, so the library refuses rather than returning a misleading entry in the per-anchor result. It wraps storage.ErrValidation; an entirely empty IDs slice is legal and just yields an empty answer.

Source

Thrown at internal/storage/issueops/edge_counts.go:51

// per-entry checks that follow tell a caller's mistake from a legitimately
// empty answer, exactly as ValidateEdgeReadRequest's do.
func ValidateEdgeCountRequest(request publicops.EdgeCountRequest) error {
	switch request.Direction {
	case publicops.EdgeDirectionIn, publicops.EdgeDirectionOut:
	case "":
		return fmt.Errorf("%w: count edges requires a direction (%q or %q)",
			storage.ErrValidation, publicops.EdgeDirectionOut, publicops.EdgeDirectionIn)
	default:
		return fmt.Errorf("%w: count edges direction %q is not %q or %q",
			storage.ErrValidation, request.Direction, publicops.EdgeDirectionOut, publicops.EdgeDirectionIn)
	}
	if request.Status != "" && request.Direction != publicops.EdgeDirectionIn {
		return fmt.Errorf("%w: count edges status %q needs direction %q: an outbound edge's far end may be a row this database does not hold",
			storage.ErrValidation, request.Status, publicops.EdgeDirectionIn)
	}
	for i, id := range request.IDs {
		if id == "" {
			return fmt.Errorf("%w: count edges id %d is empty", storage.ErrValidation, i)
		}
	}
	for i, depType := range request.Types {
		if !depType.IsValid() {
			return fmt.Errorf("%w: count edges type %d is not a usable dependency type (non-empty, max %d chars)",
				storage.ErrValidation, i, types.MaxDependencyTypeLen)
		}
	}
	return nil
}

// FinishEdgeCount assembles the per-anchor answer from the two things every
// implementation reads: which anchors exist, and the edge tallies keyed by
// anchor.
//
// It is a pure function beside the body for the reason the checklist gives: the
// parts that decide what the answer MEANS are pinned in milliseconds without a
// database, and the conformance contract is left to assert what only a real

View on GitHub (pinned to 71377f2769)

Solutions

  1. Remove empty strings from request.IDs before calling (filter with a small loop or slices.DeleteFunc).
  2. Fix the producer: use strings.Fields / a filtering split instead of strings.Split for whitespace-delimited ID lists.
  3. Validate user input at the CLI boundary and reject blank arguments early.
  4. Skip empty entries explicitly if callers may legitimately pass sparse input.

Example fix

// before
ids := strings.Split(flagValue, ",") // may contain ""
req := publicops.EdgeCountRequest{IDs: ids, Direction: "in"}
// after
ids := slices.Collect(strings.Fields(flagValue)) // or filter out ""
req := publicops.EdgeCountRequest{IDs: ids, Direction: "in"}
Defensive patterns

Strategy: validation

Validate before calling

func hasEmptyID(ids []string) bool { return slices.Contains(ids, "") }

Type guard

func nonEmptyIDs(ids []string) []string {
	return slices.DeleteFunc(slices.Clone(ids), func(s string) bool { return s == "" })
}

Try / catch

if err := ExecuteEdgeCount(ctx, tx, req); err != nil {
	if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "is empty") {
		// rebuild IDs from trimmed input and retry once
	}
}

Prevention

When it happens

Trigger: Passing EdgeCountRequest{IDs: []string{"bd-1", ""}} to CountEdges / ExecuteEdgeCount; building the IDs slice from parsed CLI args or JSON where an empty token or blank field slips in.

Common situations: Splitting a comma-separated ID list with strings.Split and getting a trailing/empty element; a JSON array containing ""; a form or script variable left unset; map iteration producing an empty key.

Related errors


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