From 02b5186998332a6834da90de2410e925441243d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Wed, 2 Sep 2026 23:18:18 +0100 Subject: [PATCH 1/5] list: add -l flag, support LIMIT clause --- beets/dbcore/db.py | 9 +++++++-- beets/library/library.py | 13 ++++++++++--- beets/ui/commands/list.py | 17 ++++++++++++----- docs/changelog.rst | 2 ++ docs/reference/cli.rst | 5 ++++- test/ui/commands/test_list.py | 24 ++++++++++++++++++++---- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index f4e6d7c93e..74e67b5855 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -1399,6 +1399,7 @@ def _get_results( model_cls: type[AnyModel], query: Query | None = None, sort: Sort | None = None, + limit: int | None = None, ) -> Results[AnyModel]: """Fetch the objects of type `model_cls` matching the given query. The query may be given as a string, string sequence, a @@ -1409,6 +1410,7 @@ def _get_results( sort = sort or NullSort() # Unsorted. where, subvals = query.clause() order_by = sort.order_clause() + # TODO: handle flex fields sort table = model_cls._table _from = table @@ -1420,7 +1422,7 @@ def _get_results( f"SELECT {table}.* " f"FROM ({_from}) " f"WHERE {where or 1} " - f"GROUP BY {table}.id" + f"GROUP BY {table}.id " ) # Fetch flexible attributes for items matching the main query. # Doing the per-item filtering in python is faster than issuing @@ -1437,7 +1439,10 @@ def _get_results( # if we try to order directly. # Since the join is required only for filtering, we can filter in # a subquery and order the result, which returns unique fields. - sql = f"SELECT * FROM ({sql}) ORDER BY {order_by}" + sql = f"SELECT * FROM ({sql}) ORDER BY {order_by} " + + if limit is not None: + sql += f"LIMIT {limit}" with self.transaction() as tx: rows = tx.query(sql, subvals) diff --git a/beets/library/library.py b/beets/library/library.py index 8a86a2e9ec..d634fbe0b7 100644 --- a/beets/library/library.py +++ b/beets/library/library.py @@ -138,6 +138,7 @@ def _fetch( model_cls: type[LM], query: str | Sequence[str] | Query | None = None, sort: Sort | None = None, + limit: int | None = None, ) -> dbcore.Results[LM]: """Parse a query and fetch. @@ -169,7 +170,7 @@ def _fetch( if parsed_sort and not isinstance(parsed_sort, NullSort): sort = parsed_sort - return super()._get_results(model_cls, parsed_query, sort) + return super()._get_results(model_cls, parsed_query, sort, limit) @staticmethod def get_default_album_sort() -> Sort: @@ -189,17 +190,23 @@ def albums( self, query: str | Sequence[str] | Query | None = None, sort: Sort | None = None, + limit: int | None = None, ) -> dbcore.Results[Album]: """Get :class:`Album` objects matching the query.""" - return self._fetch(Album, query, sort or self.get_default_album_sort()) + return self._fetch( + Album, query, sort or self.get_default_album_sort(), limit + ) def items( self, query: str | Sequence[str] | Query | None = None, sort: Sort | None = None, + limit: int | None = None, ) -> dbcore.Results[Item]: """Get :class:`Item` objects matching the query.""" - return self._fetch(Item, query, sort or self.get_default_item_sort()) + return self._fetch( + Item, query, sort or self.get_default_item_sort(), limit + ) # Convenience accessors. def get_item(self, id_: int) -> Item | None: diff --git a/beets/ui/commands/list.py b/beets/ui/commands/list.py index b1e4945a42..ea4c4dfc62 100644 --- a/beets/ui/commands/list.py +++ b/beets/ui/commands/list.py @@ -5,6 +5,7 @@ from typing import TYPE_CHECKING, Protocol from beets import ui +from beets.exceptions import UserError if TYPE_CHECKING: from collections.abc import Sequence @@ -14,24 +15,27 @@ class ListCLIOpts(Protocol): album: bool + limit: int | None def list_items( - lib: Library, query: Sequence[str], album: bool, fmt: str = "" + lib: Library, query: Sequence[str], opts: ListCLIOpts, fmt: str = "" ) -> None: """Print out items in lib matching query. If album, then search for albums instead of single items. """ - if album: - for _album in lib.albums(query): + if opts.album: + for _album in lib.albums(query, limit=opts.limit): ui.print_(format(_album, fmt)) else: - for item in lib.items(query): + for item in lib.items(query, limit=opts.limit): ui.print_(format(item, fmt)) def list_func(lib: Library, opts: ListCLIOpts, args: list[str]) -> None: - list_items(lib, args, opts.album) + if opts.limit is not None and opts.limit < 0: + raise UserError("-l / --limit argument must be a non-negative integer") + list_items(lib, args, opts) list_cmd = ui.Subcommand("list", help="query the library", aliases=("ls",)) @@ -40,4 +44,7 @@ def list_func(lib: Library, opts: ListCLIOpts, args: list[str]) -> None: + "\nExample: %prog -f '$album: $title' artist:beatles" ) list_cmd.parser.add_all_common_options() +list_cmd.parser.add_option( + "-l", "--limit", type=int, help="limit query results" +) list_cmd.func = list_func diff --git a/docs/changelog.rst b/docs/changelog.rst index c9ea8f04fb..c32caf6039 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -25,6 +25,8 @@ New features tasks. It re-reads the album's directory from disk and re-runs the match, so files can be cleaned up (duplicates, junk) while the import is paused at the prompt, without restarting the whole ``beet import`` run. +- :ref:`list-cmd` Add ``-l / --limit LIMIT`` flag to the ``list`` command to + limit query results. Bug fixes ~~~~~~~~~ diff --git a/docs/reference/cli.rst b/docs/reference/cli.rst index 5a801cdcb9..82cf433619 100644 --- a/docs/reference/cli.rst +++ b/docs/reference/cli.rst @@ -199,7 +199,7 @@ list :: - beet list [-apf] QUERY + beet list [-apf] [-l LIMIT] QUERY :doc:`Queries ` the database for music. @@ -214,6 +214,9 @@ example, you can search for ``year:1969`` but query parts for item-level fields like ``title:foo`` will be ignored. Remember that ``artist`` is an item-level field; ``albumartist`` is the corresponding album field. +Use the ``-l LIMIT`` (``--limit=LIMIT``) flag when you want to cap the maximum +number of items that are returned. + The ``-p`` option makes beets print out filenames of matched items, which might be useful for piping into other Unix commands (such as `xargs `__). Similarly, the ``-f`` option lets you diff --git a/test/ui/commands/test_list.py b/test/ui/commands/test_list.py index 054792b24c..a36b1985bf 100644 --- a/test/ui/commands/test_list.py +++ b/test/ui/commands/test_list.py @@ -1,3 +1,6 @@ +import pytest + +from beets.exceptions import UserError from beets.test import _common from beets.test.helper import BeetsTestCase, IOMixin @@ -5,10 +8,11 @@ class ListTest(IOMixin, BeetsTestCase): def setUp(self): super().setUp() - self.item = _common.item() - self.item.path = "xxx/yyy" + self.item = _common.item(path="xxx/yyy", flex=1) self.lib.add(self.item) self.lib.add_album([self.item]) + self.another_item = _common.item(path="another/path", flex=2) + self.lib.add(self.another_item) def test_list_outputs_item(self): stdout = self.run_with_output("list") @@ -25,7 +29,7 @@ def test_list_unicode_query(self): def test_list_item_path(self): stdout = self.run_with_output("list", "-f", "$path") - assert stdout.strip() == str(self.lib_path / "xxx/yyy") + assert stdout.strip().splitlines()[0] == str(self.lib_path / "xxx/yyy") def test_list_album_outputs_something(self): stdout = self.run_with_output("list", "-a") @@ -55,9 +59,21 @@ def test_list_item_format_artist(self): def test_list_item_format_multiple(self): stdout = self.run_with_output("list", "-f", "$artist - $album - $year") - assert "the artist - the album - 0001" == stdout.strip() + assert "the artist - the album - 0001" == stdout.strip().splitlines()[0] def test_list_album_format(self): stdout = self.run_with_output("list", "-a", "-f", "$genres") assert "the genre" in stdout assert "the album" not in stdout + + def test_limit_query_results(self): + args = "list", "-p" + + stdout = self.run_with_output(*args).strip() + assert len(stdout.splitlines()) == 2 + + stdout = self.run_with_output(*args, "-l", "1").strip() + assert len(stdout.splitlines()) == 1 + + with pytest.raises(UserError, match="must be a non-negative integer"): + self.run_with_output(*args, "-l", "-1") From f09a6f9e2960d67494e51fd714d1dd81ec21bc5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 3 Sep 2026 04:58:38 +0100 Subject: [PATCH 2/5] list: support sorting by flexible attributes with limit --- beets/dbcore/db.py | 28 +++++++++++++++++++++------- beets/dbcore/sort.py | 17 +++++++++++++++++ docs/changelog.rst | 2 +- test/ui/commands/test_list.py | 29 +++++++++++++++++++++++------ 4 files changed, 62 insertions(+), 14 deletions(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 74e67b5855..fc9cf6c4fd 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -15,6 +15,7 @@ from contextlib import contextmanager from dataclasses import dataclass from functools import cached_property +from itertools import islice from pathlib import Path from sqlite3 import Connection, sqlite_version_info from typing import ( @@ -744,6 +745,7 @@ def __init__( flex_rows: list[sqlite3.Row], query: Query | None = None, sort: Sort | None = None, + limit: int | None = None, ) -> None: """Create a result set that will construct objects of type `model_class`. @@ -764,6 +766,7 @@ def __init__( self.db = db self.query = query self.sort = sort + self.limit = limit self.flex_rows = flex_rows # We keep a queue of rows we haven't yet consumed for @@ -815,13 +818,16 @@ def __iter__(self) -> Iterator[AnyModel]: """Construct and generate Model objects for all matching objects, in sorted order. """ + # Objects are pre-sorted (i.e., by the database). + objects = self._get_objects() if self.sort: # Slow sort. Must build the full list first. - objects = self.sort.sort(list(self._get_objects())) - return iter(objects) + objects = iter(self.sort.sort(list(objects))) - # Objects are pre-sorted (i.e., by the database). - return self._get_objects() + if self.limit is not None: + objects = islice(objects, self.limit) + + return objects def _get_indexed_flex_attrs(self) -> dict[int, FlexAttrs]: """Index flexible attributes by the entity id they belong to""" @@ -1410,7 +1416,14 @@ def _get_results( sort = sort or NullSort() # Unsorted. where, subvals = query.clause() order_by = sort.order_clause() - # TODO: handle flex fields sort + sql_limit = flex_limit = None + if limit is not None: + if sort.field_names - model_cls.all_db_fields: + # sorting by at least one flexible attr. + # Limit will be applied after slow field sort. + flex_limit = limit + else: + sql_limit = limit table = model_cls._table _from = table @@ -1441,8 +1454,8 @@ def _get_results( # a subquery and order the result, which returns unique fields. sql = f"SELECT * FROM ({sql}) ORDER BY {order_by} " - if limit is not None: - sql += f"LIMIT {limit}" + if sql_limit is not None: + sql += f"LIMIT {sql_limit}" with self.transaction() as tx: rows = tx.query(sql, subvals) @@ -1455,6 +1468,7 @@ def _get_results( flex_rows, None if where else query, # Slow query component. sort if sort.is_slow() else None, # Slow sort component. + flex_limit, ) def _get(self, model_cls: type[AnyModel], id_: int) -> AnyModel | None: diff --git a/beets/dbcore/sort.py b/beets/dbcore/sort.py index 16a5e7c90e..f21f12a7e8 100644 --- a/beets/dbcore/sort.py +++ b/beets/dbcore/sort.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import reduce +from operator import or_ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: @@ -15,6 +17,11 @@ class Sort: the database. """ + @property + def field_names(self) -> set[str]: + """A set with fields in this sort.""" + return set() + def order_clause(self) -> str | None: """Generates a SQL fragment to be used in a ORDER BY clause, or None if no fragment is used (i.e., this is a slow sort). @@ -47,6 +54,11 @@ class MultipleSort(Sort): def __init__(self, sorts: list[Sort] | None = None) -> None: self.sorts = sorts or [] + @property + def field_names(self) -> set[str]: + """A set with fields in this sort.""" + return reduce(or_, (s.field_names for s in self.sorts), set()) + def add_sort(self, sort: Sort) -> None: self.sorts.append(sort) @@ -116,6 +128,11 @@ def __init__( self.ascending = ascending self.case_insensitive = case_insensitive + @property + def field_names(self) -> set[str]: + """A set with fields in this sort.""" + return {self.field} + def sort(self, objs: Sequence[AnyModel]) -> Sequence[AnyModel]: # TODO: Support flexible attributes with different types (e.g. a mix # of strings and numbers) without falling over. diff --git a/docs/changelog.rst b/docs/changelog.rst index c32caf6039..66f2cfed46 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,7 +26,7 @@ New features files can be cleaned up (duplicates, junk) while the import is paused at the prompt, without restarting the whole ``beet import`` run. - :ref:`list-cmd` Add ``-l / --limit LIMIT`` flag to the ``list`` command to - limit query results. + limit query results. :bug:`5076` Bug fixes ~~~~~~~~~ diff --git a/test/ui/commands/test_list.py b/test/ui/commands/test_list.py index a36b1985bf..2d32928e84 100644 --- a/test/ui/commands/test_list.py +++ b/test/ui/commands/test_list.py @@ -1,3 +1,5 @@ +import os + import pytest from beets.exceptions import UserError @@ -8,10 +10,14 @@ class ListTest(IOMixin, BeetsTestCase): def setUp(self): super().setUp() - self.item = _common.item(path="xxx/yyy", flex=1) + self.item = _common.item( + path=os.fsencode(self.lib_path / "xxx/yyy"), flex=1 + ) self.lib.add(self.item) self.lib.add_album([self.item]) - self.another_item = _common.item(path="another/path", flex=2) + self.another_item = _common.item( + path=os.fsencode(self.lib_path / "another/path"), flex=2 + ) self.lib.add(self.another_item) def test_list_outputs_item(self): @@ -28,8 +34,8 @@ def test_list_unicode_query(self): assert "na\xefve" in out def test_list_item_path(self): - stdout = self.run_with_output("list", "-f", "$path") - assert stdout.strip().splitlines()[0] == str(self.lib_path / "xxx/yyy") + stdout = self.run_with_output("list", "flex:1", "-f", "$path") + assert stdout.strip() == str(self.lib_path / "xxx/yyy") def test_list_album_outputs_something(self): stdout = self.run_with_output("list", "-a") @@ -58,8 +64,10 @@ def test_list_item_format_artist(self): assert "the artist" in stdout def test_list_item_format_multiple(self): - stdout = self.run_with_output("list", "-f", "$artist - $album - $year") - assert "the artist - the album - 0001" == stdout.strip().splitlines()[0] + stdout = self.run_with_output( + "list", "flex:1", "-f", "$artist - $album - $year" + ) + assert stdout.strip() == "the artist - the album - 0001" def test_list_album_format(self): stdout = self.run_with_output("list", "-a", "-f", "$genres") @@ -77,3 +85,12 @@ def test_limit_query_results(self): with pytest.raises(UserError, match="must be a non-negative integer"): self.run_with_output(*args, "-l", "-1") + + def test_limit_sort_by_flex_attr(self): + args = "list", "-p", "-l", "1" + + stdout = self.run_with_output(*args, "flex+").strip() + assert stdout == os.fsdecode(self.item.path) + + stdout = self.run_with_output(*args, "flex-").strip() + assert stdout == os.fsdecode(self.another_item.path) From 7bc4ec42649472032b731e1f50784776baad504c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 3 Sep 2026 13:52:47 +0100 Subject: [PATCH 3/5] Enable db sorting of related fields --- beets/dbcore/db.py | 9 ++++++++- beets/dbcore/queryparse.py | 11 +---------- beets/dbcore/sort.py | 21 +++++++++++++++------ beets/library/models.py | 27 ++++++++++++++++++++++----- test/dbcore/test_sort.py | 9 +++++---- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index fc9cf6c4fd..002c55d2e2 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -1452,7 +1452,14 @@ def _get_results( # if we try to order directly. # Since the join is required only for filtering, we can filter in # a subquery and order the result, which returns unique fields. - sql = f"SELECT * FROM ({sql}) ORDER BY {order_by} " + select = f"{table}.* FROM ({sql}) {table}" + if ( + sort.field_names & model_cls.other_db_fields + ) - model_cls._getters().keys(): + # only applies to db fields on the other model + select += f" {model_cls.relation_join}" + + sql = f"SELECT {select} ORDER BY {order_by} " if sql_limit is not None: sql += f"LIMIT {sql_limit}" diff --git a/beets/dbcore/queryparse.py b/beets/dbcore/queryparse.py index 5743aa7004..bfc9a77a36 100644 --- a/beets/dbcore/queryparse.py +++ b/beets/dbcore/queryparse.py @@ -183,16 +183,7 @@ def construct_sort_part( assert direction in ("+", "-"), "part must end with + or -" is_ascending = direction == "+" - if sort_cls := model_cls._sorts.get(field): - if isinstance(sort_cls, sort.SmartArtistSort): - field = "albumartist" if model_cls.__name__ == "Album" else "artist" - elif field in model_cls._fields: - sort_cls = sort.FixedFieldSort - else: - # Flexible or computed. - sort_cls = sort.SlowFieldSort - - return sort_cls(field, is_ascending, case_insensitive) + return model_cls.field_sort(field, is_ascending, case_insensitive) def sort_from_strings( diff --git a/beets/dbcore/sort.py b/beets/dbcore/sort.py index f21f12a7e8..df56de0fd6 100644 --- a/beets/dbcore/sort.py +++ b/beets/dbcore/sort.py @@ -122,27 +122,36 @@ class FieldSort(Sort): """ def __init__( - self, field: str, ascending: bool = True, case_insensitive: bool = True + self, + field_name: str, + ascending: bool = True, + case_insensitive: bool = True, ) -> None: - self.field = field + self.table, _, self.field_name = field_name.rpartition(".") self.ascending = ascending self.case_insensitive = case_insensitive + @property + def field(self) -> str: + return ( + f"{self.table}.{self.field_name}" if self.table else self.field_name + ) + @property def field_names(self) -> set[str]: """A set with fields in this sort.""" - return {self.field} + return {self.field_name} def sort(self, objs: Sequence[AnyModel]) -> Sequence[AnyModel]: # TODO: Support flexible attributes with different types (e.g. a mix # of strings and numbers) without falling over. def key(obj: Model) -> Any: - field_val = obj.get(self.field, None) + field_val = obj.get(self.field_name, None) if field_val is None: - if _type := obj._types.get(self.field): + if _type := obj._types.get(self.field_name): # If the field is typed, use its null value. - field_val = obj._types[self.field].null + field_val = obj._types[self.field_name].null else: # If not, fall back to using an empty string. field_val = "" diff --git a/beets/library/models.py b/beets/library/models.py index eed4a7803a..8d8531d026 100644 --- a/beets/library/models.py +++ b/beets/library/models.py @@ -13,10 +13,9 @@ import beets from beets import dbcore, logging, plugins, util -from beets.dbcore import types +from beets.dbcore import sort, types from beets.dbcore.db import FormattedMapping from beets.dbcore.pathutils import normalize_path_for_db -from beets.dbcore.sort import SmartArtistSort from beets.util import ( MoveOperation, bytestring_path, @@ -116,6 +115,22 @@ def __bytes__(self) -> bytes: return self.__str__().encode("utf-8") # Convenient queries. + @classmethod + def field_sort( + cls, field: str, is_ascending: bool, case_insensitive: bool + ) -> FieldSort: + if sort_cls := cls._sorts.get(field): + if issubclass(sort_cls, sort.SmartArtistSort): + field = "albumartist" if cls.__name__ == "Album" else "artist" + elif field in cls.all_db_fields and field not in cls._getters(): + sort_cls = sort.FixedFieldSort + if field in cls.other_db_fields: + field = f"{cls._relation._table}.{field}" + else: + # Flexible or computed. + sort_cls = sort.SlowFieldSort + + return sort_cls(field, is_ascending, case_insensitive) @classmethod def field_query( @@ -326,8 +341,8 @@ def _types(cls) -> dict[str, types.Type]: _formatter = FormattedMapping _sorts: ClassVar[dict[str, type[FieldSort]]] = { - "albumartist": SmartArtistSort, - "artist": SmartArtistSort, + "albumartist": sort.SmartArtistSort, + "artist": sort.SmartArtistSort, } # List of keys that are set on an album's items. @@ -740,7 +755,9 @@ class Item(LibModel): _formatter = FormattedItemMapping - _sorts: ClassVar[dict[str, type[FieldSort]]] = {"artist": SmartArtistSort} + _sorts: ClassVar[dict[str, type[FieldSort]]] = { + "artist": sort.SmartArtistSort + } @cached_classproperty def _queries(cls) -> dict[str, FieldQueryType]: diff --git a/test/dbcore/test_sort.py b/test/dbcore/test_sort.py index 0edd163971..a81aeec27b 100644 --- a/test/dbcore/test_sort.py +++ b/test/dbcore/test_sort.py @@ -1,7 +1,5 @@ """Various tests for querying the library database.""" -import os - import pytest import beets.library @@ -15,8 +13,8 @@ _p = pytest.param -def abs_test_path(path: str) -> str: - return os.fsdecode(util.normpath(path)) +def abs_test_path(path: str) -> bytes: + return util.normpath(path) @pytest.fixture(scope="class") @@ -36,6 +34,7 @@ def setup_library(request: pytest.FixtureRequest, helper): flex1=flex1, flex2=flex2, albumartist=albumartist, + artpath=abs_test_path(f"{id_}cover.jpg"), ) ) for id_, album, genres, year, flex1, flex2, albumartist in ( @@ -114,6 +113,8 @@ class TestSort: _p(Album, "path+ year+", [1, 2, 3], id="computed"), _p(Album, "year+ path+", [1, 2, 3], id="computed-reverse"), _p(Item, "flex2- flex1+", [1, 2, 4, 3], id="item-multi-flex-field"), + _p(Item, "artpath+", [1, 2, 3, 4], id="item-album-field"), + _p(Item, "artpath-", [4, 3, 1, 2], id="item-album-field-reverse"), ], ) def test_sort(self, model, query, expected_ids): From eecd7f003c797254b730601f2ae69b63cb4f07ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Thu, 3 Sep 2026 15:08:23 +0100 Subject: [PATCH 4/5] Deprecate limit plugin --- beetsplug/limit.py | 7 +++++++ docs/changelog.rst | 2 ++ docs/plugins/limit.rst | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/beetsplug/limit.py b/beetsplug/limit.py index f3af131982..abd0780eca 100644 --- a/beetsplug/limit.py +++ b/beetsplug/limit.py @@ -16,6 +16,7 @@ from beets.dbcore import FieldQuery from beets.plugins import BeetsPlugin from beets.ui import Subcommand, print_ +from beets.util.deprecation import deprecate_for_user if TYPE_CHECKING: from collections.abc import Iterable @@ -71,6 +72,12 @@ def lslimit(lib: Library, opts: LsLimitCLIOpts, args: list[str]) -> None: class LimitPlugin(BeetsPlugin): """Query limit functionality via command and query prefix.""" + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + deprecate_for_user( + self._log, "LimitPlugin ('limit')", "'beet ls -l '" + ) + def commands(self) -> list[Subcommand]: """Expose `lslimit` subcommand.""" return [lslimit_cmd] diff --git a/docs/changelog.rst b/docs/changelog.rst index 66f2cfed46..7eb4bc44c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -125,6 +125,8 @@ Bug fixes Previously all queries were combined, so a listen for "Song" also updated "Song (inst.)" or any other item whose title only contained the listened title. +- :doc:`plugins/limit` Deprecate the ``limit`` plugin in favor of the new ``-l`` + / ``--limit`` flag for the :ref:`list-cmd` command. .. For plugin developers diff --git a/docs/plugins/limit.rst b/docs/plugins/limit.rst index 64ed89ed2f..27d89ed6c5 100644 --- a/docs/plugins/limit.rst +++ b/docs/plugins/limit.rst @@ -1,6 +1,11 @@ Limit Query Plugin ================== +.. deprecated:: 2.14 + + Use the built-in ``-l`` / ``--limit`` flag on the :ref:`list-cmd` command + instead. + ``limit`` is a plugin to limit a query to the first or last set of results. We also provide a query prefix ``' Date: Sat, 5 Sep 2026 01:14:17 +0100 Subject: [PATCH 5/5] Make sure flex attrs query is also limited --- beets/dbcore/db.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index 002c55d2e2..407baad370 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -1437,14 +1437,6 @@ def _get_results( f"WHERE {where or 1} " f"GROUP BY {table}.id " ) - # Fetch flexible attributes for items matching the main query. - # Doing the per-item filtering in python is faster than issuing - # one query per item to sqlite. - flex_sql = ( - "SELECT * " - f"FROM {model_cls._flex_table} " - f"WHERE entity_id IN (SELECT id FROM ({sql}))" - ) if order_by: # the sort field may exist in both 'items' and 'albums' tables @@ -1464,6 +1456,15 @@ def _get_results( if sql_limit is not None: sql += f"LIMIT {sql_limit}" + # Fetch flexible attributes for items matching the main query. + # Doing the per-item filtering in python is faster than issuing + # one query per item to sqlite. + flex_sql = ( + "SELECT * " + f"FROM {model_cls._flex_table} " + f"WHERE entity_id IN (SELECT id FROM ({sql}))" + ) + with self.transaction() as tx: rows = tx.query(sql, subvals) flex_rows = tx.query(flex_sql, subvals)