{"record":{"id":"5e6c630eff412542","repo":"gastownhall/beads","slug":"get-issue-comments-scan-w","errorCode":null,"errorMessage":"get issue comments: scan: %w","messagePattern":"get issue comments: scan: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/comments.go","lineNumber":39,"sourceCode":"\t\ttable = \"wisp_comments\"\n\t}\n\n\trows, err := tx.QueryContext(ctx, fmt.Sprintf(`\n\t\tSELECT id, issue_id, author, text, created_at\n\t\tFROM %s\n\t\tWHERE issue_id = ?\n\t\tORDER BY created_at ASC, id ASC\n\t`, table), issueID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"get issue comments from %s: %w\", table, err)\n\t}\n\tdefer rows.Close()\n\n\tvar comments []*types.Comment\n\tfor rows.Next() {\n\t\tvar c types.Comment\n\t\tif err := rows.Scan(&c.ID, &c.IssueID, &c.Author, &c.Text, &c.CreatedAt); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"get issue comments: scan: %w\", err)\n\t\t}\n\t\tcomments = append(comments, &c)\n\t}\n\treturn comments, rows.Err()\n}\n\n// Comment page-read tuning. Mirrors the EventsSince keyset clamp: an unbounded\n// page defeats the purpose of paging a long thread, so a non-positive limit\n// falls back to the default and any larger request is capped.\nconst (\n\tdefaultCommentsPageLimit = 100\n\tmaxCommentsPageLimit     = 500\n)\n\n// CommentsKeysetPredicate is the SARGABLE (created_at ASC, id ASC) keyset resume\n// predicate GetIssueCommentsPageInTx ANDs in once a page cursor is set. Its three\n// ? placeholders bind, in order: created_at (the sargable lower bound),\n// created_at (strict), and id (the same-second tie-break).","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/comments.go#L21-L57","documentation":"GetIssueCommentsInTx fails while scanning a row into types.Comment (id, issue_id, author, text, created_at). Scan errors mean the result set does not match expectations: NULL in a NOT-NULL-expected column, a driver type mismatch, or an altered column list. Wrapped as \"get issue comments: scan: %w\" with the offending row context lost to the loop.","triggerScenarios":"A row in comments has NULL author/text while the Go struct scans into string; a backend driver returning created_at as []byte/string incompatible with the scan target; someone added a column or changed a type without updating this scan.","commonSituations":"Rows imported/edited manually in SQL bypassing validation; mixed-version clusters where replicas have older column types; custom drivers (non-Dolt SQLite) with differing DATETIME handling.","solutions":["Inspect the wrapped scan error to find the offending column/value; fix or delete the malformed row.","Re-insert or backfill NULL values into columns the scan expects non-NULL (author, text, created_at).","If caused by a version mismatch, align the bd binary and database schema versions."],"exampleFix":"// before (row has NULL author)\n// get issue comments: scan: sql: Scan error on column \"author\": unsupported Scan\n\n// after — repair the row\nUPDATE comments SET author = 'unknown' WHERE author IS NULL;","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"comments, err := store.GetIssueComments(ctx, id)\nif err != nil {\n\tvar scanErr error\n\tif strings.Contains(err.Error(), \"scan\") {\n\t\treturn fmt.Errorf(\"corrupt/legacy row in comments for %s: %w\", id, err)\n\t}\n\t_ = scanErr\n\treturn err\n}","preventionTips":["Never hand-edit comment rows in SQL; keep NULLs out of author/text/created_at.","Run migrations in one step so column lists always match the binary's Scan calls.","After restores or imports, spot-check rows scan cleanly before serving reads."],"tags":["database","scan","data-corruption"],"backgroundTag":"row-scan-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}