OpenNHP/opennhp · error
keystore: check column
Error message
keystore: check column %s.%s: %w
What it means
migrate() checks whether an expected column exists via pragma_table_info; if that SELECT fails it wraps the scan error with this message. It means the pragma query itself failed (SQLite error or Scan type mismatch), not that the column is missing.
Solutions
- Read the wrapped cause for the specific SQLite error code (e.g. SQLITE_BUSY, SQLITE_LOCKED).
- If SQLITE_BUSY, reduce concurrency or raise _busy_timeout in the DSN.
- Verify the driver's bundled SQLite supports pragma_table_info (SQLite >= 3.16).
- Retry once after a short backoff for transient lock errors.
Defensive patterns
Strategy: retry
Try / catch
store, err := NewAgentKeyStore(dir)
if err != nil && strings.Contains(err.Error(), "check column") {
time.Sleep(250 * time.Millisecond)
store, err = NewAgentKeyStore(dir) // retry once for transient BUSY
} Prevention
- Use a driver with SQLite >= 3.16 (pragma_table_info)
- Keep single-writer ownership of the db file
- Raise _busy_timeout for contended deployments
When it happens
Trigger: pragma_table_info unavailable (very old SQLite amalgamation embedded in the driver), the table name binding fails, or a rows/Scan error such as scanning COUNT(*) into a non-int variable.
Common situations: Driver with an old bundled SQLite lacking pragma table-valued functions; transient SQLITE_BUSY on a contended WAL database exceeding the 5000ms busy timeout.
Understand the failure class
Background: "query failed", "%w: SQL error" — wrapped database query errors in Go libraries explained — this error's family across 3 libraries.
Related errors
AI-assisted analysis of OpenNHP/opennhp@6e04ca5ff0 (2026-09-07).
Data as JSON: /api/errors/8889446c7d5385a6.
Report an issue: GitHub.
Appendix: source
Thrown at endpoints/server/keystore.go:118
}
// Incremental migrations: add columns that may be absent in older databases.
migrations := []struct {
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
View on GitHub (pinned to 6e04ca5ff0)