2022-04-22 14:05:02 -06:00
|
|
|
use std::env;
|
2020-10-04 17:59:28 -06:00
|
|
|
use std::path::Path;
|
2021-04-25 11:21:52 -06:00
|
|
|
use std::str::FromStr;
|
|
|
|
|
|
|
|
use async_trait::async_trait;
|
2021-04-25 14:27:51 -06:00
|
|
|
use chrono::prelude::*;
|
2021-04-25 11:21:52 -06:00
|
|
|
use chrono::Utc;
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-06-01 01:38:19 -06:00
|
|
|
use itertools::Itertools;
|
2022-03-18 05:37:27 -06:00
|
|
|
use regex::Regex;
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2022-04-13 11:08:49 -06:00
|
|
|
use fs_err as fs;
|
2022-04-21 01:05:57 -06:00
|
|
|
use sqlx::{
|
|
|
|
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow},
|
|
|
|
Result, Row,
|
2021-04-25 14:27:51 -06:00
|
|
|
};
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-02-14 10:18:02 -07:00
|
|
|
use super::history::History;
|
2021-09-09 04:46:46 -06:00
|
|
|
use super::ordering;
|
2022-04-22 14:05:02 -06:00
|
|
|
use super::settings::{FilterMode, SearchMode};
|
|
|
|
|
|
|
|
pub struct Context {
|
|
|
|
session: String,
|
|
|
|
cwd: String,
|
|
|
|
hostname: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn current_context() -> Context {
|
|
|
|
let session =
|
|
|
|
env::var("ATUIN_SESSION").expect("failed to find ATUIN_SESSION - check your shell setup");
|
|
|
|
let hostname = format!("{}:{}", whoami::hostname(), whoami::username());
|
|
|
|
let cwd = match env::current_dir() {
|
|
|
|
Ok(dir) => dir.display().to_string(),
|
|
|
|
Err(_) => String::from(""),
|
|
|
|
};
|
|
|
|
|
|
|
|
Context {
|
|
|
|
session,
|
|
|
|
hostname,
|
|
|
|
cwd,
|
|
|
|
}
|
|
|
|
}
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
#[async_trait]
|
2020-10-04 17:59:28 -06:00
|
|
|
pub trait Database {
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn save(&mut self, h: &History) -> Result<()>;
|
|
|
|
async fn save_bulk(&mut self, h: &[History]) -> Result<()>;
|
2021-03-19 18:50:31 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn load(&self, id: &str) -> Result<History>;
|
2022-04-22 14:05:02 -06:00
|
|
|
async fn list(
|
|
|
|
&self,
|
|
|
|
filter: FilterMode,
|
|
|
|
context: &Context,
|
|
|
|
max: Option<usize>,
|
|
|
|
unique: bool,
|
|
|
|
) -> Result<Vec<History>>;
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn range(
|
|
|
|
&self,
|
|
|
|
from: chrono::DateTime<Utc>,
|
|
|
|
to: chrono::DateTime<Utc>,
|
|
|
|
) -> Result<Vec<History>>;
|
2021-03-19 18:50:31 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn update(&self, h: &History) -> Result<()>;
|
|
|
|
async fn history_count(&self) -> Result<i64>;
|
2021-03-19 18:50:31 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn first(&self) -> Result<History>;
|
|
|
|
async fn last(&self) -> Result<History>;
|
|
|
|
async fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>>;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-05-09 01:33:56 -06:00
|
|
|
async fn search(
|
|
|
|
&self,
|
|
|
|
limit: Option<i64>,
|
|
|
|
search_mode: SearchMode,
|
2022-04-22 14:05:02 -06:00
|
|
|
filter: FilterMode,
|
|
|
|
context: &Context,
|
2021-05-09 01:33:56 -06:00
|
|
|
query: &str,
|
|
|
|
) -> Result<Vec<History>>;
|
2021-04-21 11:13:51 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn query_history(&self, query: &str) -> Result<Vec<History>>;
|
2020-10-04 17:59:28 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// Intended for use on a developer machine and not a sync server.
|
|
|
|
// TODO: implement IntoIterator
|
2021-02-14 08:15:26 -07:00
|
|
|
pub struct Sqlite {
|
2021-04-25 11:21:52 -06:00
|
|
|
pool: SqlitePool,
|
2020-10-04 17:59:28 -06:00
|
|
|
}
|
|
|
|
|
2021-02-14 08:15:26 -07:00
|
|
|
impl Sqlite {
|
2021-04-25 11:21:52 -06:00
|
|
|
pub async fn new(path: impl AsRef<Path>) -> Result<Self> {
|
2020-10-04 17:59:28 -06:00
|
|
|
let path = path.as_ref();
|
|
|
|
debug!("opening sqlite database at {:?}", path);
|
|
|
|
|
2020-10-05 10:20:48 -06:00
|
|
|
let create = !path.exists();
|
|
|
|
if create {
|
|
|
|
if let Some(dir) = path.parent() {
|
2022-04-13 11:08:49 -06:00
|
|
|
fs::create_dir_all(dir)?;
|
2020-10-05 10:20:48 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
let opts = SqliteConnectOptions::from_str(path.as_os_str().to_str().unwrap())?
|
|
|
|
.journal_mode(SqliteJournalMode::Wal)
|
|
|
|
.create_if_missing(true);
|
|
|
|
|
|
|
|
let pool = SqlitePoolOptions::new().connect_with(opts).await?;
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Self::setup_db(&pool).await?;
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(Self { pool })
|
2020-10-04 17:59:28 -06:00
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn setup_db(pool: &SqlitePool) -> Result<()> {
|
2020-10-04 17:59:28 -06:00
|
|
|
debug!("running sqlite database setup");
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
sqlx::migrate!("./migrations").run(pool).await?;
|
2021-04-21 11:13:51 -06:00
|
|
|
|
2020-10-04 17:59:28 -06:00
|
|
|
Ok(())
|
|
|
|
}
|
2021-02-14 10:18:02 -07:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn save_raw(tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, h: &History) -> Result<()> {
|
|
|
|
sqlx::query(
|
|
|
|
"insert or ignore into history(id, timestamp, duration, exit, command, cwd, session, hostname)
|
|
|
|
values(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
|
|
|
)
|
|
|
|
.bind(h.id.as_str())
|
2021-04-25 14:27:51 -06:00
|
|
|
.bind(h.timestamp.timestamp_nanos())
|
2021-04-25 11:21:52 -06:00
|
|
|
.bind(h.duration)
|
|
|
|
.bind(h.exit)
|
|
|
|
.bind(h.command.as_str())
|
|
|
|
.bind(h.cwd.as_str())
|
|
|
|
.bind(h.session.as_str())
|
|
|
|
.bind(h.hostname.as_str())
|
|
|
|
.execute(tx)
|
|
|
|
.await?;
|
2021-02-14 10:18:02 -07:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2021-04-25 14:27:51 -06:00
|
|
|
|
|
|
|
fn query_history(row: SqliteRow) -> History {
|
|
|
|
History {
|
|
|
|
id: row.get("id"),
|
|
|
|
timestamp: Utc.timestamp_nanos(row.get("timestamp")),
|
|
|
|
duration: row.get("duration"),
|
|
|
|
exit: row.get("exit"),
|
|
|
|
command: row.get("command"),
|
|
|
|
cwd: row.get("cwd"),
|
|
|
|
session: row.get("session"),
|
|
|
|
hostname: row.get("hostname"),
|
|
|
|
}
|
|
|
|
}
|
2020-10-04 17:59:28 -06:00
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
#[async_trait]
|
2021-02-14 08:15:26 -07:00
|
|
|
impl Database for Sqlite {
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn save(&mut self, h: &History) -> Result<()> {
|
2020-10-04 17:59:28 -06:00
|
|
|
debug!("saving history to sqlite");
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
let mut tx = self.pool.begin().await?;
|
|
|
|
Self::save_raw(&mut tx, h).await?;
|
|
|
|
tx.commit().await?;
|
2021-02-14 10:18:02 -07:00
|
|
|
|
|
|
|
Ok(())
|
2021-02-13 10:02:52 -07:00
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn save_bulk(&mut self, h: &[History]) -> Result<()> {
|
2021-02-13 12:37:00 -07:00
|
|
|
debug!("saving history to sqlite");
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
let mut tx = self.pool.begin().await?;
|
|
|
|
|
2021-02-13 12:37:00 -07:00
|
|
|
for i in h {
|
2021-04-25 11:21:52 -06:00
|
|
|
Self::save_raw(&mut tx, i).await?
|
2021-02-13 12:37:00 -07:00
|
|
|
}
|
2021-04-25 11:21:52 -06:00
|
|
|
|
|
|
|
tx.commit().await?;
|
2021-02-13 12:37:00 -07:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn load(&self, id: &str) -> Result<History> {
|
2021-04-21 11:13:51 -06:00
|
|
|
debug!("loading history item {}", id);
|
2021-02-13 10:02:52 -07:00
|
|
|
|
2021-04-25 14:27:51 -06:00
|
|
|
let res = sqlx::query("select * from history where id = ?1")
|
2021-04-25 11:21:52 -06:00
|
|
|
.bind(id)
|
2021-04-25 14:27:51 -06:00
|
|
|
.map(Self::query_history)
|
2021-04-25 11:21:52 -06:00
|
|
|
.fetch_one(&self.pool)
|
|
|
|
.await?;
|
2021-02-13 10:02:52 -07:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res)
|
2021-02-13 10:02:52 -07:00
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn update(&self, h: &History) -> Result<()> {
|
2021-02-13 10:02:52 -07:00
|
|
|
debug!("updating sqlite history");
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
sqlx::query(
|
2021-02-13 10:02:52 -07:00
|
|
|
"update history
|
2021-02-13 13:21:49 -07:00
|
|
|
set timestamp = ?2, duration = ?3, exit = ?4, command = ?5, cwd = ?6, session = ?7, hostname = ?8
|
2021-02-13 10:02:52 -07:00
|
|
|
where id = ?1",
|
2021-04-25 11:21:52 -06:00
|
|
|
)
|
|
|
|
.bind(h.id.as_str())
|
2021-04-25 14:27:51 -06:00
|
|
|
.bind(h.timestamp.timestamp_nanos())
|
2021-04-25 11:21:52 -06:00
|
|
|
.bind(h.duration)
|
|
|
|
.bind(h.exit)
|
|
|
|
.bind(h.command.as_str())
|
|
|
|
.bind(h.cwd.as_str())
|
|
|
|
.bind(h.session.as_str())
|
|
|
|
.bind(h.hostname.as_str())
|
|
|
|
.execute(&self.pool)
|
|
|
|
.await?;
|
2020-10-04 17:59:28 -06:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
// make a unique list, that only shows the *newest* version of things
|
2022-04-22 14:05:02 -06:00
|
|
|
async fn list(
|
|
|
|
&self,
|
|
|
|
filter: FilterMode,
|
|
|
|
context: &Context,
|
|
|
|
max: Option<usize>,
|
|
|
|
unique: bool,
|
|
|
|
) -> Result<Vec<History>> {
|
2020-10-04 17:59:28 -06:00
|
|
|
debug!("listing history");
|
2020-10-05 10:20:48 -06:00
|
|
|
|
2022-04-22 14:05:02 -06:00
|
|
|
// gotta get that query builder in soon cuz I kinda hate this
|
|
|
|
let query = if unique {
|
|
|
|
"where timestamp = (
|
|
|
|
select max(timestamp) from history
|
|
|
|
where h.command = history.command
|
|
|
|
)"
|
|
|
|
} else {
|
|
|
|
""
|
|
|
|
}
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
let mut join = if unique { "and" } else { "where" }.to_string();
|
|
|
|
|
|
|
|
let filter_query = match filter {
|
|
|
|
FilterMode::Global => {
|
|
|
|
join = "".to_string();
|
|
|
|
"".to_string()
|
|
|
|
}
|
|
|
|
FilterMode::Host => format!("hostname = '{}'", context.hostname).to_string(),
|
|
|
|
FilterMode::Session => format!("session = '{}'", context.session).to_string(),
|
|
|
|
FilterMode::Directory => format!("cwd = '{}'", context.cwd).to_string(),
|
|
|
|
};
|
|
|
|
|
2022-04-22 15:15:50 -06:00
|
|
|
let filter = if filter_query.is_empty() {
|
|
|
|
"".to_string()
|
|
|
|
} else {
|
|
|
|
format!("{} {}", join, filter_query)
|
|
|
|
};
|
2022-04-22 14:05:02 -06:00
|
|
|
|
|
|
|
let limit = if let Some(max) = max {
|
|
|
|
format!("limit {}", max)
|
|
|
|
} else {
|
|
|
|
"".to_string()
|
|
|
|
};
|
|
|
|
|
2021-04-21 11:13:51 -06:00
|
|
|
let query = format!(
|
|
|
|
"select * from history h
|
2022-04-22 15:15:50 -06:00
|
|
|
{} {}
|
2021-04-21 11:13:51 -06:00
|
|
|
order by timestamp desc
|
2022-04-22 15:15:50 -06:00
|
|
|
{}",
|
2022-04-22 14:05:02 -06:00
|
|
|
query, filter, limit,
|
2021-04-21 11:13:51 -06:00
|
|
|
);
|
2021-02-13 10:02:52 -07:00
|
|
|
|
2021-04-25 14:27:51 -06:00
|
|
|
let res = sqlx::query(query.as_str())
|
|
|
|
.map(Self::query_history)
|
2021-04-25 11:21:52 -06:00
|
|
|
.fetch_all(&self.pool)
|
|
|
|
.await?;
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res)
|
2021-02-14 09:53:18 -07:00
|
|
|
}
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn range(
|
2021-02-14 15:12:35 -07:00
|
|
|
&self,
|
|
|
|
from: chrono::DateTime<Utc>,
|
|
|
|
to: chrono::DateTime<Utc>,
|
|
|
|
) -> Result<Vec<History>> {
|
|
|
|
debug!("listing history from {:?} to {:?}", from, to);
|
2020-10-04 17:59:28 -06:00
|
|
|
|
2021-04-25 14:27:51 -06:00
|
|
|
let res = sqlx::query(
|
2021-04-25 11:21:52 -06:00
|
|
|
"select * from history where timestamp >= ?1 and timestamp <= ?2 order by timestamp asc",
|
|
|
|
)
|
|
|
|
.bind(from)
|
|
|
|
.bind(to)
|
2021-04-25 14:27:51 -06:00
|
|
|
.map(Self::query_history)
|
2021-04-25 11:21:52 -06:00
|
|
|
.fetch_all(&self.pool)
|
|
|
|
.await?;
|
2021-02-14 09:53:18 -07:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res)
|
2020-10-04 17:59:28 -06:00
|
|
|
}
|
2021-02-15 14:30:13 -07:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn first(&self) -> Result<History> {
|
2021-04-25 14:27:51 -06:00
|
|
|
let res =
|
|
|
|
sqlx::query("select * from history where duration >= 0 order by timestamp asc limit 1")
|
|
|
|
.map(Self::query_history)
|
|
|
|
.fetch_one(&self.pool)
|
|
|
|
.await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res)
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn last(&self) -> Result<History> {
|
2021-04-25 14:27:51 -06:00
|
|
|
let res = sqlx::query(
|
2021-04-25 11:21:52 -06:00
|
|
|
"select * from history where duration >= 0 order by timestamp desc limit 1",
|
|
|
|
)
|
2021-04-25 14:27:51 -06:00
|
|
|
.map(Self::query_history)
|
2021-04-25 11:21:52 -06:00
|
|
|
.fetch_one(&self.pool)
|
|
|
|
.await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res)
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn before(&self, timestamp: chrono::DateTime<Utc>, count: i64) -> Result<Vec<History>> {
|
2021-04-25 14:27:51 -06:00
|
|
|
let res = sqlx::query(
|
2021-04-25 11:21:52 -06:00
|
|
|
"select * from history where timestamp < ?1 order by timestamp desc limit ?2",
|
|
|
|
)
|
2021-04-25 14:27:51 -06:00
|
|
|
.bind(timestamp.timestamp_nanos())
|
2021-04-25 11:21:52 -06:00
|
|
|
.bind(count)
|
2021-04-25 14:27:51 -06:00
|
|
|
.map(Self::query_history)
|
2021-04-25 11:21:52 -06:00
|
|
|
.fetch_all(&self.pool)
|
|
|
|
.await?;
|
2021-04-13 12:14:07 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res)
|
2021-04-13 12:14:07 -06:00
|
|
|
}
|
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn history_count(&self) -> Result<i64> {
|
|
|
|
let res: (i64,) = sqlx::query_as("select count(1) from history")
|
|
|
|
.fetch_one(&self.pool)
|
|
|
|
.await?;
|
2021-02-15 14:30:13 -07:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res.0)
|
2021-02-15 14:30:13 -07:00
|
|
|
}
|
2021-03-19 18:50:31 -06:00
|
|
|
|
2021-05-09 01:33:56 -06:00
|
|
|
async fn search(
|
|
|
|
&self,
|
|
|
|
limit: Option<i64>,
|
|
|
|
search_mode: SearchMode,
|
2022-04-22 14:05:02 -06:00
|
|
|
filter: FilterMode,
|
|
|
|
context: &Context,
|
2021-05-09 01:33:56 -06:00
|
|
|
query: &str,
|
|
|
|
) -> Result<Vec<History>> {
|
2021-09-09 04:46:46 -06:00
|
|
|
let orig_query = query;
|
2022-03-13 13:53:49 -06:00
|
|
|
let query = query.to_string().replace('*', "%"); // allow wildcard char
|
2021-04-25 11:21:52 -06:00
|
|
|
let limit = limit.map_or("".to_owned(), |l| format!("limit {}", l));
|
2021-04-21 11:13:51 -06:00
|
|
|
|
2022-03-18 05:37:27 -06:00
|
|
|
let (query_sql, query_params) = match search_mode {
|
|
|
|
SearchMode::Prefix => ("command like ?1".to_string(), vec![format!("{}%", query)]),
|
|
|
|
SearchMode::FullText => ("command like ?1".to_string(), vec![format!("%{}%", query)]),
|
|
|
|
SearchMode::Fuzzy => {
|
|
|
|
let split_regex = Regex::new(r" +").unwrap();
|
|
|
|
let terms: Vec<&str> = split_regex.split(query.as_str()).collect();
|
|
|
|
let mut query_sql = std::string::String::new();
|
|
|
|
let mut query_params = Vec::with_capacity(terms.len());
|
|
|
|
let mut was_or = false;
|
|
|
|
for (i, query_part) in terms.into_iter().enumerate() {
|
|
|
|
// TODO smart case mode could be made configurable like in fzf
|
|
|
|
let (operator, glob) = if query_part.contains(char::is_uppercase) {
|
|
|
|
("glob", '*')
|
|
|
|
} else {
|
|
|
|
("like", '%')
|
|
|
|
};
|
|
|
|
let (is_inverse, query_part) = match query_part.strip_prefix('!') {
|
|
|
|
Some(stripped) => (true, stripped),
|
|
|
|
None => (false, query_part),
|
|
|
|
};
|
|
|
|
match query_part {
|
|
|
|
"|" => {
|
|
|
|
if !was_or {
|
|
|
|
query_sql.push_str(" OR ");
|
|
|
|
was_or = true;
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
query_params.push(format!("{glob}|{glob}"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
exact_prefix if query_part.starts_with('^') => query_params.push(format!(
|
|
|
|
"{term}{glob}",
|
|
|
|
term = exact_prefix.strip_prefix('^').unwrap()
|
|
|
|
)),
|
|
|
|
exact_suffix if query_part.ends_with('$') => query_params.push(format!(
|
|
|
|
"{glob}{term}",
|
|
|
|
term = exact_suffix.strip_suffix('$').unwrap()
|
|
|
|
)),
|
|
|
|
exact if query_part.starts_with('\'') => query_params.push(format!(
|
|
|
|
"{glob}{term}{glob}",
|
|
|
|
term = exact.strip_prefix('\'').unwrap()
|
|
|
|
)),
|
|
|
|
exact if is_inverse => {
|
|
|
|
query_params.push(format!("{glob}{term}{glob}", term = exact))
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
query_params.push(query_part.split("").join(glob.to_string().as_str()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if i > 0 && !was_or {
|
|
|
|
query_sql.push_str(" AND ");
|
|
|
|
}
|
|
|
|
if is_inverse {
|
|
|
|
query_sql.push_str("NOT ");
|
|
|
|
}
|
|
|
|
query_sql
|
|
|
|
.push_str(format!("command {} ?{}", operator, query_params.len()).as_str());
|
|
|
|
was_or = false;
|
|
|
|
}
|
|
|
|
(query_sql, query_params)
|
|
|
|
}
|
2021-05-09 01:33:56 -06:00
|
|
|
};
|
|
|
|
|
2022-04-22 15:15:50 -06:00
|
|
|
let filter_base = if query_sql.is_empty() {
|
|
|
|
"".to_string()
|
|
|
|
} else {
|
|
|
|
"and".to_string()
|
|
|
|
};
|
|
|
|
|
|
|
|
let filter_query = match filter {
|
2022-04-22 14:05:02 -06:00
|
|
|
FilterMode::Global => String::from(""),
|
2022-04-22 15:15:50 -06:00
|
|
|
FilterMode::Session => format!("session = '{}'", context.session),
|
|
|
|
FilterMode::Directory => format!("cwd = '{}'", context.cwd),
|
|
|
|
FilterMode::Host => format!("hostname = '{}'", context.hostname),
|
2022-04-22 14:05:02 -06:00
|
|
|
};
|
|
|
|
|
2022-04-22 15:15:50 -06:00
|
|
|
let filter_sql = if filter_query.is_empty() {
|
|
|
|
"".to_string()
|
|
|
|
} else {
|
|
|
|
format!("{} {}", filter_base, filter_query)
|
|
|
|
};
|
|
|
|
|
|
|
|
let sql = format!(
|
|
|
|
"select * from history h
|
|
|
|
where {} {}
|
2022-03-18 05:37:27 -06:00
|
|
|
group by command
|
|
|
|
having max(timestamp)
|
|
|
|
order by timestamp desc {}",
|
2022-04-22 15:15:50 -06:00
|
|
|
query_sql.as_str(),
|
|
|
|
filter_sql.as_str(),
|
|
|
|
limit.clone()
|
|
|
|
);
|
|
|
|
|
|
|
|
let res = query_params
|
|
|
|
.iter()
|
|
|
|
.fold(sqlx::query(sql.as_str()), |query, query_param| {
|
|
|
|
query.bind(query_param)
|
|
|
|
})
|
2022-03-18 05:37:27 -06:00
|
|
|
.map(Self::query_history)
|
|
|
|
.fetch_all(&self.pool)
|
|
|
|
.await?;
|
2021-03-19 18:50:31 -06:00
|
|
|
|
2021-09-09 04:46:46 -06:00
|
|
|
Ok(ordering::reorder_fuzzy(search_mode, orig_query, res))
|
2021-03-19 18:50:31 -06:00
|
|
|
}
|
2021-04-21 11:13:51 -06:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
async fn query_history(&self, query: &str) -> Result<Vec<History>> {
|
2021-04-25 14:27:51 -06:00
|
|
|
let res = sqlx::query(query)
|
|
|
|
.map(Self::query_history)
|
2021-04-25 11:21:52 -06:00
|
|
|
.fetch_all(&self.pool)
|
|
|
|
.await?;
|
2021-02-14 10:18:02 -07:00
|
|
|
|
2021-04-25 11:21:52 -06:00
|
|
|
Ok(res)
|
|
|
|
}
|
2021-02-14 10:18:02 -07:00
|
|
|
}
|
2021-06-01 01:38:19 -06:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
2021-12-19 03:29:01 -07:00
|
|
|
use std::time::{Duration, Instant};
|
2021-06-01 01:38:19 -06:00
|
|
|
|
2022-03-18 05:37:27 -06:00
|
|
|
async fn assert_search_eq<'a>(
|
|
|
|
db: &impl Database,
|
|
|
|
mode: SearchMode,
|
2022-04-22 14:05:02 -06:00
|
|
|
filter_mode: FilterMode,
|
2022-03-18 05:37:27 -06:00
|
|
|
query: &str,
|
|
|
|
expected: usize,
|
|
|
|
) -> Result<Vec<History>> {
|
2022-04-22 14:05:02 -06:00
|
|
|
let context = Context {
|
|
|
|
hostname: "test:host".to_string(),
|
|
|
|
session: "beepboopiamasession".to_string(),
|
|
|
|
cwd: "/home/ellie".to_string(),
|
|
|
|
};
|
|
|
|
|
|
|
|
let results = db.search(None, mode, filter_mode, &context, query).await?;
|
|
|
|
|
2022-03-18 05:37:27 -06:00
|
|
|
assert_eq!(
|
|
|
|
results.len(),
|
|
|
|
expected,
|
|
|
|
"query \"{}\", commands: {:?}",
|
|
|
|
query,
|
|
|
|
results.iter().map(|a| &a.command).collect::<Vec<&String>>()
|
|
|
|
);
|
|
|
|
Ok(results)
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn assert_search_commands(
|
|
|
|
db: &impl Database,
|
|
|
|
mode: SearchMode,
|
2022-04-22 14:05:02 -06:00
|
|
|
filter_mode: FilterMode,
|
2022-03-18 05:37:27 -06:00
|
|
|
query: &str,
|
|
|
|
expected_commands: Vec<&str>,
|
|
|
|
) {
|
2022-04-22 14:05:02 -06:00
|
|
|
let results = assert_search_eq(db, mode, filter_mode, query, expected_commands.len())
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
let commands: Vec<&str> = results.iter().map(|a| a.command.as_str()).collect();
|
|
|
|
assert_eq!(commands, expected_commands);
|
|
|
|
}
|
|
|
|
|
2021-06-01 01:38:19 -06:00
|
|
|
async fn new_history_item(db: &mut impl Database, cmd: &str) -> Result<()> {
|
|
|
|
let history = History::new(
|
|
|
|
chrono::Utc::now(),
|
|
|
|
cmd.to_string(),
|
|
|
|
"/home/ellie".to_string(),
|
|
|
|
0,
|
|
|
|
1,
|
|
|
|
Some("beep boop".to_string()),
|
|
|
|
Some("booop".to_string()),
|
|
|
|
);
|
|
|
|
return db.save(&history).await;
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
|
|
async fn test_search_prefix() {
|
|
|
|
let mut db = Sqlite::new("sqlite::memory:").await.unwrap();
|
|
|
|
new_history_item(&mut db, "ls /home/ellie").await.unwrap();
|
|
|
|
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Prefix, FilterMode::Global, "ls", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Prefix, FilterMode::Global, "/home", 0)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Prefix, FilterMode::Global, "ls ", 0)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2021-06-01 01:38:19 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
|
|
async fn test_search_fulltext() {
|
|
|
|
let mut db = Sqlite::new("sqlite::memory:").await.unwrap();
|
|
|
|
new_history_item(&mut db, "ls /home/ellie").await.unwrap();
|
|
|
|
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::FullText, FilterMode::Global, "ls", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::FullText, FilterMode::Global, "/home", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::FullText, FilterMode::Global, "ls ", 0)
|
2021-06-01 01:38:19 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
|
|
async fn test_search_fuzzy() {
|
|
|
|
let mut db = Sqlite::new("sqlite::memory:").await.unwrap();
|
|
|
|
new_history_item(&mut db, "ls /home/ellie").await.unwrap();
|
|
|
|
new_history_item(&mut db, "ls /home/frank").await.unwrap();
|
2022-03-18 05:37:27 -06:00
|
|
|
new_history_item(&mut db, "cd /home/Ellie").await.unwrap();
|
2021-06-01 01:38:19 -06:00
|
|
|
new_history_item(&mut db, "/home/ellie/.bin/rustup")
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "ls /", 3)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "ls/", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "l/h/", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "/h/e", 3)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "/hmoe/", 0)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "ellie/home", 0)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "lsellie", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, " ", 4)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2021-06-01 01:38:19 -06:00
|
|
|
|
2022-03-18 05:37:27 -06:00
|
|
|
// single term operators
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "^ls", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "'ls", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "ellie$", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "!^ls", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "!ellie", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "!ellie$", 2)
|
2021-06-01 01:38:19 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
|
2022-03-18 05:37:27 -06:00
|
|
|
// multiple terms
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "ls !ellie", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "^ls !e$", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "home !^ls", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(
|
|
|
|
&db,
|
|
|
|
SearchMode::Fuzzy,
|
|
|
|
FilterMode::Global,
|
|
|
|
"'frank | 'rustup",
|
|
|
|
2,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
assert_search_eq(
|
|
|
|
&db,
|
|
|
|
SearchMode::Fuzzy,
|
|
|
|
FilterMode::Global,
|
|
|
|
"'frank | 'rustup 'ls",
|
|
|
|
1,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2021-06-01 01:38:19 -06:00
|
|
|
|
2022-03-18 05:37:27 -06:00
|
|
|
// case matching
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "Ellie", 1)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2021-06-01 01:38:19 -06:00
|
|
|
}
|
2021-09-09 04:46:46 -06:00
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
|
|
async fn test_search_reordered_fuzzy() {
|
|
|
|
let mut db = Sqlite::new("sqlite::memory:").await.unwrap();
|
|
|
|
// test ordering of results: we should choose the first, even though it happened longer ago.
|
|
|
|
|
|
|
|
new_history_item(&mut db, "curl").await.unwrap();
|
|
|
|
new_history_item(&mut db, "corburl").await.unwrap();
|
|
|
|
|
2022-03-18 05:37:27 -06:00
|
|
|
// if fuzzy reordering is on, it should come back in a more sensible order
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_commands(
|
|
|
|
&db,
|
|
|
|
SearchMode::Fuzzy,
|
|
|
|
FilterMode::Global,
|
|
|
|
"curl",
|
|
|
|
vec!["curl", "corburl"],
|
|
|
|
)
|
|
|
|
.await;
|
2021-09-09 04:46:46 -06:00
|
|
|
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "xxxx", 0)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-04-22 14:05:02 -06:00
|
|
|
assert_search_eq(&db, SearchMode::Fuzzy, FilterMode::Global, "", 2)
|
2022-03-18 05:37:27 -06:00
|
|
|
.await
|
|
|
|
.unwrap();
|
2021-09-09 04:46:46 -06:00
|
|
|
}
|
2021-12-19 03:29:01 -07:00
|
|
|
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
|
|
async fn test_search_bench_dupes() {
|
2022-04-22 14:05:02 -06:00
|
|
|
let context = Context {
|
|
|
|
hostname: "test:host".to_string(),
|
|
|
|
session: "beepboopiamasession".to_string(),
|
|
|
|
cwd: "/home/ellie".to_string(),
|
|
|
|
};
|
|
|
|
|
2021-12-19 03:29:01 -07:00
|
|
|
let mut db = Sqlite::new("sqlite::memory:").await.unwrap();
|
|
|
|
for _i in 1..10000 {
|
|
|
|
new_history_item(&mut db, "i am a duplicated command")
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
}
|
|
|
|
let start = Instant::now();
|
2022-04-22 14:05:02 -06:00
|
|
|
let _results = db
|
|
|
|
.search(None, SearchMode::Fuzzy, FilterMode::Global, &context, "")
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2021-12-19 03:29:01 -07:00
|
|
|
let duration = start.elapsed();
|
|
|
|
|
|
|
|
assert!(duration < Duration::from_secs(15));
|
|
|
|
}
|
2021-06-01 01:38:19 -06:00
|
|
|
}
|