perf(smartplaylist): batch-load genres instead of per-row correlated subquery
Evaluate now issues a lean main SELECT over the joined metadata tables with no genre column, then batch-fetches genres with a single query using WHERE recording_id IN (...). Previously the track_metadata view's correlated GROUP_CONCAT subquery ran per row and scaled with library size rather than result size, producing multi-second load times for 100-track smart playlists. - Inline the metadata joins instead of using the track_metadata view, so the per-row GROUP_CONCAT never runs on the hot path. Other callers of the view (search, library listing) are unaffected. - Route all genre operators (is/is_not/is_any_of/contains/etc.) through a recording_genres subquery against af.recording_id. Previously text operators like "contains" matched against the view's concatenated genre column, which is no longer in scope. - Sort-by-genre falls back to Go-side sort after the batch genre merge since there is no single SQL column to sort on. - Log main_ms / genres_ms / total_ms at Debug for future tuning. - Add (*DB).Logger() accessor so smartplaylist can reuse the DB's structured logger without changing Evaluate's signature. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -608,7 +608,7 @@ func TestBuildWhereClause_GenreIsAnyOfProducesSubquery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWhereClause_GenreContainsUsesLIKE(t *testing.T) {
|
||||
func TestBuildWhereClause_GenreContainsUsesSubquery(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
clause, args, err := BuildWhereClause([]Rule{
|
||||
@@ -618,17 +618,21 @@ func TestBuildWhereClause_GenreContainsUsesLIKE(t *testing.T) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// contains on genre should use LIKE on the concatenated column,
|
||||
// NOT a subquery.
|
||||
if strings.Contains(clause, "recording_genres") {
|
||||
// Since the smart playlist query no longer projects a concatenated
|
||||
// genre column, "contains" filters genres via recording_genres
|
||||
// with g.name LIKE applied to individual genre rows.
|
||||
if !strings.Contains(clause, "recording_genres") {
|
||||
t.Errorf(
|
||||
"genre 'contains' should use LIKE, not subquery: %q",
|
||||
"genre 'contains' should use recording_genres subquery: %q",
|
||||
clause,
|
||||
)
|
||||
}
|
||||
|
||||
if clause != "genre LIKE ?" {
|
||||
t.Errorf("clause = %q, want %q", clause, "genre LIKE ?")
|
||||
if !strings.Contains(clause, "g.name LIKE ?") {
|
||||
t.Errorf(
|
||||
"genre 'contains' should filter with g.name LIKE ?: %q",
|
||||
clause,
|
||||
)
|
||||
}
|
||||
|
||||
if len(args) != 1 || args[0] != "%Rock%" {
|
||||
@@ -671,10 +675,18 @@ func TestBuildWhereClause_SameFieldMultipleTimes(t *testing.T) {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if clause != "genre LIKE ? AND genre NOT LIKE ?" {
|
||||
t.Errorf("clause = %q, want %q",
|
||||
clause,
|
||||
"genre LIKE ? AND genre NOT LIKE ?")
|
||||
// Genre text ops combine via AND across subqueries against
|
||||
// recording_genres; the exact SQL shape is asserted elsewhere.
|
||||
if !strings.Contains(clause, " AND ") {
|
||||
t.Errorf("clause should combine rules with AND: %q", clause)
|
||||
}
|
||||
|
||||
if !strings.Contains(clause, "af.recording_id IN") {
|
||||
t.Errorf("clause should include positive IN subquery: %q", clause)
|
||||
}
|
||||
|
||||
if !strings.Contains(clause, "af.recording_id NOT IN") {
|
||||
t.Errorf("clause should include NOT IN subquery: %q", clause)
|
||||
}
|
||||
|
||||
if len(args) != 2 ||
|
||||
|
||||
Reference in New Issue
Block a user