Repository: acoustid/acoustid-server
Pull request #149: Fixed logic for track_mbid handling after MBID merge
Author: login: lalinsky
Created: 2025-03-07T08:46:43Z | Merged: 2025-03-07T17:29:25Z

- Never delete `track_mbid`
- Never update `track_mbid.mbid`
- Add `track_mbid_source.updated` and `track_mbid_changes.updated`

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **Refactor**
  - Streamlined the identifier merging process to handle individual identifiers, simplifying internal operations.
- **Chores**
  - Enhanced data consistency by adding tracking fields that log update timestamps and establish record relationships across multiple tables.
- **Tests**
  - Updated automated validations to align with the revised merging behavior and improved data tracking, bolstering system reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/acoustid/data/track.py b/acoustid/data/track.py
index 2ccfef72..deb5556c 100644
--- a/acoustid/data/track.py
+++ b/acoustid/data/track.py
@@ -97,55 +97,97 @@ def lookup_tracks(conn, mbids):
 def merge_mbids(
     fingerprint_db: FingerprintDB,
     ingest_db: IngestDB,
+    source_mbid: str,
     target_mbid: str,
-    source_mbids: List[str],
 ) -> None:
-    """
-    Merge the specified MBIDs.
-    """
-    logger.info("Merging MBIDs %r into %r", source_mbids, target_mbid)
-    query = sql.select(
-        [
-            sql.func.min(schema.track_mbid.c.id).label("id"),
-            sql.func.array_agg(schema.track_mbid.c.id).label("all_ids"),
-            schema.track_mbid.c.track_id,
-            sql.func.every(schema.track_mbid.c.disabled).label("all_disabled"),
-            sql.func.sum(schema.track_mbid.c.submission_count).label("count"),
-        ],
-        schema.track_mbid.c.mbid.in_(source_mbids + [target_mbid]),
-        group_by=schema.track_mbid.c.track_id,
+    logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
+
+    affected_track_mbids_query = (
+        sql.select(
+            [
+                schema.track_mbid.c.id,
+                schema.track_mbid.c.track_id,
+                schema.track_mbid.c.mbid,
+                schema.track_mbid.c.submission_count,
+                schema.track_mbid.c.disabled,
+                schema.track_mbid.c.merged_into,
+            ]
+        )
+        .where(schema.track_mbid.c.mbid.in_([source_mbid, target_mbid]))
+        .with_for_update()
     )
-    rows = fingerprint_db.execute(query).fetchall()
-    to_delete = set()
-    to_update = []
-    for row in rows:
-        old_ids = set(row["all_ids"])
-        old_ids.remove(row["id"])
-        to_delete.update(old_ids)
-        to_update.append((old_ids, row))
-        if old_ids:
-            update_stmt = schema.track_mbid_source.update().where(
-                schema.track_mbid_source.c.track_mbid_id.in_(old_ids)
+    track_mbids_by_track_id: Dict[int, Dict[str, Any]] = {}
+    for row in fingerprint_db.execute(affected_track_mbids_query):
+        track_mbids_by_track_id.setdefault(row["track_id"], {})[row["mbid"]] = row
+
+    for track_id, track_mbids in track_mbids_by_track_id.items():
+        source = track_mbids.get(source_mbid)
+        if source is None:
+            # source mbid not found, skip this track
+            continue
+
+        target = track_mbids.get(target_mbid)
+        if target is None:
+            # we have no record with the target mbid, so we create a new one
+            target_id = fingerprint_db.execute(
+                schema.track_mbid.insert()
+                .returning(schema.track_mbid.c.id)
+                .values(
+                    track_id=track_id,
+                    mbid=target_mbid,
+                    submission_count=0,
+                    disabled=False,
+                )
+            ).scalar()
+        else:
+            # we already have a record with the target mbid, so we update it
+            target_id = target["id"]
+
+        if source["merged_into"] is not None and source["merged_into"] != target_id:
+            raise ValueError("source mbid is already merged into another mbid")
+
+        # clear submission count and disable flag for source mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == source["id"])
+            .values(
+                merged_into=target_id,
+                submission_count=0,
+                disabled=True,
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-            update_stmt = schema.track_mbid_change.update().where(
-                schema.track_mbid_change.c.track_mbid_id.in_(old_ids)
+        )
+
+        # update submission count and disable flag for target mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == target_id)
+            .values(
+                submission_count=(
+                    schema.track_mbid.c.submission_count + source["submission_count"]
+                ),
+                disabled=sql.and_(
+                    schema.track_mbid.c.disabled,
+                    source["disabled"],
+                ),
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-    if to_delete:
-        delete_stmt = schema.track_mbid.delete().where(
-            schema.track_mbid.c.id.in_(to_delete)
         )
-        fingerprint_db.execute(delete_stmt)
-    for old_ids, row in to_update:
-        update_stmt = schema.track_mbid.update().where(
-            schema.track_mbid.c.id == row["id"]
+
+        # update track_mbid_source and track_mbid_change tables
+        ingest_db.execute(
+            schema.track_mbid_source.update()
+            .where(schema.track_mbid_source.c.track_mbid_id == source["id"])
+            .values(
+                track_mbid_id=target_id,
+                updated=sql.func.current_timestamp(),
+            )
         )
-        fingerprint_db.execute(
-            update_stmt.values(
-                submission_count=row["count"],
-                mbid=target_mbid,
-                disabled=row["all_disabled"],
+        ingest_db.execute(
+            schema.track_mbid_change.update()
+            .where(schema.track_mbid_change.c.track_mbid_id == source["id"])
+            .values(
+                track_mbid_id=target_id,
                 updated=sql.func.current_timestamp(),
             )
         )
@@ -168,7 +210,7 @@ def merge_missing_mbid(
         .where(schema.mb_recording_gid_redirect.c.gid == old_mbid)
     ).scalar()
     if new_mbid:
-        merge_mbids(fingerprint_db, ingest_db, new_mbid, [old_mbid])
+        merge_mbids(fingerprint_db, ingest_db, old_mbid, new_mbid)
         return True
 
     new_mbid = musicbrainz_db.execute(
diff --git a/acoustid/scripts/merge_missing_mbids.py b/acoustid/scripts/merge_missing_mbids.py
index ce12cb36..21620723 100644
--- a/acoustid/scripts/merge_missing_mbids.py
+++ b/acoustid/scripts/merge_missing_mbids.py
@@ -35,14 +35,18 @@ def run_merge_missing_mbid(script: Script, mbid: str) -> None:
         redis = script.get_redis()
         cache_key = "unknown_mbid:{}".format(mbid)
 
-        fingerprint_db = stack.enter_context(script.db_engines["fingerprint"].begin())
+        fingerprint_db = stack.enter_context(script.db_engines["fingerprint"].connect())
+        fingerprint_db_txn = stack.enter_context(fingerprint_db.begin_twophase())
+
+        ingest_db = stack.enter_context(script.db_engines["ingest"].connect())
+        ingest_db_txn = stack.enter_context(ingest_db.begin_twophase())
+
+        musicbrainz_db = stack.enter_context(script.db_engines["musicbrainz"].connect())
+
         if not try_lock(fingerprint_db, "merge_missing_mbid", mbid):
             logger.info("MBID %s is already being merged", mbid)
             return
 
-        ingest_db = stack.enter_context(script.db_engines["ingest"].begin())
-        musicbrainz_db = stack.enter_context(script.db_engines["musicbrainz"].begin())
-
         handled = merge_missing_mbid(
             fingerprint_db=fingerprint_db,
             ingest_db=ingest_db,
@@ -50,8 +54,15 @@ def run_merge_missing_mbid(script: Script, mbid: str) -> None:
             old_mbid=mbid,
         )
         if handled:
+            fingerprint_db_txn.prepare()
+            ingest_db_txn.prepare()
+            fingerprint_db_txn.commit()
+            ingest_db_txn.commit()
             return
 
+        fingerprint_db_txn.rollback()
+        ingest_db_txn.rollback()
+
         unknown_since: datetime.datetime | None = None
         unknown_since_str = redis.get(cache_key)
         if unknown_since_str:
diff --git a/acoustid/tables.py b/acoustid/tables.py
index 78c91849..802fe3cb 100644
--- a/acoustid/tables.py
+++ b/acoustid/tables.py
@@ -361,6 +361,7 @@
     Column(
         "disabled", Boolean, default=False, server_default=sql.false(), nullable=False
     ),
+    Column("merged_into", Integer, ForeignKey("track_mbid.id")),
     Index("track_mbid_idx_uniq", "track_id", "mbid", unique=True),
     info={"bind_key": "fingerprint"},
 )
@@ -378,6 +379,7 @@
         server_default=sql.func.current_timestamp(),
         nullable=False,
     ),
+    Column("updated", DateTime(timezone=True)),
     info={"bind_key": "ingest"},
 )
 
@@ -395,6 +397,7 @@
     ),
     Column("disabled", Boolean, nullable=False),
     Column("note", Text),
+    Column("updated", DateTime(timezone=True)),
     info={"bind_key": "ingest"},
 )
 
diff --git a/tests/test_data_track.py b/tests/test_data_track.py
index 9c5f3f81..6874b86e 100644
--- a/tests/test_data_track.py
+++ b/tests/test_data_track.py
@@ -55,8 +55,8 @@ def test_merge_mbids(ctx):
     merge_mbids(
         ctx.db.get_fingerprint_db(),
         ctx.db.get_ingest_db(),
+        "d575d506-4da4-11e0-b951-0025225356f3",
         "97edb73c-4dac-11e0-9096-0025225356f3",
-        ["d575d506-4da4-11e0-b951-0025225356f3"],
     )
 
     rows = (
@@ -68,6 +68,7 @@ def test_merge_mbids(ctx):
     )
     expected_rows = [
         (1, UUID("97edb73c-4dac-11e0-9096-0025225356f3"), 20, False),
+        (1, UUID("d575d506-4da4-11e0-b951-0025225356f3"), 0, True),
     ]
     assert expected_rows == rows
 
@@ -108,8 +109,8 @@ def test_merge_mbids_disabled_target(ctx):
     merge_mbids(
         ctx.db.get_fingerprint_db(),
         ctx.db.get_ingest_db(),
+        "d575d506-4da4-11e0-b951-0025225356f3",
         "97edb73c-4dac-11e0-9096-0025225356f3",
-        ["d575d506-4da4-11e0-b951-0025225356f3"],
     )
 
     rows = (
@@ -121,6 +122,7 @@ def test_merge_mbids_disabled_target(ctx):
     )
     expected_rows = [
         (1, UUID("97edb73c-4dac-11e0-9096-0025225356f3"), 20, False),
+        (1, UUID("d575d506-4da4-11e0-b951-0025225356f3"), 0, True),
     ]
     assert expected_rows == rows
 
@@ -139,8 +141,8 @@ def test_merge_mbids_disabled_source(ctx):
     merge_mbids(
         ctx.db.get_fingerprint_db(),
         ctx.db.get_ingest_db(),
+        "d575d506-4da4-11e0-b951-0025225356f3",
         "97edb73c-4dac-11e0-9096-0025225356f3",
-        ["d575d506-4da4-11e0-b951-0025225356f3"],
     )
     rows = (
         ctx.db.get_fingerprint_db()
@@ -151,6 +153,7 @@ def test_merge_mbids_disabled_source(ctx):
     )
     expected_rows = [
         (1, UUID("97edb73c-4dac-11e0-9096-0025225356f3"), 20, False),
+        (1, UUID("d575d506-4da4-11e0-b951-0025225356f3"), 0, True),
     ]
     assert expected_rows == rows
 
@@ -169,8 +172,8 @@ def test_merge_mbids_disabled_both(ctx):
     merge_mbids(
         ctx.db.get_fingerprint_db(),
         ctx.db.get_ingest_db(),
+        "d575d506-4da4-11e0-b951-0025225356f3",
         "97edb73c-4dac-11e0-9096-0025225356f3",
-        ["d575d506-4da4-11e0-b951-0025225356f3"],
     )
     rows = (
         ctx.db.get_fingerprint_db()
@@ -181,6 +184,7 @@ def test_merge_mbids_disabled_both(ctx):
     )
     expected_rows = [
         (1, UUID("97edb73c-4dac-11e0-9096-0025225356f3"), 20, True),
+        (1, UUID("d575d506-4da4-11e0-b951-0025225356f3"), 0, True),
     ]
     assert expected_rows == rows
 
@@ -211,14 +215,18 @@ def test_merge_missing_mbid(ctx: ScriptContext) -> None:
     )
     rows = (
         ctx.db.get_fingerprint_db()
-        .execute("SELECT track_id, mbid FROM track_mbid ORDER BY track_id, mbid")
+        .execute(
+            "SELECT track_id, mbid, submission_count, disabled FROM track_mbid ORDER BY track_id, mbid"
+        )
         .fetchall()
     )
     expected_rows = [
-        (1, UUID("77ef7468-e8f8-4447-9c7e-52b11272c6cc")),
-        (1, UUID("97edb73c-4dac-11e0-9096-0025225356f3")),
-        (1, UUID("b81f83ee-4da4-11e0-9ed8-0025225356f3")),
-        (2, UUID("77ef7468-e8f8-4447-9c7e-52b11272c6cc")),
+        (1, UUID("77ef7468-e8f8-4447-9c7e-52b11272c6cc"), 1, False),
+        (1, UUID("97edb73c-4dac-11e0-9096-0025225356f3"), 1, False),
+        (1, UUID("b81f83ee-4da4-11e0-9ed8-0025225356f3"), 1, False),
+        (1, UUID("d575d506-4da4-11e0-b951-0025225356f3"), 0, True),
+        (2, UUID("77ef7468-e8f8-4447-9c7e-52b11272c6cc"), 1, False),
+        (2, UUID("d575d506-4da4-11e0-b951-0025225356f3"), 0, True),
     ]
     assert expected_rows == rows
 


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (1)</summary><blockquote>

`98-185`: **Review new single-source-to-target merge logic.**  
The function signature and core merging steps have been inverted to take a single `source_mbid` followed by a `target_mbid`. Key points to note:  
- **Row Locking**: Using `WITH FOR UPDATE` is a solid decision, helping avoid concurrency issues when adjusting submission counts.  
- **Disabled Flag Logic**: The final `disabled` state employs an AND condition (`disabled=sql.and_(...)`), meaning the target MBID remains disabled only if *both* source and target were disabled. This is a valid choice, but ensure this matches business requirements.  
- **Submission Count Aggregation**: Summing `target.submission_count` with the source count ensures no data is lost. The source is then zeroed out, which is consistent with the notion of “moving” submissions to the target.  
- **Source to Target Reassignments**: `track_mbid_source` and `track_mbid_change` get updated to point to the new target ID. This effectively consolidates references and prevents orphans.  

Overall, the refactoring is logically coherent for a single source MBID, but please confirm any edge cases (e.g., merging an MBID into itself) are either validated or disallowed.

</blockquote></details>
<details>
<summary>acoustid/tables.py (2)</summary><blockquote>

`382-382`: **Added `updated` column without default timestamp.**  
While an `updated` column is helpful for auditing changes, consider using `server_default=sql.func.current_timestamp()` or ensuring the application layer always supplies an updated value. Inconsistent usage of defaults across tables can lead to confusion.

---

`400-400`: **Similarly added `updated` column in `track_mbid_change`.**  
As with the prior column, confirm whether you want `server_default` or enforced updates on the application side for consistent timestamps.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between aa1316be4e1e1290c6d49d3d682ba444a6562b71 and 725294533a733c7f29353afb78355372a3d6a15e.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `acoustid/data/track.py` (2 hunks)
* `acoustid/tables.py` (3 hunks)
* `tests/test_data_track.py` (4 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test (3.12)
* GitHub Check: test (3.12)

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>tests/test_data_track.py (4)</summary>

`58-59`: **Confirm source vs. target order.**  
These lines correctly switch the parameter order in `merge_mbids` to use the new signature, with `"d575..."` as the source MBID and `"97edb..."` as the target MBID. This aligns with the updated function contract and the test expectations.

---

`111-112`: **Maintain consistency in test for disabled target.**  
Here again, `"d575..."` is passed as source and `"97edb..."` as target. This remains consistent with the newly inverted parameter order. The test scenario of a disabled target MBID remains accurate.

---

`142-143`: **Check source vs. target correctness with disabled source.**  
Merging `"d575..."` into `"97edb..."` in a context where `"d575..."` is disabled follows the new function signature. The test logic continues to reflect the intended behavior.

---

`172-173`: **Validate merged disabled-both scenario.**  
Using `"d575..."` as the source MBID and `"97edb..."` as the target MBID also applies to the case where both MBIDs start out disabled. The parameter usage is correct, ensuring the test covers the merged-disabled edge case.

</details>
<details>
<summary>acoustid/data/track.py (1)</summary>

`205-205`: **Match updated parameter order in call site.**  
Replacing the legacy call signature with `merge_mbids(fingerprint_db, ingest_db, old_mbid, new_mbid)` correctly designates `old_mbid` as the source and `new_mbid` as the target MBID. This maintains consistency with the refactored function.

</details>
<details>
<summary>acoustid/tables.py (1)</summary>

`364-364`: **Self-referential foreign key for `merged_into`.**  
Adding the `merged_into` column referencing `track_mbid.id` facilitates tracking which MBID replaced the original. Confirm that cascading rules (e.g., `ON DELETE`) match your intended data lifecycle.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T08:49:48Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (2)</summary><blockquote>

`105-120`: **Improved query approach for track_mbid handling**

The query now uses `with_for_update()` to prevent race conditions during the update process, and efficiently collects both source and target MBIDs in a single query.


Consider adding a comment explaining the dictionary structure in `track_mbids_by_track_id` for better maintainability. For example:
```diff
 track_mbids_by_track_id: Dict[int, Dict[str, Any]] = {}
+# Dictionary structure: {track_id: {mbid: row_data}}
 for row in fingerprint_db.execute(affected_track_mbids_query):
     track_mbids_by_track_id.setdefault(row["track_id"], {})[row["mbid"]] = row
```

---

`98-189`: **Consider adding transaction management**

The implementation performs multiple database operations across different tables. While the code is logically correct, it would benefit from explicit transaction management to ensure atomicity.

Consider wrapping the operations in a transaction to ensure they either all succeed or all fail:

```python
def merge_mbids(
    fingerprint_db: FingerprintDB,
    ingest_db: IngestDB,
    source_mbid: str,
    target_mbid: str,
) -> None:
    logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
    
    try:
        # Start transactions
        fingerprint_db.begin()
        ingest_db.begin()
        
        # Existing code...
        
        # Commit transactions if all operations were successful
        fingerprint_db.commit()
        ingest_db.commit()
    except Exception as e:
        # Rollback in case of errors
        fingerprint_db.rollback()
        ingest_db.rollback()
        logger.error("Failed to merge MBIDs: %s", str(e))
        raise
```

Note: Please verify that the database connection objects support these transaction methods before implementing.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 725294533a733c7f29353afb78355372a3d6a15e and 4ae1237af5954c09983587c3826ac88f4e6445da.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `acoustid/data/track.py` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test (3.12)
* GitHub Check: test (3.12)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (7)</summary>

`103-104`: **Updated logging statement to match new signature**

The logging statement has been properly updated to reflect the new parameter order.

---

`121-126`: **Appropriate handling of missing source MBIDs**

The code correctly skips tracks where the source MBID doesn't exist, maintaining data integrity.

---

`127-143`: **Proper logic for creating or updating target MBIDs**

The implementation correctly handles both scenarios: creating a new target MBID record when it doesn't exist or updating an existing one. The `returning` clause in the insert statement is particularly efficient.

---

`144-154`: **Updated approach for source MBID handling**

Rather than deleting the source MBID record, it's now properly preserved with updated fields, including the new `merged_into` reference and timestamp, which aligns with the PR objective of never deleting `track_mbid`.

---

`209-209`: **Updated function call in merge_missing_mbid**

The call to `merge_mbids` has been properly updated to match the new parameter order.

---

`173-189`: <details>
<summary>✅ Verification successful</summary>

**Comprehensive update of related tables**

The implementation now correctly updates the `track_mbid_source` and `track_mbid_change` tables, preserving the relationship with the appropriate timestamps.


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify the track_mbid_source and track_mbid_change tables have the updated column
rg "track_mbid_source = Table\(" --type py -A 15
rg "track_mbid_change = Table\(" --type py -A 15
```

Length of output: 1749

---



</details>

**Update Verification Completed – Code Correctly Syncs Related Tables**

The recent changes in `acoustid/data/track.py` correctly update both the `track_mbid_source` and `track_mbid_change` tables. I've verified via the queries that the `updated` column exists in both tables (as defined in `acoustid/tables.py`), and the usage of `sql.func.current_timestamp()` properly reflects this update.

- **Verified in `acoustid/tables.py`:**
  - Both `track_mbid_source` and `track_mbid_change` tables include the `updated` column.
- **In `acoustid/data/track.py` (lines 173-189):**
  - The timestamp is correctly applied during the update, ensuring the relationships and state changes are maintained.

---

`98-102`: <details>
<summary>✅ Verification successful</summary>

**Function signature changed to handle individual source MBIDs**

The function signature has been modified to accept a single `source_mbid` instead of a list of `source_mbids`, and the parameter order has been reversed. This change aligns with the PR objective to refine MBID handling during merges.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check for any other usages of merge_mbids function to ensure they've been updated
rg "merge_mbids\s*\(" --type py -A 1 -B 1
```

Length of output: 1045

---



</details>

**Confirmed: Merge MBIDs Function Signature Update**  
The merge_mbids function has been updated to accept a single `source_mbid` (renamed from a list) and the parameter order has been reversed. Our verification shows that all call sites—in particular, the tests in `tests/test_data_track.py` and the usage within `acoustid/data/track.py`—are consistent with this new signature. No further modifications are required.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T09:00:16Z
user: login: lalinsky
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T10:59:22Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (1)</summary><blockquote>

`103-104`: **Add guard clause for identical source and target MBIDs.**

The function doesn't handle the case where source and target MBIDs are identical, which could lead to unexpected behavior.

```diff
 def merge_mbids(
     fingerprint_db: FingerprintDB,
     ingest_db: IngestDB,
     source_mbid: str,
     target_mbid: str,
 ) -> None:
+    if source_mbid == target_mbid:
+        logger.info("Source and target MBIDs are identical (%r), skipping merge", source_mbid)
+        return
+
     logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 4ae1237af5954c09983587c3826ac88f4e6445da and d3e133499b0b1c08b6fbc59023723cc9b04e2c7b.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `acoustid/data/track.py` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test (3.12)
* GitHub Check: test (3.12)

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (4)</summary>

`175-191`: **Updated related tables correctly with timestamp tracking.**

The code properly updates the `track_mbid_source` and `track_mbid_change` tables, including the new `updated` timestamp columns, which aligns with the PR objectives.

---

`105-145`: **Simplified approach for track MBID handling.**

The new implementation is more straightforward:
1. Fetches both source and target MBIDs with a single query and `FOR UPDATE` locking
2. Processes tracks individually
3. Preserves source records (marked as merged) instead of deleting them
4. Creates new target records when needed

This is a good improvement that aligns with the PR objectives of never deleting `track_mbid` records and not updating the `track_mbid.mbid` field.

---

`211-212`: **Updated function call to match new parameter order.**

The call to `merge_mbids` has been updated to match the new parameter order.

---

`100-103`: <details>
<summary>✅ Verification successful</summary>

**Function signature and logic changed to handle single source MBID.**

The function now accepts a single `source_mbid` instead of a list of `source_mbids`, and the parameter order has been swapped. This change aligns with the PR objective to refine MBID handling after merges.

This is a breaking change in the API - ensure all callers are updated.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find all calls to merge_mbids in the codebase to verify they've been updated
rg "merge_mbids\(" --type py -A 2 -B 2
```

Length of output: 1600

---



</details>

**Merge MBID function signature update verified**

The change to accept a single `source_mbid` (with the parameter order now being `fingerprint_db, ingest_db, source_mbid, target_mbid`) has been confirmed. A search for `merge_mbids(` shows that all call sites—in both `acoustid/data/track.py` and the tests in `tests/test_data_track.py`—properly pass the arguments in the new order. No remnants of the old, list-based API were found.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T10:59:29Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:00:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (1)</summary><blockquote>

`146-156`: **Consider adding a comment explaining the merged_into field purpose**

Setting `merged_into=target_id` for the source MBID record creates a direct link for tracking where the MBID was merged to, which is good. Consider adding a comment explaining this relationship for future maintainers.

```diff
# clear submission count and disable flag for source mbid
fingerprint_db.execute(
    schema.track_mbid.update()
    .where(schema.track_mbid.c.id == source["id"])
    .values(
+       # Link to the target record to maintain the merge history
        merged_into=target_id,
        submission_count=0,
        disabled=True,
        updated=sql.func.current_timestamp(),
    )
)
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between d3e133499b0b1c08b6fbc59023723cc9b04e2c7b and 17cbd2176d750a665b05b7f2c8860450863c9387.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `acoustid/data/track.py` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: test (3.12)
* GitHub Check: test (3.12)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (7)</summary>

`97-102`: **Function signature change aligns with the PR objective**

The function now takes a single `source_mbid` instead of a list of `source_mbids`, which aligns with the PR objective that the `track_mbid` will never be deleted and `track_mbid.mbid` will not be updated.

---

`103-118`: **Query approach simplifies the merging logic**

The updated approach of fetching both source and target MBIDs in a single query with row-level locking (`with_for_update`) is more efficient and prevents race conditions during the merge process.

---

`119-128`: **Skip logic prevents orphaned references**

The code properly handles cases where the source MBID doesn't exist for a track by skipping it, which ensures data consistency.

---

`129-141`: **Target MBID creation logic is sound**

The code properly creates a new target MBID record if it doesn't exist, starting with a submission count of 0 and disabled=False, which will be updated in subsequent steps.

---

`174-190`: **Good handling of related records**

The code properly updates the related `track_mbid_source` and `track_mbid_change` tables to maintain referential integrity after the merge, and sets the `updated` timestamps to track when the changes occurred.

---

`210-210`: <details>
<summary>❓ Verification inconclusive</summary>

**Verify all function calls to merge_mbids are updated**

The call to `merge_mbids` has been updated to match the new signature. Ensure all other calls to this function throughout the codebase have been similarly updated to prevent runtime errors.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all calls to merge_mbids in the codebase
rg "merge_mbids\(" --type py
```

Length of output: 380

---



</details>

**Action Required: Verify consistency of merge_mbids calls across the codebase**

The function call in `acoustid/data/track.py` appears to have been updated to use the new signature:

```python
merge_mbids(fingerprint_db, ingest_db, old_mbid, new_mbid)
```

However, additional calls to `merge_mbids` exist in the tests (see `tests/test_data_track.py`). Please ensure that those test calls are also updated to match the new signature to prevent potential runtime errors.

- **acoustid/data/track.py:** Confirm the updated call at line 210.
- **tests/test_data_track.py:** Verify that all instances of `merge_mbids(` are revised according to the new parameter order.

---

`158-171`: <details>
<summary>✅ Verification successful</summary>

**Verify the logical AND operation for the disabled flag**

The code uses `sql.and_` to determine if the target MBID should be disabled, which means it will only be disabled if both the existing target and the source are disabled. This ensures that if either was enabled, the result remains enabled. Confirm this is the intended behavior.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any comments or documentation about how the disabled flag should be handled
rg "disabled.*flag|track_mbid.*disabled" --type py -A 2
```

Length of output: 6074

---



</details>

**Logical AND Behavior Verified and Approved**

After reviewing the repository code and related queries, it’s clear that the use of `sql.and_` for combining the existing `disabled` flag with `source["disabled"]` was intentional. This update operation ensures that the target MBID remains disabled only if both the current flag and the new submission indicate it should be disabled. Similar patterns elsewhere in the codebase confirm that this cumulative logic is by design.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T11:05:36Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (1)</summary><blockquote>

`97-193`: **Consider adding transaction management and more error handling**

While the implementation is solid, consider the following improvements:
1. Add explicit transaction management to ensure all operations succeed or fail together
2. Add more comprehensive error handling beyond just the single ValueError case
3. Include type annotations for local variables like `track_mbids_by_track_id`

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 37630e009d8a6e8767b22ba4d9b6a7c19a346be1 and 2eabfd0f07bfbcf163d9b99622e2c5de508aad0e.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `acoustid/data/track.py` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test (3.12)

</details>

<details>
<summary>🔇 Additional comments (7)</summary><blockquote>

<details>
<summary>acoustid/data/track.py (7)</summary>

`105-118`: **Improved query structure with row-level locking**

The new query structure with `with_for_update()` ensures transactional consistency during the merge operation. The selected columns include the newly added `merged_into` field, which helps track the merge relationships.

---

`146-147`: **Good validation to prevent cascading merges**

This validation ensures that a source MBID that's already been merged elsewhere can't be merged again, preventing potential data inconsistencies or circular references.

---

`150-159`: **Source MBID is preserved but disabled**

In line with the PR objective that "track_mbid will never be deleted," the source MBID record is properly preserved but marked as merged, disabled, and its submission count is reset. The `updated` timestamp is also set to track when this change occurred.

---

`161-175`: **Properly combining submission counts and preserving disabled state**

The target MBID's submission count is incremented by the source's count, and the disabled flag is properly combined using a logical AND to ensure consistent behavior. The `updated` timestamp tracks when this change occurred.

---

`177-193`: **Comprehensive related table updates**

The code properly updates the `track_mbid_source` and `track_mbid_change` tables to maintain referential integrity. The addition of the `updated` timestamps in these tables aligns with the PR objectives of adding these new fields.

---

`213-213`: **Function call updated to match new signature**

The call to `merge_mbids` in the `merge_missing_mbid` function has been correctly updated to pass `old_mbid` as a single string rather than in a list.

---

`97-102`: <details>
<summary>❓ Verification inconclusive</summary>

**Function signature changed from list of MBIDs to single MBID**

The `merge_mbids` function now accepts a single `source_mbid` parameter instead of a list of `source_mbids`. This is a breaking change that aligns with the PR objective to refine MBID merge handling, but requires updates to all callers of this function.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any remaining calls to merge_mbids with multiple source_mbids
rg "merge_mbids\(.*\[" --type py
```

Length of output: 34

---



</details>

**Action: Confirm Update of merge_mbids Callers**

The updated `merge_mbids` function now accepts a single `source_mbid` instead of a list of MBIDs. The recent search using the regex command did not return any instances where the function was invoked with a list argument. However, due to the possibility of variations in how parameters are passed, please manually verify that all call sites across the codebase have been updated accordingly.

- In `acoustid/data/track.py` (lines 97–102), the function signature is updated as intended.
- Ensure that any invocation of `merge_mbids` now passes a single MBID rather than a list.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T16:50:05Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>acoustid/scripts/merge_missing_mbids.py (1)</summary><blockquote>

`44-44`: **Consider using two-phase transaction for MusicBrainz DB if atomicity is required.**

Currently, `musicbrainz_db` uses a standard connection but is not enrolled in a two-phase commit. If the operation must be atomic across all three databases, converting it to a two-phase transaction can prevent inconsistent states.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2eabfd0f07bfbcf163d9b99622e2c5de508aad0e and 2bb82412631e638585c8519e4a2123b3bcb53efd.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `acoustid/scripts/merge_missing_mbids.py` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test (3.12)

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>acoustid/scripts/merge_missing_mbids.py (3)</summary>

`38-39`: **Leverage two-phase transactions carefully.**

Switching to a two-phase transaction for `fingerprint_db` is a solid step towards atomicity across multiple databases. Ensure that the second phase (commit) is consistently invoked once the transaction is prepared. If it only receives a “prepare()” call, the transaction may remain in a prepared but uncommitted state indefinitely.

---

`41-42`: **Maintain consistency with two-phase transactions for `ingest_db`.**

Likewise, confirm that a proper second-phase commit is executed after `prepare()`. Otherwise, the transaction may stay in a prepared state and lock tables or data until manually resolved.

---

`61-62`: **Rollbacks in two-phase flow.**

Rolling back here is correct for the first phase. If you intend full two-phase functionality, confirm that rolling back a prepared transaction uses the respective two-phase rollback mechanism (e.g., `rollback_prepared()`). Otherwise, partial or prepared states might persist.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:01:49Z

Review comments:
diff_hunk: @@ -97,55 +97,93 @@ def lookup_tracks(conn, mbids):
 def merge_mbids(
     fingerprint_db: FingerprintDB,
     ingest_db: IngestDB,
+    source_mbid: str,
     target_mbid: str,
-    source_mbids: List[str],
 ) -> None:
-    """
-    Merge the specified MBIDs.
-    """
-    logger.info("Merging MBIDs %r into %r", source_mbids, target_mbid)
-    query = sql.select(
-        [
-            sql.func.min(schema.track_mbid.c.id).label("id"),
-            sql.func.array_agg(schema.track_mbid.c.id).label("all_ids"),
-            schema.track_mbid.c.track_id,
-            sql.func.every(schema.track_mbid.c.disabled).label("all_disabled"),
-            sql.func.sum(schema.track_mbid.c.submission_count).label("count"),
-        ],
-        schema.track_mbid.c.mbid.in_(source_mbids + [target_mbid]),
-        group_by=schema.track_mbid.c.track_id,
+    logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
+
+    affected_track_mbids_query = (
+        sql.select(
+            [
+                schema.track_mbid.c.id,
+                schema.track_mbid.c.track_id,
+                schema.track_mbid.c.submission_count,
+                schema.track_mbid.c.disabled,
+            ]
+        )
+        .where(schema.track_mbid.c.mbid.in_([source_mbid, target_mbid]))
+        .with_for_update()
     )
-    rows = fingerprint_db.execute(query).fetchall()
-    to_delete = set()
-    to_update = []
-    for row in rows:
-        old_ids = set(row["all_ids"])
-        old_ids.remove(row["id"])
-        to_delete.update(old_ids)
-        to_update.append((old_ids, row))
-        if old_ids:
-            update_stmt = schema.track_mbid_source.update().where(
-                schema.track_mbid_source.c.track_mbid_id.in_(old_ids)
+    track_mbids_by_track_id: Dict[int, Dict[str, Any]] = {}
+    for row in fingerprint_db.execute(affected_track_mbids_query):
+        track_mbids_by_track_id.setdefault(row["track_id"], {})[row["mbid"]] = row
+
+    for track_id, track_mbids in track_mbids_by_track_id.items():
+        source = track_mbids.get(source_mbid)
+        if source is None:
+            # source mbid not found, skip this track
+            continue
+
+        target = track_mbids.get(target_mbid)
+        if target is None:
+            # we have no record with the target mbid, so we create a new one
+            target_id = fingerprint_db.execute(
+                schema.track_mbid.insert()
+                .returning(schema.track_mbid.c.id)
+                .values(
+                    track_id=track_id,
+                    mbid=target_mbid,
+                    submission_count=0,
+                    disabled=False,
+                )
+            ).scalar()
+        else:
+            # we already have a record with the target mbid, so we update it
+            target_id = target["id"]
+
+        # clear submission count and disable flag for source mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == source["id"])
+            .values(
+                merged_into=target_id,
+                submission_count=0,
+                disabled=True,
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-            update_stmt = schema.track_mbid_change.update().where(
-                schema.track_mbid_change.c.track_mbid_id.in_(old_ids)
+        )
+
+        # update submission count and disable flag for target mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == target_id)
+            .values(
+                merged_into=target_id,
+                submission_count=(
+                    schema.track_mbid.c.submission_count + source["submission_count"]
+                ),
+                disabled=sql.and_(
+                    schema.track_mbid.c.disabled,
+                    source["disabled"],
+                ),
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-    if to_delete:
-        delete_stmt = schema.track_mbid.delete().where(
-            schema.track_mbid.c.id.in_(to_delete)
         )
path: acoustid/data/track.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Robust target MBID update logic**

The update logic for the target MBID combines submission counts and properly handles the disabled flag using a logical AND operation.


---

🏁 Script executed:

```shell
#!/bin/bash
# Check the track_mbid table schema to confirm it has the merged_into and updated columns
rg "track_mbid = Table\(" --type py -A 15
```

Length of output: 923

---



</details>

**Fix Required: Missing `merged_into` Column in Table Schema**

The update logic in `acoustid/data/track.py` (lines 156–171) correctly combines the submission count and applies a logical AND to the disabled flag. However, the update query sets the `merged_into` field—which isn’t present in the `track_mbid` table as confirmed by the schema in `acoustid/tables.py`. Please address one of the following:

- **Add the `merged_into` column:** If this field is intended to track merged MBIDs, include the appropriate schema migration/update.
- **Remove the reference:** If the column was added erroneously, remove the update logic that assigns `merged_into`.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-07T09:00:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,55 +97,93 @@ def lookup_tracks(conn, mbids):
 def merge_mbids(
     fingerprint_db: FingerprintDB,
     ingest_db: IngestDB,
+    source_mbid: str,
     target_mbid: str,
-    source_mbids: List[str],
 ) -> None:
-    """
-    Merge the specified MBIDs.
-    """
-    logger.info("Merging MBIDs %r into %r", source_mbids, target_mbid)
-    query = sql.select(
-        [
-            sql.func.min(schema.track_mbid.c.id).label("id"),
-            sql.func.array_agg(schema.track_mbid.c.id).label("all_ids"),
-            schema.track_mbid.c.track_id,
-            sql.func.every(schema.track_mbid.c.disabled).label("all_disabled"),
-            sql.func.sum(schema.track_mbid.c.submission_count).label("count"),
-        ],
-        schema.track_mbid.c.mbid.in_(source_mbids + [target_mbid]),
-        group_by=schema.track_mbid.c.track_id,
+    logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
+
+    affected_track_mbids_query = (
+        sql.select(
+            [
+                schema.track_mbid.c.id,
+                schema.track_mbid.c.track_id,
+                schema.track_mbid.c.submission_count,
+                schema.track_mbid.c.disabled,
+            ]
+        )
+        .where(schema.track_mbid.c.mbid.in_([source_mbid, target_mbid]))
+        .with_for_update()
     )
-    rows = fingerprint_db.execute(query).fetchall()
-    to_delete = set()
-    to_update = []
-    for row in rows:
-        old_ids = set(row["all_ids"])
-        old_ids.remove(row["id"])
-        to_delete.update(old_ids)
-        to_update.append((old_ids, row))
-        if old_ids:
-            update_stmt = schema.track_mbid_source.update().where(
-                schema.track_mbid_source.c.track_mbid_id.in_(old_ids)
+    track_mbids_by_track_id: Dict[int, Dict[str, Any]] = {}
+    for row in fingerprint_db.execute(affected_track_mbids_query):
+        track_mbids_by_track_id.setdefault(row["track_id"], {})[row["mbid"]] = row
+
+    for track_id, track_mbids in track_mbids_by_track_id.items():
+        source = track_mbids.get(source_mbid)
+        if source is None:
+            # source mbid not found, skip this track
+            continue
+
+        target = track_mbids.get(target_mbid)
+        if target is None:
+            # we have no record with the target mbid, so we create a new one
+            target_id = fingerprint_db.execute(
+                schema.track_mbid.insert()
+                .returning(schema.track_mbid.c.id)
+                .values(
+                    track_id=track_id,
+                    mbid=target_mbid,
+                    submission_count=0,
+                    disabled=False,
+                )
+            ).scalar()
+        else:
+            # we already have a record with the target mbid, so we update it
+            target_id = target["id"]
+
+        # clear submission count and disable flag for source mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == source["id"])
+            .values(
+                merged_into=target_id,
+                submission_count=0,
+                disabled=True,
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-            update_stmt = schema.track_mbid_change.update().where(
-                schema.track_mbid_change.c.track_mbid_id.in_(old_ids)
+        )
+
+        # update submission count and disable flag for target mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == target_id)
+            .values(
+                merged_into=target_id,
+                submission_count=(
+                    schema.track_mbid.c.submission_count + source["submission_count"]
+                ),
+                disabled=sql.and_(
+                    schema.track_mbid.c.disabled,
+                    source["disabled"],
+                ),
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-    if to_delete:
-        delete_stmt = schema.track_mbid.delete().where(
-            schema.track_mbid.c.id.in_(to_delete)
         )
path: acoustid/data/track.py
user: login: lalinsky
body: The field was added here https://github.com/acoustid/acoustid-server/pull/149/files#diff-6e75a1f91e79ed8929617255e25b4af847e8f5ce0af493fc40fda620c313d779R364
created_at: 2025-03-07T10:59:22Z
author_association: MEMBER
diff_hunk: @@ -97,55 +97,95 @@ def lookup_tracks(conn, mbids):
 def merge_mbids(
     fingerprint_db: FingerprintDB,
     ingest_db: IngestDB,
+    source_mbid: str,
     target_mbid: str,
-    source_mbids: List[str],
 ) -> None:
-    """
-    Merge the specified MBIDs.
-    """
-    logger.info("Merging MBIDs %r into %r", source_mbids, target_mbid)
-    query = sql.select(
-        [
-            sql.func.min(schema.track_mbid.c.id).label("id"),
-            sql.func.array_agg(schema.track_mbid.c.id).label("all_ids"),
-            schema.track_mbid.c.track_id,
-            sql.func.every(schema.track_mbid.c.disabled).label("all_disabled"),
-            sql.func.sum(schema.track_mbid.c.submission_count).label("count"),
-        ],
-        schema.track_mbid.c.mbid.in_(source_mbids + [target_mbid]),
-        group_by=schema.track_mbid.c.track_id,
+    logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
+
+    affected_track_mbids_query = (
+        sql.select(
+            [
+                schema.track_mbid.c.id,
+                schema.track_mbid.c.track_id,
+                schema.track_mbid.c.mbid,
+                schema.track_mbid.c.submission_count,
+                schema.track_mbid.c.disabled,
+                schema.track_mbid.c.merged_info,
+            ]
+        )
+        .where(schema.track_mbid.c.mbid.in_([source_mbid, target_mbid]))
+        .with_for_update()
     )
-    rows = fingerprint_db.execute(query).fetchall()
-    to_delete = set()
-    to_update = []
-    for row in rows:
-        old_ids = set(row["all_ids"])
-        old_ids.remove(row["id"])
-        to_delete.update(old_ids)
-        to_update.append((old_ids, row))
-        if old_ids:
-            update_stmt = schema.track_mbid_source.update().where(
-                schema.track_mbid_source.c.track_mbid_id.in_(old_ids)
+    track_mbids_by_track_id: Dict[int, Dict[str, Any]] = {}
+    for row in fingerprint_db.execute(affected_track_mbids_query):
+        track_mbids_by_track_id.setdefault(row["track_id"], {})[row["mbid"]] = row
+
+    for track_id, track_mbids in track_mbids_by_track_id.items():
+        source = track_mbids.get(source_mbid)
+        if source is None:
+            # source mbid not found, skip this track
+            continue
+
+        target = track_mbids.get(target_mbid)
+        if target is None:
+            # we have no record with the target mbid, so we create a new one
+            target_id = fingerprint_db.execute(
+                schema.track_mbid.insert()
+                .returning(schema.track_mbid.c.id)
+                .values(
+                    track_id=track_id,
+                    mbid=target_mbid,
+                    submission_count=0,
+                    disabled=False,
+                )
+            ).scalar()
+        else:
+            # we already have a record with the target mbid, so we update it
+            target_id = target["id"]
+
+        # clear submission count and disable flag for source mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == source["id"])
+            .values(
+                merged_into=target_id,
+                submission_count=0,
+                disabled=True,
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-            update_stmt = schema.track_mbid_change.update().where(
-                schema.track_mbid_change.c.track_mbid_id.in_(old_ids)
+        )
path: acoustid/data/track.py
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Verify merged_into column exists in track_mbid table.**

Previous review comments mentioned that the `merged_into` column might be missing from the table schema. Please verify this column exists or has been added in a schema migration.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check the track_mbid table schema to confirm it has the merged_into column
rg "track_mbid = Table\(" --type py -A 15
```

Length of output: 923

---



</details>

**Action Required: Missing `merged_into` Column in Table Schema**

The search in `acoustid/tables.py` shows that the `merged_into` column is not defined in the `track_mbid` table schema. This absence may cause runtime failures when the code in `acoustid/data/track.py` tries to update this column. Please address this by either adding the `merged_into` column directly into the table definition or by creating a migration that adds the column to the schema.

- **File to review:** `acoustid/tables.py` (definition of `track_mbid` table)
- **Issue:** Missing `merged_into` column
- **Action:** Add the column or ensure an appropriate schema migration is applied

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-07T10:59:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,55 +97,95 @@ def lookup_tracks(conn, mbids):
 def merge_mbids(
     fingerprint_db: FingerprintDB,
     ingest_db: IngestDB,
+    source_mbid: str,
     target_mbid: str,
-    source_mbids: List[str],
 ) -> None:
-    """
-    Merge the specified MBIDs.
-    """
-    logger.info("Merging MBIDs %r into %r", source_mbids, target_mbid)
-    query = sql.select(
-        [
-            sql.func.min(schema.track_mbid.c.id).label("id"),
-            sql.func.array_agg(schema.track_mbid.c.id).label("all_ids"),
-            schema.track_mbid.c.track_id,
-            sql.func.every(schema.track_mbid.c.disabled).label("all_disabled"),
-            sql.func.sum(schema.track_mbid.c.submission_count).label("count"),
-        ],
-        schema.track_mbid.c.mbid.in_(source_mbids + [target_mbid]),
-        group_by=schema.track_mbid.c.track_id,
+    logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
+
+    affected_track_mbids_query = (
+        sql.select(
+            [
+                schema.track_mbid.c.id,
+                schema.track_mbid.c.track_id,
+                schema.track_mbid.c.mbid,
+                schema.track_mbid.c.submission_count,
+                schema.track_mbid.c.disabled,
+                schema.track_mbid.c.merged_info,
+            ]
+        )
+        .where(schema.track_mbid.c.mbid.in_([source_mbid, target_mbid]))
+        .with_for_update()
     )
-    rows = fingerprint_db.execute(query).fetchall()
-    to_delete = set()
-    to_update = []
-    for row in rows:
-        old_ids = set(row["all_ids"])
-        old_ids.remove(row["id"])
-        to_delete.update(old_ids)
-        to_update.append((old_ids, row))
-        if old_ids:
-            update_stmt = schema.track_mbid_source.update().where(
-                schema.track_mbid_source.c.track_mbid_id.in_(old_ids)
+    track_mbids_by_track_id: Dict[int, Dict[str, Any]] = {}
+    for row in fingerprint_db.execute(affected_track_mbids_query):
+        track_mbids_by_track_id.setdefault(row["track_id"], {})[row["mbid"]] = row
+
+    for track_id, track_mbids in track_mbids_by_track_id.items():
+        source = track_mbids.get(source_mbid)
+        if source is None:
+            # source mbid not found, skip this track
+            continue
+
+        target = track_mbids.get(target_mbid)
+        if target is None:
+            # we have no record with the target mbid, so we create a new one
+            target_id = fingerprint_db.execute(
+                schema.track_mbid.insert()
+                .returning(schema.track_mbid.c.id)
+                .values(
+                    track_id=track_id,
+                    mbid=target_mbid,
+                    submission_count=0,
+                    disabled=False,
+                )
+            ).scalar()
+        else:
+            # we already have a record with the target mbid, so we update it
+            target_id = target["id"]
+
+        # clear submission count and disable flag for source mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == source["id"])
+            .values(
+                merged_into=target_id,
+                submission_count=0,
+                disabled=True,
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-            update_stmt = schema.track_mbid_change.update().where(
-                schema.track_mbid_change.c.track_mbid_id.in_(old_ids)
+        )
+
+        # update submission count and disable flag for target mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == target_id)
+            .values(
+                merged_into=target_id,
+                submission_count=(
path: acoustid/data/track.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix self-referential merge relationship.**

Setting `merged_into=target_id` for the target record creates a self-reference, which seems incorrect. A record shouldn't be merged into itself.

```diff
-                merged_into=target_id,
+                merged_into=None,
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                merged_into=None,
                submission_count=(
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-07T10:59:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,55 +97,93 @@ def lookup_tracks(conn, mbids):
 def merge_mbids(
     fingerprint_db: FingerprintDB,
     ingest_db: IngestDB,
+    source_mbid: str,
     target_mbid: str,
-    source_mbids: List[str],
 ) -> None:
-    """
-    Merge the specified MBIDs.
-    """
-    logger.info("Merging MBIDs %r into %r", source_mbids, target_mbid)
-    query = sql.select(
-        [
-            sql.func.min(schema.track_mbid.c.id).label("id"),
-            sql.func.array_agg(schema.track_mbid.c.id).label("all_ids"),
-            schema.track_mbid.c.track_id,
-            sql.func.every(schema.track_mbid.c.disabled).label("all_disabled"),
-            sql.func.sum(schema.track_mbid.c.submission_count).label("count"),
-        ],
-        schema.track_mbid.c.mbid.in_(source_mbids + [target_mbid]),
-        group_by=schema.track_mbid.c.track_id,
+    logger.info("Merging MBID %r into %r", source_mbid, target_mbid)
+
+    affected_track_mbids_query = (
+        sql.select(
+            [
+                schema.track_mbid.c.id,
+                schema.track_mbid.c.track_id,
+                schema.track_mbid.c.submission_count,
+                schema.track_mbid.c.disabled,
+            ]
+        )
+        .where(schema.track_mbid.c.mbid.in_([source_mbid, target_mbid]))
+        .with_for_update()
     )
-    rows = fingerprint_db.execute(query).fetchall()
-    to_delete = set()
-    to_update = []
-    for row in rows:
-        old_ids = set(row["all_ids"])
-        old_ids.remove(row["id"])
-        to_delete.update(old_ids)
-        to_update.append((old_ids, row))
-        if old_ids:
-            update_stmt = schema.track_mbid_source.update().where(
-                schema.track_mbid_source.c.track_mbid_id.in_(old_ids)
+    track_mbids_by_track_id: Dict[int, Dict[str, Any]] = {}
+    for row in fingerprint_db.execute(affected_track_mbids_query):
+        track_mbids_by_track_id.setdefault(row["track_id"], {})[row["mbid"]] = row
+
+    for track_id, track_mbids in track_mbids_by_track_id.items():
+        source = track_mbids.get(source_mbid)
+        if source is None:
+            # source mbid not found, skip this track
+            continue
+
+        target = track_mbids.get(target_mbid)
+        if target is None:
+            # we have no record with the target mbid, so we create a new one
+            target_id = fingerprint_db.execute(
+                schema.track_mbid.insert()
+                .returning(schema.track_mbid.c.id)
+                .values(
+                    track_id=track_id,
+                    mbid=target_mbid,
+                    submission_count=0,
+                    disabled=False,
+                )
+            ).scalar()
+        else:
+            # we already have a record with the target mbid, so we update it
+            target_id = target["id"]
+
+        # clear submission count and disable flag for source mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == source["id"])
+            .values(
+                merged_into=target_id,
+                submission_count=0,
+                disabled=True,
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-            update_stmt = schema.track_mbid_change.update().where(
-                schema.track_mbid_change.c.track_mbid_id.in_(old_ids)
+        )
+
+        # update submission count and disable flag for target mbid
+        fingerprint_db.execute(
+            schema.track_mbid.update()
+            .where(schema.track_mbid.c.id == target_id)
+            .values(
+                merged_into=target_id,
+                submission_count=(
+                    schema.track_mbid.c.submission_count + source["submission_count"]
+                ),
+                disabled=sql.and_(
+                    schema.track_mbid.c.disabled,
+                    source["disabled"],
+                ),
+                updated=sql.func.current_timestamp(),
             )
-            ingest_db.execute(update_stmt.values(track_mbid_id=row["id"]))
-    if to_delete:
-        delete_stmt = schema.track_mbid.delete().where(
-            schema.track_mbid.c.id.in_(to_delete)
         )
path: acoustid/data/track.py
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@lalinsky Thanks for pointing out the location. I've verified that the `merged_into` column was indeed added to the `track_mbid` table schema in this PR. This addresses the concern raised in the previous comment about the column reference in the update logic.



---

🏁 Script executed:
```
#!/bin/bash
# Viewing the diff of the file mentioned in the URL to confirm the merged_into column addition
git show HEAD:acoustid/tables.py | grep -A 20 "track_mbid = Table"
```

Length of output: 823

---



</details>

@lalinsky I've confirmed that the `merged_into` column is correctly defined in the `track_mbid` table schema with the proper foreign key reference back to the same table. The implementation is consistent between the schema definition and the usage in the merge logic. Everything looks correct.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Great%20insights%20from%20%40coderabbitai!%20%F0%9F%9A%80%20Ensure%20your%20target%20MBID%20update%20logic%20aligns%20with%20the%20schema%20by%20addressing%20the%20missing%20%60merged_into%60%20column.%20Let's%20keep%20our%20data%20robust!%20%F0%9F%92%AA&url=https%3A%2F%2Fgithub.com%2Facoustid%2Facoustid-server%2Fpull%2F149%23discussion_r1984692727)?
							Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-03-07T11:00:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,23 +35,32 @@ def run_merge_missing_mbid(script: Script, mbid: str) -> None:
         redis = script.get_redis()
         cache_key = "unknown_mbid:{}".format(mbid)
 
-        fingerprint_db = stack.enter_context(script.db_engines["fingerprint"].begin())
+        fingerprint_db = stack.enter_context(script.db_engines["fingerprint"].connect())
+        fingerprint_db_txn = stack.enter_context(fingerprint_db.begin_twophase())
+
+        ingest_db = stack.enter_context(script.db_engines["ingest"].connect())
+        ingest_db_txn = stack.enter_context(ingest_db.begin_twophase())
+
+        musicbrainz_db = stack.enter_context(script.db_engines["musicbrainz"].connect())
+
         if not try_lock(fingerprint_db, "merge_missing_mbid", mbid):
             logger.info("MBID %s is already being merged", mbid)
             return
 
-        ingest_db = stack.enter_context(script.db_engines["ingest"].begin())
-        musicbrainz_db = stack.enter_context(script.db_engines["musicbrainz"].begin())
-
         handled = merge_missing_mbid(
             fingerprint_db=fingerprint_db,
             ingest_db=ingest_db,
             musicbrainz_db=musicbrainz_db,
             old_mbid=mbid,
         )
         if handled:
+            fingerprint_db_txn.prepare()
+            ingest_db_txn.prepare()
path: acoustid/scripts/merge_missing_mbids.py
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Include the final commit phase when preparing transactions.**

After calling `prepare()`, you typically need an explicit commit (e.g., `fingerprint_db_txn.commit_prepared()`) to finalize the transaction. Relying solely on the context manager may lead to a “prepared but not committed” scenario, especially in two-phase transactions.

```diff
-        fingerprint_db_txn.prepare()
-        ingest_db_txn.prepare()
+        fingerprint_db_txn.prepare()
+        ingest_db_txn.prepare()
+        fingerprint_db_txn.commit()
+        ingest_db_txn.commit()
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            fingerprint_db_txn.prepare()
            ingest_db_txn.prepare()
            fingerprint_db_txn.commit()
            ingest_db_txn.commit()
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-07T17:01:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-07T08:46:51Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The changes update the MBID merge functionality and related database schema. In the `acoustid/data/track.py` file, the `merge_mbids` function signature is altered to accept a single source MBID, and its internal logic is restructured to handle each track individually. The SQL query and subsequent updates now ensure consistent updates across multiple tables. In `acoustid/tables.py`, new columns are added to support self-referential relationships and update tracking. The tests in `tests/test_data_track.py` have been modified to pass a single UUID and adjust expected outputs accordingly.

## Changes

| File(s)                                           | Change Summary                                                                                                                                                                                                                                                        |
|---------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| acoustid/data/track.py                            | Updated `merge_mbids`: signature now accepts a single `source_mbid` instead of a list; restructured SQL query; modified logic to check for existence and update or create entries; updated call within `merge_missing_mbid`.                                         |
| acoustid/tables.py                                | Added new columns: In `track_mbid`, added `merged_into` for self-referencing; in `track_mbid_source`, `track_mbid_change`, `track_puid`, and `track_meta`, added an `updated` column with timezone support to track record modifications.                        |
| tests/…/test_data_track.py                        | Modified tests to pass a single UUID (changed from list to string) to `merge_mbids`; updated expected results to include new fields (submission count and disabled flag) consistent with the revised merge logic.                                                 |
| acoustid/scripts/merge_missing_mbids.py          | Changed database connection handling from `begin()` to `connect()` for two-phase transactions; updated transaction management to prepare or roll back based on merge success.                                                                                       |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Caller
    participant MergeLogic as Merge_mbids Function
    participant DB as Database Schema

    Caller->>+MergeLogic: merge_mbids(source_mbid, target_mbid)
    MergeLogic->>DB: Query tracks for source_mbid & target_mbid
    DB-->>MergeLogic: Return track data
    alt Source MBID exists
        MergeLogic->>DB: If target missing, create new track entry
        MergeLogic->>DB: Else, update existing track entry
    else
        MergeLogic-->>Caller: Skip processing for missing source
    end
    MergeLogic->>DB: Update track_mbid_source and track_mbid_change records
    MergeLogic-->>Caller: Return merge status
```

## Possibly related PRs

- **acoustid/acoustid-server#138**: Updates in the `merge_mbids` function signature and logic are coordinated with modifications to the `merge_missing_mbid` invocation.
- **acoustid/acoustid-server#140**: Changes to the `merge_mbids` parameters and internal processing are reflected in the updated test cases for proper parameter formatting and expected outcomes.

## Poem

> Oh, I hopped with glee through rows and code,  
> Merging MBIDs along a winding road.  
> With each SQL twist and table update shine,  
> Data joins in rhythm, oh so fine!  
> A bunny’s cheer with every change in sight,  
> Coding magic in the moonlit night.  
> 🐇✨ Happy rhythms in every byte!

<!-- walkthrough_end -->


---

<details>
<summary>📜 Recent review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 2bb82412631e638585c8519e4a2123b3bcb53efd and 5da75a2c45cf9d5ffdcff5320253b41016b10f85.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `acoustid/scripts/merge_missing_mbids.py` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* acoustid/scripts/merge_missing_mbids.py

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: test (3.12)

</details>

</details>
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=acoustid/acoustid-server&utm_content=149):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: lalinsky
created_at: 2025-03-07T11:58:08Z
body: @coderabbitai review
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-03-07T11:58:15Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: CONTRIBUTOR