gastownhall/beads · error · storage.ErrValidation

%w: update estimated_minutes: %w

Error message

%w: update estimated_minutes: %w

What it means

This error wraps types.ValidateIssueEstimatedMinutes failures when a patch sets EstimatedMinutes. The aggregate validator rejects invalid estimates (e.g. negative or over-limit values) before the row is updated, and the error chain carries both storage.ErrValidation and the underlying cause.

Source

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

		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)
	}
	return nil
}

// ValidateScalarUpdates checks typed scalar values before they reach SQL.
func ValidateScalarUpdates(ctx context.Context, tx DBTX, updates map[string]interface{}) error {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Clamp/reject estimates before patching: require 0 <= minutes <= the accepted maximum
  2. Parse durations into minutes explicitly (hours*60) instead of passing raw units
  3. Check the wrapped inner error to confirm which bound was violated

Example fix

// before
patch.EstimatedMinutes = publicops.SetField[int]{Set: true, Value: mins} // mins could be -5
// after
if mins < 0 || mins > maxEstimatedMinutes { return fmt.Errorf("estimate out of range: %d", mins) }
patch.EstimatedMinutes = publicops.SetField[int]{Set: true, Value: mins}
Defensive patterns

Strategy: validation

Validate before calling

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

Type guard

func estimateOK(m int) bool { return types.ValidateIssueEstimatedMinutes(m) == nil }

Try / catch

if err := issueops.ExecuteUpdate(ctx, tx, req); errors.Is(err, storage.ErrValidation) {
  // handle estimate out-of-range: clamp or prompt user
}

Prevention

When it happens

Trigger: ExecuteUpdate with Patch.EstimatedMinutes.Set=true and a value failing types.ValidateIssueEstimatedMinutes — typically a negative duration or a value above the allowed ceiling.

Common situations: Import scripts writing estimated durations from spreadsheets with negative or placeholder values (-1, 999999); UIs letting users type minutes freely without bounds; unit confusion (hours entered where minutes are expected, producing huge numbers).

Related errors


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