{"record":{"id":"7ffdb68d3049df50","repo":"gastownhall/beads","slug":"classify-child-count-target-in-wisps-w","errorCode":null,"errorMessage":"classify child-count target in wisps: %w","messagePattern":"classify child-count target in wisps: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":283,"sourceCode":"// preferring a durable row when an id is present in both tables.\nfunc dependencyTargetColumnForIDInTx(ctx context.Context, tx DBTX, id string) (string, error) {\n\tvar found int\n\terr := tx.QueryRowContext(ctx, \"SELECT 1 FROM issues WHERE id = ?\", id).Scan(&found)\n\tif err == nil {\n\t\treturn \"depends_on_issue_id\", nil\n\t}\n\tif !errors.Is(err, sql.ErrNoRows) {\n\t\treturn \"\", fmt.Errorf(\"classify child-count target in issues: %w\", err)\n\t}\n\terr = tx.QueryRowContext(ctx, \"SELECT 1 FROM wisps WHERE id = ?\", id).Scan(&found)\n\tif err == nil {\n\t\treturn \"depends_on_wisp_id\", nil\n\t}\n\tif optionalBlockedTable(\"wisps\") && isTableNotExistError(err) {\n\t\treturn \"depends_on_issue_id\", nil\n\t}\n\tif !errors.Is(err, sql.ErrNoRows) {\n\t\treturn \"\", fmt.Errorf(\"classify child-count target in wisps: %w\", err)\n\t}\n\treturn \"depends_on_issue_id\", nil\n}\n\n// isClosedInTx reports whether the issue (or wisp) identified by id exists and\n// is already closed. It probes the issues table then the optional wisps table,\n// mirroring IsBlockedInTx's table order.\n//\n//nolint:gosec // G201: table is a hardcoded \"issues\" or \"wisps\".\nfunc isClosedInTx(ctx context.Context, tx DBTX, id string) (closed bool, targetColumn string, found bool, err error) {\n\tfor _, target := range []struct {\n\t\ttable  string\n\t\tcolumn string\n\t}{\n\t\t{table: \"issues\", column: \"depends_on_issue_id\"},\n\t\t{table: \"wisps\", column: \"depends_on_wisp_id\"},\n\t} {\n\t\tvar status string","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L265-L301","documentation":"Beads throws this when the second classification probe 'SELECT 1 FROM wisps WHERE id = ?' fails with something other than 'no rows'. A missing wisps table is tolerated (wisps are optional; the code falls back to depends_on_issue_id), but any other driver error aborts the close-policy classification.","triggerScenarios":"countOpenChildrenInTx → dependencyTargetColumnForIDInTx when the id is absent from issues and the wisps probe fails: connection loss, lock timeout, permission error, corrupt/partial wisp schema (wisps table exists but unreadable), or context cancellation.","commonSituations":"Wisp tables half-created by an interrupted migration; database unavailable between the two probes; revoked privileges on wisps; context deadline during the wisps lookup.","solutions":["Inspect the wrapped driver error after 'classify child-count target in wisps:'","Retry the close if the failure was transient (connection, lock, timeout)","Re-run wisp table migrations or drop/recreate the optional wisp schema if it is corrupt","Confirm the DB user has SELECT on wisps if your deployment enables wisps"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Optional: verify wisp schema is healthy before close operations\nvar one int\nerr := db.QueryRow(\"SELECT 1 FROM wisps LIMIT 1\").Scan(&one)\nwispBroken := err != nil && !dberrors.IsTableNotExist(err)\nif wispBroken {\n\treturn fmt.Errorf(\"wisps table exists but unreadable — run migrations: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"count, err := countOpenChildrenInTx(ctx, tx, id)\nif err != nil {\n\tif dberrors.IsTableNotExist(err) {\n\t\t// wisps optional and missing is tolerated upstream; only surface other errors\n\t\tlog.Printf(\"wisp probe failed for %s: %v\", id, err)\n\t} else if dberrors.IsTransient(err) {\n\t\treturn retryClose(ctx, id)\n\t} else {\n\t\treturn err\n\t}\n}","preventionTips":["Create wisps and wisp_dependencies in one atomic migration; never half-provision","If wisps are disabled, ensure the optional-table gate (sqlbuild.OptionalWispTable) matches your deployment","Grant SELECT on wisps to the application DB user when wisps are enabled","Retry close operations after transient database errors"],"tags":["sql","wisps","optional-table"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}