Repository: beetbox/beets

Issue:
Implicit path queries in a boolean OR yield a match-all query
This is best illustrated by example:
- `beet list /path/to/library/A` — works
- `beet list /path/to/library/B` — works
- `beet list /path/to/library/A , /path/to/library/B` — **lists the whole library**
- `beet list path:/path/to/library/A , path:/path/to/library/B` — works



Discussion:
Thanks for the report! This is definitely a bug. I added a unit test for it, which we should enable when it's fixed.

Here's the problem. The `beets.library.parse_query_parts` function has to play some dirty tricks to special-case implicit path queries. It does this by stripping out path-esque pieces from the query, parsing them separately, and then adding them back into the result of parsing the rest of the query. This worked fine before we added OR queries, but now it (a) leaves blank gaps in the original query, which is the cause of this bug, and (b) adds the path queries back in at the end, which seems messy and might break in the future.

We'll need to rethink how that works. Maybe we need to add a general "implicit query type" functionality to `dbcore`'s query parser.

> It does this by stripping out path-esque pieces from the query, parsing them separately, and then adding them back into the result of parsing the rest of the query.

Hm, why does it need to do such complex things? Isn't it possible to straight-out replace undecorated path-like pieces with proper queries to the path field, in a "preprocessor" manner?

Actually, that's not a bad idea! I feel dumb now, but I'd never thought of that, and I don't see any problems with it. Good thinking. :sparkles:


Resolving patch:
diff --git a/beets/library.py b/beets/library.py
--- a/beets/library.py
+++ b/beets/library.py
@@ -1382,7 +1382,7 @@ def parse_query_parts(parts, model_cls):
     `Query` and `Sort` they represent.
 
     Like `dbcore.parse_sorted_query`, with beets query prefixes and
-    special path query detection.
+    ensuring that implicit path queries are made explicit with 'path::<query>'
     """
     # Get query types and their prefix characters.
     prefixes = {
@@ -1394,28 +1394,14 @@ def parse_query_parts(parts, model_cls):
 
     # Special-case path-like queries, which are non-field queries
     # containing path separators (/).
-    path_parts = []
-    non_path_parts = []
-    for s in parts:
-        if PathQuery.is_path_query(s):
-            path_parts.append(s)
-        else:
-            non_path_parts.append(s)
+    parts = [f"path:{s}" if PathQuery.is_path_query(s) else s for s in parts]
 
     case_insensitive = beets.config['sort_case_insensitive'].get(bool)
 
-    query, sort = dbcore.parse_sorted_query(
-        model_cls, non_path_parts, prefixes, case_insensitive
+    return dbcore.parse_sorted_query(
+        model_cls, parts, prefixes, case_insensitive
     )
 
-    # Add path queries to aggregate query.
-    # Match field / flexattr depending on whether the model has the path field
-    fast_path_query = 'path' in model_cls._fields
-    query.subqueries += [PathQuery('path', s, fast_path_query)
-                         for s in path_parts]
-
-    return query, sort
-
 
 def parse_query_string(s, model_cls):
     """Given a beets query string, return the `Query` and `Sort` they


Test patch:
diff --git a/test/test_query.py b/test/test_query.py
--- a/test/test_query.py
+++ b/test/test_query.py
@@ -31,7 +31,10 @@
                                 InvalidQueryArgumentValueError)
 from beets.library import Library, Item
 from beets import util
-import platform
+
+# Because the absolute path begins with something like C:, we
+# can't disambiguate it from an ordinary query.
+WIN32_NO_IMPLICIT_PATHS = 'Implicit paths are not supported on Windows'
 
 
 class TestHelper(helper.TestHelper):
@@ -521,6 +524,7 @@ def test_nonnorm_path(self):
         results = self.lib.albums(q)
         self.assert_albums_matched(results, ['path album'])
 
+    @unittest.skipIf(sys.platform == 'win32', WIN32_NO_IMPLICIT_PATHS)
     def test_slashed_query_matches_path(self):
         q = '/a/b'
         results = self.lib.items(q)
@@ -529,7 +533,7 @@ def test_slashed_query_matches_path(self):
         results = self.lib.albums(q)
         self.assert_albums_matched(results, ['path album'])
 
-    @unittest.skip('unfixed (#1865)')
+    @unittest.skipIf(sys.platform == 'win32', WIN32_NO_IMPLICIT_PATHS)
     def test_path_query_in_or_query(self):
         q = '/a/b , /a/b'
         results = self.lib.items(q)
@@ -649,12 +653,8 @@ def test_path_sep_detection(self, mock_os):
         self.assertFalse(is_path('foo:bar/'))
         self.assertFalse(is_path('foo:/bar'))
 
+    @unittest.skipIf(sys.platform == 'win32', WIN32_NO_IMPLICIT_PATHS)
     def test_detect_absolute_path(self):
-        if platform.system() == 'Windows':
-            # Because the absolute path begins with something like C:, we
-            # can't disambiguate it from an ordinary query.
-            self.skipTest('Windows absolute paths do not work as queries')
-
         # Don't patch `os.path.exists`; we'll actually create a file when
         # it exists.
         self.patcher_exists.stop()