{"record":{"id":"c3e85e5b1f5dcbaa","repo":"gastownhall/beads","slug":"count-open-wisp-children-for-s-w","errorCode":null,"errorMessage":"count open wisp children for %s: %w","messagePattern":"count open wisp children for (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":228,"sourceCode":"\n\tvar wispCount int\n\t//nolint:gosec // G201: targetColumn is validated above against two hardcoded identifiers.\n\twispQuery := fmt.Sprintf(`\n\t\tSELECT COUNT(DISTINCT dependency.issue_id)\n\t\tFROM wisp_dependencies AS dependency\n\t\tJOIN wisps AS child ON child.id = dependency.issue_id\n\t\tWHERE dependency.%s = ?\n\t\t  AND dependency.type = 'parent-child'\n\t\t  AND child.status != 'closed'\n\t\t  AND NOT EXISTS (\n\t\t\tSELECT 1 FROM dependencies AS durable WHERE durable.id = dependency.id\n\t\t  )\n\t`, targetColumn)\n\tif err := tx.QueryRowContext(ctx, wispQuery, id).Scan(&wispCount); err != nil {\n\t\tif optionalBlockedTable(\"wisp_dependencies\") && isTableNotExistError(err) {\n\t\t\treturn durableCount, nil\n\t\t}\n\t\treturn 0, fmt.Errorf(\"count open wisp children for %s: %w\", id, err)\n\t}\n\treturn durableCount + wispCount, nil\n}\n\nfunc createCloseCheckedSavepoint(ctx context.Context, tx DBTX) (string, error) {\n\tname := closeCheckedSavepointPrefix + strconv.FormatUint(closeCheckedSavepointCounter.Add(1), 10)\n\t//nolint:gosec // G201: name is a fixed identifier-safe prefix plus an atomic decimal counter.\n\tif _, err := tx.ExecContext(ctx, \"SAVEPOINT \"+name); err != nil {\n\t\treturn \"\", fmt.Errorf(\"create checked close savepoint: %w\", err)\n\t}\n\treturn name, nil\n}\n\nfunc rollbackAndReleaseCloseCheckedSavepoint(ctx context.Context, tx DBTX, name string) error {\n\trollbackErr := rollbackToCloseCheckedSavepoint(ctx, tx, name)\n\treleaseErr := releaseCloseCheckedSavepoint(ctx, tx, name)\n\treturn errors.Join(rollbackErr, releaseErr)\n}","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L210-L246","documentation":"Beads throws this when the wisp-side child count query — counting open parent-child dependencies in the optional wisp_dependencies table joined with wisps — fails with a driver error other than 'table does not exist'. If the wisp tables simply don't exist (optional feature) the count gracefully degrades to the durable count; any other SQL failure is wrapped with the parent id and aborts the close-policy check.","triggerScenarios":"Calling CloseIssue / EnforceClosePolicyInTx when wisp_dependencies exists but the query fails: connection error, lock timeout, context cancellation, permission denied, or a malformed/corrupt wisp_dependencies table (e.g. missing the NOT EXISTS-referenced dependencies.id column).","commonSituations":"Database became unavailable between the durable and wisp queries; wisp schema partially created (wisp_dependencies exists but wisps missing or vice versa); migration created wisp tables without needed columns; lock contention on wisp_dependencies.","solutions":["Read the wrapped driver error after 'count open wisp children for <id>:' for the true cause","Retry the close if transient (connection, lock timeout)","Run schema verification/migrations — wisp tables and columns must both exist and match the expected shape","If wisps are unused in your deployment, ensure the optional-table gating (sqlbuild.OptionalWispTable) is configured so missing tables are skipped rather than half-present"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Verify wisp schema is complete if you use wisps\nvar one int\nif err := db.QueryRow(\"SELECT 1 FROM wisp_dependencies LIMIT 1\").Scan(&one); err != nil && !dberrors.IsTableNotExist(err) {\n\treturn fmt.Errorf(\"wisp_dependencies unreadable: %w\", err)\n}\nif err := db.QueryRow(\"SELECT 1 FROM wisps LIMIT 1\").Scan(&one); err != nil && !dberrors.IsTableNotExist(err) {\n\treturn fmt.Errorf(\"wisps unreadable: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"count, err := countOpenChildrenForTargetInTx(ctx, tx, id, col)\nif err != nil {\n\tif dberrors.IsTableNotExist(err) {\n\t\tlog.Printf(\"wisp schema missing for %s; proceeding with durable count only\", id)\n\t} else {\n\t\treturn fmt.Errorf(\"close of %s blocked: %w\", id, err)\n\t}\n}","preventionTips":["Run migrations atomically so wisp_dependencies and wisps are created together or not at all","If you don't use wisps, leave the optional-table gating enabled rather than creating partial tables","Grant the DB user SELECT on both durable and wisp tables","Retry closes after transient connection or lock-timeout errors"],"tags":["sql","database","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"}