Simplified query

pull/1/head
Benedikt Terhechte 3 years ago
parent be4970da6e
commit ce11839ae4

35
Cargo.lock generated

@ -755,6 +755,8 @@ dependencies = [
"rusqlite",
"serde",
"serde_json",
"strum",
"strum_macros",
"thiserror",
"tracing",
"tracing-subscriber",
@ -779,6 +781,15 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
@ -1668,6 +1679,24 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]]
name = "strum"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2"
[[package]]
name = "strum_macros"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.77"
@ -1842,6 +1871,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6"
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-xid"
version = "0.2.2"

@ -24,6 +24,8 @@ eframe = { version = "*", optional = true}
rsql_builder = "0.1.2"
treemap = "0.3.2"
num-format = "0.4.0"
strum = "0.21"
strum_macros = "0.21"
[features]
default = ["gui"]

@ -12,7 +12,7 @@ use crossbeam_channel::{unbounded, Receiver, Sender};
use eyre::{Report, Result};
use crate::database::{
query::{Filter, GroupByField, Query},
query::{Field, Filter, Query},
query_result::QueryResult,
Database,
};
@ -20,14 +20,21 @@ use crate::types::Config;
use super::partitions::{Partition, Partitions};
// FIXME:
// - query and request are the same, just make the query the request.
// - instead of Query<'a> move just Query into the thread and
// then in there move &'a Query around.
// - give query a range
// - use strum
pub enum Request {
Grouped {
filters: Vec<Filter>,
group_by: GroupByField,
group_by: Field,
},
Normal {
filters: Vec<Filter>,
fields: Vec<GroupByField>,
fields: Vec<Field>,
},
}

@ -3,34 +3,16 @@ use std::ops::RangeInclusive;
use eframe::egui::Rect;
use eyre::{eyre, Result};
use crate::database::query::{Filter, GroupByField, ValueField};
use crate::database::query::{Field, Filter, ValueField};
use crate::types::Config;
use super::calc::{Link, Request};
use super::partitions::{Partition, Partitions};
// FIXME: Use strum or one of the enum to string crates
const DEFAULT_GROUP_BY_FIELDS: &[GroupByField] = {
use GroupByField::*;
&[
SenderDomain,
SenderLocalPart,
SenderName,
Year,
Month,
Day,
ToGroup,
ToName,
ToAddress,
IsReply,
IsSend,
]
};
// FIXME: Try with lifetimes. For this use case it might just work
pub struct Grouping {
value: Option<ValueField>,
field: GroupByField,
field: Field,
index: usize,
}
@ -43,7 +25,7 @@ impl Grouping {
self.field.as_str()
}
pub fn index(&self, in_fields: &[GroupByField]) -> Option<usize> {
pub fn index(&self, in_fields: &[Field]) -> Option<usize> {
in_fields.iter().position(|p| p == &self.field)
}
}
@ -59,7 +41,7 @@ pub enum Action {
pub struct Engine {
search_stack: Vec<ValueField>,
group_by_stack: Vec<GroupByField>,
group_by_stack: Vec<Field>,
link: Link<Action>,
partitions: Vec<Partitions>,
action: Option<Action>,
@ -141,7 +123,7 @@ impl Engine {
result
}
pub fn update_grouping(&mut self, grouping: &Grouping, field: &GroupByField) -> Result<()> {
pub fn update_grouping(&mut self, grouping: &Grouping, field: &Field) -> Result<()> {
self.group_by_stack
.get_mut(grouping.index)
.map(|e| *e = field.clone());
@ -236,14 +218,13 @@ impl Engine {
/// Return all group fields which are still available based
/// on the current stack.
/// Also always include the current one, so we can choose between
pub fn available_group_by_fields(&self, grouping: &Grouping) -> Vec<GroupByField> {
DEFAULT_GROUP_BY_FIELDS
.iter()
pub fn available_group_by_fields(&self, grouping: &Grouping) -> Vec<Field> {
Field::all_cases()
.filter_map(|f| {
if f == &grouping.field {
if f == grouping.field {
return Some(f.clone());
}
if self.group_by_stack.contains(f) {
if self.group_by_stack.contains(&f) {
None
} else {
Some(f.clone())
@ -277,13 +258,13 @@ impl Engine {
}
/// Return the default group by fields index for each stack entry
pub fn default_group_by_stack(index: usize) -> GroupByField {
pub fn default_group_by_stack(index: usize) -> Field {
match index {
0 => GroupByField::Year,
1 => GroupByField::SenderDomain,
2 => GroupByField::SenderLocalPart,
3 => GroupByField::Month,
4 => GroupByField::Day,
0 => Field::Year,
1 => Field::SenderDomain,
2 => Field::SenderLocalPart,
3 => Field::Month,
4 => Field::Day,
_ => panic!(),
}
}

@ -6,7 +6,7 @@ use eyre::{bail, eyre, Result};
use rusqlite::{self, types, Row};
use serde_json::Value;
use super::query::{GroupByField, ValueField, AMOUNT_FIELD_NAME};
use super::query::{Field, ValueField, AMOUNT_FIELD_NAME};
use super::query_result::QueryResult;
use crate::types::{EmailEntry, EmailMeta};
@ -33,12 +33,12 @@ pub fn json_to_value(input: &Value) -> Result<types::Value> {
}
pub trait RowConversion<'a>: Sized {
fn grouped_from_row<'stmt>(field: &'a GroupByField, row: &Row<'stmt>) -> Result<Self>;
fn from_row<'stmt>(fields: &'a [GroupByField], row: &Row<'stmt>) -> Result<Self>;
fn grouped_from_row<'stmt>(field: &'a Field, row: &Row<'stmt>) -> Result<Self>;
fn from_row<'stmt>(fields: &'a [Field], row: &Row<'stmt>) -> Result<Self>;
}
impl<'a> RowConversion<'a> for QueryResult {
fn grouped_from_row<'stmt>(field: &'a GroupByField, row: &Row<'stmt>) -> Result<Self> {
fn grouped_from_row<'stmt>(field: &'a Field, row: &Row<'stmt>) -> Result<Self> {
let amount: usize = row.get(AMOUNT_FIELD_NAME)?;
let values = values_from_fields(&[*field], &row)?;
@ -47,55 +47,34 @@ impl<'a> RowConversion<'a> for QueryResult {
values,
})
}
fn from_row<'stmt>(fields: &'a [GroupByField], row: &Row<'stmt>) -> Result<Self> {
fn from_row<'stmt>(fields: &'a [Field], row: &Row<'stmt>) -> Result<Self> {
let values = values_from_fields(&fields, &row)?;
Ok(QueryResult::Normal(values))
}
}
fn values_from_fields<'stmt>(fields: &[GroupByField], row: &Row<'stmt>) -> Result<Vec<ValueField>> {
fn values_from_fields<'stmt>(fields: &[Field], row: &Row<'stmt>) -> Result<Vec<ValueField>> {
let mut values = vec![];
for field in fields {
use GroupByField::*;
use Field::*;
// Use type safety when unpacking
match field {
// Str fields
SenderDomain => values.push(ValueField::SenderDomain(
row.get::<&str, String>(field.as_str())?.into(),
)),
SenderLocalPart => values.push(ValueField::SenderLocalPart(
row.get::<&str, String>(field.as_str())?.into(),
)),
SenderName => values.push(ValueField::SenderName(
row.get::<&str, String>(field.as_str())?.into(),
)),
ToGroup => values.push(ValueField::ToGroup(
row.get::<&str, String>(field.as_str())?.into(),
)),
ToName => values.push(ValueField::ToName(
row.get::<&str, String>(field.as_str())?.into(),
)),
ToAddress => values.push(ValueField::ToAddress(
row.get::<&str, String>(field.as_str())?.into(),
)),
// usize field
Year => values.push(ValueField::Year(
row.get::<&str, usize>(field.as_str())?.into(),
)),
Month => values.push(ValueField::Day(
row.get::<&str, usize>(field.as_str())?.into(),
)),
Day => values.push(ValueField::Day(
row.get::<&str, usize>(field.as_str())?.into(),
)),
// bool field
IsReply => values.push(ValueField::IsReply(
row.get::<&str, bool>(field.as_str())?.into(),
)),
IsSend => values.push(ValueField::IsSend(
row.get::<&str, bool>(field.as_str())?.into(),
)),
SenderDomain | SenderLocalPart | SenderName | ToGroup | ToName | ToAddress => {
let string: String = row.get::<&str, String>(field.as_str())?.into();
values.push(ValueField::string(&field, &string));
}
Year | Month | Day => {
values.push(ValueField::usize(
&field,
row.get::<&str, usize>(field.as_str())?.into(),
));
}
IsReply | IsSend => {
values.push(ValueField::bool(
&field,
row.get::<&str, bool>(field.as_str())?.into(),
));
}
}
}
Ok(values)

@ -1,33 +1,19 @@
use rsql_builder;
use serde_json;
use serde_json::{self, Value};
use strum::{self, IntoEnumIterator};
use strum_macros::{EnumIter, IntoStaticStr};
pub const AMOUNT_FIELD_NAME: &str = "amount";
/// For In-Queries, we need a Vec of at least one, so we make a new type
pub struct VecOfMinOne<T> {
inner: Vec<T>,
}
impl<T> VecOfMinOne<T> {
/// Create a new `VecOfMinOne`.
/// If `from` is empty, return `None`
pub fn new(from: Vec<T>) -> Option<Self> {
if from.is_empty() {
return None;
}
Some(Self { inner: from })
}
}
pub enum Filter {
Like(ValueField),
NotLike(ValueField),
Is(ValueField),
In(VecOfMinOne<ValueField>),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum GroupByField {
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoStaticStr, EnumIter)]
#[strum(serialize_all = "snake_case")]
pub enum Field {
SenderDomain,
SenderLocalPart,
SenderName,
@ -41,237 +27,62 @@ pub enum GroupByField {
IsSend,
}
impl GroupByField {
pub fn as_str(&self) -> &str {
use GroupByField::*;
match self {
SenderDomain => "sender_domain",
SenderLocalPart => "sender_local_part",
SenderName => "sender_name",
Year => "year",
Month => "month",
Day => "day",
ToGroup => "to_group",
ToName => "to_name",
ToAddress => "to_address",
IsReply => "is_reply",
IsSend => "is_send",
}
impl Field {
pub fn all_cases() -> impl Iterator<Item = Field> {
Field::iter()
}
}
impl<'a> ValueField {
pub fn as_field(&self) -> GroupByField {
use GroupByField::*;
match self {
ValueField::SenderDomain(_) => SenderDomain,
ValueField::SenderLocalPart(_) => SenderLocalPart,
ValueField::SenderName(_) => SenderName,
ValueField::Year(_) => Year,
ValueField::Month(_) => Month,
ValueField::Day(_) => Day,
ValueField::ToGroup(_) => ToGroup,
ValueField::ToName(_) => ToName,
ValueField::ToAddress(_) => ToAddress,
ValueField::IsReply(_) => IsReply,
ValueField::IsSend(_) => IsSend,
}
/// Just a wrapper to offer `into` without the type ambiguity
/// that sometimes arises
pub fn as_str(&self) -> &'static str {
self.into()
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum ValueField {
SenderDomain(String),
SenderLocalPart(String),
SenderName(String),
Year(usize),
Month(usize),
Day(usize),
ToGroup(String),
ToName(String),
ToAddress(String),
IsReply(bool),
IsSend(bool),
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ValueField {
field: Field,
value: Value,
}
// FIXME: Maybe use `json-value` instead?
impl ValueField {
pub fn value(&self) -> Value {
match (self.is_bool(), self.is_str(), self.is_usize()) {
(true, false, false) => Value::Bool(*self.as_bool()),
(false, true, false) => Value::String(self.as_str().to_string()),
(false, false, true) => Value::Number(*self.as_usize()),
_ => panic!("Invalid field: {:?}", &self),
pub fn string<S: AsRef<str>>(field: &Field, value: S) -> ValueField {
ValueField {
field: field.clone(),
value: Value::String(value.as_ref().to_string()),
}
}
}
#[derive(Debug, Hash, Clone)]
pub enum Value {
Number(usize),
String(String),
Bool(bool),
}
impl std::fmt::Display for Value {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Value::Number(n) => f.write_str(&n.to_string()),
Value::Bool(n) => f.write_str(&n.to_string()),
Value::String(n) => f.write_str(&n),
pub fn bool(field: &Field, value: bool) -> ValueField {
ValueField {
field: field.clone(),
value: Value::Bool(value),
}
}
}
pub trait DynamicType<'a> {
type BoolType;
type StrType;
type UsizeType;
fn is_str(&self) -> bool;
fn is_bool(&self) -> bool;
fn is_usize(&self) -> bool;
fn as_str(&'a self) -> Self::StrType;
fn as_bool(&'a self) -> Self::BoolType;
fn as_usize(&'a self) -> Self::UsizeType;
}
impl<'a> DynamicType<'a> for ValueField {
type BoolType = &'a bool;
type StrType = &'a str;
type UsizeType = &'a usize;
fn is_str(&self) -> bool {
!self.is_bool() && !self.is_usize()
}
fn is_bool(&self) -> bool {
use ValueField::*;
match self {
IsReply(_) | IsSend(_) => true,
_ => false,
}
}
fn is_usize(&self) -> bool {
use ValueField::*;
match self {
Year(_) | Month(_) | Day(_) => true,
_ => false,
pub fn usize(field: &Field, value: usize) -> ValueField {
ValueField {
field: field.clone(),
value: Value::Number(value.into()),
}
}
fn as_str(&'a self) -> Self::StrType {
use ValueField::*;
match self {
SenderDomain(a) | SenderLocalPart(a) | SenderName(a) | ToGroup(a) | ToName(a)
| ToAddress(a) => &a,
_ => panic!(),
}
pub fn field(&self) -> &Field {
&self.field
}
fn as_bool(&'a self) -> Self::BoolType {
use ValueField::*;
match self {
IsReply(a) | IsSend(a) => a,
_ => panic!(),
}
}
fn as_usize(&'a self) -> Self::UsizeType {
use ValueField::*;
match self {
Year(a) | Month(a) | Day(a) => a,
_ => panic!(),
}
}
}
impl<'a> DynamicType<'a> for &VecOfMinOne<ValueField> {
type BoolType = Vec<bool>;
type StrType = Vec<&'a str>;
type UsizeType = Vec<usize>;
fn is_str(&self) -> bool {
self.inner[0].is_str()
}
fn is_bool(&self) -> bool {
self.inner[0].is_bool()
}
fn is_usize(&self) -> bool {
self.inner[0].is_usize()
}
fn as_str(&'a self) -> Self::StrType {
self.inner.iter().map(|e| e.as_str()).collect()
}
fn as_bool(&'a self) -> Self::BoolType {
self.inner.iter().map(|e| *e.as_bool()).collect()
}
fn as_usize(&'a self) -> Self::UsizeType {
self.inner.iter().map(|e| *e.as_usize()).collect()
}
}
impl<'a> From<&VecOfMinOne<ValueField>> for &'a str {
fn from(vector: &VecOfMinOne<ValueField>) -> Self {
use ValueField::*;
match &vector.inner[0] {
SenderDomain(_) => "sender_domain",
SenderLocalPart(_) => "sender_local_part",
SenderName(_) => "sender_name",
Year(_) => "year",
Month(_) => "month",
Day(_) => "day",
ToGroup(_) => "to_group",
ToName(_) => "to_name",
ToAddress(_) => "to_address",
IsReply(_) => "is_reply",
IsSend(_) => "is_send",
}
}
}
impl<'a> From<&'a ValueField> for &'a str {
fn from(field: &'a ValueField) -> Self {
use ValueField::*;
match field {
SenderDomain(_) => "sender_domain",
SenderLocalPart(_) => "sender_local_part",
SenderName(_) => "sender_name",
Year(_) => "year",
Month(_) => "month",
Day(_) => "day",
ToGroup(_) => "to_group",
ToName(_) => "to_name",
ToAddress(_) => "to_address",
IsReply(_) => "is_reply",
IsSend(_) => "is_send",
}
}
}
impl ValueField {
pub fn as_group_field(&self) -> GroupByField {
use ValueField::*;
match self {
SenderDomain(_) => GroupByField::SenderDomain,
SenderLocalPart(_) => GroupByField::SenderLocalPart,
SenderName(_) => GroupByField::SenderName,
Year(_) => GroupByField::Year,
Month(_) => GroupByField::Month,
Day(_) => GroupByField::Day,
ToGroup(_) => GroupByField::ToGroup,
ToName(_) => GroupByField::ToName,
ToAddress(_) => GroupByField::ToAddress,
IsReply(_) => GroupByField::IsReply,
IsSend(_) => GroupByField::IsSend,
}
pub fn value(&self) -> &Value {
&self.value
}
}
pub enum Query<'a> {
Grouped {
filters: &'a [Filter],
group_by: &'a GroupByField,
group_by: &'a Field,
},
Normal {
fields: &'a [GroupByField],
fields: &'a [Field],
filters: &'a [Filter],
},
}
@ -291,25 +102,9 @@ impl<'a> Query<'a> {
let mut whr = rsql_builder::B::new_where();
for filter in self.filters() {
match filter {
// Bool
Filter::Like(f) if f.is_bool() => whr.like(f.into(), f.as_bool()),
Filter::NotLike(f) if f.is_bool() => whr.not_like(f.into(), f.as_bool()),
Filter::In(f) if f.is_bool() => whr.r#in(f.into(), &f.as_bool()),
Filter::Is(f) if f.is_bool() => whr.eq(f.into(), f.as_bool()),
// usize
Filter::Like(f) if f.is_usize() => whr.like(f.into(), f.as_usize()),
Filter::NotLike(f) if f.is_usize() => whr.not_like(f.into(), f.as_usize()),
Filter::In(f) if f.is_usize() => whr.r#in(f.into(), &f.as_usize()),
Filter::Is(f) if f.is_usize() => whr.eq(f.into(), f.as_usize()),
// str
Filter::Like(f) if f.is_str() => whr.like(f.into(), &f.as_str()),
Filter::NotLike(f) if f.is_str() => whr.not_like(f.into(), &f.as_str()),
Filter::In(f) if f.is_str() => whr.r#in(f.into(), &f.as_str()),
Filter::Is(f) if f.is_str() => whr.eq(f.into(), &f.as_str()),
_ => &whr,
Filter::Like(f) => whr.like(f.field.into(), f.value()),
Filter::NotLike(f) => whr.not_like(f.field.into(), f.value()),
Filter::Is(f) => whr.eq(f.field.into(), f.value()),
};
}
whr
@ -325,7 +120,7 @@ impl<'a> Query<'a> {
format!("GROUP BY {}", group_by.as_str()),
),
Query::Normal { fields, .. } => {
let fields: Vec<&str> = fields.iter().map(|e| e.as_str()).collect();
let fields: Vec<&str> = fields.iter().map(|e| e.into()).collect();
(
format!("SELECT {} FROM emails", fields.join(", ")),
"".to_owned(),
@ -349,14 +144,12 @@ mod tests {
#[test]
fn test_test() {
let value = format!("bx");
let query = Query::Grouped {
filters: &[
Filter::Is(ValueField::ToName("bam".into())),
Filter::Like(ValueField::SenderName(value.into())),
Filter::Like(ValueField::Year(2323)),
Filter::Like(ValueField::string(&Field::SenderDomain, "gmail.com")),
Filter::Is(ValueField::usize(&Field::Year, 2021)),
],
group_by: &GroupByField::Month,
group_by: &Field::Month,
};
dbg!(&query.to_sql());
}

@ -1,14 +1,15 @@
use std::collections::hash_map::DefaultHasher;
use crate::cluster_engine::{Engine, Partition};
use eframe::egui::{self, epaint::Galley, vec2, Pos2, Rect, Rgba, Stroke, TextStyle, Widget};
use eframe::egui::{self, epaint::Galley, Pos2, Rgba, Stroke, TextStyle, Widget};
use eyre::Report;
use num_format::{Locale, ToFormattedString};
fn partition_to_color(partition: &Partition) -> Rgba {
let mut hasher = DefaultHasher::new();
use std::hash::{Hash, Hasher};
partition.field.hash(&mut hasher);
let value = partition.field.value().to_string();
value.hash(&mut hasher);
let value = hasher.finish();
let [r1, r2, g1, g2, b1, b2, _, _] = value.to_be_bytes();

Loading…
Cancel
Save