gastownhall/beads · error

db: MovePersistence %s: get issue: %w

Error message

db: MovePersistence %s: get issue: %w

What it means

Wraps failure to fetch the issue row during MovePersistence, which migrates an issue between ephemeral and persistent storage. The error comes from issueops.GetIssueInTx; this wrapper adds the issue ID and the 'get issue' stage for diagnosis. No issue found or any select failure lands here.

Source

Thrown at internal/storage/domain/db/issue.go:126

// PromoteFromEphemeral promotes an active wisp into the Dolt-versioned issues
// plane in place: same id, wisp_type retained, labels/dependencies/events/
// comments carried across to the permanent tables, inbound wisp-targeted
// dependency edges retargeted, and blocked state recomputed. It delegates to
// the exact issueops implementation the classic (direct/embedded) route runs,
// so the two modes cannot drift. The issueops error is returned unwrapped on
// purpose: the CLI surfaces it verbatim ("wisp <id> not found"), and that
// text is part of the classic error contract.
func (r *issueSQLRepositoryImpl) PromoteFromEphemeral(ctx context.Context, id, actor string) error {
	if id == "" {
		return errors.New("db: PromoteFromEphemeral: id must not be empty")
	}
	return issueops.PromoteFromEphemeralInTx(ctx, r.runner, id, actor)
}

func (r *issueSQLRepositoryImpl) MovePersistence(ctx context.Context, id string, mode types.PersistenceMode, actor string) (bool, error) {
	issue, err := issueops.GetIssueInTx(ctx, r.runner, id)
	if err != nil {
		return false, fmt.Errorf("db: MovePersistence %s: get issue: %w", id, err)
	}
	result, err := issueops.MoveIssuePersistenceInTx(ctx, r.runner, issue, mode, actor)
	if err != nil {
		return false, fmt.Errorf("db: MovePersistence %s: %w", id, err)
	}
	return result.Changed, nil
}

func (r *issueSQLRepositoryImpl) Update(ctx context.Context, id string, updates map[string]any, actor string, opts domain.IssueTableOpts) error {
	if id == "" {
		return errors.New("db: Update: id must not be empty")
	}
	if len(updates) == 0 {
		return nil
	}
	updates = cloneUpdateFields(updates)
	// Pop the close-policy override before anything reads the map as a set of
	// columns, mirroring issueops.updateIssueInTx. The no-op filter below keeps

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify the issue ID exists (bd show <id>) before moving persistence
  2. Check the wrapped error: sql.ErrNoRows means the ID is absent, other errors are infrastructure
  3. Retry on transient connection errors
  4. If IDs come from user input, validate the format before the call

Example fix

// before
changed, err := repo.MovePersistence(ctx, id, mode, actor)
// after
if _, err := repo.Get(ctx, id, domain.IssueTableOpts{}); err != nil {
    return fmt.Errorf("issue %s not found: %w", id, err)
}
changed, err := repo.MovePersistence(ctx, id, mode, actor)
Defensive patterns

Strategy: validation

Validate before calling

if id == "" { return fmt.Errorf("issue ID required") }
if !strings.Contains(id, "-") { return fmt.Errorf("malformed issue ID %q", id) }
if _, err := repo.Get(ctx, id, opts); err != nil { return fmt.Errorf("issue %s not found", id) }

Type guard

func isNotFoundErr(err error) bool { return errors.Is(err, sql.ErrNoRows) }

Try / catch

changed, err := repo.MovePersistence(ctx, id, mode, actor)
if err != nil {
    if isNotFoundErr(err) { return ErrIssueNotFound }
    return fmt.Errorf("move persistence for %s: %w", id, err)
}

Prevention

When it happens

Trigger: Calling MovePersistence with an ID that doesn't exist (sql.ErrNoRows propagated through GetIssueInTx), a corrupted row that fails scanning, or a transaction/connection error during the select.

Common situations: Caller passes an issue ID deleted by another process mid-workflow; ID typo or wrong-prefix ID; database unreachable mid-transaction.

Related errors


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