atuinsh/atuin · error

issue in stats exits query

Error message

issue in stats exits query

What it means

Panic while compiling the exit-code histogram query. SqlBuilder::sql() returns Err only when the builder cannot render valid SQL; the guarded builder (database.rs:858-859) selects `exit, count(1) as count` from history filtered by `command = ?1` and grouped by `exit` — all hardcoded, so the failure branch is unreachable for any runtime input.

Solutions

  1. Restore the pinned sqlbuilder version in Cargo.lock (this is the only realistic cause)
  2. Guard dependency upgrades with a unit test that builds all seven stats queries and asserts .sql() returns Ok
  3. If it reproduces on the pinned version, capture the builder construction (fields, and_where, group_by) and file an atuin issue — the panic text does not say 'please report', but it is the same class as the list-query one
Defensive patterns

Strategy: try-catch

Try / catch

let stats = std::panic::catch_unwind(|| {
    tokio::task::block_on(database.stats(&history))
});
let _ = stats.map_err(|_| tracing::error!("stats exits histogram builder panicked"));

Prevention

When it happens

Trigger: Database::stats() with an incompatible sqlbuilder version that rejects a fields()+and_where()+group_by() chain. Triggered purely by the linked library version, never by the history database contents.

Common situations: Semver-drifted sqlbuilder after a dependency refresh; building with a forked sqlbuilder. Not reproducible on the pinned workspace versions regardless of DB state.

Related errors


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

Appendix: source

Thrown at crates/atuin-client/src/database.rs:889

        // Intentionally format the string with 01 hardcoded. We want the average runtime for the
        // _entire month_, but will later parse it as a datetime for sorting
        // Sqlite has no datetime so we cannot do it there, and otherwise sorting will just be a
        // string sort, which won't be correct.
        let mut duration_over_time = SqlBuilder::select_from("history");
        duration_over_time
            .fields(&[
                "strftime('01-%m-%Y', ROUND(timestamp / 1000000000), 'unixepoch') AS month_year",
                "avg(duration) as duration",
            ])
            .and_where("command = ?1")
            .group_by("month_year")
            .having("duration > 0");

        let prev = prev.sql().expect("issue in stats previous query");
        let next = next.sql().expect("issue in stats next query");
        let total = total.sql().expect("issue in stats average query");
        let average = average.sql().expect("issue in stats previous query");
        let exits = exits.sql().expect("issue in stats exits query");
        let day_of_week = day_of_week.sql().expect("issue in stats day of week query");
        let duration_over_time =
            duration_over_time.sql().expect("issue in stats duration over time query");

        // The queries are all independent, so run them concurrently on the pool.
        #[allow(clippy::type_complexity)]
        let (prev, next, total, average, exits, day_of_week, duration_over_time): (
            _,
            _,
            (i64,),
            (f64,),
            Vec<(i64, i64)>,
            Vec<(String, i64)>,
            Vec<(String, f64)>,
        ) = tokio::try_join!(
            sqlx::query(sqlx::AssertSqlSafe(prev))
                .bind(h.timestamp.unix_timestamp_nanos() as i64)
                .bind(&h.session)

View on GitHub (pinned to 15fe1318f1)