pinakes: import in parallel; various UI improvements

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1eb47cd79cd4145c56af966f6756fe1d6a6a6964
This commit is contained in:
raf 2026-02-03 10:31:20 +03:00
commit 116fe7b059
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
42 changed files with 4316 additions and 316 deletions

View file

@ -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)
}
);
}
}