mecomp_storage/db/schemas/dynamic/
mod.rs1#![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#[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 #[cfg_attr(feature = "db", field("any"))]
28 pub id: DynamicPlaylistId,
29
30 #[cfg_attr(feature = "db", field(dt = "string", index(unique)))]
32 pub name: Arc<str>,
33
34 #[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 "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}