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

  1. 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
  2. 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
  3. 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)
  4. 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

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


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)