atuinsh/atuin · error

failed to format sqlite date

Error message

failed to format sqlite date

What it means

This is a Rust panic from `.expect()` on `time::Date::format` inside `sort_duration_over_time` (inspector.rs:176-201), which prepares the "duration over time" bar chart in Atuin's TUI inspector. The `time` crate's `format` returns `Err` when the format description requires a component the type cannot supply — a `time::Date` only carries year/month/day, so any time-of-day or offset component makes formatting fail. Here the output format is the compile-time `format_description!("[month]/[year repr:last_two]")`, which `Date` fully supports, so the panic is a defensive assertion that only fires if the format is changed to something `Date` cannot render (its sibling at line 184 panics first if sqlite hands back a string that is not `DD-MM-YYYY`).

Source

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

    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()
}

fn draw_stats_charts(f: &mut Frame<'_>, parent: Rect, stats: &HistoryStats, theme: &Theme) {
    let exits: Vec<Bar> = stats
        .exits
        .iter()
        .map(|(exit, count)| {
            Bar::default()
                .label(exit.to_string())
                .value(u64_or_zero(*count))
        })
        .collect();

    let exits = BarChart::default()

View on GitHub (pinned to 202f6ad98e)

Solutions

  1. Keep the output format limited to date components (`[month]`, `[year]`, `[day]`, `[weekday]`) since the values are parsed as `time::Date`
  2. If time-of-day components are needed, parse into a `PrimitiveDateTime`/`OffsetDateTime` (e.g. midnight in `settings.timezone`) instead of `Date` before formatting
  3. Replace `.expect` with graceful handling: `date.format(output).unwrap_or_else(|e| { tracing::warn!(...); date.to_string() })` so one bad bucket cannot crash the whole TUI
  4. If the panic message is actually 'invalid date string from sqlite' (line 184), inspect the `duration_over_time` rows returned by `db.stats` for a legacy/corrupted date format and migrate the DB

Example fix

// before
let output = format_description!("[month]/[year repr:last_two] [hour]");
...
date.format(output).expect("failed to format sqlite date"),

// after
let output = format_description!("[month]/[year repr:last_two]");
...
date.format(output).unwrap_or_else(|e| {
    tracing::warn!("failed to format sqlite date {date}: {e}");
    date.to_string()
}),
Defensive patterns

Strategy: validation

Validate before calling

// Before charting, confirm every sqlite bucket parses with the input format
// (catches the sibling 'invalid date string from sqlite' panic early)
use time::parsing::Parsable;
let input = time::macros::format_description!("[day]-[month]-[year]");
let clean: Vec<_> = durations
    .iter()
    .filter(|(d, _)| time::Date::parse(d.as_str(), &input).is_ok())
    .collect();
assert_eq!(clean.len(), durations.len(), "unexpected sqlite date bucket");

Type guard

// Only date-level components are formattable on a time::Date;
// keep the whitelist explicit when the format is configurable.
const DATE_SAFE_COMPONENTS: [&str; 5] = ["year", "month", "day", "weekday", "week_number"];

Try / catch

// Treat formatting as fallible; degrade to a default rendering instead of panicking
let label = match date.format(output) {
    Ok(s) => s,
    Err(e) => {
        tracing::warn!("failed to format sqlite date {date}: {e}");
        date.to_string()
    }
};

Prevention

When it happens

Trigger: Calling `sort_duration_over_time(&stats.duration_over_time)` while drawing the inspector tab, where `stats.duration_over_time` comes from `db.stats(&selected)`; the `.expect` at line 196 trips only when `date.format(output)` errors, i.e. the `output` format_description was edited to include components a `Date` lacks (e.g. `[hour]`, `[minute]`, `[offset]`). Editing `output` to a time-aware format compiles fine (formats are runtime values) but panics on the first inspector draw.

Common situations: A contributor customizing the inspector chart labels (e.g. wanting `08/25 14:00`-style buckets) adds time components to `output` without switching from `time::Date` to `PrimitiveDateTime`/`OffsetDateTime`; or a DB written by a different/older Atuin version returns duration buckets whose date strings don't match `[day]-[month]-[year]` (hits the adjacent parse expect at line 184 with 'invalid date string from sqlite').

Related errors


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