diff options
Diffstat (limited to 'database/src/query_syntax.rs')
| -rw-r--r-- | database/src/query_syntax.rs | 193 |
1 files changed, 193 insertions, 0 deletions
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, + }), + } + ) +} |