Skip to content

Fix Model.store() crash when fields= contains a flexible attribute - #6859

Open
e4779 wants to merge 2 commits into
beetbox:masterfrom
e4779:fix/store-flex-media-fields
Open

Fix Model.store() crash when fields= contains a flexible attribute#6859
e4779 wants to merge 2 commits into
beetbox:masterfrom
e4779:fix/store-flex-media-fields

Conversation

@e4779

@e4779 e4779 commented Jul 19, 2026

Copy link
Copy Markdown

Summary

Model.store(fields=...) filters the requested field set to fixed columns before building the main-table UPDATE, so a flexible attribute passed via fields= is routed through the existing _flex_table INSERT path instead of crashing.

Fixes #5580.

Root cause

A plugin can register the same field name two ways:

​```python
class FooPlugin(BeetsPlugin):
item_types = {"foo": types.Float()} # typed flex attribute

def __init__(self):
    super().__init__()
    self.add_media_field("foo", mediafile.MediaField(...))  # adds to Item._media_fields

​```

item_types keeps the field as a flexible attribute (no schema column). add_media_field adds the name to library.Item._media_fields. When beet update is called without --fields, update_items falls back to library.Item._media_fields, which now contains the flex attribute name. It is then forwarded to Model.store(fields=...), whose store loop built UPDATE <table> SET <flex_field>=? for every dirty field without checking whether the field had a real column. For flex attributes the column does not exist, so:

sqlite3.OperationalError: no such column: foo File ".../beets/dbcore/db.py", line 608, in store tx.mutate(query, subvars) ​

This matches the trace in #5580 (originally reported for beets 2.2.0; still reproducible on current master).

Fix

In Model.store(), partition the incoming fields:

python column_fields = [f for f in fields if f in self._fields] ​

and use column_fields for the main-table UPDATE. Flexible attributes are already persisted just below via the _values_flex INSERT path, so they keep round-tripping correctly — no behaviour change for the common case (no plugin, or plugin that only uses item_types). The previous code already iterated self._values_flex separately, so the only missing piece was the filter.

The fields=None default path (fields = self._fields) is already all-columns, so it is unaffected.

How I validated

  • Added a regression test in test/dbcore/test_db.py::ModelTest::test_store_with_flex_field_in_fields_argument that stores a typed flex attribute (some_float_field, declared in ModelFixture1._types) through store(fields=[...]) and asserts the value survives a fresh load. The test fails on master with OperationalError: no such column: some_float_field and passes with this change.
  • Full test suite (poe test, deselecting the two known root-in-container / poe docs expected failures documented in the dev guide): 2572 passed, 156 skipped — one more than the clean-HEAD baseline of 2571 (the new test), with no regressions.
  • poe lint, poe format, and poe check-types . all clean.

Why not roll back commit acff509 (the alternative proposed in the issue)

The issue author suggests reverting acff509 so that update_items falls back to Item._fields.keys() instead of Item._media_fields. That treats the symptom in one caller; this fix treats the root cause in Model.store() so any future caller that forwards a flex attribute through fields= is also safe. The change is also strictly narrower: existing fixed-column behaviour is byte-identical (the filter is a no-op when every entry of fields is a column).

Checklist

  • Tests added
  • Changelog entry added under "Bug fixes"
  • poe test, poe lint, poe format, poe check-types all pass

`Model.store(fields=...)` used to walk every field name passed in and
build an `UPDATE <table> SET <field>=?` clause for each dirty one,
without checking whether the field was a fixed column or a flexible
attribute. Callers such as `beet update` pass
`library.Item._media_fields` when no explicit `fields=` is given, and a
plugin that registers the same field via both `item_types` (typed flex
attr) and `add_media_field()` lands in that set. For such fields there
is no column, so the UPDATE crashed with
`sqlite3.OperationalError: no such column: <field>`.

Filter the requested field set to fixed columns before building the
UPDATE; flexible attributes are already persisted just below via the
`_flex_table` INSERT path, so they keep round-tripping correctly.

Adds a regression test that stores a typed flex attribute through the
`fields=` argument and asserts the value survives a fresh load.

Fixes beetbox#5580.
@e4779
e4779 requested a review from a team as a code owner July 19, 2026 10:59
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.62%. Comparing base (4e3884a) to head (bae9665).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #6859      +/-   ##
==========================================
- Coverage   75.63%   75.62%   -0.01%     
==========================================
  Files         163      163              
  Lines       21312    21313       +1     
  Branches     3360     3360              
==========================================
- Hits        16119    16118       -1     
- Misses       4401     4402       +1     
- Partials      792      793       +1     
Files with missing lines Coverage Δ
beets/dbcore/db.py 94.42% <100.00%> (+<0.01%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

e4779 added a commit to e4779/beets that referenced this pull request Jul 19, 2026
…ash (beetbox#5580)

Merges the topic-branch sent upstream as PR beetbox#6859 into fork
master so prod can deploy from fork. Topic-branch kept intact for the
upstream PR; this merge only advances fork master.
CI's Check docs job failed on the original entry because docstrfmt
enforces a 72-column wrap. Reformat with .
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Update mediafields regression

1 participant