{"record":{"id":"9968f673110844a4","repo":"gastownhall/beads","slug":"count-wisp-only-dependent-records-w","errorCode":null,"errorMessage":"count wisp-only dependent records: %w","messagePattern":"count wisp-only dependent records: %w","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependency_queries.go","lineNumber":410,"sourceCode":"func countWispDependentsNotInDurableInTx(ctx context.Context, tx DBTX, targetID, depType string) (int, error) {\n\twispWhere := depTargetEqualsOr()\n\tdurableWhere := depTargetEqualsOr()\n\targs := []any{targetID, targetID, targetID}\n\tif depType != \"\" {\n\t\twispWhere += \" AND type = ?\"\n\t\targs = append(args, depType)\n\t}\n\targs = append(args, targetID, targetID, targetID)\n\tif depType != \"\" {\n\t\tdurableWhere += \" AND type = ?\"\n\t\targs = append(args, depType)\n\t}\n\tquery := fmt.Sprintf(\n\t\t\"SELECT COUNT(*) FROM wisp_dependencies WHERE %s AND id NOT IN (SELECT id FROM dependencies WHERE %s)\",\n\t\twispWhere, durableWhere)\n\tvar n int\n\tif err := tx.QueryRowContext(ctx, query, args...).Scan(&n); err != nil {\n\t\treturn 0, fmt.Errorf(\"count wisp-only dependent records: %w\", err)\n\t}\n\treturn n, nil\n}\n\n//nolint:gosec // G201: depTable is a hardcoded constant; targetID/depType are bound as parameters.\nfunc countDependentRecordsFromTable(ctx context.Context, tx DBTX, depTable, targetID, depType string) (int, error) {\n\tquery := fmt.Sprintf(\"SELECT COUNT(*) FROM %s WHERE %s\", depTable, depTargetEqualsOr())\n\targs := []any{targetID, targetID, targetID}\n\tif depType != \"\" {\n\t\tquery += \" AND type = ?\"\n\t\targs = append(args, depType)\n\t}\n\tvar n int\n\tif err := tx.QueryRowContext(ctx, query, args...).Scan(&n); err != nil {\n\t\treturn 0, fmt.Errorf(\"count dependent records from %s: %w\", depTable, err)\n\t}\n\treturn n, nil\n}","sourceCodeStart":392,"sourceCodeEnd":428,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependency_queries.go#L392-L428","documentation":"This error wraps the failure of the COUNT(*) query that counts wisp-only inbound dependency edges (rows in wisp_dependencies whose id is not present in the durable dependencies table for the same target). Called from CountDependentRecordsInTx via countWispDependentsNotInDurableInTx.","triggerScenarios":"Calling CountDependentRecordsInTx when wisp_dependencies or dependencies table is missing (though absence is usually tolerated upstream via isTableNotExistError), the context is canceled, or the connection fails while running the NOT IN subquery.","commonSituations":"Unmigrated database lacking wisp_dependencies; transient connection errors during count; permission issues preventing reads on either table.","solutions":["Inspect the wrapped driver error; if it is 'table not found' for wisp_dependencies, run migrations (note the caller may already treat it as optional and return the durable count).","Verify both dependencies and wisp_dependencies exist and are readable by the DB user.","Retry if the error is transient (connection/context).","Check grants for the database user on both tables.","Confirm count semantics: absence of wisp table means durable count is the whole answer."],"exampleFix":"// before: user lacks SELECT on wisp table\nGRANT SELECT ON db.wisp_dependencies TO 'beads'@'%';\n// after: grant covers both dependency tables\nGRANT SELECT ON db.* TO 'beads'@'%';","handlingStrategy":"try-catch","validationCode":"// Ensure wisp_dependencies exists and is readable before counting\nif err := tx.QueryRowContext(ctx, \"SELECT 1 FROM wisp_dependencies LIMIT 1\").Scan(&one); err != nil {\n    if !isTableNotExistError(err) { return fmt.Errorf(\"wisp count will fail: %w\", err) }\n}","typeGuard":"func wispTableReadable(ctx context.Context, tx DBTX) bool {\n    var one int\n    return tx.QueryRowContext(ctx, \"SELECT 1 FROM wisp_dependencies LIMIT 1\").Scan(&one) == nil\n}","tryCatchPattern":"n, err := CountDependentRecordsInTx(ctx, tx, targetID, depType)\nif err != nil {\n    if isTableNotExistError(err) { n = durableOnlyCount /* wisp absent: durable count is the answer */ }\n    else if isTransientNetError(err) { /* retry with backoff */ }\n    else { return err }\n}","preventionTips":["Grant SELECT on both dependency tables to the app user","Run migrations so wisp_dependencies exists before reads","Note that a missing wisp table degrades to durable-only counts by design","Watch connection stability for count-heavy workloads"],"tags":["database","sql","count-query","storage"],"backgroundTag":"sql-query-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}