atuinsh/atuin · error

invalid date string from sqlite

Error message

invalid date string from sqlite

What it means

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'.

Source

Thrown at crates/atuin/src/command/client/search/inspector.rs:184

        "1" => "Monday".to_string(),
        "2" => "Tuesday".to_string(),
        "3" => "Wednesday".to_string(),
        "4" => "Thursday".to_string(),
        "5" => "Friday".to_string(),
        "6" => "Saturday".to_string(),
        _ => "Invalid day".to_string(),
    }
}

fn sort_duration_over_time(durations: &[(String, i64)]) -> Vec<(String, i64)> {
    let format = format_description!("[day]-[month]-[year]");
    let output = format_description!("[month]/[year repr:last_two]");

    let mut durations: Vec<(time::Date, i64)> = durations
        .iter()
        .map(|d| {
            (
                time::Date::parse(d.0.as_str(), &format).expect("invalid date string from sqlite"),
                d.1,
            )
        })
        .collect();

    durations.sort_by_key(|a| a.0);

    durations
        .iter()
        .map(|(date, duration)| {
            (
                date.format(output).expect("failed to format sqlite date"),
                *duration,
            )
        })
        .collect()
}

View on GitHub (pinned to 202f6ad98e)

Solutions

  1. 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
  2. Back up the DB, then fix or remove history rows with invalid timestamps
  3. Ensure only one atuin version touches the DB at a time (stop the daemon while inspecting)

Example fix

// before
time::Date::parse(d.0.as_str(), &format).expect("invalid date string from sqlite")

// after — skip malformed days instead of crashing the inspector
let Ok(date) = time::Date::parse(d.0.as_str(), &format) else {
    eprintln!("skipping malformed date from sqlite: {d:?}");
    continue;
};
Defensive patterns

Strategy: validation

Validate before calling

let format = time::macros::format_description!("[day]-[month]-[year]");
let clean: Vec<_> = durations
    .iter()
    .filter(|(day, _)| time::Date::parse(day.as_str(), &format).is_ok())
    .cloned()
    .collect();
// feed `clean` to sort_duration_over_time

Type guard

fn is_dd_mm_yyyy(s: &str) -> bool {
    let mut it = s.split('-');
    matches!(
        (it.next(), it.next(), it.next(), it.next()),
        (Some(d), Some(m), Some(y), None)
            if d.len() == 2 && m.len() == 2 && y.len() == 4
                && d.chars().chain(m.chars()).chain(y.chars()).all(|c| c.is_ascii_digit())
    )
}

Try / catch

let sorted = std::panic::catch_unwind(|| sort_duration_over_time(&durations));
match sorted {
    Ok(rows) => render(rows),
    Err(_) => eprintln!("history DB contains malformed day rows; run the inspector on a repaired copy"),
}

Prevention

When it happens

Trigger: 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.

Common situations: Imported or hand-edited history DBs; upgrades/downgrades that alter the stats query; rows with zero, negative, or missing timestamps.

Related errors


AI-assisted analysis of atuinsh/atuin@202f6ad98e (2026-08-16). Data as JSON: /api/errors/190b6dd114243e4a. Report an issue: GitHub.