gastownhall/beads · error

descendants: hydrate wisps: %w

Error message

descendants: hydrate wisps: %w

What it means

GetDescendants wraps a failure from fetchIssuesByIDs when hydrating wisp rows matched by the descendants walk. The wisps plane is optional, so a table-not-exist error is deliberately tolerated (wispsByID stays nil); only other failures — connection drops, scan errors, hydration failures on wisp_labels/wisp_dependencies — are wrapped and returned as "descendants: hydrate wisps".

Source

Thrown at internal/storage/domain/db/issue_descendants.go:97

	rows, err := r.runner.QueryContext(ctx, cte, allArgs...)
	if err != nil {
		return nil, fmt.Errorf("descendants: query: %w", err)
	}
	page, err := scanIDSrcPage(rows)
	if err != nil {
		return nil, fmt.Errorf("descendants: %w", err)
	}

	issuesByID, err := r.fetchIssuesByIDs(ctx, page.issueIDs, issuesFilterTables, filter)
	if err != nil {
		return nil, fmt.Errorf("descendants: hydrate issues: %w", err)
	}

	var wispsByID map[string]*types.Issue
	if len(page.wispIDs) > 0 {
		wispsByID, err = r.fetchIssuesByIDs(ctx, page.wispIDs, wispsFilterTables, filter)
		if err != nil && !dberrors.IsTableNotExist(err) {
			return nil, fmt.Errorf("descendants: hydrate wisps: %w", err)
		}
	}

	return reassembleBySrc(page.ordered, issuesByID, wispsByID), nil
}

// buildDescendantsCTE walks parent-child edges AND the dotted-ID fallback the
// classic ParentID filter applies (issueops/filters.go): a row named
// <node>.<suffix> with no parent-child edge at all is a child of <node>.
// Rows carry a via marker: 'e' for edge-found, 'd' for dotted-found. Dotted
// recursion only fires from 'e' rows — a dotted node's own dotted
// descendants share its prefix and were already matched by whichever LIKE
// found it, so re-expanding them would only multiply duplicate rows.
// Filter predicates are hoisted into named matches CTEs (see predBundle) to
// dodge a dolt 2.1.6 analyzer bug.
func buildDescendantsCTE(rootID string, walkWisps bool, issuePred, wispPred predBundle) (string, []any) {
	var b strings.Builder
	var args []any

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the wrapped inner error; if it is a table-not-exist for an optional wisp table it would have been swallowed, so this indicates a different failure
  2. Run bd doctor to check wisp-plane table consistency (wisps, wisp_labels, wisp_dependencies)
  3. Retry on transient connection errors
  4. If the wisp plane is unusable and unwanted, re-run with SkipWisps=true to bypass wisp hydration entirely

Example fix

// before
issues, err := repo.GetDescendants(ctx, root, filter)
// after: bypass a broken optional wisp plane when only durable issues are needed
issues, err := repo.GetDescendants(ctx, root, types.IssueFilter{SkipWisps: true})
Defensive patterns

Strategy: fallback

Type guard

func isWispPlaneBroken(err error) bool {
    return err != nil && strings.Contains(err.Error(), "hydrate wisps") && !dberrors.IsTableNotExist(err)
}

Try / catch

issues, err := repo.GetDescendants(ctx, rootID, filter)
if isWispPlaneBroken(err) {
    // degrade gracefully: durable issues only
    issues, err = repo.GetDescendants(ctx, rootID, types.IssueFilter{SkipWisps: true})
}

Prevention

When it happens

Trigger: Calling GetDescendants on a store where wisps exist (the walk found wisp IDs) but hydrating them fails for a reason other than the wisps/wisp_labels/wisp_dependencies tables being absent: e.g. wisp_labels is missing while wisps is present, or the connection died mid-fetch.

Common situations: Partially migrated databases where wisps exists but wisp_labels/wisp_dependencies do not — wait, the latter are tolerated only via MissingTableName on any table; a schema where wisps exists but its companion tables were dropped inconsistently; transient connection failure during hydration.

Related errors


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