gastownhall/beads · error · storage.ErrValidation

%w: update priority: %w

Error message

%w: update priority: %w

What it means

ValidateUpdateRequest wraps types.ValidateIssuePriority failures under this message when the patch sets Priority. Validation runs before any SQL executes so an invalid priority cannot partially apply, and the wrapped chain preserves both storage.ErrValidation and the specific priority error.

Source

Thrown at internal/storage/issueops/aggregate.go:87

// ValidateUpdateRequest checks mutually exclusive guarded-update options and
// the canonical field values every backend must reject identically. Backends
// call it before touching the row so an invalid patch cannot half-apply.
func ValidateUpdateRequest(request publicops.UpdateRequest) error {
	if request.Claim && (request.ExpectedAssignee != nil || request.ExpectedStatus != nil) {
		return fmt.Errorf("%w: claim cannot use expected assignee or status", storage.ErrValidation)
	}
	if request.ForceAssigneeTransfer && (request.Claim || !request.Patch.Assignee.Set || request.ExpectedAssignee != nil) {
		return fmt.Errorf("%w: invalid forced assignee transfer", storage.ErrValidation)
	}
	patch := request.Patch
	if patch.Title.Set {
		if err := types.ValidateIssueTitle(patch.Title.Value); err != nil {
			return fmt.Errorf("%w: update title: %w", storage.ErrValidation, err)
		}
	}
	if patch.Priority.Set {
		if err := types.ValidateIssuePriority(patch.Priority.Value); err != nil {
			return fmt.Errorf("%w: update priority: %w", storage.ErrValidation, err)
		}
	}
	if patch.EstimatedMinutes.Set {
		if err := types.ValidateIssueEstimatedMinutes(patch.EstimatedMinutes.Value); err != nil {
			return fmt.Errorf("%w: update estimated_minutes: %w", storage.ErrValidation, err)
		}
	}
	if patch.Persistence.Set && !patch.Persistence.Value.IsValid() {
		return fmt.Errorf("%w: invalid persistence mode %q", storage.ErrValidation, patch.Persistence.Value)
	}
	return nil
}

// ValidateMetadataPatch checks mutually exclusive metadata edits.
func ValidateMetadataPatch(patch publicops.MetadataPatch) error {
	if patch.Replace.Set && (patch.Merge.Set || len(patch.Set) > 0 || len(patch.Unset) > 0) {
		return fmt.Errorf("%w: cannot combine metadata replacement with incremental metadata edits", storage.ErrValidation)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Validate/normalize the priority against the accepted range before building the patch
  2. Read the wrapped inner error for the exact rule violated
  3. Coerce string inputs through a whitelist parser into the numeric priority type

Example fix

// before
patch.Priority = publicops.SetField[int]{Set: true, Value: p} // p from raw user input
// after
p, err := strconv.Atoi(rawPriority)
if err != nil || p < 0 || p > 4 { return fmt.Errorf("priority must be 0-4, got %q", rawPriority) }
patch.Priority = publicops.SetField[int]{Set: true, Value: p}
Defensive patterns

Strategy: validation

Validate before calling

if patch.Priority.Set {
  if err := types.ValidateIssuePriority(patch.Priority.Value); err != nil {
    return fmt.Errorf("pre-check priority: %w", err)
  }
}

Type guard

func priorityOK(p int) bool { return types.ValidateIssuePriority(p) == nil }

Try / catch

if err := issueops.ExecuteUpdate(ctx, tx, req); errors.Is(err, storage.ErrValidation) {
  // inspect wrapped priority error, correct value, retry once
}

Prevention

When it happens

Trigger: ExecuteUpdate with Patch.Priority.Set=true and a value that fails types.ValidateIssuePriority — usually a priority outside the accepted range or an unparsable numeric/string value.

Common situations: Parsing priority from CLI flags or JSON where '0' or blank slips through as a sentinel; APIs accepting free-text priorities ('urgent', 'high') instead of the numeric scale; off-by-one values beyond the valid P0–P4 range.

Related errors


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