From a94c6a7c6e3129026b0bd1938e49f26981cfb654 Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:53:47 +0800 Subject: [PATCH 1/3] fix(dbcore): NumericQuery range crashes on a null value NumericQuery.match() compares the field value against rangemin/rangemax without a None guard, so a range query against a nullable numeric field raises instead of returning False: NumericQuery("rg_track_gain", "-10..0").match(Item()) TypeError: '<' not supported between instances of 'NoneType' and 'int' Nullable types (NullFloat, NullInteger) use None as their null value, unlike Integer/Float whose null is 0 -- which is why year queries never hit this. Affected fields include album_id (None for every singleton) and the whole rg_*/r128_* family (None until ReplayGain has been run, i.e. by default). The query's own col_clause() defines the correct answer: `rg_track_gain >= ? AND rg_track_gain <= ?` against NULL is never true in SQL, so the row is quietly excluded. match() should agree, and now does. Point queries were already fine. Reachable from two callers, and the first has no SQL path at all: - library/models.py:1203 -- Item.destination() evaluates `paths:` config rules through match() unconditionally. A rule like `rg_track_gain:0..` crashes on any item without ReplayGain. - dbcore/db.py:805 -- the Results slow path, used whenever clause() returns None (any flexattr subquery forces this). This is the sibling of #3461: 2293d2f6 fixed the identical None-comparison crash on the sort side (sort.py deliberately groups nulls to match SQLite's NULL ordering); the query side was missed. Co-Authored-By: Claude Opus 4.8 (1M context) --- beets/dbcore/query.py | 6 ++++++ test/dbcore/test_query.py | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/beets/dbcore/query.py b/beets/dbcore/query.py index 3653434c16..4c86dd07c6 100644 --- a/beets/dbcore/query.py +++ b/beets/dbcore/query.py @@ -472,6 +472,12 @@ def match(self, obj: Model) -> bool: if self.point is not None: return value == self.point + if value is None: + # Nullable types (``NullInteger``/``NullFloat``) use ``None`` as + # their null value, so the field may be missing on some objects. + # ``col_clause`` compares against NULL in SQL, which is never true, + # so a range never matches a null value. + return False if self.rangemin is not None and value < self.rangemin: return False if self.rangemax is not None and value > self.rangemax: diff --git a/test/dbcore/test_query.py b/test/dbcore/test_query.py index 8f2a8921ce..0d9cd82d18 100644 --- a/test/dbcore/test_query.py +++ b/test/dbcore/test_query.py @@ -293,6 +293,18 @@ def test_match(self, item, q, should_match): assert q.match(item) == should_match assert not NotQuery(q).match(item) == should_match + @pytest.mark.parametrize("pattern", ["-10..0", "0..", "..0"]) + def test_numeric_range_match_null_field(self, pattern): + """A range never matches a null value, as in the SQL clause. + + Nullable types (``NullFloat``/``NullInteger``) use ``None`` as their + null value, so e.g. ``rg_track_gain`` is None until ReplayGain runs. + Comparing that against the range bounds used to raise a TypeError. + """ + item = Item(rg_track_gain=None) + + assert NumericQuery("rg_track_gain", pattern).match(item) is False + class TestPathQuery: """Tests for path-based querying functionality in the database system. From 47cef7a22819ca9950e9406d45db300ff8acf07d Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:54:26 +0800 Subject: [PATCH 2/3] docs: changelog entry for the NumericQuery null-range fix Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 096a35a6db..6367695961 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -95,6 +95,10 @@ Bug fixes valid date/time string" error instead of crashing with an uncaught ``KeyError``. A ``|`` was being accepted as a relative-date unit due to a regular expression character-class typo. +- A range query against a nullable numeric field (such as ``rg_track_gain`` on + an item without ReplayGain data, or ``album_id`` on a singleton) no longer + crashes with a ``TypeError``. The range now matches nothing, as it already + did on the SQL side. .. For plugin developers From e4fa79923e99f5ec9a9ed7c7e82963b4cef7e86c Mon Sep 17 00:00:00 2001 From: chuenchen309 <48723787+chuenchen309@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:24:11 +0800 Subject: [PATCH 3/3] docs: reflow changelog entry to satisfy docstrfmt Co-Authored-By: Claude Opus 4.8 --- docs/changelog.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 6367695961..e71c4f5f0f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -97,8 +97,8 @@ Bug fixes regular expression character-class typo. - A range query against a nullable numeric field (such as ``rg_track_gain`` on an item without ReplayGain data, or ``album_id`` on a singleton) no longer - crashes with a ``TypeError``. The range now matches nothing, as it already - did on the SQL side. + crashes with a ``TypeError``. The range now matches nothing, as it already did + on the SQL side. .. For plugin developers