From 051912055eee37a1d1e2069f1941abb11fca8719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 16 May 2026 10:44:49 +0100 Subject: [PATCH] Move sort to a separate module Purely a mechanical code relocation. --- beets/dbcore/query.py | 210 ------------------------------------------ beets/dbcore/sort.py | 210 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 210 deletions(-) create mode 100644 beets/dbcore/sort.py diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 5bd7fe54b..8df7fba75 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -942,213 +942,3 @@ class SingletonQuery(FieldQuery[str]): if util.str2bool(value): return query return NotQuery(query) - - -# Sorting. - - -class Sort: - """An abstract class representing a sort operation for a query into - the database. - """ - - 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). - """ - return None - - def sort(self, items: list[AnyModel]) -> list[AnyModel]: - """Sort the list of objects and return a list.""" - return sorted(items) - - def is_slow(self) -> bool: - """Indicate whether this query is *slow*, meaning that it cannot - be executed in SQL and must be executed in Python. - """ - return False - - def __hash__(self) -> int: - return 0 - - def __eq__(self, other) -> bool: - return type(self) is type(other) - - def __repr__(self): - return f"{self.__class__.__name__}()" - - -class MultipleSort(Sort): - """Sort that encapsulates multiple sub-sorts.""" - - def __init__(self, sorts: list[Sort] | None = None): - self.sorts = sorts or [] - - def add_sort(self, sort: Sort): - self.sorts.append(sort) - - def order_clause(self) -> str: - """Return the list SQL clauses for those sub-sorts for which we can be - (at least partially) fast. - - A contiguous suffix of fast (SQL-capable) sub-sorts are - executable in SQL. The remaining, even if they are fast - independently, must be executed slowly. - """ - order_strings = [] - for sort in reversed(self.sorts): - clause = sort.order_clause() - if clause is None: - break - order_strings.append(clause) - order_strings.reverse() - - return ", ".join(order_strings) - - def is_slow(self) -> bool: - for sort in self.sorts: - if sort.is_slow(): - return True - return False - - def sort(self, items): - slow_sorts = [] - switch_slow = False - for sort in reversed(self.sorts): - if switch_slow: - slow_sorts.append(sort) - elif sort.order_clause() is None: - switch_slow = True - slow_sorts.append(sort) - else: - pass - - for sort in slow_sorts: - items = sort.sort(items) - return items - - def __repr__(self): - return f"{self.__class__.__name__}({self.sorts!r})" - - def __hash__(self): - return hash(tuple(self.sorts)) - - def __eq__(self, other): - return super().__eq__(other) and self.sorts == other.sorts - - -class FieldSort(Sort): - """An abstract sort criterion that orders by a specific field (of - any kind). - """ - - def __init__( - self, - field: str, - ascending: bool = True, - case_insensitive: bool = True, - ): - self.field = field - self.ascending = ascending - self.case_insensitive = case_insensitive - - def sort(self, objs: list[AnyModel]) -> list[AnyModel]: - # TODO: Conversion and null-detection here. In Python 3, - # comparisons with None fail. We should also support flexible - # attributes with different types without falling over. - - def key(obj: Model) -> Any: - field_val = obj.get(self.field, None) - if field_val is None: - if _type := obj._types.get(self.field): - # If the field is typed, use its null value. - field_val = obj._types[self.field].null - else: - # If not, fall back to using an empty string. - field_val = "" - if self.case_insensitive and isinstance(field_val, str): - field_val = field_val.lower() - return field_val - - return sorted(objs, key=key, reverse=not self.ascending) - - def __repr__(self) -> str: - return ( - f"{self.__class__.__name__}" - f"({self.field!r}, ascending={self.ascending!r})" - ) - - def __hash__(self) -> int: - return hash((self.field, self.ascending)) - - def __eq__(self, other) -> bool: - return ( - super().__eq__(other) - and self.field == other.field - and self.ascending == other.ascending - ) - - -class FixedFieldSort(FieldSort): - """Sort object to sort on a fixed field.""" - - def order_clause(self) -> str: - order = "ASC" if self.ascending else "DESC" - if self.case_insensitive: - field = ( - "(CASE " - f"WHEN TYPEOF({self.field})='text' THEN LOWER({self.field}) " - f"WHEN TYPEOF({self.field})='blob' THEN LOWER({self.field}) " - f"ELSE {self.field} END)" - ) - else: - field = self.field - return f"{field} {order}" - - -class SlowFieldSort(FieldSort): - """A sort criterion by some model field other than a fixed field: - i.e., a computed or flexible field. - """ - - def is_slow(self) -> bool: - return True - - -class NullSort(Sort): - """No sorting. Leave results unsorted.""" - - def sort(self, items: list[AnyModel]) -> list[AnyModel]: - return items - - def __nonzero__(self) -> bool: - return self.__bool__() - - def __bool__(self) -> bool: - return False - - def __eq__(self, other) -> bool: - return type(self) is type(other) or other is None - - def __hash__(self) -> int: - return 0 - - -class SmartArtistSort(FieldSort): - """Sort by artist (either album artist or track artist), - prioritizing the sort field over the raw field. - """ - - def order_clause(self): - order = "ASC" if self.ascending else "DESC" - collate = "COLLATE NOCASE" if self.case_insensitive else "" - field = self.field - - return f"COALESCE(NULLIF({field}_sort, ''), {field}) {collate} {order}" - - def sort(self, objs: list[AnyModel]) -> list[AnyModel]: - def key(o): - val = o[f"{self.field}_sort"] or o[self.field] - return val.lower() if self.case_insensitive else val - - return sorted(objs, key=key, reverse=not self.ascending) diff --git a/beets/dbcore/sort.py b/beets/dbcore/sort.py new file mode 100644 index 000000000..aa9ac88e2 --- /dev/null +++ b/beets/dbcore/sort.py @@ -0,0 +1,210 @@ + + +# Sorting. + + +class Sort: + """An abstract class representing a sort operation for a query into + the database. + """ + + 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). + """ + return None + + def sort(self, items: list[AnyModel]) -> list[AnyModel]: + """Sort the list of objects and return a list.""" + return sorted(items) + + def is_slow(self) -> bool: + """Indicate whether this query is *slow*, meaning that it cannot + be executed in SQL and must be executed in Python. + """ + return False + + def __hash__(self) -> int: + return 0 + + def __eq__(self, other) -> bool: + return type(self) is type(other) + + def __repr__(self): + return f"{self.__class__.__name__}()" + + +class MultipleSort(Sort): + """Sort that encapsulates multiple sub-sorts.""" + + def __init__(self, sorts: list[Sort] | None = None): + self.sorts = sorts or [] + + def add_sort(self, sort: Sort): + self.sorts.append(sort) + + def order_clause(self) -> str: + """Return the list SQL clauses for those sub-sorts for which we can be + (at least partially) fast. + + A contiguous suffix of fast (SQL-capable) sub-sorts are + executable in SQL. The remaining, even if they are fast + independently, must be executed slowly. + """ + order_strings = [] + for sort in reversed(self.sorts): + clause = sort.order_clause() + if clause is None: + break + order_strings.append(clause) + order_strings.reverse() + + return ", ".join(order_strings) + + def is_slow(self) -> bool: + for sort in self.sorts: + if sort.is_slow(): + return True + return False + + def sort(self, items): + slow_sorts = [] + switch_slow = False + for sort in reversed(self.sorts): + if switch_slow: + slow_sorts.append(sort) + elif sort.order_clause() is None: + switch_slow = True + slow_sorts.append(sort) + else: + pass + + for sort in slow_sorts: + items = sort.sort(items) + return items + + def __repr__(self): + return f"{self.__class__.__name__}({self.sorts!r})" + + def __hash__(self): + return hash(tuple(self.sorts)) + + def __eq__(self, other): + return super().__eq__(other) and self.sorts == other.sorts + + +class FieldSort(Sort): + """An abstract sort criterion that orders by a specific field (of + any kind). + """ + + def __init__( + self, + field: str, + ascending: bool = True, + case_insensitive: bool = True, + ): + self.field = field + self.ascending = ascending + self.case_insensitive = case_insensitive + + def sort(self, objs: list[AnyModel]) -> list[AnyModel]: + # TODO: Conversion and null-detection here. In Python 3, + # comparisons with None fail. We should also support flexible + # attributes with different types without falling over. + + def key(obj: Model) -> Any: + field_val = obj.get(self.field, None) + if field_val is None: + if _type := obj._types.get(self.field): + # If the field is typed, use its null value. + field_val = obj._types[self.field].null + else: + # If not, fall back to using an empty string. + field_val = "" + if self.case_insensitive and isinstance(field_val, str): + field_val = field_val.lower() + return field_val + + return sorted(objs, key=key, reverse=not self.ascending) + + def __repr__(self) -> str: + return ( + f"{self.__class__.__name__}" + f"({self.field!r}, ascending={self.ascending!r})" + ) + + def __hash__(self) -> int: + return hash((self.field, self.ascending)) + + def __eq__(self, other) -> bool: + return ( + super().__eq__(other) + and self.field == other.field + and self.ascending == other.ascending + ) + + +class FixedFieldSort(FieldSort): + """Sort object to sort on a fixed field.""" + + def order_clause(self) -> str: + order = "ASC" if self.ascending else "DESC" + if self.case_insensitive: + field = ( + "(CASE " + f"WHEN TYPEOF({self.field})='text' THEN LOWER({self.field}) " + f"WHEN TYPEOF({self.field})='blob' THEN LOWER({self.field}) " + f"ELSE {self.field} END)" + ) + else: + field = self.field + return f"{field} {order}" + + +class SlowFieldSort(FieldSort): + """A sort criterion by some model field other than a fixed field: + i.e., a computed or flexible field. + """ + + def is_slow(self) -> bool: + return True + + +class NullSort(Sort): + """No sorting. Leave results unsorted.""" + + def sort(self, items: list[AnyModel]) -> list[AnyModel]: + return items + + def __nonzero__(self) -> bool: + return self.__bool__() + + def __bool__(self) -> bool: + return False + + def __eq__(self, other) -> bool: + return type(self) is type(other) or other is None + + def __hash__(self) -> int: + return 0 + + +class SmartArtistSort(FieldSort): + """Sort by artist (either album artist or track artist), + prioritizing the sort field over the raw field. + """ + + def order_clause(self): + order = "ASC" if self.ascending else "DESC" + collate = "COLLATE NOCASE" if self.case_insensitive else "" + field = self.field + + return f"COALESCE(NULLIF({field}_sort, ''), {field}) {collate} {order}" + + def sort(self, objs: list[AnyModel]) -> list[AnyModel]: + def key(o): + val = o[f"{self.field}_sort"] or o[self.field] + return val.lower() if self.case_insensitive else val + + return sorted(objs, key=key, reverse=not self.ascending)