gastownhall/beads · error

search %s: hydrate: %w

Error message

search %s: hydrate: %w

What it means

Raised when hydrateIssues fails after the core search rows were fetched successfully. Hydration fetches labels (and optionally dependency records) for the page of issues from their side tables; any error there is wrapped as "search <table>: hydrate: <err>". The search itself worked, but assembling full Issue objects failed.

Source

Thrown at internal/storage/domain/db/issue_search.go:289

		if seen[issue.ID] {
			continue
		}
		seen[issue.ID] = true
		issues = append(issues, issue)
	}
	_ = rows.Close()
	if err := rows.Err(); err != nil {
		return domain.SearchPage{}, fmt.Errorf("search %s: rows: %w", tables.Main, err)
	}

	sortRowsGoSide(issues, func(i *types.Issue) string { return i.ID }, filter.SortBy, filter.SortDesc)
	items, hasMore, err := finishWindow(issues, window)
	if err != nil {
		return domain.SearchPage{}, err
	}

	if err := r.hydrateIssues(ctx, items, tables, filter.IncludeDependencies, filter.SkipLabels); err != nil {
		return domain.SearchPage{}, fmt.Errorf("search %s: hydrate: %w", tables.Main, err)
	}

	return domain.SearchPage{Items: items, HasMore: hasMore}, nil
}

func (r *issueSQLRepositoryImpl) scanFilterIDs(ctx context.Context, selectKw, fromSQL, whereSQL string, args []any, filter types.IssueFilter, tables filterTables) ([]string, bool, error) {
	orderBy := orderBySQL(filter.SortBy, filter.SortDesc, tables.Main)
	window := searchWindowForFilter(filter)
	//nolint:gosec // G201: SQL fragments from fixed table names and parameterized filters.
	idQuery := fmt.Sprintf(`%s%s.id FROM %s %s %s %s`,
		selectKw, tables.Main, fromSQL, whereSQL, orderBy, window.sql)

	rows, err := r.runner.QueryContext(ctx, idQuery, args...)
	if err != nil {
		return nil, false, fmt.Errorf("search %s (id scan): %w", tables.Main, err)
	}
	defer func() { _ = rows.Close() }()

View on GitHub (pinned to 71377f2769)

Solutions

  1. Unwrap to see which sub-fetch (labels vs dependencies) failed and why.
  2. Run `bd doctor` and migrations to ensure labels/dependencies tables exist and match the schema.
  3. Clear DB locks by stopping other bd processes, then retry the search.
  4. If a wisp side table is legitimately absent, set SkipWisps / Ephemeral filters to search the issues plane only.

Example fix

// before
filter := types.IssueFilter{} // hits wisps plane, missing wisp_labels table
// after
filter := types.IssueFilter{SkipWisps: true} // search issues plane only
Defensive patterns

Strategy: validation

Validate before calling

// confirm side tables exist before dependency-inclusive or wisp searches
if err := bd.CheckTables(dbPath, "issues", "labels", "dependencies"); err != nil {
	return fmt.Errorf("run `bd migrate`: %w", err)
}

Type guard

func isHydrateError(err error) bool {
	return err != nil && strings.Contains(err.Error(), ": hydrate: ")
}

Try / catch

page, err := store.Search(ctx, q, filter)
if err != nil && isHydrateError(err) {
	if name, ok := dberrors.MissingTableName(err); ok {
		// fall back to a plain search without hydration-dependent filters
		filter.IncludeDependencies = false
		filter.SkipWisps = true
		page, err = store.Search(ctx, q, filter)
	}
}

Prevention

When it happens

Trigger: searchTable succeeds on the main query but getLabelsFromTable or getDependencyRecordsFromTable fails for the hydrated IDs: labels/dependencies/wisp_labels table missing, locked DB, or a mid-hydration connection loss.

Common situations: Older database missing the labels or dependencies table; corrupt side tables after a manual edit; concurrent bd process holding a write lock so secondary reads stall/timeout; ephemeral wisp searches against a DB missing optional wisp side tables (though those are usually tolerated as missing-optional).

Related errors


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