atuinsh/atuin · error

failed to parse uuid for local store status

Error message

failed to parse uuid for local store status

What it means

Panic while building the local record-store status: the query `select host, tag, max(idx) from store group by host, tag` succeeds, then each host string is parsed back into a Uuid. Because every store row's host is a UUID at write time, a parse failure means at least one host group in the table holds non-UUID text — store corruption. Unlike query_row's fail-fast comment, this site is inside a function returning Result, yet still uses expect.

Solutions

  1. List the offending host values: `sqlite3 ~/.local/share/atuin/store.db 'select host, count(*) from store group by host;'` and look for non-UUID strings
  2. Restore the store from backup, or delete just the bad rows (`delete from store where host = '<bad value>';`) if the rest is intact, then re-run sync
  3. If the store is broadly damaged, move it aside and rebuild the local store via `atuin sync` from the server
  4. When patching the crate, replace the expect with `let host = Uuid::from_str(&i.0).wrap_err("invalid host in local store status")?;` since the enclosing function already returns Result

Example fix

// before (crates/atuin-client/src/record/sqlite_store.rs:305)
let host = HostId(Uuid::from_str(i.0.as_str()).expect("failed to parse uuid for local store status"));
// after
let host = HostId(Uuid::from_str(i.0.as_str()).wrap_err("failed to parse uuid for local store status")?);
Defensive patterns

Strategy: validation

Validate before calling

async fn status_hosts_valid(pool: &sqlx::SqlitePool) -> Result<bool, sqlx::Error> {
    let rows: Vec<(String, i64)> = sqlx::query_as(
        "select host, count(*) from store group by host",
    )
    .fetch_all(pool)
    .await?;
    Ok(rows.iter().all(|(host, _)| uuid::Uuid::parse_str(host).is_ok()))
}

Type guard

fn is_valid_uuid(s: &str) -> bool {
    uuid::Uuid::parse_str(s).is_ok()
}

Try / catch

let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
    tokio::task::block_on(store.status())
}));
match res {
    Ok(Ok(status)) => { /* compute sync tail */ }
    Ok(Err(e)) => return Err(e.into()),
    Err(_) => tracing::error!("status build hit a bad host UUID — prune bad rows and re-sync"),
}

Prevention

When it happens

Trigger: Calling the local store status builder (used by `atuin sync` status calculation and store status commands) when the store table contains any host value that is not a UUID. One corrupt row anywhere in the table poisons the whole status computation because the GROUP BY aggregates all hosts.

Common situations: Corrupt or externally modified store.db; a torn write after power loss; a fork or script having inserted rows with arbitrary host strings; restored-from-partial-backup data directories.

Understand the failure class

Related errors


AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19). Data as JSON: /api/errors/6087eb6e50d7e253. Report an issue: GitHub.

Appendix: source

Thrown at crates/atuin-client/src/record/sqlite_store.rs:305

        }
    }

    pub async fn status(&self) -> Result<RecordStatus> {
        let mut status = RecordStatus::new();

        let res: Result<Vec<(String, String, i64)>, sqlx::Error> =
            sqlx::query_as("select host, tag, max(idx) from store group by host, tag")
                .fetch_all(&self.pool)
                .await;

        let res = match res {
            Err(e) => return Err(eyre!("failed to fetch local store status: {}", e)),
            Ok(v) => v,
        };

        for i in res {
            let host = HostId(
                Uuid::from_str(i.0.as_str()).expect("failed to parse uuid for local store status"),
            );

            status.set_raw(host, RecordTag::from(i.1), i.2 as u64);
        }

        Ok(status)
    }

    pub async fn all_tagged(
        &self,
        tag: &RecordTag,
    ) -> Result<Vec<Record<paseto_v4::EncryptedData>>> {
        let res = sqlx::query("select * from store where tag = ?1 order by timestamp asc")
            .bind(tag.as_str())
            .map(Self::query_row)
            .fetch_all(&self.pool)
            .await?;

View on GitHub (pinned to 15fe1318f1)