diff options
| -rw-r--r-- | common/object/src/lib.rs | 2 | ||||
| -rw-r--r-- | common/object/src/path.rs | 18 | ||||
| -rw-r--r-- | common/object/src/value.rs | 35 | ||||
| -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 | ||||
| -rw-r--r-- | server/src/ui/home.rs | 47 |
8 files changed, 261 insertions, 135 deletions
diff --git a/common/object/src/lib.rs b/common/object/src/lib.rs index 3e60d58..c057163 100644 --- a/common/object/src/lib.rs +++ b/common/object/src/lib.rs @@ -3,7 +3,7 @@ which is licensed under the GNU Affero General Public License (version 3); see /COPYING. Copyright (C) 2026 metamuffin <metamuffin.org> */ -#![feature(iter_array_chunks)] +#![feature(iter_array_chunks, strip_circumfix)] mod buffer; pub mod debug; diff --git a/common/object/src/path.rs b/common/object/src/path.rs index 0751ff0..4779cd5 100644 --- a/common/object/src/path.rs +++ b/common/object/src/path.rs @@ -5,7 +5,7 @@ */ use crate::{Object, Tag, TypedTag}; -use std::{fmt::Display, marker::PhantomData}; +use std::{fmt::Display, marker::PhantomData, str::FromStr}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Path(pub Vec<Tag>); @@ -48,3 +48,19 @@ impl Display for Path { Ok(()) } } +impl FromStr for Path { + type Err = &'static str; + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(Self( + s.split(".") + .map(|e| { + e.as_bytes() + .try_into() + .map_err(|_| "path component not 4 bytes") + .map(Tag::new) + }) + .collect::<Result<Vec<Tag>, &'static str>>()?, + )) + } +} + diff --git a/common/object/src/value.rs b/common/object/src/value.rs index 3db7e7c..8371a79 100644 --- a/common/object/src/value.rs +++ b/common/object/src/value.rs @@ -5,7 +5,7 @@ */ use crate::{Object, ObjectBuffer, Tag}; -use std::borrow::Cow; +use std::{borrow::Cow, fmt::Display, str::FromStr}; pub trait ValueLoad<'a>: ValueStore + Sized { const ALIGNED: bool; @@ -54,7 +54,7 @@ impl ValueType { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Value<'a> { Tag(Tag), U32(u32), @@ -95,6 +95,37 @@ impl From<Tag> for Value<'static> { } } +impl Display for Value<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Value::Tag(tag) => write!(f, "{tag}"), + Value::U32(x) => write!(f, "{x}"), + Value::U64(x) => write!(f, "{x}"), + Value::I64(x) => write!(f, "{x}"), + Value::String(x) => write!(f, "{x:?}"), + Value::Binary(x) => write!(f, "{x:?}"), + } + } +} +impl FromStr for Value<'static> { + type Err = &'static str; + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(if s.len() == 4 { + Value::Tag(Tag::new(s.as_bytes().try_into().unwrap())) + } else if let Some(s) = s.strip_suffix("i64") { + Value::I64(s.parse().map_err(|_| "invalid i64 literal")?) + } else if let Some(s) = s.strip_suffix("u64") { + Value::U64(s.parse().map_err(|_| "invalid u64 literal")?) + } else if let Some(s) = s.strip_suffix("u32") { + Value::U32(s.parse().map_err(|_| "invalid u32 literal")?) + } else if let Some(s) = s.strip_circumfix("\"", "\"") { + Value::String(s.to_owned().into()) + } else { + return Err("invalid value literal"); + }) + } +} + pub trait ValueStore { fn get_type(&self) -> ValueType; fn store_aligned(&self, _buf: &mut Vec<u32>) {} 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, + }), + } + ) +} diff --git a/server/src/ui/home.rs b/server/src/ui/home.rs index c25add1..f6340be 100644 --- a/server/src/ui/home.rs +++ b/server/src/ui/home.rs @@ -4,14 +4,16 @@ Copyright (C) 2026 metamuffin <metamuffin.org> */ +use std::str::FromStr; + use super::error::MyResult; use crate::{request_info::RequestInfo, ui_responder::UiResponse}; -use anyhow::Result; +use anyhow::{Context, Result}; use jellycommon::{ - jellyobject::{Object, ObjectBuffer, ObjectBufferBuilder, Path}, + jellyobject::{Object, ObjectBuffer, ObjectBufferBuilder}, *, }; -use jellydb::{Filter, MultiBehaviour, Query, Sort, SortOrder, ValueSort}; +use jellydb::Query; use jellyui::tr; use rocket::get; @@ -28,18 +30,7 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<UiResponse> { home_row( &ri, "home.bin.latest_video", - Query { - filter: Filter::All(vec![ - Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), - Filter::Match(Path(vec![NO_KIND.0]), KIND_VIDEO.into()), - ]), - sort: Sort::Value(ValueSort { - order: SortOrder::Descending, - path: Path(vec![NO_RELEASEDATE.0]), - multi: MultiBehaviour::First, - offset: None, - }), - }, + "FILTER (visi = visi AND kind = vide) SORT DESCENDING BY FIRST rldt", )? .as_object(), ); @@ -48,18 +39,7 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<UiResponse> { home_row( &ri, "home.bin.latest_music", - Query { - filter: Filter::All(vec![ - Filter::Match(Path(vec![NO_VISIBILITY.0]), VISI_VISIBLE.into()), - Filter::Match(Path(vec![NO_KIND.0]), KIND_MUSIC.into()), - ]), - sort: Sort::Value(ValueSort { - order: SortOrder::Descending, - path: Path(vec![NO_RELEASEDATE.0]), - multi: MultiBehaviour::First, - offset: None, - }), - }, + "FILTER (visi = visi AND kind = musi) SORT DESCENDING BY FIRST rldt", )? .as_object(), ); @@ -68,15 +48,7 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<UiResponse> { home_row( &ri, "home.bin.max_rating", - Query { - filter: Filter::True, - sort: Sort::Value(ValueSort { - order: SortOrder::Descending, - path: Path(vec![NO_RATINGS.0, RTYP_TMDB.0]), - multi: MultiBehaviour::First, - offset: None, - }), - }, + "SORT DESCENDING BY FIRST rtng.imdb", )? .as_object(), ); @@ -84,7 +56,8 @@ pub fn r_home(ri: RequestInfo<'_>) -> MyResult<UiResponse> { Ok(ri.respond_ui(page.finish())) } -fn home_row(ri: &RequestInfo<'_>, title: &str, q: Query) -> Result<ObjectBuffer> { +fn home_row(ri: &RequestInfo<'_>, title: &str, query: &str) -> Result<ObjectBuffer> { + let q = Query::from_str(query).context("parse query")?; let mut res = ObjectBuffer::empty(); ri.state.database.transaction(&mut |txn| { let rows = txn.query(q.clone())?.take(16).collect::<Result<Vec<_>>>()?; |