parent
22a7d8866b
commit
eab1dbf414
3 changed files with 141 additions and 131 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -31,6 +31,12 @@ dependencies = [
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.57"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.53"
|
version = "0.1.53"
|
||||||
|
@ -110,6 +116,7 @@ dependencies = [
|
||||||
"eyre",
|
"eyre",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"minspan",
|
"minspan",
|
||||||
"parse_duration",
|
"parse_duration",
|
||||||
|
@ -121,6 +128,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"sodiumoxide",
|
"sodiumoxide",
|
||||||
|
"sql-builder",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"tokio",
|
"tokio",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
@ -2095,6 +2103,16 @@ dependencies = [
|
||||||
"lock_api",
|
"lock_api",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sql-builder"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1008d95d2ec2d062959352527be30e10fec42a1aa5e5a48d990a5ff0fb9bdc0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlformat"
|
name = "sqlformat"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|
|
@ -46,6 +46,8 @@ sqlx = { version = "0.5", features = [
|
||||||
minspan = "0.1.1"
|
minspan = "0.1.1"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
fs-err = "2.7"
|
fs-err = "2.7"
|
||||||
|
sql-builder = "3"
|
||||||
|
lazy_static = "1"
|
||||||
|
|
||||||
# sync
|
# sync
|
||||||
urlencoding = { version = "2.1.0", optional = true }
|
urlencoding = { version = "2.1.0", optional = true }
|
||||||
|
|
|
@ -5,11 +5,11 @@ use std::str::FromStr;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use regex::Regex;
|
||||||
|
use sql_builder::{esc, quote, SqlBuilder, SqlName};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow},
|
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePool, SqlitePoolOptions, SqliteRow},
|
||||||
Result, Row,
|
Result, Row,
|
||||||
|
@ -219,50 +219,30 @@ impl Database for Sqlite {
|
||||||
) -> Result<Vec<History>> {
|
) -> Result<Vec<History>> {
|
||||||
debug!("listing history");
|
debug!("listing history");
|
||||||
|
|
||||||
// gotta get that query builder in soon cuz I kinda hate this
|
let mut query = SqlBuilder::select_from(SqlName::new("history").alias("h").baquoted());
|
||||||
let query = if unique {
|
query.field("*").order_desc("timestamp");
|
||||||
"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();
|
match filter {
|
||||||
|
FilterMode::Global => &mut query,
|
||||||
let filter_query = match filter {
|
FilterMode::Host => query.and_where_eq("hostname", quote(&context.hostname)),
|
||||||
FilterMode::Global => {
|
FilterMode::Session => query.and_where_eq("session", quote(&context.session)),
|
||||||
join = "".to_string();
|
FilterMode::Directory => query.and_where_eq("cwd", quote(&context.cwd)),
|
||||||
"".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(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter = if filter_query.is_empty() {
|
if unique {
|
||||||
"".to_string()
|
query.and_where_eq(
|
||||||
} else {
|
"timestamp",
|
||||||
format!("{} {}", join, filter_query)
|
"(select max(timestamp) from history where h.command = history.command)",
|
||||||
};
|
|
||||||
|
|
||||||
let limit = if let Some(max) = max {
|
|
||||||
format!("limit {}", max)
|
|
||||||
} else {
|
|
||||||
"".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let query = format!(
|
|
||||||
"select * from history h
|
|
||||||
{} {}
|
|
||||||
order by timestamp desc
|
|
||||||
{}",
|
|
||||||
query, filter, limit,
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let res = sqlx::query(query.as_str())
|
if let Some(max) = max {
|
||||||
|
query.limit(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = query.sql().expect("bug in list query. please report");
|
||||||
|
|
||||||
|
let res = sqlx::query(&query)
|
||||||
.map(Self::query_history)
|
.map(Self::query_history)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -339,108 +319,78 @@ impl Database for Sqlite {
|
||||||
context: &Context,
|
context: &Context,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<Vec<History>> {
|
) -> Result<Vec<History>> {
|
||||||
let orig_query = query;
|
let mut sql = SqlBuilder::select_from("history");
|
||||||
let query = query.to_string().replace('*', "%"); // allow wildcard char
|
|
||||||
let limit = limit.map_or("".to_owned(), |l| format!("limit {}", l));
|
|
||||||
|
|
||||||
let (query_sql, query_params) = match search_mode {
|
sql.group_by("command")
|
||||||
SearchMode::Prefix => ("command like ?1".to_string(), vec![format!("{}%", query)]),
|
.having("max(timestamp)")
|
||||||
SearchMode::FullText => ("command like ?1".to_string(), vec![format!("%{}%", query)]),
|
.order_desc("timestamp");
|
||||||
SearchMode::Fuzzy => {
|
|
||||||
let split_regex = Regex::new(r" +").unwrap();
|
if let Some(limit) = limit {
|
||||||
let terms: Vec<&str> = split_regex.split(query.as_str()).collect();
|
sql.limit(limit);
|
||||||
let mut query_sql = std::string::String::new();
|
}
|
||||||
let mut query_params = Vec::with_capacity(terms.len());
|
|
||||||
let mut was_or = false;
|
match filter {
|
||||||
for (i, query_part) in terms.into_iter().enumerate() {
|
FilterMode::Global => &mut sql,
|
||||||
// TODO smart case mode could be made configurable like in fzf
|
FilterMode::Host => sql.and_where_eq("hostname", quote(&context.hostname)),
|
||||||
let (operator, glob) = if query_part.contains(char::is_uppercase) {
|
FilterMode::Session => sql.and_where_eq("session", quote(&context.session)),
|
||||||
("glob", '*')
|
FilterMode::Directory => sql.and_where_eq("cwd", quote(&context.cwd)),
|
||||||
} else {
|
|
||||||
("like", '%')
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let orig_query = query;
|
||||||
|
let query = query.replace('*', "%"); // allow wildcard char
|
||||||
|
|
||||||
|
match search_mode {
|
||||||
|
SearchMode::Prefix => sql.and_where_like_left("command", query),
|
||||||
|
SearchMode::FullText => sql.and_where_like_any("command", query),
|
||||||
|
SearchMode::Fuzzy => {
|
||||||
|
// don't recompile the regex on successive calls!
|
||||||
|
lazy_static! {
|
||||||
|
static ref SPLIT_REGEX: Regex = Regex::new(r" +").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut is_or = false;
|
||||||
|
for query_part in SPLIT_REGEX.split(query.as_str()) {
|
||||||
|
// TODO smart case mode could be made configurable like in fzf
|
||||||
|
let (is_glob, glob) = if query_part.contains(char::is_uppercase) {
|
||||||
|
(true, "*")
|
||||||
|
} else {
|
||||||
|
(false, "%")
|
||||||
|
};
|
||||||
|
|
||||||
let (is_inverse, query_part) = match query_part.strip_prefix('!') {
|
let (is_inverse, query_part) = match query_part.strip_prefix('!') {
|
||||||
Some(stripped) => (true, stripped),
|
Some(stripped) => (true, stripped),
|
||||||
None => (false, query_part),
|
None => (false, query_part),
|
||||||
};
|
};
|
||||||
match query_part {
|
|
||||||
"|" => {
|
let param = if query_part == "|" {
|
||||||
if !was_or {
|
if !is_or {
|
||||||
query_sql.push_str(" OR ");
|
is_or = true;
|
||||||
was_or = true;
|
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} else {
|
||||||
query_params.push(format!("{glob}|{glob}"));
|
format!("{glob}|{glob}")
|
||||||
}
|
}
|
||||||
}
|
} else if let Some(term) = query_part.strip_prefix('^') {
|
||||||
exact_prefix if query_part.starts_with('^') => query_params.push(format!(
|
format!("{term}{glob}")
|
||||||
"{term}{glob}",
|
} else if let Some(term) = query_part.strip_suffix('$') {
|
||||||
term = exact_prefix.strip_prefix('^').unwrap()
|
format!("{glob}{term}")
|
||||||
)),
|
} else if let Some(term) = query_part.strip_prefix('\'') {
|
||||||
exact_suffix if query_part.ends_with('$') => query_params.push(format!(
|
format!("{glob}{term}{glob}")
|
||||||
"{glob}{term}",
|
} else if is_inverse {
|
||||||
term = exact_suffix.strip_suffix('$').unwrap()
|
format!("{glob}{term}{glob}", term = query_part)
|
||||||
)),
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter_base = if query_sql.is_empty() {
|
|
||||||
"".to_string()
|
|
||||||
} else {
|
} else {
|
||||||
"and".to_string()
|
query_part.split("").join(glob)
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter_query = match filter {
|
sql.fuzzy_condition("command", param, is_inverse, is_glob, is_or);
|
||||||
FilterMode::Global => String::from(""),
|
is_or = false;
|
||||||
FilterMode::Session => format!("session = '{}'", context.session),
|
}
|
||||||
FilterMode::Directory => format!("cwd = '{}'", context.cwd),
|
&mut sql
|
||||||
FilterMode::Host => format!("hostname = '{}'", context.hostname),
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter_sql = if filter_query.is_empty() {
|
let query = sql.sql().expect("bug in search query. please report");
|
||||||
"".to_string()
|
|
||||||
} else {
|
|
||||||
format!("{} {}", filter_base, filter_query)
|
|
||||||
};
|
|
||||||
|
|
||||||
let sql = format!(
|
let res = sqlx::query(&query)
|
||||||
"select * from history h
|
|
||||||
where {} {}
|
|
||||||
group by command
|
|
||||||
having max(timestamp)
|
|
||||||
order by timestamp desc {}",
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
.map(Self::query_history)
|
.map(Self::query_history)
|
||||||
.fetch_all(&self.pool)
|
.fetch_all(&self.pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -687,3 +637,43 @@ mod test {
|
||||||
assert!(duration < Duration::from_secs(15));
|
assert!(duration < Duration::from_secs(15));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait SqlBuilderExt {
|
||||||
|
fn fuzzy_condition<S: ToString, T: ToString>(
|
||||||
|
&mut self,
|
||||||
|
field: S,
|
||||||
|
mask: T,
|
||||||
|
inverse: bool,
|
||||||
|
glob: bool,
|
||||||
|
is_or: bool,
|
||||||
|
) -> &mut Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SqlBuilderExt for SqlBuilder {
|
||||||
|
/// adapted from the sql-builder *like functions
|
||||||
|
fn fuzzy_condition<S: ToString, T: ToString>(
|
||||||
|
&mut self,
|
||||||
|
field: S,
|
||||||
|
mask: T,
|
||||||
|
inverse: bool,
|
||||||
|
glob: bool,
|
||||||
|
is_or: bool,
|
||||||
|
) -> &mut Self {
|
||||||
|
let mut cond = field.to_string();
|
||||||
|
if inverse {
|
||||||
|
cond.push_str(" NOT");
|
||||||
|
}
|
||||||
|
if glob {
|
||||||
|
cond.push_str(" GLOB '");
|
||||||
|
} else {
|
||||||
|
cond.push_str(" LIKE '");
|
||||||
|
}
|
||||||
|
cond.push_str(&esc(&mask.to_string()));
|
||||||
|
cond.push('\'');
|
||||||
|
if is_or {
|
||||||
|
self.or_where(cond)
|
||||||
|
} else {
|
||||||
|
self.and_where(cond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue