{"record":{"id":"b99823278ad9a62c","repo":"atuinsh/atuin","slug":"issue-in-stats-average-query","errorCode":null,"errorMessage":"issue in stats average query","messagePattern":"issue in stats average query","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-client/src/database.rs","lineNumber":887,"sourceCode":"            .group_by(\"day_of_week\");\n\n        // Intentionally format the string with 01 hardcoded. We want the average runtime for the\n        // _entire month_, but will later parse it as a datetime for sorting\n        // Sqlite has no datetime so we cannot do it there, and otherwise sorting will just be a\n        // string sort, which won't be correct.\n        let mut duration_over_time = SqlBuilder::select_from(\"history\");\n        duration_over_time\n            .fields(&[\n                \"strftime('01-%m-%Y', ROUND(timestamp / 1000000000), 'unixepoch') AS month_year\",\n                \"avg(duration) as duration\",\n            ])\n            .and_where(\"command = ?1\")\n            .group_by(\"month_year\")\n            .having(\"duration > 0\");\n\n        let prev = prev.sql().expect(\"issue in stats previous query\");\n        let next = next.sql().expect(\"issue in stats next query\");\n        let total = total.sql().expect(\"issue in stats average query\");\n        let average = average.sql().expect(\"issue in stats previous query\");\n        let exits = exits.sql().expect(\"issue in stats exits query\");\n        let day_of_week = day_of_week.sql().expect(\"issue in stats day of week query\");\n        let duration_over_time =\n            duration_over_time.sql().expect(\"issue in stats duration over time query\");\n\n        // The queries are all independent, so run them concurrently on the pool.\n        #[allow(clippy::type_complexity)]\n        let (prev, next, total, average, exits, day_of_week, duration_over_time): (\n            _,\n            _,\n            (i64,),\n            (f64,),\n            Vec<(i64, i64)>,\n            Vec<(String, i64)>,\n            Vec<(String, f64)>,\n        ) = tokio::try_join!(\n            sqlx::query(sqlx::AssertSqlSafe(prev))","sourceCodeStart":869,"sourceCodeEnd":905,"githubUrl":"https://github.com/atuinsh/atuin/blob/15fe1318f1df51de604262eb50734c9883d48e7b/crates/atuin-client/src/database.rs#L869-L905","documentation":"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'.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Confirm the panic is reproducible, then diff Cargo.lock for a sqlbuilder version change and restore the previously pinned version","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","Fix the swapped expect message (says 'stats average query' but guards the total query) so future reports are diagnosable","Report the builder state that fails to the sqlbuilder upstream issue tracker, including the exact fields/and_where calls used here"],"exampleFix":"// before (crates/atuin-client/src/database.rs:887, message is wrong for this query)\nlet total = total.sql().expect(\"issue in stats average query\");\n// after\nlet total = total.sql().expect(\"issue in stats total query\");","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// stats() panics only on builder/dependency regressions; isolate if you call it in-process\nlet stats = std::panic::catch_unwind(|| {\n    tokio::task::block_on(database.stats(&history))\n});\nmatch stats {\n    Ok(Ok(s)) => { /* use stats */ }\n    Ok(Err(e)) => tracing::error!(?e, \"stats query failed\"),\n    Err(_) => tracing::error!(\"stats query builder panicked — check sqlbuilder version\"),\n}","preventionTips":["Keep sqlbuilder locked to the version atuin was built against; review Cargo.lock diffs before deploying","Run a smoke `atuin stats` in CI after any dependency refresh","Report a panic here upstream with your sqlbuilder version"],"tags":["rust","sqlite","sqlbuilder","panic","invariant","stats","unreachable"],"backgroundTag":"query-builder-invalid-state","analyzedSha":"15fe1318f1df51de604262eb50734c9883d48e7b","analyzedAt":"2026-08-19T08:56:57.719Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}