atuinsh/atuin · error
invalid id UUID format in sqlite DB
Error message
invalid id UUID format in sqlite DB
What it means
Panic while converting the `id` column of the record store's `store` table (SQLite) back into a Uuid. The comment above it ('tbh at this point things are pretty fucked so just panic') shows this is a deliberate fail-fast on data corruption: every record written by atuin carries a UUIDv7 id, so a row whose id is not a valid UUID means the store file was corrupted or written by something else. query_row runs on every record load (sync, KV, dotfiles, history v2 reads).
Solutions
- Check whether the store file was manually modified or pointed at the wrong path (ATUIN_DB_PATH / data dir settings) and restore the intended file
- Recover from backup: copy the last good store.db (and -wal/-shm) from backups/snapshots, then re-run sync to re-download records from the server
- If no backup exists, rename the corrupt store aside and let `atuin sync` rebuild a fresh local store from the remote (local-only records will be lost)
- Verify integrity with `sqlite3 ~/.local/share/atuin/store.db 'select id from store where length(id) != 36 or id not like "%-%-%-%-%";'` to list the offending rows before deciding to prune them
Example fix
// before (crates/atuin-client/src/record/sqlite_store.rs:101)
let id = Uuid::from_str(row.get("id")).expect("invalid id UUID format in sqlite DB");
// after (surface corruption as an error instead of aborting the process)
let id = Uuid::from_str(row.get("id"))
.map_err(|e| sqlx::Error::ColumnDecode { index: "id".into(), source: e.into() })?; // query_row returns Result Defensive patterns
Strategy: validation
Validate before calling
// Before bulk store reads, scan for rows whose id is not a valid UUID:
use uuid::Uuid;
async fn store_ids_valid(pool: &sqlx::SqlitePool) -> Result<bool, sqlx::Error> {
let bad: i64 = sqlx::query_scalar(
"select count(*) from store where id not glob
'[0-9a-f]*-[0-9a-f]*-[0-9a-f]*-[0-9a-f]*-[0-9a-f]*' or length(id) != 36",
)
.fetch_one(pool)
.await?;
Ok(bad == 0)
} Type guard
fn is_valid_uuid(s: &str) -> bool {
uuid::Uuid::parse_str(s).is_ok()
} Try / catch
// Loading records with an external process (e.g., a sync tool) — keep the panic out of your loop:
let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
tokio::task::block_on(store.load(tag, offset, batch))
}));
match res {
Ok(Ok(records)) => { /* process */ }
Ok(Err(e)) => return Err(e.into()),
Err(_) => {
// invalid-UUID panic: quarantine the store file and rebuild from server
tracing::error!("record store corrupt (bad id UUID) — restore from backup or re-sync");
}
} Prevention
- Keep exclusive, atomic backups of store.db before manual maintenance
- Never edit store.db with external sqlite tools
- Don't share the atuin data dir between different atuin forks
- On abnormal shutdown, run a quick UUID sanity query before the next sync
When it happens
Trigger: Any record-store read (e.g., `atuin sync`, `atuin store status`, KV/dotfiles operations) hitting a row where store.id is not a parseable UUID: truncated/garbled file from disk issues or crash during write, manual sqlite edits, a store file produced by a divergent fork, or a file replaced with non-atuin data at ~/.local/share/atuin/store.db.
Common situations: Users who hand-edited the sqlite store; a full disk or power loss that left a partially written WAL; symlinking ATUIN's data dir to the wrong file; running two incompatible atuin forks against the same data directory.
Related errors
- failed to parse uuid for local store status
- invalid host UUID format in sqlite DB
- issue in stats average query
- issue in stats day of week query
- issue in stats duration over time query
AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19).
Data as JSON: /api/errors/f8fb7ecc59cc3b2f.
Report an issue: GitHub.
Appendix: source
Thrown at crates/atuin-client/src/record/sqlite_store.rs:101
.bind(r.idx as i64)
.bind(r.host.id.0.as_hyphenated().to_string())
.bind(r.tag.as_str())
.bind(r.timestamp as i64)
.bind(r.version.as_str())
.bind(r.data.raw.as_str())
.bind(r.data.cek.as_str())
.execute(&mut **tx)
.await?;
Ok(())
}
fn query_row(row: SqliteRow) -> Record<paseto_v4::EncryptedData> {
let idx: i64 = row.get("idx");
let timestamp: i64 = row.get("timestamp");
// tbh at this point things are pretty fucked so just panic
let id = Uuid::from_str(row.get("id")).expect("invalid id UUID format in sqlite DB");
let host = Uuid::from_str(row.get("host")).expect("invalid host UUID format in sqlite DB");
Record {
id: RecordId(id),
idx: idx as u64,
host: Host::new(HostId(host)),
timestamp: timestamp as u64,
tag: RecordTag::from(row.get::<String, _>("tag")),
version: RecordVersion::from(row.get::<String, _>("version")),
data: paseto_v4::EncryptedData {
raw: row.get("data"),
cek: row.get("cek"),
},
}
}
async fn load_all(&self) -> Result<Vec<Record<paseto_v4::EncryptedData>>> {
let res =View on GitHub (pinned to 15fe1318f1)