{"record":{"id":"1132661b8f0d28f7","repo":"OpenNHP/opennhp","slug":"keystore-sweep-otp-rows-affected-w","errorCode":null,"errorMessage":"keystore: sweep otp rows affected: %w","messagePattern":"keystore: sweep otp rows affected: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"endpoints/server/keystore.go","lineNumber":566,"sourceCode":"// to 86400s (24 hours) when passed a negative value. Pass 0 to delete all\n// used or expired OTPs regardless of age.\nfunc (s *AgentKeyStore) SweepStaleOTPs(retentionSeconds int64) (int64, error) {\n\tif retentionSeconds < 0 {\n\t\tretentionSeconds = 86400\n\t}\n\tcutoff := time.Now().Unix() - retentionSeconds\n\tres, err := s.db.Exec(\n\t\t`DELETE FROM otp_records\n\t\t WHERE created_at < ?\n\t\t   AND (used = 1 OR expires_at <= ?)`,\n\t\tcutoff, time.Now().Unix(),\n\t)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep otp: %w\", err)\n\t}\n\tn, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep otp rows affected: %w\", err)\n\t}\n\treturn n, nil\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────\n\nfunc randomDigits(n int) (string, error) {\n\tif n <= 0 {\n\t\treturn \"\", fmt.Errorf(\"invalid digit count: %d\", n)\n\t}\n\n\tbuf := make([]byte, n)\n\tfor i := range buf {\n\t\tdigit, err := rand.Int(rand.Reader, big.NewInt(10))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tbuf[i] = byte('0') + byte(digit.Int64())","sourceCodeStart":548,"sourceCodeEnd":584,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L548-L584","documentation":"SweepStaleOTPs wraps the failure of res.RowsAffected() after the OTP DELETE succeeded. Same driver-level limitation as the agent_keys sweep: some drivers/Result implementations cannot report the number of deleted rows.","triggerScenarios":"Calling SweepStaleOTPs against a driver whose Result lacks RowsAffected support, even though the DELETE executed.","commonSituations":"Driver swap or version upgrade in go.mod; custom driver wrappers that drop row-count metadata; embedded SQLite variants with limited metadata support.","solutions":["Verify the SQLite driver implements RowsAffected; upgrade or switch drivers if it does not.","If the deleted count is only for logging, log the error and continue instead of failing the sweep.","Pin known-good driver versions in go.mod to avoid behavior changes on upgrade.","If an exact count matters, wrap the DELETE in a transaction and pre-count matching rows."],"exampleFix":"// before\nn, err := res.RowsAffected()\nif err != nil {\n    return 0, fmt.Errorf(\"keystore: sweep otp rows affected: %w\", err)\n}\n// after\nn, err := res.RowsAffected()\nif err != nil {\n    log.Warnf(\"keystore: otp sweep completed, count unavailable: %v\", err)\n    n = -1\n}","handlingStrategy":"fallback","validationCode":"null","typeGuard":"n, err := res.RowsAffected()\nif err != nil {\n    db.QueryRow(\"SELECT COUNT(*) FROM otp_records WHERE created_at < ? AND (used=1 OR expires_at <= ?)\", cutoff, time.Now().Unix()).Scan(&remaining)\n}","tryCatchPattern":"n, err := res.RowsAffected()\nif err != nil {\n    log.Warnf(\"otp sweep count unavailable: %v\", err)\n    n = -1\n}","preventionTips":["Prefer SQLite drivers with full Result metadata","Keep driver versions pinned and smoke-test RowsAffected in CI","Treat the count as best-effort telemetry, not a correctness requirement","If counts matter, pre-compute matches in the same transaction"],"tags":["go","database","sqlite","driver"],"backgroundTag":"database-write-failed","analyzedSha":"6e04ca5ff03222a699c24205cd4bf8fee9af7ffe","analyzedAt":"2026-09-07T15:44:59.941Z","contentChangedAt":"2026-09-07T15:44:59.941Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}