diff --git a/beets/dbcore/db.py b/beets/dbcore/db.py index e66f4548bf..f002146ed9 100755 --- a/beets/dbcore/db.py +++ b/beets/dbcore/db.py @@ -586,10 +586,19 @@ def store(self, fields: Iterable[str] | None = None) -> None: if fields is None: fields = self._fields + # Separate the requested fields into fixed (real columns in the main + # table) and flexible (persisted via ``_flex_table``). Callers such as + # ``beet update`` may pass a flex attribute through ``fields=`` -- for + # example, when a plugin registers it via both ``item_types`` and + # ``add_media_field``. Treating a flex attribute as a column here used + # to build ``UPDATE SET =?`` and crash with + # ``sqlite3.OperationalError: no such column: `` (see #5580). + column_fields = [f for f in fields if f in self._fields] + # Build assignments for query. assignments = [] subvars: list[SQLiteType] = [] - for key in fields: + for key in column_fields: if key != "id" and key in self._dirty: self._dirty.remove(key) assignments.append(f"{key}=?") diff --git a/docs/changelog.rst b/docs/changelog.rst index 096a35a6db..84fc808d1b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -58,6 +58,13 @@ New features Bug fixes ~~~~~~~~~ +- Fixed a ``sqlite3.OperationalError: no such column`` crash in ``Model.store`` + when a field passed via the ``fields`` argument was a flexible attribute + rather than a fixed column. The fields set is now filtered to fixed columns + for the main-table ``UPDATE``; flexible attributes continue to be persisted + via the ``_flex_table`` path. This affected plugins that register fields via + both ``item_types`` and ``add_media_field`` and were later updated with ``beet + update``. :bug:`5580` - :doc:`plugins/edit`: Preserve missing album art paths when editing album metadata, instead of turning ``artpath: null`` into a path ending in ``None``. :bug:`2438` diff --git a/test/dbcore/test_db.py b/test/dbcore/test_db.py index 829919847a..fb33b49e33 100644 --- a/test/dbcore/test_db.py +++ b/test/dbcore/test_db.py @@ -281,6 +281,27 @@ def test_store_and_retrieve_flexattr(self): other_model = self.db._get(ModelFixture1, model.id) assert other_model.foo == "bar" + def test_store_with_flex_field_in_fields_argument(self): + """A flex attribute passed via ``fields=`` must not crash. + + Regression test for #5580: when a plugin registers a typed flex + attribute (via ``item_types``) and a caller such as ``beet update`` + passes its name through ``fields=`` to ``Model.store()``, the store + loop used to build ``UPDATE
SET =?`` and crash + with ``sqlite3.OperationalError: no such column: ``. + Flex attributes have no column in the main table; they live in the + ``_flex_table`` and must be persisted via the INSERT path. + """ + model = ModelFixture1() + model.add(self.db) + model.field_one = 42 + model.some_float_field = 1.5 + model.store(fields=["field_one", "some_float_field"]) + + other_model = self.db._get(ModelFixture1, model.id) + assert other_model.field_one == 42 + assert other_model.some_float_field == 1.5 + def test_delete_flexattr(self): model = ModelFixture1() model["foo"] = "bar"