use anyhow::Context as _;
use rusqlite::Connection;
use super::config::HistoryConfig;
use super::types::{HistoryEntry, HistoryRecord};
pub(super) fn map_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<HistoryEntry> {
Ok(HistoryEntry {
id: row.get(0)?,
timestamp: row.get(1)?,
project: row.get(2)?,
command: row.get(3)?,
filter_name: row.get(4)?,
raw_output: row.get(5)?,
filtered_output: row.get(6)?,
exit_code: row.get(7)?,
})
}
pub fn record_history(
conn: &Connection,
record: &HistoryRecord,
config: &HistoryConfig,
) -> anyhow::Result<i64> {
conn.execute(
"INSERT INTO history
(timestamp, project, command, filter_name, raw_output, filtered_output, exit_code)
VALUES
(strftime('%Y-%m-%dT%H:%M:%SZ','now'), ?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
record.project,
record.command,
record.filter_name,
record.raw_output,
record.filtered_output,
record.exit_code
],
)
.context("insert history entry")?;
let id = conn.last_insert_rowid();
let retention_i64 = i64::from(config.retention_count);
conn.execute(
"DELETE FROM history
WHERE project = ?1
AND id NOT IN (
SELECT id FROM history
WHERE project = ?1
ORDER BY id DESC
LIMIT ?2
)",
rusqlite::params![record.project, retention_i64],
)
.context("enforce history retention")?;
Ok(id)
}
pub fn list_history(
conn: &Connection,
limit: usize,
project: Option<&str>,
) -> anyhow::Result<Vec<HistoryEntry>> {
#[allow(clippy::cast_possible_wrap)]
let limit_i64 = limit as i64;
let mut stmt = conn.prepare(
"SELECT id, timestamp, project, command, filter_name,
raw_output, filtered_output, exit_code
FROM history
WHERE (?1 IS NULL OR project = ?1)
ORDER BY id DESC
LIMIT ?2",
)?;
let rows = stmt.query_map(rusqlite::params![project, limit_i64], map_row)?;
let mut result = Vec::new();
for row in rows {
result.push(row.context("read history row")?);
}
Ok(result)
}
pub fn get_history_entry(conn: &Connection, id: i64) -> anyhow::Result<Option<HistoryEntry>> {
let mut stmt = conn.prepare(
"SELECT id, timestamp, project, command, filter_name,
raw_output, filtered_output, exit_code
FROM history
WHERE id = ?1",
)?;
let mut rows = stmt.query([id])?;
if let Some(row) = rows.next()? {
Ok(Some(map_row(row)?))
} else {
Ok(None)
}
}
pub fn search_history(
conn: &Connection,
query: &str,
limit: usize,
project: Option<&str>,
) -> anyhow::Result<Vec<HistoryEntry>> {
#[allow(clippy::cast_possible_wrap)]
let limit_i64 = limit as i64;
let search_pattern = format!("%{query}%");
let mut stmt = conn.prepare(
"SELECT id, timestamp, project, command, filter_name,
raw_output, filtered_output, exit_code
FROM history
WHERE (?1 IS NULL OR project = ?1)
AND (command LIKE ?2 OR raw_output LIKE ?2 OR filtered_output LIKE ?2)
ORDER BY id DESC
LIMIT ?3",
)?;
let rows = stmt.query_map(
rusqlite::params![project, search_pattern, limit_i64],
map_row,
)?;
let mut result = Vec::new();
for row in rows {
result.push(row.context("read history row")?);
}
Ok(result)
}
pub fn get_latest_entry(
conn: &Connection,
project: Option<&str>,
) -> anyhow::Result<Option<HistoryEntry>> {
let mut stmt = conn.prepare(
"SELECT id, timestamp, project, command, filter_name,
raw_output, filtered_output, exit_code
FROM history
WHERE (?1 IS NULL OR project = ?1)
ORDER BY id DESC
LIMIT 1",
)?;
let mut rows = stmt.query([project])?;
if let Some(row) = rows.next()? {
Ok(Some(map_row(row)?))
} else {
Ok(None)
}
}
pub(super) fn most_recent_command(
conn: &Connection,
project: &str,
) -> anyhow::Result<Option<String>> {
let mut stmt =
conn.prepare("SELECT command FROM history WHERE project = ?1 ORDER BY id DESC LIMIT 1")?;
let mut rows = stmt.query([project])?;
if let Some(row) = rows.next()? {
Ok(Some(row.get(0)?))
} else {
Ok(None)
}
}
pub fn clear_history(conn: &Connection, project: Option<&str>) -> anyhow::Result<()> {
conn.execute(
"DELETE FROM history WHERE (?1 IS NULL OR project = ?1)",
rusqlite::params![project],
)
.context("clear history")?;
if project.is_none() {
let _ = conn.execute("DELETE FROM sqlite_sequence WHERE name='history'", []);
}
Ok(())
}