diff --git a/persistence/sql_search_fts.go b/persistence/sql_search_fts.go index 25f16cb8..ea70518b 100644 --- a/persistence/sql_search_fts.go +++ b/persistence/sql_search_fts.go @@ -134,7 +134,7 @@ func isDottedAbbreviation(w string, subTokens []string) bool { // special characters to prevent query injection. func buildFTS5Query(userInput string) string { q := strings.TrimSpace(userInput) - if q == "" { + if q == "" || q == `""` { return "" } @@ -239,7 +239,7 @@ var ftsSearchColumns = map[string]string{ func ftsSearchExpr(tableName string, s string) Sqlizer { q := buildFTS5Query(s) if q == "" { - s = strings.TrimSpace(s) + s = strings.TrimSpace(strings.ReplaceAll(s, `"`, "")) if s != "" { log.Trace("Search using LIKE fallback for non-tokenizable query", "table", tableName, "query", s) return likeSearchExpr(tableName, s) diff --git a/persistence/sql_search_fts_test.go b/persistence/sql_search_fts_test.go index 31725295..e0fead8a 100644 --- a/persistence/sql_search_fts_test.go +++ b/persistence/sql_search_fts_test.go @@ -49,6 +49,7 @@ var _ = DescribeTable("buildFTS5Query", Entry("preserves quoted abbreviation verbatim", `"R.E.M."`, `"R.E.M."`), Entry("returns empty string for punctuation-only input", "!!!!!!!", ""), Entry("returns empty string for mixed punctuation", "!@#$%^&", ""), + Entry("returns empty string for empty quoted phrase", `""`, ""), ) var _ = DescribeTable("normalizeForFTS", @@ -204,6 +205,10 @@ var _ = Describe("ftsSearchExpr", func() { Expect(ftsSearchExpr("media_file", "")).To(BeNil()) Expect(ftsSearchExpr("media_file", " ")).To(BeNil()) }) + + It("returns nil for empty quoted phrase", func() { + Expect(ftsSearchExpr("media_file", `""`)).To(BeNil()) + }) }) var _ = Describe("FTS5 Integration Search", func() { diff --git a/server/e2e/subsonic_searching_test.go b/server/e2e/subsonic_searching_test.go index 3a7512fd..6ea6559c 100644 --- a/server/e2e/subsonic_searching_test.go +++ b/server/e2e/subsonic_searching_test.go @@ -107,6 +107,16 @@ var _ = Describe("Search Endpoints", func() { Expect(resp.SearchResult3.Artist[0].Id).ToNot(BeEmpty()) }) + It("returns all results when query is empty (OpenSubsonic)", func() { + resp := doReq("search3", "query", "") + + Expect(resp.Status).To(Equal(responses.StatusOK)) + Expect(resp.SearchResult3).ToNot(BeNil()) + Expect(resp.SearchResult3.Artist).To(HaveLen(4)) + Expect(resp.SearchResult3.Album).To(HaveLen(5)) + Expect(resp.SearchResult3.Song).To(HaveLen(6)) + }) + It("finds across all entity types simultaneously", func() { // "Beatles" should match artist, albums, and songs by The Beatles resp := doReq("search3", "query", "Beatles")