Fix Model.store() crash when fields= contains a flexible attribute - #6859
Open
e4779 wants to merge 2 commits into
Open
Fix Model.store() crash when fields= contains a flexible attribute#6859e4779 wants to merge 2 commits into
Model.store() crash when fields= contains a flexible attribute#6859e4779 wants to merge 2 commits into
Conversation
`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.
Codecov Report✅ All modified and coverable lines are covered by tests. 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
🚀 New features to boost your workflow:
|
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 .
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Model.store(fields=...)filters the requested field set to fixed columns before building the main-tableUPDATE, so a flexible attribute passed viafields=is routed through the existing_flex_tableINSERT 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
```
item_typeskeeps the field as a flexible attribute (no schema column).add_media_fieldadds the name tolibrary.Item._media_fields. Whenbeet updateis called without--fields,update_itemsfalls back tolibrary.Item._media_fields, which now contains the flex attribute name. It is then forwarded toModel.store(fields=...), whose store loop builtUPDATE <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 incomingfields:
python column_fields = [f for f in fields if f in self._fields] and use
column_fieldsfor the main-tableUPDATE. Flexible attributes are already persisted just below via the_values_flexINSERT path, so they keep round-tripping correctly — no behaviour change for the common case (no plugin, or plugin that only usesitem_types). The previous code already iteratedself._values_flexseparately, so the only missing piece was the filter.The
fields=Nonedefault path (fields = self._fields) is already all-columns, so it is unaffected.How I validated
test/dbcore/test_db.py::ModelTest::test_store_with_flex_field_in_fields_argumentthat stores a typed flex attribute (some_float_field, declared inModelFixture1._types) throughstore(fields=[...])and asserts the value survives a fresh load. The test fails onmasterwithOperationalError: no such column: some_float_fieldand passes with this change.poe test, deselecting the two known root-in-container /poe docsexpected 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, andpoe check-types .all clean.Why not roll back commit acff509 (the alternative proposed in the issue)
The issue author suggests reverting
acff509so thatupdate_itemsfalls back toItem._fields.keys()instead ofItem._media_fields. That treats the symptom in one caller; this fix treats the root cause inModel.store()so any future caller that forwards a flex attribute throughfields=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 offieldsis a column).Checklist
poe test,poe lint,poe format,poe check-typesall pass