{"record":{"id":"cbdc5a5e7a6c1a51","repo":"OpenNHP/opennhp","slug":"keystore-sweep-rows-affected-w","errorCode":null,"errorMessage":"keystore: sweep rows affected: %w","messagePattern":"keystore: sweep rows affected: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"endpoints/server/keystore.go","lineNumber":540,"sourceCode":"// never swept (they are configured to never expire). The result of\n// FindAgentByPublicKey / IsAgentRegistered does not depend on this\n// sweeper — those functions already filter on expires_at — so this\n// method is purely a hygiene / index-utility measure.\nfunc (s *AgentKeyStore) SweepExpiredDeactivates() (int64, error) {\n\tres, err := s.db.Exec(\n\t\t`UPDATE agent_keys\n\t\t SET active = 0\n\t\t WHERE active = 1\n\t\t   AND expires_at IS NOT NULL\n\t\t   AND expires_at <= ?`,\n\t\ttime.Now().Unix(),\n\t)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep expired: %w\", err)\n\t}\n\tn, err := res.RowsAffected()\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"keystore: sweep rows affected: %w\", err)\n\t}\n\treturn n, nil\n}\n\n// SweepStaleOTPs deletes OTP rows that are already used or expired and\n// were created more than retentionSeconds ago. Returns the number of rows\n// deleted. Unused, non-expired OTPs are never swept. Retention defaults\n// 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 <= ?)`,","sourceCodeStart":522,"sourceCodeEnd":558,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L522-L558","documentation":"SweepExpiredDeactivates wraps the failure of res.RowsAffected() after the UPDATE succeeded. Some drivers cannot report affected-row counts (e.g. certain SQLite builds or when the statement was prepared through an interface that discards metadata); this error surfaces that limitation.","triggerScenarios":"Calling SweepExpiredDeactivates() against a driver/driver-version whose Result does not support RowsAffected (returns an error), after the deactivation UPDATE itself already ran.","commonSituations":"Switching SQLite drivers (mattn/go-sqlite3 vs modernc.org/sqlite) or upgrading versions changes RowsAffected behavior; using a proxy/ConnectionHook that returns a driver.Result without row counts.","solutions":["Check which SQLite driver is compiled in; prefer one that implements RowsAffected correctly (e.g. mattn/go-sqlite3 or modernc.org/sqlite current versions).","If the row count is only informational, degrade to logging a warning and treat the sweep as successful.","Pin the driver version and re-run 'go mod tidy' after driver changes.","Count affected rows separately with a SELECT COUNT(*) of rows still active and expired if exact numbers are required."],"exampleFix":"// before\nn, err := res.RowsAffected()\nif err != nil {\n    return 0, fmt.Errorf(\"keystore: sweep rows affected: %w\", err)\n}\n// after\nn, err := res.RowsAffected()\nif err != nil {\n    log.Warnf(\"keystore: sweep ran but row count unavailable: %v\", err)\n    n = -1\n}","handlingStrategy":"fallback","validationCode":"null","typeGuard":"n, err := res.RowsAffected()\nif err != nil {\n    // treat as unknown count; verify via follow-up SELECT COUNT(*)\n    db.QueryRow(\"SELECT COUNT(*) FROM agent_keys WHERE active=1 AND expires_at IS NOT NULL AND expires_at <= ?\", time.Now().Unix()).Scan(&remaining)\n}","tryCatchPattern":"n, err := res.RowsAffected()\nif err != nil {\n    log.Warnf(\"rows affected unavailable: %v\", err)\n    n = -1 // sweep already applied; do not fail\n}","preventionTips":["Use a SQLite driver that supports RowsAffected (mattn/go-sqlite3, modernc.org/sqlite)","Pin driver versions in go.mod and re-test after upgrades","Only require the count when callers consume it; otherwise log-and-continue","Add a driver smoke test that performs an UPDATE and asserts RowsAffected works"],"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"}