atuinsh/atuin · error

issue in stats duration over time query

Error message

issue in stats duration over time query

What it means

Panic while compiling the duration-over-time query. The guarded builder (database.rs:875-883) selects `strftime('01-%m-%Y', ...) AS month_year` and `avg(duration)`, filtered by `command = ?1`, grouped by month_year with `having("duration > 0")` — the only stats query using .having(). All parts are hardcoded, so sql() Err is an unreachable invariant.

Solutions

  1. Pin sqlbuilder back to the last working version from Cargo.lock history
  2. Include this builder (with its .having()) in the compile-check unit test for all stats queries
  3. If reproducible on the pinned version, report the .group_by(alias) + .having() combination to sqlbuilder upstream
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 duration-over-time builder panicked"));

Prevention

When it happens

Trigger: Database::stats() with a sqlbuilder version that mishandles the .having() call or the aliased group_by/having combination. Runtime data never reaches the builder; only the library version matters.

Common situations: Dependency upgrade that changes having() validation in sqlbuilder; local forks of sqlbuilder. The '01-' hardcoded month format comment shows the string is intentional, so a naive 'fix' of the strftime literal is not the issue.

Related errors


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

Appendix: source

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

        // 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)
                .map(Self::row_to_history)
                .fetch_optional(&self.pool),
            sqlx::query(sqlx::AssertSqlSafe(next))

View on GitHub (pinned to 15fe1318f1)