mecomp_storage/db/schemas/dynamic/
mod.rs

1#![allow(clippy::module_name_repetitions)]
2use std::sync::Arc;
3
4#[cfg(not(feature = "db"))]
5use super::{Id, Thing};
6use query::Query;
7#[cfg(feature = "db")]
8use surrealdb::{
9    opt::IntoQuery,
10    sql::{Id, Thing},
11};
12
13pub mod query;
14
15pub type DynamicPlaylistId = Thing;
16
17pub const TABLE_NAME: &str = "dynamic";
18
19/// This struct holds all the metadata about a particular [`DynamicPlaylist`].
20/// A [`DynamicPlaylist`] is essentially a query that returns a list of [`super::song::Song`]s.
21#[derive(Clone, Debug, PartialEq, Eq)]
22#[cfg_attr(feature = "db", derive(surrealqlx::Table))]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24#[cfg_attr(feature = "db", Table("dynamic"))]
25pub struct DynamicPlaylist {
26    /// the unique identifier for this [`DynamicPlaylist`].
27    #[cfg_attr(feature = "db", field("any"))]
28    pub id: DynamicPlaylistId,
29
30    /// The [`DynamicPlaylist`]'s name.
31    #[cfg_attr(feature = "db", field(dt = "string", index(unique)))]
32    pub name: Arc<str>,
33
34    /// The query that generates the list of songs.
35    /// This is a type that can compile into an SQL query that returns a list of song IDs.
36    /// NOTE: we store it as the compiled string because `SurrealDB` wasn't storing records properly
37    #[cfg_attr(feature = "db", field("string"))]
38    pub query: Query,
39}
40
41impl DynamicPlaylist {
42    #[must_use]
43    #[inline]
44    pub fn generate_id() -> DynamicPlaylistId {
45        Thing::from((TABLE_NAME, Id::ulid()))
46    }
47
48    #[must_use]
49    #[cfg(feature = "db")]
50    #[inline]
51    pub fn get_query(&self) -> impl IntoQuery {
52        use query::Compile;
53
54        format!(
55            // This query would make "artist ANYINSIDE ['foo', 'bar']" type queries work, but breaks almost everything else
56            // "SELECT * FROM (SELECT id, title, album, track, disc, path, extension, release_year, runtime, array::flatten([artist][? $this]) AS artist, array::flatten([album_artist][? $this]) AS album_artist, array::flatten([genre][? $this]) AS genre FROM {table_name}) WHERE {conditions};",
57            "SELECT * FROM {table_name} WHERE {conditions};",
58            table_name = super::song::TABLE_NAME,
59            conditions = self.query.compile(query::Context::Execution)
60        )
61    }
62}
63
64#[derive(Debug, Default, Clone, PartialEq, Eq)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub struct DynamicPlaylistChangeSet {
67    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
68    pub name: Option<Arc<str>>,
69    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
70    pub query: Option<Query>,
71}
72
73impl DynamicPlaylistChangeSet {
74    #[must_use]
75    #[inline]
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    #[must_use]
81    #[inline]
82    pub fn name(mut self, name: impl Into<Arc<str>>) -> Self {
83        self.name = Some(name.into());
84        self
85    }
86
87    #[must_use]
88    #[inline]
89    pub fn query(mut self, query: Query) -> Self {
90        self.query = Some(query);
91        self
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_generate_id() {
101        let id = DynamicPlaylist::generate_id();
102        assert_eq!(id.tb, TABLE_NAME);
103    }
104}
105
106#[cfg(all(test, feature = "db"))]
107mod query_tests {
108    use super::*;
109    use pretty_assertions::assert_eq;
110    use query::{Clause, CompoundClause, CompoundKind, Field, LeafClause, Operator, Value};
111    use rstest::rstest;
112
113    #[rstest]
114    #[case::leaf_clause(
115        Query {root: Clause::Leaf(LeafClause {
116            left: Value::Field(Field::Title),
117            operator: Operator::Equal,
118            right: Value::String("foo".to_string())
119        })},
120        "SELECT * FROM song WHERE title = 'foo';"
121    )]
122    #[case::leaf_clause(
123        Query { root: Clause::Leaf(LeafClause {
124            left: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)]),
125            operator: Operator::Contains,
126            right: Value::Int(42)
127        })},
128        "SELECT * FROM song WHERE [\"foo\", 42] CONTAINS 42;"
129    )]
130    #[case::compound_clause(
131        Query { root: Clause::Compound( CompoundClause {
132            clauses: vec![
133                Clause::Leaf(LeafClause {
134                    left: Value::Field(Field::Title),
135                    operator: Operator::Equal,
136                    right: Value::String("foo".to_string())
137                }),
138                Clause::Leaf(LeafClause {
139                    left: Value::Field(Field::Artists),
140                    operator: Operator::Contains,
141                    right: Value::String("bar".to_string())
142                }),
143            ],
144            kind: CompoundKind::And
145        })},
146        "SELECT * FROM song WHERE (title = \"foo\" AND array::flatten([artist][? $this]) CONTAINS \"bar\");"
147    )]
148    #[case::compound_clause(
149        Query { root: Clause::Compound(CompoundClause {
150            clauses: vec![
151                Clause::Leaf(LeafClause {
152                    left: Value::Field(Field::Title),
153                    operator: Operator::Equal,
154                    right: Value::String("foo".to_string())
155                }),
156                Clause::Leaf(LeafClause {
157                    left: Value::Field(Field::Artists),
158                    operator: Operator::Contains,
159                    right: Value::String("bar".to_string())
160                }),
161            ],
162            kind: CompoundKind::Or
163        })},
164        "SELECT * FROM song WHERE (title = \"foo\" OR array::flatten([artist][? $this]) CONTAINS \"bar\");"
165    )]
166    #[case::query(
167        Query {
168            root: Clause::Compound(CompoundClause {
169                clauses: vec![
170                    Clause::Compound(
171                        CompoundClause {
172                            clauses: vec![
173                                Clause::Leaf(LeafClause {
174                                    left: Value::Field(Field::Artists),
175                                    operator: Operator::AnyInside,
176                                    right: Value::Set(vec![Value::String("foo".to_string()), Value::String("bar".to_string())])
177                                }),
178                                Clause::Compound(CompoundClause {
179                                    clauses: vec![
180                                        Clause::Leaf(LeafClause {
181                                            left: Value::Field(Field::AlbumArtists),
182                                            operator: Operator::Contains,
183                                            right: Value::String("bar".to_string())
184                                        }),
185                                        Clause::Leaf(LeafClause {
186                                            left: Value::Field(Field::Genre),
187                                            operator: Operator::AnyLike,
188                                            right: Value::String("baz".to_string())
189                                        }),
190                                    ],
191                                    kind: CompoundKind::Or
192                                }),
193                            ],
194                            kind: CompoundKind::And
195                        }
196                    ),
197                    Clause::Leaf(LeafClause {
198                        left: Value::Field(Field::ReleaseYear),
199                        operator: Operator::GreaterThan,
200                        right: Value::Int(2020)
201                    }),
202                ],
203                kind: CompoundKind::And
204            })
205        },
206        "SELECT * FROM song WHERE ((array::flatten([artist][? $this]) ANYINSIDE [\"foo\", \"bar\"] AND (array::flatten([album_artist][? $this]) CONTAINS \"bar\" OR array::flatten([genre][? $this])  ?~ \"baz\")) AND release_year > 2020);"
207    )]
208    fn test_compile(#[case] query: Query, #[case] expected: impl IntoQuery) {
209        let dynamic_playlist = DynamicPlaylist {
210            id: DynamicPlaylist::generate_id(),
211            name: Arc::from("test"),
212            query,
213        };
214
215        let compiled = dynamic_playlist.get_query().into_query();
216
217        assert!(compiled.is_ok());
218        assert_eq!(compiled.unwrap(), expected.into_query().unwrap());
219    }
220}