aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/object/src/lib.rs2
-rw-r--r--common/object/src/path.rs18
-rw-r--r--common/object/src/value.rs35
-rw-r--r--database/src/kv/mod.rs2
-rw-r--r--database/src/lib.rs11
-rw-r--r--database/src/query_ser.rs88
-rw-r--r--database/src/query_syntax.rs193
-rw-r--r--server/src/ui/home.rs47
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<_>>>()?;