diff --git a/examples/demo.rs b/examples/demo.rs index b5d6066..cc5200c 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -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 diff --git a/src/engine.rs b/src/engine.rs index cc13b8e..08cf5c1 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -158,14 +158,15 @@ impl Reedline { let hinter = None; let validator = None; let edit_mode = Box::::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 { + pub fn create_history_session_id() -> Option { 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) -> 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) -> 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, + ) -> 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; } diff --git a/src/hinter/default.rs b/src/hinter/default.rs index 9622083..e1aa31e 100644 --- a/src/hinter/default.rs +++ b/src/hinter/default.rs @@ -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| { diff --git a/src/history/base.rs b/src/history/base.rs index 798d321..ba74b55 100644 --- a/src/history/base.rs +++ b/src/history/base.rs @@ -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, /// Filter whether the command completed pub exit_successful: Option, + /// Filter on the session id + pub session: Option, } 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, + ) -> 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) -> 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) -> 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, + ) -> 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; /// return the total number of history items fn count_all(&self) -> Result { - 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>; @@ -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; } #[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])?; diff --git a/src/history/cursor.rs b/src/history/cursor.rs index 8181a23..ab41ba4 100644 --- a/src/history/cursor.rs +++ b/src/history/cursor.rs @@ -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, skip_dupes: bool, + session: Option, } impl HistoryCursor { - pub const fn new(query: HistoryNavigationQuery) -> HistoryCursor { + pub fn new(query: HistoryNavigationQuery, session: Option) -> 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::::default(); ( hist, - HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default())), + HistoryCursor::new(HistoryNavigationQuery::Normal(LineBuffer::default()), None), ) } fn create_history_at(cap: usize, path: &Path) -> (Box, 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 { 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(()) diff --git a/src/history/file_backed.rs b/src/history/file_backed.rs index ca4d491..d048fe1 100644 --- a/src/history/file_backed.rs +++ b/src/history/file_backed.rs @@ -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, file: Option, len_on_disk: usize, // Keep track what was previously written to disk + session: Option, } impl Default for FileBackedHistory { @@ -268,6 +269,10 @@ impl History for FileBackedHistory { } Ok(()) } + + fn session(&self) -> Option { + self.session + } } impl FileBackedHistory { @@ -285,6 +290,7 @@ impl FileBackedHistory { entries: VecDeque::new(), file: None, len_on_disk: 0, + session: None, } } diff --git a/src/history/item.rs b/src/history/item.rs index c4f4c19..79cd4b5 100644 --- a/src/history/item.rs +++ b/src/history/item.rs @@ -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> { + Ok(rusqlite::types::ToSqlOutput::Owned( + rusqlite::types::Value::Integer(self.0), + )) + } +} + impl From for i64 { fn from(id: HistorySessionId) -> Self { id.0 diff --git a/src/history/sqlite_backed.rs b/src/history/sqlite_backed.rs index 5108441..8b0c5b5 100644 --- a/src/history/sqlite_backed.rs +++ b/src/history/sqlite_backed.rs @@ -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, } fn deserialize_history_item(row: &rusqlite::Row) -> rusqlite::Result { @@ -167,6 +168,10 @@ impl History for SqliteBackedHistory { // no-op (todo?) Ok(()) } + + fn session(&self) -> Option { + 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();