{"record":{"id":"ec228accc63392e9","repo":"gastownhall/beads","slug":"scan-wisp-dependent-w","errorCode":null,"errorMessage":"scan wisp dependent: %w","messagePattern":"scan wisp dependent: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/bulk_ops.go","lineNumber":396,"sourceCode":"\t\tif end > len(toProcess) {\n\t\t\tend = len(toProcess)\n\t\t}\n\t\tbatch := toProcess[:end]\n\t\ttoProcess = toProcess[end:]\n\n\t\tplaceholders, args := buildSQLInClause(batch)\n\t\trows, err := tx.QueryContext(ctx,\n\t\t\tfmt.Sprintf(`SELECT issue_id FROM wisp_dependencies WHERE %s IN (%s)`, DepTargetExpr, placeholders),\n\t\t\targs...)\n\t\tif err != nil {\n\t\t\treturn discovered, fmt.Errorf(\"query wisp dependents: %w\", err)\n\t\t}\n\n\t\tfor rows.Next() {\n\t\t\tvar depID string\n\t\t\tif err := rows.Scan(&depID); err != nil {\n\t\t\t\t_ = rows.Close()\n\t\t\t\treturn discovered, fmt.Errorf(\"scan wisp dependent: %w\", err)\n\t\t\t}\n\t\t\tif !seen[depID] {\n\t\t\t\tseen[depID] = true\n\t\t\t\tdiscovered[depID] = true\n\t\t\t\ttoProcess = append(toProcess, depID)\n\t\t\t}\n\t\t}\n\t\t_ = rows.Close()\n\t\tif err := rows.Err(); err != nil {\n\t\t\treturn discovered, fmt.Errorf(\"iterate wisp dependents: %w\", err)\n\t\t}\n\t}\n\n\treturn discovered, nil\n}\n\n// GetRepoMtimeInTx returns the cached mtime (nanoseconds) for a repo path.\n// Returns 0 if no cache entry exists.","sourceCodeStart":378,"sourceCodeEnd":414,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/bulk_ops.go#L378-L414","documentation":"During the recursive wisp-dependents traversal, scanning a row's issue_id into a string failed. This wraps rows.Scan errors — the row shape didn't match the expected single string column. The row set is closed and traversal aborts, returning the partial discovered set.","triggerScenarios":"Calling FindWispDependentsRecursiveInTx when the wisp_dependencies row returned has a different column type/shape than expected — e.g. schema drift after an upgrade, or DepTargetExpr resolving to a non-string column.","commonSituations":"Running new code against an old (unmigrated) wisp_dependencies table; a driver returning NULL for issue_id into a non-nullable string scan; mixed-version replicas during a rolling upgrade.","solutions":["Run pending schema migrations so wisp_dependencies matches the expected shape.","Check the wrapped error for NULL-vs-non-string mismatches and clean bad rows.","Verify all replicas/nodes run the same beads version during upgrades.","Retry after fixing the schema; the traversal is read-only so it is safe to re-run."],"exampleFix":"// before\n// schema drift: issue_id column is NULLable and contains NULLs -> scan fails\n// after\n// migrate: UPDATE wisp_dependencies SET issue_id='' WHERE issue_id IS NULL; ALTER TABLE wisp_dependencies MODIFY issue_id TEXT NOT NULL;","handlingStrategy":"validation","validationCode":"// preflight: ensure the column is present and non-NULL\nrow := db.QueryRow(\"SELECT COUNT(*) FROM wisp_dependencies WHERE issue_id IS NULL\")\nvar n int; _ = row.Scan(&n)\nif n > 0 { return fmt.Errorf(\"%d rows with NULL issue_id; run migration first\", n) }","typeGuard":null,"tryCatchPattern":"_, err := store.FindWispDependentsRecursive(ctx, tx, rootID, max)\nif err != nil && strings.Contains(err.Error(), \"scan wisp dependent:\") {\n    return fmt.Errorf(\"wisp_dependencies schema mismatch; run migrations: %w\", err)\n}","preventionTips":["Run migrations before deploying new beads versions.","Add a NULL/shape preflight check on wisp_dependencies in CI.","Keep all replicas on the same schema version."],"tags":["database","scan","schema","wisp"],"backgroundTag":"row-scan-type-mismatch","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}