use std::path::PathBuf;
use rusqlite::{params, OptionalExtension};
use crate::sqlite_support::{OwnerOnlySqlite, ParentDirectoryPrivacy};
use super::{migrations, PromptHistoryError};
#[derive(Clone, Debug)]
pub(crate) struct PromptHistoryStore {
db: OwnerOnlySqlite,
}
impl PromptHistoryStore {
pub(crate) fn open_path(path: impl Into<PathBuf>) -> Result<Self, PromptHistoryError> {
Self::open_with(path.into(), ParentDirectoryPrivacy::PreserveExisting)
}
pub(crate) fn open_path_if_exists(
path: impl Into<PathBuf>,
) -> Result<Option<Self>, PromptHistoryError> {
Self::open_existing_with(path.into(), ParentDirectoryPrivacy::PreserveExisting)
}
pub(crate) fn at_default_path() -> Result<Self, PromptHistoryError> {
Self::open_with(default_path()?, ParentDirectoryPrivacy::EnforcePrivate)
}
pub(crate) fn at_default_path_if_exists() -> Result<Option<Self>, PromptHistoryError> {
Self::open_existing_with(default_path()?, ParentDirectoryPrivacy::EnforcePrivate)
}
fn open_with(
path: PathBuf,
parent_privacy: ParentDirectoryPrivacy,
) -> Result<Self, PromptHistoryError> {
Ok(Self {
db: OwnerOnlySqlite::open(path, parent_privacy, migrations::migrate)?,
})
}
fn open_existing_with(
path: PathBuf,
parent_privacy: ParentDirectoryPrivacy,
) -> Result<Option<Self>, PromptHistoryError> {
Ok(
OwnerOnlySqlite::open_existing(path, parent_privacy, migrations::migrate)?
.map(|db| Self { db }),
)
}
#[cfg(test)]
pub(crate) fn path(&self) -> &std::path::Path {
self.db.path()
}
pub(crate) fn append(&self, text: &str, max_entries: usize) -> Result<(), PromptHistoryError> {
self.db.with_immediate_transaction(|transaction| {
let last: Option<String> = transaction
.query_row(
"SELECT text FROM prompt_entries ORDER BY id DESC LIMIT 1",
[],
|row| row.get(0),
)
.optional()?;
if last.as_deref() != Some(text) {
transaction.execute(
"INSERT INTO prompt_entries (text) VALUES (?1)",
params![text],
)?;
trim_to_newest(transaction, max_entries)?;
}
Ok(())
})
}
pub(crate) fn count(&self) -> Result<usize, PromptHistoryError> {
let connection = self.db.open_write_connection()?;
let count: i64 =
connection.query_row("SELECT COUNT(*) FROM prompt_entries", [], |row| row.get(0))?;
usize::try_from(count).map_err(|_| {
PromptHistoryError::Sqlite(rusqlite::Error::IntegralValueOutOfRange(0, count))
})
}
pub(crate) fn enforce_limit(&self, max_entries: usize) -> Result<(), PromptHistoryError> {
self.db
.with_immediate_transaction(|transaction| trim_to_newest(transaction, max_entries))
}
pub(crate) fn load_tail(&self, limit: usize) -> Result<Vec<String>, PromptHistoryError> {
let connection = self.db.open_write_connection()?;
let limit = i64::try_from(limit).unwrap_or(i64::MAX);
let mut statement =
connection.prepare("SELECT text FROM prompt_entries ORDER BY id DESC LIMIT ?1")?;
let mut rows = statement.query(params![limit])?;
let mut texts = Vec::new();
while let Some(row) = rows.next()? {
texts.push(row.get(0)?);
}
texts.reverse();
Ok(texts)
}
pub(crate) fn clear(&self) -> Result<(), PromptHistoryError> {
self.db.with_immediate_transaction(|transaction| {
transaction.execute("DELETE FROM prompt_entries", [])?;
Ok(())
})
}
}
fn default_path() -> Result<PathBuf, PromptHistoryError> {
crate::paths::prompt_history_database_path().map_err(|_| PromptHistoryError::DataDirectory)
}
fn trim_to_newest(
transaction: &rusqlite::Transaction<'_>,
max_entries: usize,
) -> Result<(), PromptHistoryError> {
let limit = i64::try_from(max_entries).unwrap_or(i64::MAX);
transaction.execute(
"DELETE FROM prompt_entries
WHERE id NOT IN (
SELECT id FROM prompt_entries ORDER BY id DESC LIMIT ?1
)",
params![limit],
)?;
Ok(())
}