gastownhall/beads · error · storage.ErrValidation

%w: invalid persistence mode %q

Error message

%w: invalid persistence mode %q

What it means

ValidateUpdateRequest returns this when the patch sets Persistence but the supplied mode string fails patch.Persistence.Value.IsValid(), meaning it is not one of the recognized persistence modes. The check runs pre-write so an unknown mode never reaches SQL, and the offending value is quoted in the message.

Source

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

	}
	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 {
	if rawType, ok := updates["issue_type"]; ok {
		var issueType types.IssueType
		switch value := rawType.(type) {
		case types.IssueType:

View on GitHub (pinned to 71377f2769)

Solutions

  1. Use the exported typed constants for the persistence mode instead of free strings
  2. Call IsValid() on the value before building the patch to confirm it is recognized
  3. Check the quoted value in the message against the current IsValid() implementation for the accepted set (watch case sensitivity and renames across versions)

Example fix

// before
patch.Persistence = publicops.SetField[string]{Set: true, Value: "Persistent"} // wrong case
// after
mode := storage.PersistenceEphemeral // typed constant
if !mode.IsValid() { return fmt.Errorf("bad persistence mode %q", mode) }
patch.Persistence = publicops.SetField[string]{Set: true, Value: mode}
Defensive patterns

Strategy: validation

Validate before calling

if patch.Persistence.Set && !patch.Persistence.Value.IsValid() {
  return fmt.Errorf("unknown persistence mode %q", patch.Persistence.Value)
}

Type guard

func persistenceModeOK(s string) bool { return s != "" && (types)(s).IsValid() == true } // use the actual Persistence type
// prefer: func persistenceModeOK(m Persistence) bool { return m.IsValid() }

Try / catch

if err := issueops.ExecuteUpdate(ctx, tx, req); errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "persistence mode") {
  // map value to nearest valid typed constant and retry
}

Prevention

When it happens

Trigger: ExecuteUpdate with Patch.Persistence.Set=true and a Persistence.Value that is not a valid mode (typo, wrong case, empty string, or a mode from an older API version).

Common situations: Hand-written config or JSON passing 'persistent'/'ephemeral' variants that don't match the canonical enum spelling; upgrading beads where a mode was renamed; programmatic request construction using a raw string instead of the typed constant.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


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