gastownhall/beads · error · issueops.ErrValidation
%w: expected-version delete names %d issues; one row version
Error message
%w: expected-version delete names %d issues; one row version cannot describe more than one row
What it means
ValidateDeleteRequest refuses an expected-version delete that names more than one distinct issue. A single row version can only guard one row, so a conditional delete across multiple distinct ids is unsatisfiable and rejected. Duplicate mentions like ["a","a"] are legal because ids are normalized (duplicates collapse) before counting.
Source
Thrown at internal/workapi/delete.go:48
// 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
}
// NormalizeDeleteIDs collapses duplicates, keeping the caller's FIRST mention
// of each id, and trims surrounding whitespace.
//
// First-mention order rather than sorted, because it is the order the front
// doors echo back in their "issues not found" line and in the confirmation
// hint they print.
//
// It assumes a request already accepted by ValidateDeleteRequest, so no entry
// trims to empty.
func NormalizeDeleteIDs(ids []string) []string {
seen := make(map[string]bool, len(ids))
out := make([]string, 0, len(ids))View on GitHub (pinned to 71377f2769)
Solutions
- Issue one delete per issue, each with its own ExpectedVersion
- Drop ExpectedVersion for bulk deletes if unconditional removal is acceptable
- Deduplicate ids first and confirm count is 1 whenever a version is attached
Example fix
// before
req := issueops.DeleteRequest{IDs: []string{"bd-1","bd-2"}, ExpectedVersion: &ver}
api.Delete(ctx, req)
// after
for _, id := range []string{"bd-1","bd-2"} {
v := fetchVersion(ctx, id)
if err := api.Delete(ctx, issueops.DeleteRequest{IDs: []string{id}, ExpectedVersion: &v}); err != nil { return err }
} Defensive patterns
Strategy: validation
Validate before calling
if req.ExpectedVersion != nil && len(workapi.NormalizeDeleteIDs(req.IDs)) > 1 {
return fmt.Errorf("cannot attach one expected version to %d distinct ids", len(req.IDs))
} Try / catch
if err := api.Delete(ctx, req); err != nil {
if errors.Is(err, issueops.ErrValidation) && strings.Contains(err.Error(), "expected-version delete") {
return splitIntoPerIssueVersionedDeletes(ctx, req)
}
return err
} Prevention
- Keep versioned (conditional) deletes single-issue by construction
- Never reuse single-issue request structs for bulk deletes
- Deduplicate ids with NormalizeDeleteIDs before count checks
When it happens
Trigger: Calling delete with ExpectedVersion set and two or more distinct ids in IDs, e.g. DeleteRequest{IDs: ["bd-1","bd-2"], ExpectedVersion: &v}.
Common situations: Batch delete code reusing a struct that also carries an optimistic-concurrency version from a single-issue edit; copy-paste between single-delete and bulk-delete call sites.
Related errors
- %w: delete requires at least one issue id
- %w: delete id at position %d is blank
- %w: apply batch item %d guards on a row version, but an earl
- DeleteIssue: id must not be empty
- %w: bootstrap requires a non-empty issue prefix
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/fb81f6e4198e5a99.
Report an issue: GitHub.