mod receipt;
mod repair;
mod runtime;
use super::artifact::{self, HistoryKind};
use super::metadata::HistoryExtraInfo;
use reedline::{
History, HistoryItem, HistoryItemExtraInfo, HistoryItemId, HistorySessionId, Result,
SearchQuery, SqliteBackedHistory,
};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
pub use receipt::{HistorySaveOutcome, HistorySaveReceipt};
pub use runtime::{HistoryFailureDetail, HistoryHandle, HistoryRuntime, VolatileHistoryReason};
#[derive(Clone)]
pub struct HistoryStore {
inner: Arc<Mutex<SqliteBackedHistory>>,
path: Option<PathBuf>,
session: Option<HistorySessionId>,
}
impl HistoryStore {
pub(crate) fn open(
path: PathBuf,
history_kind: HistoryKind,
session_id: Option<HistorySessionId>,
session_timestamp: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<Self> {
artifact::prepare_history_path(&path, history_kind).map_err(|error| {
reedline::ReedlineError(reedline::ReedlineErrorVariants::HistoryDatabaseError(
format!("invalid history database artifact: {error:#}"),
))
})?;
let history = SqliteBackedHistory::with_file(path.clone(), session_id, session_timestamp)?;
Ok(Self {
inner: Arc::new(Mutex::new(history)),
path: Some(path),
session: session_id,
})
}
pub fn in_memory(
session_id: Option<HistorySessionId>,
_session_timestamp: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<Self> {
Ok(Self {
inner: Arc::new(Mutex::new(SqliteBackedHistory::in_memory()?)),
path: None,
session: session_id,
})
}
pub(crate) fn same_owner(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
}
pub(crate) fn path(&self) -> Option<&std::path::Path> {
self.path.as_deref()
}
pub fn save_unknown(&self, item: HistoryItem) -> Result<HistoryItem> {
let mut item = item;
if item.session_id.is_none() {
item.session_id = self.session;
}
let typed = convert_history_item(item, None::<HistoryExtraInfo>);
let saved = self
.inner
.lock()
.map_err(|_| lock_error())?
.save_with_extra(typed)?;
Ok(convert_history_item(saved, None))
}
pub fn save_known(&self, item: HistoryItem, metadata: HistoryExtraInfo) -> Result<HistoryItem> {
let mut item = item;
if item.session_id.is_none() {
item.session_id = self.session;
}
let typed = convert_history_item(item, Some(metadata));
let saved = self
.inner
.lock()
.map_err(|_| lock_error())?
.save_with_extra(typed)?;
Ok(convert_history_item(saved, None))
}
pub(crate) fn save_imported(
&self,
item: HistoryItem<HistoryExtraInfo>,
) -> Result<HistoryItem<HistoryExtraInfo>> {
let mut item = item;
if item.session_id.is_none() {
item.session_id = self.session;
}
self.inner
.lock()
.map_err(|_| lock_error())?
.save_with_extra(item)
}
pub fn finalize_meta_command(&self, id: HistoryItemId, is_meta_command: bool) -> Result<()> {
self.inner
.lock()
.map_err(|_| lock_error())?
.update_with_extra::<HistoryExtraInfo>(id, &|mut item| {
let mut metadata: HistoryExtraInfo = item.more_info.unwrap_or_default();
metadata.set_meta_command(is_meta_command);
item.more_info = Some(metadata);
item
})
}
#[allow(dead_code)]
pub fn set_metadata(&self, id: HistoryItemId, metadata: HistoryExtraInfo) -> Result<()> {
self.inner
.lock()
.map_err(|_| lock_error())?
.update_with_extra::<HistoryExtraInfo>(id, &|mut item| {
item.more_info = Some(metadata.clone());
item
})
}
pub fn set_exit_status(&self, id: HistoryItemId, exit_status: i64) -> Result<()> {
self.inner
.lock()
.map_err(|_| lock_error())?
.update_with_extra::<HistoryExtraInfo>(id, &|mut item| {
item.exit_status = Some(exit_status);
item
})
}
pub fn count_all(&self) -> Result<i64> {
self.count(SearchQuery::everything(
reedline::SearchDirection::Backward,
None,
))
}
pub fn load(&self, id: HistoryItemId) -> Result<HistoryItem> {
self.inner.lock().map_err(|_| lock_error())?.load(id)
}
#[allow(dead_code)]
pub fn load_with_metadata(&self, id: HistoryItemId) -> Result<HistoryItem<HistoryExtraInfo>> {
self.inner
.lock()
.map_err(|_| lock_error())?
.load_with_extra(id)
}
pub fn count(&self, query: SearchQuery) -> Result<i64> {
self.inner.lock().map_err(|_| lock_error())?.count(query)
}
pub fn search(&self, query: SearchQuery) -> Result<Vec<HistoryItem>> {
self.inner.lock().map_err(|_| lock_error())?.search(query)
}
pub(crate) fn search_strict_session<F>(
&self,
mut make_query: F,
session_id: Option<HistorySessionId>,
all_sessions: bool,
limit: i64,
start_time: Option<chrono::DateTime<chrono::Utc>>,
) -> Result<Vec<HistoryItem>>
where
F: FnMut(Option<HistoryItemId>) -> SearchQuery,
{
const PAGE_SIZE: i64 = 128;
if limit <= 0 {
return Ok(Vec::new());
}
if all_sessions && start_time.is_none() {
let mut query = make_query(None);
query.limit = Some(limit);
return self.search(query);
}
let mut matched = Vec::new();
let mut start_id = None;
loop {
let mut query = make_query(start_id);
query.filter.session = None;
query.limit = Some(PAGE_SIZE);
let page = self.search(query)?;
let page_len = page.len();
let next_start_id = page.last().and_then(|item| item.id);
matched.extend(page.into_iter().filter(|item| {
(all_sessions || item.session_id == session_id)
&& start_time.is_none_or(|start| {
item.start_timestamp
.is_some_and(|timestamp| timestamp >= start)
})
}));
if matched.len() >= limit as usize || page_len < PAGE_SIZE as usize {
matched.truncate(limit as usize);
return Ok(matched);
}
let Some(next_start_id) = next_start_id else {
matched.truncate(limit as usize);
return Ok(matched);
};
start_id = Some(next_start_id);
}
}
pub fn update(
&self,
id: HistoryItemId,
updater: &dyn Fn(HistoryItem) -> HistoryItem,
) -> Result<()> {
self.inner
.lock()
.map_err(|_| lock_error())?
.update(id, updater)
}
pub fn clear(&self) -> Result<()> {
self.inner.lock().map_err(|_| lock_error())?.clear()
}
pub fn delete(&self, id: HistoryItemId) -> Result<()> {
self.inner.lock().map_err(|_| lock_error())?.delete(id)
}
pub(crate) fn delete_many(&self, ids: &[HistoryItemId]) -> Result<usize> {
let mut history = self.inner.lock().map_err(|_| lock_error())?;
let mut deleted = 0;
for id in ids {
history.delete(*id)?;
deleted += 1;
}
Ok(deleted)
}
pub fn sync(&self) -> std::io::Result<()> {
self.inner
.lock()
.map_err(|_| std::io::Error::other("history store lock poisoned"))?
.sync()
}
pub fn session(&self) -> Option<HistorySessionId> {
self.session
}
#[cfg(test)]
pub(crate) fn drop_table_for_test(&self, path: &std::path::Path) {
rusqlite::Connection::open(path)
.unwrap()
.execute_batch("DROP TABLE history")
.unwrap();
}
}
pub(crate) fn convert_history_item<A: HistoryItemExtraInfo, B: HistoryItemExtraInfo>(
item: HistoryItem<A>,
more_info: Option<B>,
) -> HistoryItem<B> {
HistoryItem {
id: item.id,
start_timestamp: item.start_timestamp,
command_line: item.command_line,
session_id: item.session_id,
hostname: item.hostname,
cwd: item.cwd,
duration: item.duration,
exit_status: item.exit_status,
more_info,
}
}
fn lock_error() -> reedline::ReedlineError {
reedline::ReedlineError(reedline::ReedlineErrorVariants::HistoryDatabaseError(
"history store lock poisoned".to_string(),
))
}
#[cfg(test)]
mod tests;