Allow history searching via session id (#562)

* add the ability to search history with session id

* clippy
This commit is contained in:
Darren Schroeder 2023-04-18 07:13:49 -05:00 committed by GitHub
parent 27f4417191
commit 61c6409fb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 233 additions and 73 deletions

View File

@ -66,7 +66,17 @@ fn main() -> Result<()> {
emacs: None,
};
// Setting history_per_session to true will allow the history to be isolated to the current session
// Setting history_per_session to false will allow the history to be shared across all sessions
let history_per_session = false;
let mut history_session_id = if history_per_session {
Reedline::create_history_session_id()
} else {
None
};
let mut line_editor = Reedline::create()
.with_history_session_id(history_session_id)
.with_history(history)
.with_completer(completer)
.with_quick_completions(true)
@ -145,10 +155,34 @@ fn main() -> Result<()> {
line_editor.clear_scrollback()?;
continue;
}
// Get the full history
if buffer.trim() == "history" {
line_editor.print_history()?;
continue;
}
// Get the history only pertinent to the current session
if buffer.trim() == "history session" {
line_editor.print_history_session()?;
continue;
}
// Get this history session identifier
if buffer.trim() == "history sessionid" {
line_editor.print_history_session_id()?;
continue;
}
// Toggle between the full history and the history pertinent to the current session
if buffer.trim() == "toggle history_session" {
let hist_session_id = if history_session_id.is_none() {
// If we never created a history session ID, create one now
let sesh = Reedline::create_history_session_id();
history_session_id = sesh;
sesh
} else {
history_session_id
};
line_editor.toggle_history_session_matching(hist_session_id)?;
continue;
}
if buffer.trim() == "clear-history" {
let hstry = Box::new(line_editor.history_mut());
hstry

View File

@ -158,14 +158,15 @@ impl Reedline {
let hinter = None;
let validator = None;
let edit_mode = Box::<Emacs>::default();
let hist_session_id = Self::create_history_session_id();
let hist_session_id = None;
Reedline {
editor: Editor::default(),
history,
history_cursor: HistoryCursor::new(HistoryNavigationQuery::Normal(
LineBuffer::default(),
)),
history_cursor: HistoryCursor::new(
HistoryNavigationQuery::Normal(LineBuffer::default()),
hist_session_id,
),
history_session_id: hist_session_id,
history_last_run_id: None,
input_mode: InputMode::Regular,
@ -188,7 +189,7 @@ impl Reedline {
}
/// Get a new history session id based on the current time and the first commit datetime of reedline
fn create_history_session_id() -> Option<HistorySessionId> {
pub fn create_history_session_id() -> Option<HistorySessionId> {
let nanos = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(n) => n.as_nanos() as i64,
Err(_) => 0,
@ -202,6 +203,14 @@ impl Reedline {
self.history_session_id
}
/// Set a new history session id
/// This should be used in situations where the user initially did not have a history_session_id
/// and then later realized they want to have one without restarting the application.
pub fn set_history_session_id(&mut self, session: Option<HistorySessionId>) -> Result<()> {
self.history_session_id = session;
Ok(())
}
/// A builder to include a [`Hinter`] in your instance of the Reedline engine
/// # Example
/// ```rust
@ -380,6 +389,13 @@ impl Reedline {
self
}
/// A builder that adds the history item id
#[must_use]
pub fn with_history_session_id(mut self, session: Option<HistorySessionId>) -> Self {
self.history_session_id = session;
self
}
/// A builder that enables reedline changing the cursor shape based on the current edit mode.
/// The current implementation sets the cursor shape when drawing the prompt.
/// Do not use this if the cursor shape is set elsewhere, e.g. in the terminal settings or by ansi escape sequences.
@ -397,7 +413,7 @@ impl Reedline {
pub fn print_history(&mut self) -> Result<()> {
let history: Vec<_> = self
.history
.search(SearchQuery::everything(SearchDirection::Forward))
.search(SearchQuery::everything(SearchDirection::Forward, None))
.expect("todo: error handling");
for (i, entry) in history.iter().enumerate() {
@ -406,6 +422,40 @@ impl Reedline {
Ok(())
}
/// Output the complete [`History`] for this session, chronologically with numbering to the terminal
pub fn print_history_session(&mut self) -> Result<()> {
let history: Vec<_> = self
.history
.search(SearchQuery::everything(
SearchDirection::Forward,
self.get_history_session_id(),
))
.expect("todo: error handling");
for (i, entry) in history.iter().enumerate() {
self.print_line(&format!("{}\t{}", i, entry.command_line))?;
}
Ok(())
}
/// Print the history session id
pub fn print_history_session_id(&mut self) -> Result<()> {
println!("History Session Id: {:?}", self.get_history_session_id());
Ok(())
}
/// Toggle between having a history that uses the history session id and one that does not
pub fn toggle_history_session_matching(
&mut self,
session: Option<HistorySessionId>,
) -> Result<()> {
self.history_session_id = match self.get_history_session_id() {
Some(_) => None,
None => session,
};
Ok(())
}
/// Read-only view of the history
pub fn history(&self) -> &dyn History {
&*self.history
@ -1066,8 +1116,10 @@ impl Reedline {
fn previous_history(&mut self) {
if self.input_mode != InputMode::HistoryTraversal {
self.input_mode = InputMode::HistoryTraversal;
self.history_cursor =
HistoryCursor::new(self.get_history_navigation_based_on_line_buffer());
self.history_cursor = HistoryCursor::new(
self.get_history_navigation_based_on_line_buffer(),
self.get_history_session_id(),
);
}
self.history_cursor
@ -1082,8 +1134,10 @@ impl Reedline {
fn next_history(&mut self) {
if self.input_mode != InputMode::HistoryTraversal {
self.input_mode = InputMode::HistoryTraversal;
self.history_cursor =
HistoryCursor::new(self.get_history_navigation_based_on_line_buffer());
self.history_cursor = HistoryCursor::new(
self.get_history_navigation_based_on_line_buffer(),
self.get_history_session_id(),
);
}
self.history_cursor
@ -1118,8 +1172,10 @@ impl Reedline {
///
/// This mode uses a separate prompt and handles keybindings slightly differently!
fn enter_history_search(&mut self) {
self.history_cursor =
HistoryCursor::new(HistoryNavigationQuery::SubstringSearch("".to_string()));
self.history_cursor = HistoryCursor::new(
HistoryNavigationQuery::SubstringSearch("".to_string()),
self.get_history_session_id(),
);
self.input_mode = InputMode::HistorySearch;
}
@ -1133,11 +1189,14 @@ impl Reedline {
let navigation = self.history_cursor.get_navigation();
if let HistoryNavigationQuery::SubstringSearch(mut substring) = navigation {
substring.push(*c);
self.history_cursor =
HistoryCursor::new(HistoryNavigationQuery::SubstringSearch(substring));
self.history_cursor = HistoryCursor::new(
HistoryNavigationQuery::SubstringSearch(substring),
self.get_history_session_id(),
);
} else {
self.history_cursor = HistoryCursor::new(
HistoryNavigationQuery::SubstringSearch(String::from(*c)),
self.get_history_session_id(),
);
}
self.history_cursor
@ -1152,6 +1211,7 @@ impl Reedline {
self.history_cursor = HistoryCursor::new(
HistoryNavigationQuery::SubstringSearch(new_substring.to_string()),
self.get_history_session_id(),
);
self.history_cursor
.back(self.history.as_mut())
@ -1275,7 +1335,7 @@ impl Reedline {
start_id: None,
end_id: None,
limit: Some(1), // fetch the latest one entries
filter: SearchFilter::anything(),
filter: SearchFilter::anything(self.get_history_session_id()),
})
.unwrap_or_else(|_| Vec::new())
.get(index.saturating_sub(1))
@ -1295,7 +1355,7 @@ impl Reedline {
start_id: None,
end_id: None,
limit: Some(index as i64), // fetch the latest n entries
filter: SearchFilter::anything(),
filter: SearchFilter::anything(self.get_history_session_id()),
})
.unwrap_or_else(|_| Vec::new())
.get(index.saturating_sub(1))
@ -1315,7 +1375,7 @@ impl Reedline {
start_id: None,
end_id: None,
limit: Some((index + 1) as i64), // fetch the oldest n entries
filter: SearchFilter::anything(),
filter: SearchFilter::anything(self.get_history_session_id()),
})
.unwrap_or_else(|_| Vec::new())
.get(index)
@ -1328,7 +1388,9 @@ impl Reedline {
}),
ParseAction::LastToken => self
.history
.search(SearchQuery::last_with_search(SearchFilter::anything()))
.search(SearchQuery::last_with_search(SearchFilter::anything(
self.get_history_session_id(),
)))
.unwrap_or_else(|_| Vec::new())
.get(0)
.and_then(|history| history.command_line.split_whitespace().rev().next())
@ -1533,7 +1595,7 @@ impl Reedline {
self.repaint(prompt)?;
if !buffer.is_empty() {
let mut entry = HistoryItem::from_command_line(&buffer);
entry.session_id = self.history_session_id;
entry.session_id = self.get_history_session_id();
let entry = self.history.save(entry).expect("todo: error handling");
self.history_last_run_id = entry.id;
}

View File

@ -20,7 +20,10 @@ impl Hinter for DefaultHinter {
) -> String {
self.current_hint = if line.chars().count() >= self.min_chars {
history
.search(SearchQuery::last_with_prefix(line.to_string()))
.search(SearchQuery::last_with_prefix(
line.to_string(),
history.session(),
))
.expect("todo: error handling")
.get(0)
.map_or_else(String::new, |entry| {

View File

@ -1,8 +1,6 @@
use chrono::Utc;
use crate::{core_editor::LineBuffer, HistoryItem, Result};
use super::HistoryItemId;
use crate::{core_editor::LineBuffer, HistoryItem, HistorySessionId, Result};
use chrono::Utc;
/// Browsing modes for a [`History`]
#[derive(Debug, Clone, PartialEq, Eq)]
@ -53,18 +51,23 @@ pub struct SearchFilter {
pub cwd_prefix: Option<String>,
/// Filter whether the command completed
pub exit_successful: Option<bool>,
/// Filter on the session id
pub session: Option<HistorySessionId>,
}
impl SearchFilter {
/// Create a search filter with a [`CommandLineSearch`]
pub fn from_text_search(cmd: CommandLineSearch) -> SearchFilter {
let mut s = SearchFilter::anything();
pub fn from_text_search(
cmd: CommandLineSearch,
session: Option<HistorySessionId>,
) -> SearchFilter {
let mut s = SearchFilter::anything(session);
s.command_line = Some(cmd);
s
}
/// No filter constraint
pub const fn anything() -> SearchFilter {
/// anything within this session
pub fn anything(session: Option<HistorySessionId>) -> SearchFilter {
SearchFilter {
command_line: None,
not_command_line: None,
@ -72,6 +75,7 @@ impl SearchFilter {
cwd_exact: None,
cwd_prefix: None,
exit_successful: None,
session,
}
}
}
@ -105,7 +109,7 @@ impl SearchQuery {
start_id: None,
end_id: None,
limit: None,
filter: SearchFilter::from_text_search(CommandLineSearch::Substring(contains)),
filter: SearchFilter::from_text_search(CommandLineSearch::Substring(contains), None),
}
}
@ -123,14 +127,18 @@ impl SearchQuery {
}
/// Get the most recent entry starting with the `prefix`
pub fn last_with_prefix(prefix: String) -> SearchQuery {
SearchQuery::last_with_search(SearchFilter::from_text_search(CommandLineSearch::Prefix(
prefix,
)))
pub fn last_with_prefix(prefix: String, session: Option<HistorySessionId>) -> SearchQuery {
SearchQuery::last_with_search(SearchFilter::from_text_search(
CommandLineSearch::Prefix(prefix),
session,
))
}
/// Query to get all entries in the given [`SearchDirection`]
pub const fn everything(direction: SearchDirection) -> SearchQuery {
pub fn everything(
direction: SearchDirection,
session: Option<HistorySessionId>,
) -> SearchQuery {
SearchQuery {
direction,
start_time: None,
@ -138,7 +146,7 @@ impl SearchQuery {
start_id: None,
end_id: None,
limit: None,
filter: SearchFilter::anything(),
filter: SearchFilter::anything(session),
}
}
}
@ -159,7 +167,7 @@ pub trait History: Send {
fn count(&self, query: SearchQuery) -> Result<i64>;
/// return the total number of history items
fn count_all(&self) -> Result<i64> {
self.count(SearchQuery::everything(SearchDirection::Forward))
self.count(SearchQuery::everything(SearchDirection::Forward, None))
}
/// return the results of a query
fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>>;
@ -176,6 +184,8 @@ pub trait History: Send {
fn delete(&mut self, h: HistoryItemId) -> Result<()>;
/// ensure that this history is written to disk
fn sync(&mut self) -> std::io::Result<()>;
/// get the history session id
fn session(&self) -> Option<HistorySessionId>;
}
#[cfg(test)]
@ -265,7 +275,7 @@ mod test {
let history = create_filled_example_history()?;
println!(
"{:#?}",
history.search(SearchQuery::everything(SearchDirection::Forward))
history.search(SearchQuery::everything(SearchDirection::Forward, None))
);
assert_eq!(history.count_all()?, if IS_FILE_BASED { 13 } else { 12 });
@ -275,7 +285,7 @@ mod test {
#[test]
fn get_latest() -> Result<()> {
let history = create_filled_example_history()?;
let res = history.search(SearchQuery::last_with_search(SearchFilter::anything()))?;
let res = history.search(SearchQuery::last_with_search(SearchFilter::anything(None)))?;
search_returned(&*history, res, vec![12])?;
Ok(())
@ -286,7 +296,7 @@ mod test {
let history = create_filled_example_history()?;
let res = history.search(SearchQuery {
limit: Some(1),
..SearchQuery::everything(SearchDirection::Forward)
..SearchQuery::everything(SearchDirection::Forward, None)
})?;
search_returned(&*history, res, vec![if IS_FILE_BASED { 0 } else { 1 }])?;
Ok(())
@ -296,8 +306,11 @@ mod test {
fn search_prefix() -> Result<()> {
let history = create_filled_example_history()?;
let res = history.search(SearchQuery {
filter: SearchFilter::from_text_search(CommandLineSearch::Prefix("ls ".to_string())),
..SearchQuery::everything(SearchDirection::Backward)
filter: SearchFilter::from_text_search(
CommandLineSearch::Prefix("ls ".to_string()),
None,
),
..SearchQuery::everything(SearchDirection::Backward, None)
})?;
search_returned(&*history, res, vec![9, 6])?;
@ -308,10 +321,11 @@ mod test {
fn search_includes() -> Result<()> {
let history = create_filled_example_history()?;
let res = history.search(SearchQuery {
filter: SearchFilter::from_text_search(CommandLineSearch::Substring(
"foo.zip".to_string(),
)),
..SearchQuery::everything(SearchDirection::Forward)
filter: SearchFilter::from_text_search(
CommandLineSearch::Substring("foo.zip".to_string()),
None,
),
..SearchQuery::everything(SearchDirection::Forward, None)
})?;
search_returned(&*history, res, vec![2, 3])?;
Ok(())
@ -321,9 +335,12 @@ mod test {
fn search_includes_limit() -> Result<()> {
let history = create_filled_example_history()?;
let res = history.search(SearchQuery {
filter: SearchFilter::from_text_search(CommandLineSearch::Substring("c".to_string())),
filter: SearchFilter::from_text_search(
CommandLineSearch::Substring("c".to_string()),
None,
),
limit: Some(2),
..SearchQuery::everything(SearchDirection::Forward)
..SearchQuery::everything(SearchDirection::Forward, None)
})?;
search_returned(&*history, res, vec![1, 4])?;

View File

@ -1,4 +1,4 @@
use crate::{History, HistoryNavigationQuery};
use crate::{History, HistoryNavigationQuery, HistorySessionId};
use super::base::CommandLineSearch;
use super::base::SearchDirection;
@ -13,14 +13,16 @@ pub struct HistoryCursor {
query: HistoryNavigationQuery,
current: Option<HistoryItem>,
skip_dupes: bool,
session: Option<HistorySessionId>,
}
impl HistoryCursor {
pub const fn new(query: HistoryNavigationQuery) -> HistoryCursor {
pub fn new(query: HistoryNavigationQuery, session: Option<HistorySessionId>) -> HistoryCursor {
HistoryCursor {
query,
current: None,
skip_dupes: true,
session,
}
}
@ -38,13 +40,14 @@ impl HistoryCursor {
fn get_search_filter(&self) -> SearchFilter {
let filter = match self.query.clone() {
HistoryNavigationQuery::Normal(_) => SearchFilter::anything(),
HistoryNavigationQuery::Normal(_) => SearchFilter::anything(self.session),
HistoryNavigationQuery::PrefixSearch(prefix) => {
SearchFilter::from_text_search(CommandLineSearch::Prefix(prefix))
}
HistoryNavigationQuery::SubstringSearch(substring) => {
SearchFilter::from_text_search(CommandLineSearch::Substring(substring))
SearchFilter::from_text_search(CommandLineSearch::Prefix(prefix), self.session)
}
HistoryNavigationQuery::SubstringSearch(substring) => SearchFilter::from_text_search(
CommandLineSearch::Substring(substring),
self.session,
),
};
if let (true, Some(current)) = (self.skip_dupes, &self.current) {
SearchFilter {
@ -112,20 +115,20 @@ mod tests {
let hist = Box::<FileBackedHistory>::default();
(
hist,
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default())),
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
)
}
fn create_history_at(cap: usize, path: &Path) -> (Box<dyn History>, HistoryCursor) {
let hist = Box::new(FileBackedHistory::with_file(cap, path.to_owned()).unwrap());
(
hist,
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default())),
HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None),
)
}
fn get_all_entry_texts(hist: &dyn History) -> Vec<String> {
let res = hist
.search(SearchQuery::everything(SearchDirection::Forward))
.search(SearchQuery::everything(SearchDirection::Forward, None))
.unwrap();
let actual: Vec<_> = res.iter().map(|e| e.command_line.to_string()).collect();
actual
@ -207,8 +210,10 @@ mod tests {
hist.save(HistoryItem::from_command_line("test"))?;
hist.save(HistoryItem::from_command_line("find me"))?;
let mut cursor =
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
let mut cursor = HistoryCursor::new(
HistoryNavigationQuery::PrefixSearch("find".to_string()),
None,
);
cursor.back(&*hist)?;
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
@ -227,8 +232,10 @@ mod tests {
hist.save(HistoryItem::from_command_line("test"))?;
hist.save(HistoryItem::from_command_line("find me"))?;
let mut cursor =
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
let mut cursor = HistoryCursor::new(
HistoryNavigationQuery::PrefixSearch("find".to_string()),
None,
);
cursor.back(&*hist)?;
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
cursor.back(&*hist)?;
@ -253,8 +260,10 @@ mod tests {
hist.save(HistoryItem::from_command_line("test"))?;
hist.save(HistoryItem::from_command_line("find me"))?;
let mut cursor =
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
let mut cursor = HistoryCursor::new(
HistoryNavigationQuery::PrefixSearch("find".to_string()),
None,
);
cursor.back(&*hist)?;
assert_eq!(cursor.string_at_cursor(), Some("find me".to_string()));
cursor.back(&*hist)?;
@ -279,8 +288,10 @@ mod tests {
hist.save(HistoryItem::from_command_line("test"))?;
hist.save(HistoryItem::from_command_line("find me once"))?;
let mut cursor =
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
let mut cursor = HistoryCursor::new(
HistoryNavigationQuery::PrefixSearch("find".to_string()),
None,
);
cursor.back(&*hist)?;
assert_eq!(cursor.string_at_cursor(), Some("find me once".to_string()));
cursor.back(&*hist)?;
@ -299,8 +310,10 @@ mod tests {
hist.save(HistoryItem::from_command_line("find me once"))?;
hist.save(HistoryItem::from_command_line("find me as well"))?;
let mut cursor =
HistoryCursor::new(HistoryNavigationQuery::PrefixSearch("find".to_string()));
let mut cursor = HistoryCursor::new(
HistoryNavigationQuery::PrefixSearch("find".to_string()),
None,
);
cursor.back(&*hist)?;
assert_eq!(
cursor.string_at_cursor(),
@ -328,9 +341,10 @@ mod tests {
hist.save(HistoryItem::from_command_line("don't find me"))?;
hist.save(HistoryItem::from_command_line("prefix substring suffix"))?;
let mut cursor = HistoryCursor::new(HistoryNavigationQuery::SubstringSearch(
"substring".to_string(),
));
let mut cursor = HistoryCursor::new(
HistoryNavigationQuery::SubstringSearch("substring".to_string()),
None,
);
cursor.back(&*hist)?;
assert_eq!(
cursor.string_at_cursor(),
@ -351,7 +365,10 @@ mod tests {
let (mut hist, _) = create_history();
hist.save(HistoryItem::from_command_line("substring"))?;
let cursor = HistoryCursor::new(HistoryNavigationQuery::SubstringSearch("".to_string()));
let cursor = HistoryCursor::new(
HistoryNavigationQuery::SubstringSearch("".to_string()),
None,
);
assert_eq!(cursor.string_at_cursor(), None);
Ok(())

View File

@ -3,7 +3,7 @@ use super::{
};
use crate::{
result::{ReedlineError, ReedlineErrorVariants},
Result,
HistorySessionId, Result,
};
use std::{
@ -30,6 +30,7 @@ pub struct FileBackedHistory {
entries: VecDeque<String>,
file: Option<PathBuf>,
len_on_disk: usize, // Keep track what was previously written to disk
session: Option<HistorySessionId>,
}
impl Default for FileBackedHistory {
@ -268,6 +269,10 @@ impl History for FileBackedHistory {
}
Ok(())
}
fn session(&self) -> Option<HistorySessionId> {
self.session
}
}
impl FileBackedHistory {
@ -285,6 +290,7 @@ impl FileBackedHistory {
entries: VecDeque::new(),
file: None,
len_on_disk: 0,
session: None,
}
}

View File

@ -1,4 +1,6 @@
use chrono::Utc;
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
use rusqlite::ToSql;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{fmt::Display, time::Duration};
@ -32,6 +34,15 @@ impl Display for HistorySessionId {
}
}
#[cfg(any(feature = "sqlite", feature = "sqlite-dynlib"))]
impl ToSql for HistorySessionId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::Owned(
rusqlite::types::Value::Integer(self.0),
))
}
}
impl From<HistorySessionId> for i64 {
fn from(id: HistorySessionId) -> Self {
id.0

View File

@ -16,6 +16,7 @@ const SQLITE_APPLICATION_ID: i32 = 1151497937;
/// to add information such as a timestamp, running directory, result...
pub struct SqliteBackedHistory {
db: rusqlite::Connection,
session: Option<HistorySessionId>,
}
fn deserialize_history_item(row: &rusqlite::Row) -> rusqlite::Result<HistoryItem> {
@ -167,6 +168,10 @@ impl History for SqliteBackedHistory {
// no-op (todo?)
Ok(())
}
fn session(&self) -> Option<HistorySessionId> {
self.session
}
}
fn map_sqlite_err(err: rusqlite::Error) -> ReedlineError {
// TODO: better error mapping
@ -243,8 +248,9 @@ impl SqliteBackedHistory {
",
)
.map_err(map_sqlite_err)?;
Ok(SqliteBackedHistory { db })
Ok(SqliteBackedHistory { db, session: None })
}
fn construct_query<'a>(
&self,
query: &'a SearchQuery,
@ -331,6 +337,10 @@ impl SqliteBackedHistory {
wheres.push("exit_status != 0");
}
}
if let Some(session_id) = query.filter.session {
wheres.push("session_id = :session_id");
params.push((":session_id", Box::new(session_id)));
}
let mut wheres = wheres.join(" and ");
if wheres.is_empty() {
wheres = "true".to_string();