{"record":{"id":"2e9737c865299cf8","repo":"OpenNHP/opennhp","slug":"keystore-migrate-s-s-w","errorCode":null,"errorMessage":"keystore: migrate %s.%s: %w","messagePattern":"keystore: migrate (.+?)\\.(.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"endpoints/server/keystore.go","lineNumber":122,"sourceCode":"\t\ttable  string\n\t\tcolumn string\n\t\tddl    string\n\t}{\n\t\t{\"otp_records\", \"pub_key\", \"ALTER TABLE otp_records ADD COLUMN pub_key TEXT NOT NULL DEFAULT ''\"},\n\t\t{\"otp_records\", \"attempts\", \"ALTER TABLE otp_records ADD COLUMN attempts INTEGER DEFAULT 0\"},\n\t}\n\tfor _, m := range migrations {\n\t\tvar exists int\n\t\terr := s.db.QueryRow(\n\t\t\t`SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?`,\n\t\t\tm.table, m.column,\n\t\t).Scan(&exists)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"keystore: check column %s.%s: %w\", m.table, m.column, err)\n\t\t}\n\t\tif exists == 0 {\n\t\t\tif _, err := s.db.Exec(m.ddl); err != nil {\n\t\t\t\treturn fmt.Errorf(\"keystore: migrate %s.%s: %w\", m.table, m.column, err)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// ── OTP operations ────────────────────────────────────────────────────────\n\n// OTPCooldownSeconds is the minimum interval between successive OTP\n// generations for the same user+device. A request that arrives before\n// the cooldown elapses is rejected with ErrOTPCooldown.\nconst OTPCooldownSeconds int64 = 60\n\n// MaxOTPPerUserPerWindow caps the total number of OTP generations for a\n// single userId across all deviceIds within the cooldown window. This\n// closes the deviceId-rotation bypass: without it, an attacker can vary\n// the (unauthenticated, attacker-controlled) deviceId on each request to","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/OpenNHP/opennhp/blob/6e04ca5ff03222a699c24205cd4bf8fee9af7ffe/endpoints/server/keystore.go#L104-L140","documentation":"migrate() executes an ALTER TABLE-style DDL statement to add a missing column; a failure here is wrapped as 'keystore: migrate <table>.<column>'. Common SQLite causes are duplicates during ALTER, disk I/O errors, or a locked database.","triggerScenarios":"Running Exec on migration DDL like ALTER TABLE agent_keys ADD COLUMN ... when the DB is locked by another writer, the disk is full, or the table is corrupted.","commonSituations":"Two replicas racing to add the same column (one gets 'duplicate column name' if the pragma check raced), embedded storage full on small VMs/containers.","solutions":["Read the wrapped SQLite error: 'duplicate column name' means a race — re-check pragma and treat as success or serialize migrations.","Free disk space / check filesystem writability if the error is disk I/O.","Use an exclusive lock or a single-migrator pattern (e.g. file lock) when multiple daemons share the file.","Re-run startup after fixing; migrations are guarded by the pragma existence check and are idempotent."],"exampleFix":"// before\nif _, err := s.db.Exec(m.ddl); err != nil {\n    return fmt.Errorf(\"keystore: migrate %s.%s: %w\", m.table, m.column, err)\n}\n// after\nif _, err := s.db.Exec(m.ddl); err != nil {\n    if strings.Contains(err.Error(), \"duplicate column name\") {\n        continue // raced migration; column already added\n    }\n    return fmt.Errorf(\"keystore: migrate %s.%s: %w\", m.table, m.column, err)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"keystore: migrate\") {\n    if strings.Contains(err.Error(), \"duplicate column\") {\n        // raced migration; treat as success\n    } else {\n        return err\n    }\n}","preventionTips":["Serialize migrations with a file lock when multiple processes share the db","Treat 'duplicate column name' as success in idempotent migrations","Ensure adequate disk space before startup"],"tags":["go","sqlite","migration","ddl"],"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"}