diff --git a/src/libs/database/impl/objects/Release.cpp b/src/libs/database/impl/objects/Release.cpp index 8bc1fef4..d7aa9e0d 100644 --- a/src/libs/database/impl/objects/Release.cpp +++ b/src/libs/database/impl/objects/Release.cpp @@ -97,6 +97,9 @@ namespace lms::db query.join("track t ON t.release_id = r.id"); } + if (!params.keywords.empty()) + query.leftJoin("medium m ON m.release_id = r.id"); + if (params.parentDirectory.isValid()) { query.join("directory d ON t.directory_id = d.id"); @@ -149,8 +152,25 @@ namespace lms::db if (!params.name.empty()) query.where("r.name = ?").bind(params.name); - for (std::string_view keyword : params.keywords) - query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); + if (!params.keywords.empty()) + { + std::vector nameClauses; + std::vector mediumNameClauses; + + for (const std::string_view keyword : params.keywords) + { + nameClauses.push_back("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); + query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); + } + + for (const std::string_view keyword : params.keywords) + { + mediumNameClauses.push_back("m.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); + query.bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); + } + + query.where("(" + core::stringUtils::joinStrings(nameClauses, " AND ") + ") OR (" + core::stringUtils::joinStrings(mediumNameClauses, " AND ") + ")"); + } if (params.starringUser.isValid()) { diff --git a/src/libs/database/test/Release.cpp b/src/libs/database/test/Release.cpp index 3eb5b69a..1378bc9f 100644 --- a/src/libs/database/test/Release.cpp +++ b/src/libs/database/test/Release.cpp @@ -416,6 +416,72 @@ namespace lms::db::tests } } + TEST_F(DatabaseFixture, ReleaseSearchByMediumName) + { + ScopedRelease release{ session, "The Beatles In Mono" }; + ScopedMedium medium{ session, release.lockAndGet() }; + ScopedTrack track{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + + track.get().modify()->setRelease(release.get()); + track.get().modify()->setMedium(medium.get()); + medium.get().modify()->setName("Sgt. Pepper's Lonely Hearts Club Band"); + } + + { + auto transaction{ session.createReadTransaction() }; + + // matches only the medium (discsubtitle), not the release name + { + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Sgt. Pepper" })) }; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); + } + // release name search still works + { + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Beatles In Mono" })) }; + ASSERT_EQ(releases.size(), 1); + EXPECT_EQ(releases.front(), release.getId()); + } + // no match on either field + { + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "NoSuchKeyword" })) }; + EXPECT_EQ(releases.size(), 0); + } + } + } + + TEST_F(DatabaseFixture, ReleaseSearchByMediumName_multipleMediaNoDuplicates) + { + ScopedRelease release{ session, "The Beatles In Mono" }; + ScopedMedium medium1{ session, release.lockAndGet() }; + ScopedMedium medium2{ session, release.lockAndGet() }; + ScopedTrack track1{ session }; + ScopedTrack track2{ session }; + + { + auto transaction{ session.createWriteTransaction() }; + + track1.get().modify()->setRelease(release.get()); + track1.get().modify()->setMedium(medium1.get()); + medium1.get().modify()->setName("Sgt. Pepper's Lonely Hearts Club Band"); + + track2.get().modify()->setRelease(release.get()); + track2.get().modify()->setMedium(medium2.get()); + medium2.get().modify()->setName("Abbey Road"); + } + + { + auto transaction{ session.createReadTransaction() }; + + const auto releases{ Release::findIds(session, Release::FindParameters{}.setKeywords({ "Sgt. Pepper" })) }; + ASSERT_EQ(releases.size(), 1); // not duplicated despite the 1:N join + EXPECT_EQ(releases.front(), release.getId()); + } + } + TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack) { ScopedRelease release1{ session, "MyRelease" };