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
- Pin sqlbuilder back to the last working version from Cargo.lock history
- Include this builder (with its .having()) in the compile-check unit test for all stats queries
- 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
- Keep Cargo.lock under review so sqlbuilder cannot drift silently
- Unit-test the .having() builder variant specifically on upgrades
- Remember the '01-' month format is intentional — do not 'fix' it when diagnosing
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
- issue in stats average query
- issue in stats day of week query
- issue in stats exits query
- fixed-layout structure cannot fail serialization
- the vector is not empty
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)