OpenNHP/opennhp · error
keystore: migrate .
Error message
keystore: migrate %s.%s: %w
What it means
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.
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.
Example fix
// before
if _, err := s.db.Exec(m.ddl); err != nil {
return fmt.Errorf("keystore: migrate %s.%s: %w", m.table, m.column, err)
}
// after
if _, err := s.db.Exec(m.ddl); err != nil {
if strings.Contains(err.Error(), "duplicate column name") {
continue // raced migration; column already added
}
return fmt.Errorf("keystore: migrate %s.%s: %w", m.table, m.column, err)
} Defensive patterns
Strategy: retry
Try / catch
if err != nil && strings.Contains(err.Error(), "keystore: migrate") {
if strings.Contains(err.Error(), "duplicate column") {
// raced migration; treat as success
} else {
return err
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- keystore: migrate
- keystore: check column
- keystore: open database
- keystore: insert otp
- keystore: query otp
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/2e9737c865299cf8.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/keystore.go:122
table string
column string
ddl string
}{
{"otp_records", "pub_key", "ALTER TABLE otp_records ADD COLUMN pub_key TEXT NOT NULL DEFAULT ''"},
{"otp_records", "attempts", "ALTER TABLE otp_records ADD COLUMN attempts INTEGER DEFAULT 0"},
}
for _, m := range migrations {
var exists int
err := s.db.QueryRow(
`SELECT COUNT(*) FROM pragma_table_info(?) WHERE name = ?`,
m.table, m.column,
).Scan(&exists)
if err != nil {
return fmt.Errorf("keystore: check column %s.%s: %w", m.table, m.column, err)
}
if exists == 0 {
if _, err := s.db.Exec(m.ddl); err != nil {
return fmt.Errorf("keystore: migrate %s.%s: %w", m.table, m.column, err)
}
}
}
return nil
}
// ── OTP operations ────────────────────────────────────────────────────────
// OTPCooldownSeconds is the minimum interval between successive OTP
// generations for the same user+device. A request that arrives before
// the cooldown elapses is rejected with ErrOTPCooldown.
const OTPCooldownSeconds int64 = 60
// MaxOTPPerUserPerWindow caps the total number of OTP generations for a
// single userId across all deviceIds within the cooldown window. This
// closes the deviceId-rotation bypass: without it, an attacker can vary
// the (unauthenticated, attacker-controlled) deviceId on each request toView on GitHub (pinned to 6e04ca5ff0)