{"record":{"id":"2acb5356f80c99f2","repo":"atuinsh/atuin","slug":"bug-in-search-query-please-report","errorCode":null,"errorMessage":"bug in search query. please report","messagePattern":"bug in search query\\. please report","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-client/src/database.rs","lineNumber":766,"sourceCode":"        }\n\n        if let Some(after) = filter_options.after {\n            let parsed =\n                interim::parse_date_string(after, OffsetDateTime::now_utc(), interim::Dialect::Uk)\n                    .map_err(|e| {\n                        sqlx::Error::Decode(format!(\"invalid `after` filter {after:?}: {e}\").into())\n                    })?;\n            sql.and_where_gt(\"timestamp\", quote(parsed.unix_timestamp_nanos() as i64));\n        }\n\n        apply_author_filter(&mut sql, filter_options.authors);\n        apply_shell_filter(&mut sql, filter_options.shells);\n\n        sql.and_where_is_null(\"deleted_at\");\n\n        // sql_builder inlines every bound value, so the inner query carries no\n        // positional parameters and is safe to embed (twice) as a derived table.\n        let inner = sql.sql().expect(\"bug in search query. please report\");\n        let inner = inner.trim().trim_end_matches(';');\n\n        let order = if filter_options.reverse {\n            \"ASC\"\n        } else {\n            \"DESC\"\n        };\n\n        let tail = match (filter_options.limit, filter_options.offset) {\n            (Some(limit), Some(offset)) => format!(\" LIMIT {limit} OFFSET {offset}\"),\n            (Some(limit), None) => format!(\" LIMIT {limit}\"),\n            // SQLite requires a LIMIT before OFFSET; -1 means \"no limit\".\n            (None, Some(offset)) => format!(\" LIMIT -1 OFFSET {offset}\"),\n            (None, None) => String::new(),\n        };\n\n        // Deduplicate by keeping, for each command, only its most recent entry\n        // within the filtered set. Expressed as a correlated NOT EXISTS rather","sourceCodeStart":748,"sourceCodeEnd":784,"githubUrl":"https://github.com/atuinsh/atuin/blob/202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1/crates/atuin-client/src/database.rs#L748-L784","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Update Atuin if you hit this as a user — the message means an internal bug, please report it with the stack trace","Developers: unit-test sql() rendering for every filter_options permutation before fetch","Keep bound values inlined via quote()/helpers so the derived-table embedding stays parameter-free, as the comment requires","Reproduce with a fixed seed database and bisect the filter that breaks rendering"],"exampleFix":"// before (development cause): a filter adds a positional param\nsql.and_where(\"timestamp > ?\");\n// after: inline the value as the codebase does\nsql.and_where_gt(\"timestamp\", quote(parsed.unix_timestamp_nanos() as i64));","handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":"// expect() panic: contain it only by isolating the call, e.g. a spawned thread:\nlet handle = std::thread::spawn(move || db.search(query, filter_options));\nmatch handle.join() {\n    Ok(Ok(rows)) => { /* use rows */ }\n    Ok(Err(e)) => { /* sqlx error */ }\n    Err(_) => { /* internal invariant panic — report upstream */ }\n}","preventionTips":["Update Atuin and report the panic; it marks an internal regression, not misuse","Developers: keep all bound values inlined (quote()) so the derived-table embedding stays parameter-free","Unit-test rendering of the inner query for every filter_options permutation","Smoke-test search with each filter (host/session/directory/workspace) after local builds"],"tags":["database","panic","invariant","sqlite","search","sql-builder","rust","atuin"],"backgroundTag":"internal-invariant-panic","analyzedSha":"202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1","analyzedAt":"2026-08-16T19:30:24.731Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}