Repository: beetbox/beets

Issue:
Removing a flexible field from an album doesn't remove corresponding field from its items
<!--
Describe your problem, feature request, or discussion topic here.

Please fill out this and the "Setup" section below and remember to include
enough detail so that other people can reproduce the problem.
-->

### Problem

When importing an album with the `--set` flag, beets imports the album with "flex1" set on both the album and each of its tracks:

```sh
$ beet import <path> --set flex1=something
```

Later, when deleting the flex field with `-a`, the value is updated on the album, but is still set on the items:

```sh
$ beet modify -a "Some album" flex1!
```

After modifying, search for matching tracks, and they still return

```sh
$ beet ls flex1::something
```
```
Some artist - Some album - Track name 1
Some artist - Some album - Track name 2
...
```

Search for matching albums, returns nothing

```sh
$ beet ls -a flex1::something
```
```sh
# no results
```

### Setup

* OS: Arch
* Python version: 3.10.9
* beets version: 1.6.1
* Turning off plugins made problem go away (yes/no): no

<!--
You can turn off plugins temporarily by passing --plugins= on the command line:

$ beet --plugins= version
-->

My configuration (output of `beet config`) is:

```
(tested with default settings)
```

### Extra info

Also of note, this issue naturally doesn't come up if the flex field was added to just the album after import using `beet mod -a flex1=something` which is how I configured a lot of my library, so this issue only happens to albums imported with `import --set`


Discussion:

Resolving patch:
diff --git a/beets/library.py b/beets/library.py
--- a/beets/library.py
+++ b/beets/library.py
@@ -1355,9 +1355,12 @@ def store(self, fields=None):
         """
         # Get modified track fields.
         track_updates = {}
-        for key in self.item_keys:
-            if key in self._dirty:
+        track_deletes = set()
+        for key in self._dirty:
+            if key in self.item_keys:
                 track_updates[key] = self[key]
+            elif key not in self:
+                track_deletes.add(key)
 
         with self._db.transaction():
             super().store(fields)
@@ -1366,6 +1369,12 @@ def store(self, fields=None):
                     for key, value in track_updates.items():
                         item[key] = value
                     item.store()
+            if track_deletes:
+                for item in self.items():
+                    for key in track_deletes:
+                        if key in item:
+                            del item[key]
+                    item.store()
 
     def try_sync(self, write, move):
         """Synchronize the album and its items with the database.


Test patch:
diff --git a/test/test_library.py b/test/test_library.py
--- a/test/test_library.py
+++ b/test/test_library.py
@@ -77,6 +77,19 @@ def test_store_clears_dirty_flags(self):
         self.i.store()
         self.assertTrue('composer' not in self.i._dirty)
 
+    def test_store_album_cascades_flex_deletes(self):
+        album = _common.album()
+        album.flex1 = "Flex-1"
+        self.lib.add(album)
+        item = _common.item()
+        item.album_id = album.id
+        item.flex1 = "Flex-1"
+        self.lib.add(item)
+        del album.flex1
+        album.store()
+        self.assertNotIn('flex1', album)
+        self.assertNotIn('flex1', album.items()[0])
+
 
 class AddTest(_common.TestCase):
     def setUp(self):