#![allow(clippy::module_name_repetitions)]
use super::Id;
#[cfg(not(feature = "db"))]
use super::RecordId;
use query::Query;
#[cfg(feature = "db")]
use surrealdb::{RecordId, opt::IntoQuery};
pub mod query;
pub type DynamicPlaylistId = RecordId;
pub const TABLE_NAME: &str = "dynamic";
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "db", derive(surrealqlx::Table))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "db", Table("dynamic"))]
pub struct DynamicPlaylist {
#[cfg_attr(feature = "db", field(dt = "record"))]
pub id: DynamicPlaylistId,
#[cfg_attr(feature = "db", field(dt = "string"))]
#[cfg_attr(feature = "db", index(unique))]
pub name: String,
#[cfg_attr(feature = "db", field(dt = "string"))]
pub query: Query,
}
impl DynamicPlaylist {
#[must_use]
#[inline]
pub fn generate_id() -> DynamicPlaylistId {
RecordId::from_table_key(TABLE_NAME, Id::ulid())
}
#[must_use]
#[cfg(feature = "db")]
#[inline]
pub fn get_query(&self) -> impl IntoQuery + use<> {
use query::Compile;
format!(
"SELECT * FROM {table_name} WHERE {conditions};",
table_name = super::song::TABLE_NAME,
conditions = self.query.compile(query::Context::Execution)
)
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DynamicPlaylistChangeSet {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub name: Option<String>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub query: Option<Query>,
}
impl DynamicPlaylistChangeSet {
#[must_use]
#[inline]
pub fn new() -> Self {
Self::default()
}
#[must_use]
#[inline]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
#[inline]
pub fn query(mut self, query: Query) -> Self {
self.query = Some(query);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_id() {
let id = DynamicPlaylist::generate_id();
assert_eq!(id.table(), TABLE_NAME);
}
}
#[cfg(all(test, feature = "db"))]
mod query_tests {
use super::*;
use pretty_assertions::assert_eq;
use query::{Clause, CompoundClause, CompoundKind, Field, LeafClause, Operator, Value};
use rstest::rstest;
#[rstest]
#[case::leaf_clause(
Query {root: Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
})},
"SELECT * FROM song WHERE title = 'foo';"
)]
#[case::leaf_clause(
Query { root: Clause::Leaf(LeafClause {
left: Value::Set(vec![Value::String("foo".to_string()), Value::Int(42)]),
operator: Operator::Contains,
right: Value::Int(42)
})},
"SELECT * FROM song WHERE [\"foo\", 42] CONTAINS 42;"
)]
#[case::compound_clause(
Query { root: Clause::Compound( CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::Contains,
right: Value::String("bar".to_string())
}),
],
kind: CompoundKind::And
})},
"SELECT * FROM song WHERE (title = \"foo\" AND array::flatten([artist][? $this]) CONTAINS \"bar\");"
)]
#[case::compound_clause(
Query { root: Clause::Compound(CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::Contains,
right: Value::String("bar".to_string())
}),
],
kind: CompoundKind::Or
})},
"SELECT * FROM song WHERE (title = \"foo\" OR array::flatten([artist][? $this]) CONTAINS \"bar\");"
)]
#[case::query(
Query {
root: Clause::Compound(CompoundClause {
clauses: vec![
Clause::Compound(
CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::AnyInside,
right: Value::Set(vec![Value::String("foo".to_string()), Value::String("bar".to_string())])
}),
Clause::Compound(CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::AlbumArtists),
operator: Operator::Contains,
right: Value::String("bar".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Genre),
operator: Operator::AnyLike,
right: Value::String("baz".to_string())
}),
],
kind: CompoundKind::Or
}),
],
kind: CompoundKind::And
}
),
Clause::Leaf(LeafClause {
left: Value::Field(Field::ReleaseYear),
operator: Operator::GreaterThan,
right: Value::Int(2020)
}),
],
kind: CompoundKind::And
})
},
"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);"
)]
fn test_compile(#[case] query: Query, #[case] expected: impl IntoQuery) {
let dynamic_playlist = DynamicPlaylist {
id: DynamicPlaylist::generate_id(),
name: "test".into(),
query,
};
#[expect(deprecated)]
let compiled = dynamic_playlist.get_query().into_query();
#[expect(deprecated)]
let expected = expected.into_query();
assert!(compiled.is_ok());
assert_eq!(compiled.unwrap(), expected.unwrap());
}
}