{"record":{"id":"12050cd291af5952","repo":"gastownhall/beads","slug":"wisp-id-set-scan-w","errorCode":null,"errorMessage":"wisp id set: scan: %w","messagePattern":"wisp id set: scan: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/wisp_routing.go","lineNumber":98,"sourceCode":"\t\t\tend = len(ids)\n\t\t}\n\t\tbatch := ids[start:end]\n\t\tplaceholders := make([]string, len(batch))\n\t\targs := make([]any, len(batch))\n\t\tfor i, id := range batch {\n\t\t\tplaceholders[i] = \"?\"\n\t\t\targs[i] = id\n\t\t}\n\t\tq := fmt.Sprintf(\"SELECT id FROM wisps WHERE id IN (%s)\", strings.Join(placeholders, \",\"))\n\t\trows, err := tx.QueryContext(ctx, q, args...)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"wisp id set: %w\", err)\n\t\t}\n\t\tfor rows.Next() {\n\t\t\tvar id string\n\t\t\tif err := rows.Scan(&id); err != nil {\n\t\t\t\t_ = rows.Close()\n\t\t\t\treturn nil, fmt.Errorf(\"wisp id set: scan: %w\", err)\n\t\t\t}\n\t\t\tset[id] = struct{}{}\n\t\t}\n\t\t_ = rows.Close()\n\t\tif err := rows.Err(); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"wisp id set: rows: %w\", err)\n\t\t}\n\t}\n\treturn set, nil\n}\n\n// partitionByWispSet splits ids into (wispIDs, permIDs) using the provided\n// wisp-id set. If wispSet is nil the caller must populate it first via\n// WispIDSetInTx; this helper does no I/O.\nfunc partitionByWispSet(ids []string, wispSet map[string]struct{}) (wispIDs, permIDs []string) {\n\tfor _, id := range ids {\n\t\tif _, isWisp := wispSet[id]; isWisp {\n\t\t\twispIDs = append(wispIDs, id)","sourceCodeStart":80,"sourceCodeEnd":116,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/wisp_routing.go#L80-L116","documentation":"Thrown when rows.Scan fails decoding an id column from the wisp batch SELECT into a string. Like the deferred-wake scan error, this indicates a row whose id cannot be converted — NULL id, incompatible driver type, or corrupted row. Rows are closed before returning to release the cursor.","triggerScenarios":"Calling WispIDSetInTx when a row in wisps has a NULL or non-string id, or the driver mis-declares the id column type/charset so Scan cannot convert it.","commonSituations":"Manually inserted rows with NULL id; Dolt/MySQL driver charset mismatches on the id column; binary-typed id columns after schema tampering.","solutions":["Find and fix the row with the malformed id in the wisps table.","Enforce id VARCHAR NOT NULL PRIMARY KEY on the wisps table.","Update the driver if the id column type/charset conversion is mishandled.","Run storage integrity checks to detect further corruption."],"exampleFix":"// before\nid VARCHAR(255) NULL\n// after\nid VARCHAR(255) CHARACTER SET utf8mb4 NOT NULL PRIMARY KEY","handlingStrategy":"validation","validationCode":"rows, err := db.Query(\"SELECT id FROM wisps WHERE id IS NULL\")\nif err != nil { return err }\ndefer rows.Close()\nif rows.Next() { return errors.New(\"wisps rows with NULL id found: repair table before routing\") }","typeGuard":null,"tryCatchPattern":"if err != nil {\n    if strings.Contains(err.Error(), \"converting NULL to string\") {\n        return fmt.Errorf(\"corrupt wisps row (NULL id): repair wisps table\")\n    }\n    return err\n}","preventionTips":["Keep wisps.id NOT NULL PRIMARY KEY.","Insert wisps rows only through beads APIs, never by hand.","Use a current driver with proper charset/type handling for id columns.","Schedule periodic integrity checks on the wisps table."],"tags":["database","row-scan","wisp-routing"],"backgroundTag":"sql-row-scan-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}