diff options
Diffstat (limited to 'database')
| -rw-r--r-- | database/src/kv/mod.rs | 2 | ||||
| -rw-r--r-- | database/src/lib.rs | 11 | ||||
| -rw-r--r-- | database/src/query_ser.rs | 88 | ||||
| -rw-r--r-- | database/src/query_syntax.rs | 193 |
4 files changed, 200 insertions, 94 deletions
diff --git a/database/src/kv/mod.rs b/database/src/kv/mod.rs index 257eec4..ae5608e 100644 --- a/database/src/kv/mod.rs +++ b/database/src/kv/mod.rs @@ -102,7 +102,7 @@ impl Transaction for &mut dyn jellykv::Transaction { &'a mut self, query: Query, ) -> Result<Box<dyn Iterator<Item = Result<(RowNum, Vec<u8>)>> + 'a>> { - debug!("query: {}", query.show()); + debug!("query: {query}"); let mut prefixes = Vec::new(); for (binning, mut prefix) in query.filter.get_bins() { let ik = IndexKey(binning, query.sort.key()); diff --git a/database/src/lib.rs b/database/src/lib.rs index ea12447..5567020 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -3,8 +3,9 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ +#![feature(if_let_guard)] pub mod kv; -pub mod query_ser; +pub mod query_syntax; #[cfg(test)] pub mod test_shared; @@ -32,13 +33,13 @@ pub trait Transaction { fn debug_info(&self) -> Result<String>; } -#[derive(Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub struct Query<'a> { pub filter: Filter<'a>, pub sort: Sort, } -#[derive(Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq)] pub enum Sort { #[default] None, @@ -46,7 +47,7 @@ pub enum Sort { TextSearch(Path, String), } -#[derive(Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ValueSort { pub order: SortOrder, pub path: Path, @@ -69,7 +70,7 @@ pub enum SortOrder { Descending, } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq)] pub enum Filter<'a> { #[default] True, diff --git a/database/src/query_ser.rs b/database/src/query_ser.rs deleted file mode 100644 index 78eee09..0000000 --- a/database/src/query_ser.rs +++ /dev/null @@ -1,88 +0,0 @@ -/* - This file is part of jellything (https://codeberg.org/metamuffin/jellything) - which is licensed under the GNU Affero General Public License (version 3); see /COPYING. - Copyright (C) 2026 metamuffin <metamuffin.org> -*/ - -use jellyobject::Value; - -use crate::{Filter, MultiBehaviour, Query, Sort, SortOrder, ValueSort}; - -impl Query<'_> { - pub fn show(&self) -> String { - let mut o = String::new(); - if !matches!(self.filter, Filter::True) { - o += &format!("FILTER {} ", self.filter.show()) - } - if !matches!(self.sort, Sort::None) { - o += &format!("SORT {} ", self.sort.show()) - } - o - } -} -impl Filter<'_> { - pub fn show(&self) -> String { - match self { - Filter::True => "TRUE".to_string(), - Filter::All(filters) => format!( - "({})", - filters - .iter() - .map(|f| f.show()) - .collect::<Vec<_>>() - .join(" AND ") - ), - Filter::Any(filters) => format!( - "({})", - filters - .iter() - .map(|f| f.show()) - .collect::<Vec<_>>() - .join(" OR ") - ), - Filter::Match(path, value) => { - format!("{path} = {}", show_value(value)) - } - Filter::Has(path) => format!("{path}"), - } - } -} -impl Sort { - pub fn show(&self) -> String { - match self { - Sort::None => "NONE".to_string(), - Sort::Value(ValueSort { - multi, order, path, .. - }) => { - format!( - "{} BY {} {path}", - match order { - SortOrder::Ascending => "ASCENDING", - SortOrder::Descending => "DESCENDING", - }, - match multi { - MultiBehaviour::Count => "COUNT", - MultiBehaviour::First => "FIRST", - MultiBehaviour::ForEach => "EACH", - MultiBehaviour::Max => "MAX", - MultiBehaviour::Min => "MIN", - }, - ) - } - Sort::TextSearch(path, value) => { - format!("TEXT SEARCH {path} = {value:?}") - } - } - } -} - -fn show_value(value: &Value) -> String { - match value { - Value::Tag(tag) => format!("{tag}"), - Value::U32(x) => format!("{x}"), - Value::U64(x) => format!("{x}"), - Value::I64(x) => format!("{x}"), - Value::String(x) => format!("{x:?}"), - Value::Binary(x) => format!("{x:?}"), - } -} diff --git a/database/src/query_syntax.rs b/database/src/query_syntax.rs new file mode 100644 index 0000000..8720912 --- /dev/null +++ b/database/src/query_syntax.rs @@ -0,0 +1,193 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin <metamuffin.org> +*/ + +use anyhow::{Error, anyhow, bail}; +use jellyobject::{Path, Tag, Value}; + +use crate::{Filter, MultiBehaviour, Query, Sort, SortOrder, ValueSort}; +use std::{fmt::Display, str::FromStr}; + +impl Display for Query<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if !matches!(self.filter, Filter::True) { + write!(f, "FILTER {} ", self.filter)?; + } + if !matches!(self.sort, Sort::None) { + write!(f, "SORT {} ", self.sort)?; + } + Ok(()) + } +} +impl Display for Filter<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Filter::True => write!(f, "TRUE"), + Filter::All(filters) => write!( + f, + "({})", + filters + .iter() + .map(|f| f.to_string()) + .collect::<Vec<_>>() + .join(" AND ") + ), + Filter::Any(filters) => write!( + f, + "({})", + filters + .iter() + .map(|f| f.to_string()) + .collect::<Vec<_>>() + .join(" OR ") + ), + Filter::Match(path, value) => { + write!(f, "{path} = {value}") + } + Filter::Has(path) => write!(f, "{path}"), + } + } +} + +impl Display for Sort { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Sort::None => write!(f, "NONE"), + Sort::Value(ValueSort { + multi, order, path, .. + }) => { + write!( + f, + "{} BY {} {path}", + match order { + SortOrder::Ascending => "ASCENDING", + SortOrder::Descending => "DESCENDING", + }, + match multi { + MultiBehaviour::Count => "COUNT", + MultiBehaviour::First => "FIRST", + MultiBehaviour::ForEach => "EACH", + MultiBehaviour::Max => "MAX", + MultiBehaviour::Min => "MIN", + }, + ) + } + Sort::TextSearch(path, value) => { + write!(f, "TEXT SEARCH {path} = {value:?}") + } + } + } +} + +impl FromStr for Query<'static> { + type Err = Error; + fn from_str(s: &str) -> Result<Self, Self::Err> { + if let Some((filter, sort)) = s.split_once(" SORT ") + && let Some(filter) = filter.strip_prefix("FILTER ") + { + Ok(Self { + filter: Filter::from_str(filter)?, + sort: Sort::from_str(sort)?, + }) + } else if let Some(sort) = s.strip_prefix("SORT ") { + Ok(Self { + filter: Filter::True, + sort: Sort::from_str(sort)?, + }) + } else if let Some(filter) = s.strip_prefix("FILTER") { + Ok(Self { + filter: Filter::from_str(filter)?, + sort: Sort::None, + }) + } else { + bail!("invalid query") + } + } +} +impl FromStr for Filter<'static> { + type Err = Error; + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s { + "TRUE" => Self::True, + x if let Some(x) = x.strip_prefix("(") + && let Some(x) = x.strip_suffix(")") + && let Some((l, r)) = x.split_once("AND") => + { + Self::All(vec![Filter::from_str(l)?, Filter::from_str(r)?]) + } + x if let Some(x) = x.strip_prefix("(") + && let Some(x) = x.strip_suffix(")") + && let Some((l, r)) = x.split_once("OR") => + { + Self::Any(vec![ + Filter::from_str(l.trim())?, + Filter::from_str(r.trim())?, + ]) + } + x if let Some((l, r)) = x.split_once("=") => Self::Match( + Path::from_str(l.trim()).map_err(|e| anyhow!("{e}"))?, + Value::from_str(r.trim()).map_err(|e| anyhow!("{e}"))?, + ), + _ => bail!("invalid filter"), + }) + } +} +impl FromStr for Sort { + type Err = Error; + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(if s == "NONE" { + Sort::None + } else if let Some(s) = s.strip_prefix("TEXT SEARCH ") + && let Some((path, value)) = s.split_once(" = ") + { + Sort::TextSearch( + Path::from_str(path).map_err(|e| anyhow!("{e}"))?, + value.to_owned(), + ) + } else if let Some((order, rest)) = s.split_once(" BY ") + && let Some((multi, path)) = rest.split_once(" ") + { + Sort::Value(ValueSort { + order: match order { + "ASCENDING" => SortOrder::Ascending, + "DESCENDING" => SortOrder::Descending, + _ => bail!("unknown order"), + }, + multi: match multi { + "COUNT" => MultiBehaviour::Count, + "FIRST" => MultiBehaviour::First, + "EACH" => MultiBehaviour::ForEach, + "MAX" => MultiBehaviour::Max, + "MIN" => MultiBehaviour::Min, + _ => bail!("unknown multi bahav"), + }, + path: Path::from_str(path).map_err(|e| anyhow!("{e}"))?, + offset: None, + }) + } else { + bail!("unknown sort") + }) + } +} + +#[test] +fn test_parse() { + assert_eq!( + Query::from_str("FILTER (visi = visi AND kind = vide) SORT DESCENDING BY FIRST rldt") + .unwrap(), + Query { + filter: Filter::All(vec![ + Filter::Match(Path(vec![Tag::new(b"visi")]), Tag::new(b"visi").into()), + Filter::Match(Path(vec![Tag::new(b"kind")]), Tag::new(b"vide").into()), + ]), + sort: Sort::Value(ValueSort { + order: SortOrder::Descending, + path: Path(vec![Tag::new(b"rldt")]), + multi: MultiBehaviour::First, + offset: None, + }), + } + ) +} |