You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
postsack/src/database/query.rs

243 lines
5.9 KiB
Rust

use rsql_builder;
use serde_json;
pub use serde_json::Value;
use strum::{self, IntoEnumIterator};
use strum_macros::{EnumIter, IntoStaticStr};
use std::ops::Range;
pub const AMOUNT_FIELD_NAME: &str = "amount";
#[derive(Clone, Debug)]
pub enum Filter {
/// A database Like Operation
Like(ValueField),
NotLike(ValueField),
/// A extended like that implies:
/// - wildcards on both sides (like '%test%')
/// - case in-sensitive comparison
/// - Trying to handle values as strings
Contains(ValueField),
Is(ValueField),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoStaticStr, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum Field {
Path,
SenderDomain,
SenderLocalPart,
SenderName,
Year,
Month,
Day,
Timestamp,
ToGroup,
ToName,
ToAddress,
IsReply,
IsSend,
Subject,
MetaIsSeen,
MetaTags,
}
const INVALID_FIELDS: &[Field] = &[
Field::Path,
Field::Subject,
Field::Timestamp,
Field::IsReply,
Field::IsSend,
Field::MetaIsSeen,
Field::MetaTags,
];
impl Field {
pub fn all_cases() -> impl Iterator<Item = Field> {
Field::iter().filter(|f| !INVALID_FIELDS.contains(f))
}
/// Just a wrapper to offer `into` without the type ambiguity
/// that sometimes arises
pub fn as_str(&self) -> &'static str {
self.into()
}
/// A human readable name
pub fn name(&self) -> &str {
use Field::*;
match self {
SenderDomain => "Domain",
SenderLocalPart => "Address",
SenderName => "Name",
ToGroup => "Group",
ToName => "To name",
ToAddress => "To address",
Year => "Year",
Month => "Month",
Day => "Day",
Subject => "Subject",
_ => self.as_str(),
}
}
}
impl std::fmt::Display for Field {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ValueField {
field: Field,
value: Value,
}
impl ValueField {
pub fn string<S: AsRef<str>>(field: &Field, value: S) -> ValueField {
ValueField {
field: *field,
value: Value::String(value.as_ref().to_string()),
}
}
pub fn bool(field: &Field, value: bool) -> ValueField {
ValueField {
field: *field,
value: Value::Bool(value),
}
}
pub fn usize(field: &Field, value: usize) -> ValueField {
ValueField {
field: *field,
value: Value::Number(value.into()),
}
}
pub fn array(field: &Field, value: Vec<Value>) -> ValueField {
ValueField {
field: *field,
value: Value::Array(value),
}
}
pub fn field(&self) -> &Field {
&self.field
}
pub fn value(&self) -> &Value {
&self.value
}
#[allow(clippy::inherent_to_string)]
pub fn to_string(&self) -> String {
match &self.value {
Value::String(s) => s.clone(),
_ => format!("{}", &self.value),
}
}
}
#[derive(Debug, Clone)]
pub enum OtherQuery {
/// Get all contents of a specific field
All(Field),
}
#[derive(Clone, Debug)]
pub enum Query {
Grouped {
filters: Vec<Filter>,
group_by: Field,
},
Normal {
fields: Vec<Field>,
filters: Vec<Filter>,
range: Range<usize>,
},
Other {
query: OtherQuery,
},
}
impl Query {
fn filters(&self) -> &[Filter] {
match self {
Query::Grouped { ref filters, .. } => filters,
Query::Normal { ref filters, .. } => filters,
Query::Other { .. } => &[],
}
}
}
impl Query {
pub fn to_sql(&self) -> (String, Vec<serde_json::Value>) {
let mut conditions = {
let mut whr = rsql_builder::B::new_where();
for filter in self.filters() {
match filter {
Filter::Like(f) => whr.like(f.field.into(), f.value()),
Filter::NotLike(f) => whr.not_like(f.field.into(), f.value()),
Filter::Contains(f) => whr.like(
f.field.into(),
&format!("%{}%", f.to_string().to_lowercase()),
),
Filter::Is(f) => whr.eq(f.field.into(), f.value()),
};
}
whr
};
let (header, group_by) = match self {
Query::Grouped { group_by, .. } => (
format!(
"SELECT count(path) as {}, {} FROM emails",
AMOUNT_FIELD_NAME,
group_by.as_str()
),
format!("GROUP BY {}", group_by.as_str()),
),
Query::Normal { fields, range, .. } => {
let fields: Vec<&str> = fields.iter().map(|e| e.into()).collect();
(
format!("SELECT {} FROM emails", fields.join(", ")),
format!("LIMIT {}, {}", range.start, range.end - range.start),
)
}
Query::Other {
query: OtherQuery::All(field),
} => (
format!("SELECT {} FROM emails", field.as_str()),
format!(""),
),
};
let (sql, values) = rsql_builder::B::prepare(
rsql_builder::B::new_sql(&header)
.push_build(&mut conditions)
.push_sql(&group_by),
);
(sql, values)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_test() {
let query = Query::Grouped {
filters: vec![
Filter::Like(ValueField::string(&Field::SenderDomain, "gmail.com")),
Filter::Is(ValueField::usize(&Field::Year, 2021)),
],
group_by: Field::Month,
};
dbg!(&query.to_sql());
}
}