Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion beets/dbcore/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <table> SET <flex>=?`` and crash with
# ``sqlite3.OperationalError: no such column: <flex>`` (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}=?")
Expand Down
7 changes: 7 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
21 changes: 21 additions & 0 deletions test/dbcore/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <table> SET <flex_field>=?`` and crash
with ``sqlite3.OperationalError: no such column: <flex_field>``.
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"
Expand Down
Loading