gastownhall/beads · error · issueops.ErrValidation

%w: delete requires at least one issue id

Error message

%w: delete requires at least one issue id

What it means

ValidateDeleteRequest wraps issueops.ErrValidation when the delete request contains no issue ids. A delete with an empty ID list is ambiguous (delete nothing? everything?), so the library refuses it up front before any storage work. This check runs in the shared validator every backend implementation uses before opening a transaction.

Source

Thrown at internal/workapi/delete.go:35

//
// What is NOT here is the deletion. The existence probe, the guard and the
// erasure need one transaction (issueops.Deleter.Delete); the bodies live in
// internal/storage/issueops/delete.go and in the unit-of-work provider.

// ValidateDeleteRequest applies the request rules every Deleter implementation
// shares, before anything is read.
//
// There is deliberately no require-a-filter analog of the sweep gate here: a
// delete request carries no predicate at all, so a caller cannot spell
// "everything" without typing every id. The guard that does matter — dependents
// outside the request — needs the graph and therefore lives in the bodies.
//
// The ExpectedVersion arity rule is here rather than in the bodies for the
// reason the rest of this file exists: it needs no database, and a rule the
// bodies each spelled themselves is a rule that can differ per backend.
func ValidateDeleteRequest(in issueops.DeleteRequest) error {
	if len(in.IDs) == 0 {
		return fmt.Errorf("%w: delete requires at least one issue id", issueops.ErrValidation)
	}
	for i, id := range in.IDs {
		if strings.TrimSpace(id) == "" {
			return fmt.Errorf("%w: delete id at position %d is blank", issueops.ErrValidation, i)
		}
	}
	// DISTINCT ids, not mentions: DeleteRequest.IDs promises duplicates
	// collapse, so an IDs of {"a", "a"} carrying a version names ONE row and is
	// legal. Counting the raw slice here would refuse the request the role's own
	// normalization rule says is fine.
	if in.ExpectedVersion != nil {
		if distinct := len(NormalizeDeleteIDs(in.IDs)); distinct > 1 {
			return fmt.Errorf("%w: expected-version delete names %d issues; one row version cannot describe more than one row",
				issueops.ErrValidation, distinct)
		}
	}
	return nil
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Ensure at least one issue id is present before calling delete; check len(req.IDs) > 0
  2. If the source list may legitimately be empty, skip the delete call or return a no-op in your code
  3. Confirm your id-collection query actually returned rows

Example fix

// before
return api.Delete(ctx, issueops.DeleteRequest{IDs: ids})
// after
if len(ids) == 0 { return nil }
return api.Delete(ctx, issueops.DeleteRequest{IDs: ids})
Defensive patterns

Strategy: validation

Validate before calling

if len(req.IDs) == 0 { return nil } // nothing to delete
if err := workapi.ValidateDeleteRequest(req); err != nil { return err }

Try / catch

if err := api.Delete(ctx, req); err != nil {
    if errors.Is(err, issueops.ErrValidation) { return fmt.Errorf("delete request invalid: %w", err) }
    return err
}

Prevention

When it happens

Trigger: Calling a delete API with issueops.DeleteRequest{IDs: nil} or IDs: []string{}; building the ID slice from a filtered list that ended up empty.

Common situations: Script computing IDs via a query that matched nothing; forgetting to append parsed ids; passing a request struct literal without IDs.

Related errors


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