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 realView on GitHub (pinned to 71377f2769)
Solutions
- Remove empty strings from request.IDs before calling (filter with a small loop or slices.DeleteFunc).
- Fix the producer: use strings.Fields / a filtering split instead of strings.Split for whitespace-delimited ID lists.
- Validate user input at the CLI boundary and reject blank arguments early.
- 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
- Trim and filter ID inputs at the CLI/API boundary
- Use strings.Fields instead of strings.Split for user-supplied lists
- Validate before calling; the error position tells you the index but fixing input upstream is cleaner
- Deduplicate and normalize IDs in one shared helper
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
- db: Exists: id must not be empty
- list dep metadata: sourceID must not be empty
- iter dep metadata: sourceID must not be empty
- count by id: sourceID must not be empty
- reopen: id must not be empty
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/db0e2cbb203b0186.
Report an issue: GitHub.