{"record":{"id":"190b6dd114243e4a","repo":"atuinsh/atuin","slug":"invalid-date-string-from-sqlite","errorCode":null,"errorMessage":"invalid date string from sqlite","messagePattern":"invalid date string from sqlite","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin/src/command/client/search/inspector.rs","lineNumber":184,"sourceCode":"        \"1\" => \"Monday\".to_string(),\n        \"2\" => \"Tuesday\".to_string(),\n        \"3\" => \"Wednesday\".to_string(),\n        \"4\" => \"Thursday\".to_string(),\n        \"5\" => \"Friday\".to_string(),\n        \"6\" => \"Saturday\".to_string(),\n        _ => \"Invalid day\".to_string(),\n    }\n}\n\nfn sort_duration_over_time(durations: &[(String, i64)]) -> Vec<(String, i64)> {\n    let format = format_description!(\"[day]-[month]-[year]\");\n    let output = format_description!(\"[month]/[year repr:last_two]\");\n\n    let mut durations: Vec<(time::Date, i64)> = durations\n        .iter()\n        .map(|d| {\n            (\n                time::Date::parse(d.0.as_str(), &format).expect(\"invalid date string from sqlite\"),\n                d.1,\n            )\n        })\n        .collect();\n\n    durations.sort_by_key(|a| a.0);\n\n    durations\n        .iter()\n        .map(|(date, duration)| {\n            (\n                date.format(output).expect(\"failed to format sqlite date\"),\n                *duration,\n            )\n        })\n        .collect()\n}\n","sourceCodeStart":166,"sourceCodeEnd":202,"githubUrl":"https://github.com/atuinsh/atuin/blob/202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1/crates/atuin/src/command/client/search/inspector.rs#L166-L202","documentation":"The search inspector computes per-day duration stats: the sqlite layer produces day keys formatted dd-mm-yyyy, and sort_duration_over_time re-parses each with time::Date::parse using the [day]-[month]-[year] format description. Any key that does not match exactly (different separator, reordered fields, NULL rendered as empty, corrupt timestamp) makes the expect panic with 'invalid date string from sqlite'.","triggerScenarios":"sort_duration_over_time receives a first tuple element not shaped dd-mm-yyyy: history rows with NULL/garbage timestamps feeding the sqlite strftime grouping, a database written or edited by a different atuin version, or a change in the grouping SQL's date format.","commonSituations":"Imported or hand-edited history DBs; upgrades/downgrades that alter the stats query; rows with zero, negative, or missing timestamps.","solutions":["Run the inspector's day-grouping query directly against the history DB (sqlite3) and look for keys that are not dd-mm-yyyy or are NULL","Back up the DB, then fix or remove history rows with invalid timestamps","Ensure only one atuin version touches the DB at a time (stop the daemon while inspecting)"],"exampleFix":"// before\ntime::Date::parse(d.0.as_str(), &format).expect(\"invalid date string from sqlite\")\n\n// after — skip malformed days instead of crashing the inspector\nlet Ok(date) = time::Date::parse(d.0.as_str(), &format) else {\n    eprintln!(\"skipping malformed date from sqlite: {d:?}\");\n    continue;\n};","handlingStrategy":"validation","validationCode":"let format = time::macros::format_description!(\"[day]-[month]-[year]\");\nlet clean: Vec<_> = durations\n    .iter()\n    .filter(|(day, _)| time::Date::parse(day.as_str(), &format).is_ok())\n    .cloned()\n    .collect();\n// feed `clean` to sort_duration_over_time","typeGuard":"fn is_dd_mm_yyyy(s: &str) -> bool {\n    let mut it = s.split('-');\n    matches!(\n        (it.next(), it.next(), it.next(), it.next()),\n        (Some(d), Some(m), Some(y), None)\n            if d.len() == 2 && m.len() == 2 && y.len() == 4\n                && d.chars().chain(m.chars()).chain(y.chars()).all(|c| c.is_ascii_digit())\n    )\n}","tryCatchPattern":"let sorted = std::panic::catch_unwind(|| sort_duration_over_time(&durations));\nmatch sorted {\n    Ok(rows) => render(rows),\n    Err(_) => eprintln!(\"history DB contains malformed day rows; run the inspector on a repaired copy\"),\n}","preventionTips":["Never hand-edit history timestamps; keep the dd-mm-yyyy grouping format intact","Validate imported history for NULL or non-timestamp values","Run the inspector against a copy of the DB when data was edited externally"],"tags":["sqlite","date-parsing","time-crate","inspector","panic"],"backgroundTag":"date-string-parse-failed","analyzedSha":"202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1","analyzedAt":"2026-08-16T19:30:24.731Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}