atuinsh/atuin · error

issue in stats average query

Error message

issue in stats average query

What it means

Panic while compiling the 'total' stats SQL with sqlbuilder's SqlBuilder::sql(), which returns Result<String,String> when the accumulated builder state cannot render valid SQL. The guarded builder (database.rs:852-853) is `SELECT count(1) FROM history WHERE command = ?1` with hardcoded table/field names, so the Err branch is an unreachable invariant, not a data-dependent failure. Note the expect message is mislabeled: line 887 builds the total-count query even though the text says 'average'.

Solutions

  1. Confirm the panic is reproducible, then diff Cargo.lock for a sqlbuilder version change and restore the previously pinned version
  2. If a new sqlbuilder version is required, write a unit test that builds each stats query and calls .sql() so CI catches the regression before release
  3. Fix the swapped expect message (says 'stats average query' but guards the total query) so future reports are diagnosable
  4. Report the builder state that fails to the sqlbuilder upstream issue tracker, including the exact fields/and_where calls used here

Example fix

// before (crates/atuin-client/src/database.rs:887, message is wrong for this query)
let total = total.sql().expect("issue in stats average query");
// after
let total = total.sql().expect("issue in stats total query");
Defensive patterns

Strategy: try-catch

Try / catch

// stats() panics only on builder/dependency regressions; isolate if you call it in-process
let stats = std::panic::catch_unwind(|| {
    tokio::task::block_on(database.stats(&history))
});
match stats {
    Ok(Ok(s)) => { /* use stats */ }
    Ok(Err(e)) => tracing::error!(?e, "stats query failed"),
    Err(_) => tracing::error!("stats query builder panicked — check sqlbuilder version"),
}

Prevention

When it happens

Trigger: Calling atuin-client's Database::stats() (the `atuin stats` path) after the sqlbuilder crate has been upgraded/downgraded to a version whose sql() rejects a builder shape this code produces (e.g., a field list or and_where combination it considers invalid). No user input, history row, or setting reaches this branch; the query is fully static.

Common situations: Cargo.lock regeneration that silently bumps sqlbuilder across an incompatible API/behavior change; using a vendored or patched sqlbuilder fork; building against a workspace with a version mismatch. Never fires in normal stats runs on the pinned dependency set.

Related errors


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

Appendix: source

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

            .group_by("day_of_week");

        // 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))

View on GitHub (pinned to 15fe1318f1)