aboutsummaryrefslogtreecommitdiff
path: root/database/src/query_syntax.rs
diff options
context:
space:
mode:
Diffstat (limited to 'database/src/query_syntax.rs')
-rw-r--r--database/src/query_syntax.rs193
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,
+ }),
+ }
+ )
+}