Skip to content

Commit

Permalink
feat(tui): implement query builder for dynamic playlists (#225)
Browse files Browse the repository at this point in the history
Also:
- increase transparency of parsing errors
- fix string parsing
- fix a panic that could occur when entering non-ascii characters into an input box
- simplified `NameSort`ing logic
- refactor query compilation to operate differently when compiling for storage vs for execution
- add operator validation for query parsing (parsing only succeeds when operators are used correctly)
- ...

Basically, wrap up all the stuff needed for Dynamic Playlists to be considered implemented.

closes #216
  • Loading branch information
AnthonyMichaelTDM authored Feb 10, 2025
2 parents 64adee8 + d82ddb0 commit f93ef6b
Show file tree
Hide file tree
Showing 24 changed files with 2,749 additions and 234 deletions.
2 changes: 1 addition & 1 deletion cli/src/handlers/implementations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,7 @@ The syntax for queries is as follows:
<value> ::= <string> | <int> | <set> | <field>
<field> ::= "title" | "artist" | "album" | "album_artist" | "genre" | "year"
<field> ::= "title" | "artist" | "album" | "album_artist" | "genre" | "release_year"
<operator> ::= "=" | "!=" | "?=" | "*=" | ">" | ">=" | "<" | "<=" | "~" | "!~" | "?~" | "*~" | "IN" | "NOT IN" | "CONTAINS" | "CONTAINSNOT" | "CONTAINSALL" | "CONTAINSANY" | "CONTAINSNONE"
Expand Down
2 changes: 1 addition & 1 deletion cli/src/handlers/printing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ pub fn dynamic_playlist_list(
"\t{}: \"{}\" ({})",
playlist.id,
playlist.name,
playlist.query.compile()
playlist.query.compile_for_storage()
)?;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: cli/src/handlers/smoke_tests.rs
assertion_line: 572
expression: "String::from_utf8(stdout.0.clone()).unwrap()"
---
Dynamic playlists are playlists that are generated based on a query.
Expand All @@ -17,7 +18,7 @@ The syntax for queries is as follows:
<value> ::= <string> | <int> | <set> | <field>
<field> ::= "title" | "artist" | "album" | "album_artist" | "genre" | "year"
<field> ::= "title" | "artist" | "album" | "album_artist" | "genre" | "release_year"
<operator> ::= "=" | "!=" | "?=" | "*=" | ">" | ">=" | "<" | "<=" | "~" | "!~" | "?~" | "*~" | "IN" | "NOT IN" | "CONTAINS" | "CONTAINSNOT" | "CONTAINSALL" | "CONTAINSANY" | "CONTAINSNONE"
Expand Down
3 changes: 2 additions & 1 deletion storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ db = [
serde = ["one-or-many/serde", "dep:serde"]
test_utils = ["dep:tempfile", "dep:anyhow"]
analysis = ["dep:futures", "dep:mecomp-analysis"]
trace_parser = ["pom/trace"]

[dependencies]
# shared dependencies
pom = { version = "3.4.0" }
lofty = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
one-or-many = { workspace = true }
pom = { version = "3.4.0", features = ["trace"] }
rand = { workspace = true }
serde = { workspace = true, optional = true }
surrealdb = { workspace = true, optional = true }
Expand Down
39 changes: 25 additions & 14 deletions storage/src/db/schemas/dynamic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@ impl DynamicPlaylist {
use query::Compile;

format!(
// This query would make "artist ANYINSIDE ['foo', 'bar']" type queries work, but breaks almost everything else
// "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};",
"SELECT * FROM {table_name} WHERE {conditions};",
table_name = super::song::TABLE_NAME,
conditions = self.query.compile()
conditions = self.query.compile(query::Context::Execution)
)
}
}

#[derive(Debug, Default)]
#[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"))]
Expand Down Expand Up @@ -99,6 +101,7 @@ mod tests {
#[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;

Expand All @@ -109,13 +112,15 @@ mod query_tests {
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 {
Expand All @@ -127,12 +132,13 @@ mod query_tests {
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::Equal,
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 {
Expand All @@ -144,12 +150,13 @@ mod query_tests {
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Artists),
operator: Operator::Equal,
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 {
Expand All @@ -159,20 +166,20 @@ mod query_tests {
CompoundClause {
clauses: vec![
Clause::Leaf(LeafClause {
left: Value::Field(Field::Title),
operator: Operator::Equal,
right: Value::String("foo".to_string())
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::Artists),
operator: Operator::Equal,
left: Value::Field(Field::AlbumArtists),
operator: Operator::Contains,
right: Value::String("bar".to_string())
}),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Album),
operator: Operator::Equal,
left: Value::Field(Field::Genre),
operator: Operator::AnyLike,
right: Value::String("baz".to_string())
}),
],
Expand All @@ -183,22 +190,26 @@ mod query_tests {
}
),
Clause::Leaf(LeafClause {
left: Value::Field(Field::Year),
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) {
fn test_compile(#[case] query: Query, #[case] expected: impl IntoQuery) {
let dynamic_playlist = DynamicPlaylist {
id: DynamicPlaylist::generate_id(),
name: Arc::from("test"),
query,
};

assert!(dynamic_playlist.get_query().into_query().is_ok());
let compiled = dynamic_playlist.get_query().into_query();

assert!(compiled.is_ok());
assert_eq!(compiled.unwrap(), expected.into_query().unwrap());
}
}
Loading

0 comments on commit f93ef6b

Please sign in to comment.