{"record":{"id":"9b4540487fa76edc","repo":"atuinsh/atuin","slug":"issue-in-stats-exits-query","errorCode":null,"errorMessage":"issue in stats exits query","messagePattern":"issue in stats exits query","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-client/src/database.rs","lineNumber":889,"sourceCode":"        // 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))\n                .bind(h.timestamp.unix_timestamp_nanos() as i64)\n                .bind(&h.session)","sourceCodeStart":871,"sourceCodeEnd":907,"githubUrl":"https://github.com/atuinsh/atuin/blob/15fe1318f1df51de604262eb50734c9883d48e7b/crates/atuin-client/src/database.rs#L871-L907","documentation":"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.","triggerScenarios":"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.","commonSituations":"Semver-drifted sqlbuilder after a dependency refresh; building with a forked sqlbuilder. Not reproducible on the pinned workspace versions regardless of DB state.","solutions":["Restore the pinned sqlbuilder version in Cargo.lock (this is the only realistic cause)","Guard dependency upgrades with a unit test that builds all seven stats queries and asserts .sql() returns Ok","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"let stats = std::panic::catch_unwind(|| {\n    tokio::task::block_on(database.stats(&history))\n});\nlet _ = stats.map_err(|_| tracing::error!(\"stats exits histogram builder panicked\"));","preventionTips":["Freeze sqlbuilder in CI builds; breakage here only follows dependency changes","Add compile-assert unit tests for each stats builder when upgrading","Never assume database contents can trigger this — it cannot"],"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"}