{"record":{"id":"03f924fab5327af7","repo":"gastownhall/beads","slug":"search-wisps-ephemeral-filter-w","errorCode":null,"errorMessage":"search wisps (ephemeral filter): %w","messagePattern":"search wisps \\(ephemeral filter\\): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/db/issue_search.go","lineNumber":43,"sourceCode":"\tissuesFilterTables = sqlbuild.IssuesFilterTables\n\twispsFilterTables  = sqlbuild.WispsFilterTables\n)\n\n// missingOptionalWispTable reports whether err is the absence of a wisp-plane\n// table a database may legitimately not have. A wisp search touches more than\n// that: its FROM clause carries sqlbuild.LeaseJoin and its hydration reads\n// wisp_labels, so a blanket table-not-exist check reports a broken database as\n// an empty wisp plane and silently drops live rows from the result.\nfunc missingOptionalWispTable(err error) bool {\n\tname, ok := dberrors.MissingTableName(err)\n\treturn ok && sqlbuild.OptionalWispTable(name)\n}\n\nfunc (r *issueSQLRepositoryImpl) searchAcrossIssuesAndWisps(ctx context.Context, query string, filter types.IssueFilter) (domain.SearchPage, error) {\n\tif filter.Ephemeral != nil && *filter.Ephemeral {\n\t\tpage, err := r.searchTable(ctx, query, filter, wispsFilterTables)\n\t\tif err != nil && !missingOptionalWispTable(err) {\n\t\t\treturn domain.SearchPage{}, fmt.Errorf(\"search wisps (ephemeral filter): %w\", err)\n\t\t}\n\t\tif len(page.Items) > 0 {\n\t\t\treturn page, nil\n\t\t}\n\t}\n\n\tif filter.SkipWisps {\n\t\tpage, err := r.searchTable(ctx, query, filter, issuesFilterTables)\n\t\tif err != nil {\n\t\t\treturn domain.SearchPage{}, fmt.Errorf(\"search issues: %w\", err)\n\t\t}\n\t\treturn page, nil\n\t}\n\n\tempty, probeErr := r.wispsTableEmptyOrMissing(ctx)\n\tif probeErr != nil {\n\t\treturn domain.SearchPage{}, fmt.Errorf(\"search wisps (merge): probe: %w\", probeErr)\n\t}","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/db/issue_search.go#L25-L61","documentation":"searchAcrossIssuesAndWisps wraps a failure from searchTable against the wisps table when the caller set filter.Ephemeral=true. When Ephemeral is set, only the wisps table is searched first; an error that missingOptionalWispTable does not classify as a legitimately-absent optional wisp table (e.g. connection failure, a missing non-optional table like the search FROM's lease join) is wrapped and returned instead of being treated as \"no wisps\".","triggerScenarios":"Calling SearchAcrossIssuesAndWisps (bd query/search) with Ephemeral=true when the wisps search fails on something other than a missing optional table: broken connection, missing leases table used in the FROM join, engine error, or a genuinely broken wisp schema.","commonSituations":"Ephemeral-only search during a Dolt outage; a database where wisps exists but wisp_labels (needed for hydration) is missing — the code's own comment warns a blanket table-not-exist tolerance would mask this as an empty result, so real breakage surfaces here.","solutions":["Read the wrapped inner error and dberrors.MissingTableName to see exactly which table or failure is involved","Run bd doctor to verify wisp-plane tables (wisps, wisp_labels) and the leases table exist","Retry if the inner error is a transient connection failure","Drop Ephemeral=true and search both planes normally if you do not strictly need ephemeral-only results"],"exampleFix":"// before\npage, err := store.Search(ctx, q, types.IssueFilter{Ephemeral: &trueVal})\n// after: search both planes unless ephemeral-only is a hard requirement\npage, err := store.Search(ctx, q, types.IssueFilter{})","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// mirror the library's own classification\nfunc isOptionalWispTableMissing(err error) bool {\n    name, ok := dberrors.MissingTableName(err)\n    return ok && sqlbuild.OptionalWispTable(name)\n}","tryCatchPattern":"page, err := store.Search(ctx, q, types.IssueFilter{Ephemeral: &t})\nif err != nil && strings.Contains(err.Error(), \"search wisps (ephemeral filter)\") {\n    if !isOptionalWispTableMissing(errors.Unwrap(err)) {\n        // real breakage: repair or fall back to a full search\n        page, err = store.Search(ctx, q, types.IssueFilter{})\n    }\n}","preventionTips":["Only set Ephemeral=true when the wisp plane is known healthy","Check schema consistency (wisps + wisp_labels + leases) after clone/restore","Classify errors with dberrors.MissingTableName instead of string matching","Fall back to a full two-plane search when the ephemeral-only path fails"],"tags":["database","wisps","search","ephemeral"],"backgroundTag":"missing-table","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}