atuinsh/atuin · error

bug in search query. please report

Error message

bug in search query. please report

What it means

A panic in Database::search when SqlBuilder::sql() fails to render the inner query used for full-text search pagination. The builder assembles the FTS match, context filters, the optional 'after' timestamp bound, author/shell filters, and deleted_at IS NULL, then embeds the rendered SQL twice as a derived table (which is why it must render cleanly and without positional parameters). The expect fires only when builder misuse leaves it in an unrenderable state — an Atuin code bug, not something user data or settings can trigger.

Source

Thrown at crates/atuin-client/src/database.rs:766

        }

        if let Some(after) = filter_options.after {
            let parsed =
                interim::parse_date_string(after, OffsetDateTime::now_utc(), interim::Dialect::Uk)
                    .map_err(|e| {
                        sqlx::Error::Decode(format!("invalid `after` filter {after:?}: {e}").into())
                    })?;
            sql.and_where_gt("timestamp", quote(parsed.unix_timestamp_nanos() as i64));
        }

        apply_author_filter(&mut sql, filter_options.authors);
        apply_shell_filter(&mut sql, filter_options.shells);

        sql.and_where_is_null("deleted_at");

        // sql_builder inlines every bound value, so the inner query carries no
        // positional parameters and is safe to embed (twice) as a derived table.
        let inner = sql.sql().expect("bug in search query. please report");
        let inner = inner.trim().trim_end_matches(';');

        let order = if filter_options.reverse {
            "ASC"
        } else {
            "DESC"
        };

        let tail = match (filter_options.limit, filter_options.offset) {
            (Some(limit), Some(offset)) => format!(" LIMIT {limit} OFFSET {offset}"),
            (Some(limit), None) => format!(" LIMIT {limit}"),
            // SQLite requires a LIMIT before OFFSET; -1 means "no limit".
            (None, Some(offset)) => format!(" LIMIT -1 OFFSET {offset}"),
            (None, None) => String::new(),
        };

        // Deduplicate by keeping, for each command, only its most recent entry
        // within the filtered set. Expressed as a correlated NOT EXISTS rather

View on GitHub (pinned to 202f6ad98e)

Solutions

  1. Update Atuin if you hit this as a user — the message means an internal bug, please report it with the stack trace
  2. Developers: unit-test sql() rendering for every filter_options permutation before fetch
  3. Keep bound values inlined via quote()/helpers so the derived-table embedding stays parameter-free, as the comment requires
  4. Reproduce with a fixed seed database and bisect the filter that breaks rendering

Example fix

// before (development cause): a filter adds a positional param
sql.and_where("timestamp > ?");
// after: inline the value as the codebase does
sql.and_where_gt("timestamp", quote(parsed.unix_timestamp_nanos() as i64));
Defensive patterns

Strategy: validation

Try / catch

// expect() panic: contain it only by isolating the call, e.g. a spawned thread:
let handle = std::thread::spawn(move || db.search(query, filter_options));
match handle.join() {
    Ok(Ok(rows)) => { /* use rows */ }
    Ok(Err(e)) => { /* sqlx error */ }
    Err(_) => { /* internal invariant panic — report upstream */ }
}

Prevention

When it happens

Trigger: Calling Database::search with any query/filter_options combination; the panic requires a regression such as a new filter helper that invalidates the builder or leaves positional '?' placeholders (which the comment warns the derived-table embedding cannot tolerate).

Common situations: Hit by Atuin developers extending search filters (authors/shells/after were recent additions) whose helper mutates the builder incorrectly; never observed from config in released builds.

Related errors


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