pinakes: import in parallel; various UI improvements
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I1eb47cd79cd4145c56af966f6756fe1d6a6a6964
This commit is contained in:
parent
278bcaa4b0
commit
116fe7b059
42 changed files with 4316 additions and 316 deletions
|
|
@ -6,7 +6,10 @@ use winnow::{ModalResult, Parser};
|
|||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SearchQuery {
|
||||
FullText(String),
|
||||
FieldMatch { field: String, value: String },
|
||||
FieldMatch {
|
||||
field: String,
|
||||
value: String,
|
||||
},
|
||||
And(Vec<SearchQuery>),
|
||||
Or(Vec<SearchQuery>),
|
||||
Not(Box<SearchQuery>),
|
||||
|
|
@ -14,6 +17,45 @@ pub enum SearchQuery {
|
|||
Fuzzy(String),
|
||||
TypeFilter(String),
|
||||
TagFilter(String),
|
||||
/// Range query: field:start..end (inclusive)
|
||||
RangeQuery {
|
||||
field: String,
|
||||
start: Option<i64>,
|
||||
end: Option<i64>,
|
||||
},
|
||||
/// Comparison query: field:>value, field:<value, field:>=value, field:<=value
|
||||
CompareQuery {
|
||||
field: String,
|
||||
op: CompareOp,
|
||||
value: i64,
|
||||
},
|
||||
/// Date query: created:today, modified:last-week, etc.
|
||||
DateQuery {
|
||||
field: String,
|
||||
value: DateValue,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CompareOp {
|
||||
GreaterThan,
|
||||
GreaterOrEqual,
|
||||
LessThan,
|
||||
LessOrEqual,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum DateValue {
|
||||
Today,
|
||||
Yesterday,
|
||||
ThisWeek,
|
||||
LastWeek,
|
||||
ThisMonth,
|
||||
LastMonth,
|
||||
ThisYear,
|
||||
LastYear,
|
||||
/// Days ago: last-7d, last-30d
|
||||
DaysAgo(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -69,14 +111,143 @@ fn not_expr(input: &mut &str) -> ModalResult<SearchQuery> {
|
|||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a date value like "today", "yesterday", "last-week", "last-30d"
|
||||
fn parse_date_value(s: &str) -> Option<DateValue> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"today" => Some(DateValue::Today),
|
||||
"yesterday" => Some(DateValue::Yesterday),
|
||||
"this-week" | "thisweek" => Some(DateValue::ThisWeek),
|
||||
"last-week" | "lastweek" => Some(DateValue::LastWeek),
|
||||
"this-month" | "thismonth" => Some(DateValue::ThisMonth),
|
||||
"last-month" | "lastmonth" => Some(DateValue::LastMonth),
|
||||
"this-year" | "thisyear" => Some(DateValue::ThisYear),
|
||||
"last-year" | "lastyear" => Some(DateValue::LastYear),
|
||||
other => {
|
||||
// Try to parse "last-Nd" format (e.g., "last-7d", "last-30d")
|
||||
if let Some(rest) = other.strip_prefix("last-") {
|
||||
if let Some(days_str) = rest.strip_suffix('d') {
|
||||
if let Ok(days) = days_str.parse::<u32>() {
|
||||
return Some(DateValue::DaysAgo(days));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse size strings like "10MB", "1GB", "500KB" to bytes
|
||||
fn parse_size_value(s: &str) -> Option<i64> {
|
||||
let s = s.to_uppercase();
|
||||
if let Some(num) = s.strip_suffix("GB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024 * 1024 * 1024)
|
||||
} else if let Some(num) = s.strip_suffix("MB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024 * 1024)
|
||||
} else if let Some(num) = s.strip_suffix("KB") {
|
||||
num.parse::<i64>().ok().map(|n| n * 1024)
|
||||
} else if let Some(num) = s.strip_suffix('B') {
|
||||
num.parse::<i64>().ok()
|
||||
} else {
|
||||
s.parse::<i64>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
fn field_match(input: &mut &str) -> ModalResult<SearchQuery> {
|
||||
let field_name =
|
||||
take_while(1.., |c: char| c.is_alphanumeric() || c == '_').map(|s: &str| s.to_string());
|
||||
(field_name, ':', word_or_quoted)
|
||||
.map(|(field, _, value)| match field.as_str() {
|
||||
"type" => SearchQuery::TypeFilter(value),
|
||||
"tag" => SearchQuery::TagFilter(value),
|
||||
_ => SearchQuery::FieldMatch { field, value },
|
||||
.map(|(field, _, value)| {
|
||||
// Handle special field types
|
||||
match field.as_str() {
|
||||
"type" => return SearchQuery::TypeFilter(value),
|
||||
"tag" => return SearchQuery::TagFilter(value),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Check for range queries: field:start..end
|
||||
if value.contains("..") {
|
||||
let parts: Vec<&str> = value.split("..").collect();
|
||||
if parts.len() == 2 {
|
||||
let start = if parts[0].is_empty() {
|
||||
None
|
||||
} else if field == "size" {
|
||||
parse_size_value(parts[0])
|
||||
} else {
|
||||
parts[0].parse().ok()
|
||||
};
|
||||
let end = if parts[1].is_empty() {
|
||||
None
|
||||
} else if field == "size" {
|
||||
parse_size_value(parts[1])
|
||||
} else {
|
||||
parts[1].parse().ok()
|
||||
};
|
||||
return SearchQuery::RangeQuery { field, start, end };
|
||||
}
|
||||
}
|
||||
|
||||
// Check for comparison queries: >=, <=, >, <
|
||||
if let Some(rest) = value.strip_prefix(">=") {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::GreaterOrEqual,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix("<=") {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::LessOrEqual,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix('>') {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::GreaterThan,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
if let Some(rest) = value.strip_prefix('<') {
|
||||
let val = if field == "size" {
|
||||
parse_size_value(rest).unwrap_or(0)
|
||||
} else {
|
||||
rest.parse().unwrap_or(0)
|
||||
};
|
||||
return SearchQuery::CompareQuery {
|
||||
field,
|
||||
op: CompareOp::LessThan,
|
||||
value: val,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for date queries on created/modified fields
|
||||
if field == "created" || field == "modified" {
|
||||
if let Some(date_val) = parse_date_value(&value) {
|
||||
return SearchQuery::DateQuery {
|
||||
field,
|
||||
value: date_val,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Default: simple field match
|
||||
SearchQuery::FieldMatch { field, value }
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
|
@ -253,4 +424,131 @@ mod tests {
|
|||
let q = parse_search_query("\"hello world\"").unwrap();
|
||||
assert_eq!(q, SearchQuery::FullText("hello world".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_query_year() {
|
||||
let q = parse_search_query("year:2020..2023").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: Some(2020),
|
||||
end: Some(2023)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_query_open_start() {
|
||||
let q = parse_search_query("year:..2023").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: None,
|
||||
end: Some(2023)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_query_open_end() {
|
||||
let q = parse_search_query("year:2020..").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "year".into(),
|
||||
start: Some(2020),
|
||||
end: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_greater_than() {
|
||||
let q = parse_search_query("year:>2020").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::CompareQuery {
|
||||
field: "year".into(),
|
||||
op: CompareOp::GreaterThan,
|
||||
value: 2020
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compare_less_or_equal() {
|
||||
let q = parse_search_query("year:<=2023").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::CompareQuery {
|
||||
field: "year".into(),
|
||||
op: CompareOp::LessOrEqual,
|
||||
value: 2023
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_size_compare_mb() {
|
||||
let q = parse_search_query("size:>10MB").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::CompareQuery {
|
||||
field: "size".into(),
|
||||
op: CompareOp::GreaterThan,
|
||||
value: 10 * 1024 * 1024
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_size_range_gb() {
|
||||
let q = parse_search_query("size:1GB..2GB").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::RangeQuery {
|
||||
field: "size".into(),
|
||||
start: Some(1024 * 1024 * 1024),
|
||||
end: Some(2 * 1024 * 1024 * 1024)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_query_today() {
|
||||
let q = parse_search_query("created:today").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::DateQuery {
|
||||
field: "created".into(),
|
||||
value: DateValue::Today
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_query_last_week() {
|
||||
let q = parse_search_query("modified:last-week").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::DateQuery {
|
||||
field: "modified".into(),
|
||||
value: DateValue::LastWeek
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date_query_days_ago() {
|
||||
let q = parse_search_query("created:last-30d").unwrap();
|
||||
assert_eq!(
|
||||
q,
|
||||
SearchQuery::DateQuery {
|
||||
field: "created".into(),
|
||||
value: DateValue::DaysAgo(30)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue