From ba6d0ef9db31e528b1c6a520b7f9d2c1bc4c856b Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Thu, 25 Jun 2026 19:17:26 -0400 Subject: [PATCH] cleanup --- .../versions/0091_unique_platform_fs_name.py | 18 ++--------------- backend/endpoints/roms/__init__.py | 20 +++++++++---------- backend/endpoints/sockets/scan.py | 4 +--- backend/models/rom.py | 4 +--- 4 files changed, 13 insertions(+), 33 deletions(-) diff --git a/backend/alembic/versions/0091_unique_platform_fs_name.py b/backend/alembic/versions/0091_unique_platform_fs_name.py index 75d9372d1..e2f320471 100644 --- a/backend/alembic/versions/0091_unique_platform_fs_name.py +++ b/backend/alembic/versions/0091_unique_platform_fs_name.py @@ -1,11 +1,5 @@ """Enforce unique (platform_id, fs_name) on roms -A platform folder can't physically hold two entries with the same name, so a -ROM is uniquely identified by (platform_id, fs_name). The index covering this -pair was non-unique, which let two concurrent scans (e.g. the manual scan the -patcher fires after uploading a patched ROM, racing a filesystem-watcher or -scheduled scan) both insert the same ROM, producing duplicate library entries. - This migration removes any pre-existing duplicates (keeping the lowest id) and upgrades the index to unique so the duplicate can never be created again. @@ -32,13 +26,7 @@ def upgrade() -> None: connection = op.get_bind() # Drop duplicate roms sharing (platform_id, fs_name), keeping the lowest id. - # The nested derived table is required so MySQL/MariaDB don't reject reading - # the same table being deleted from; it's a valid no-op on PostgreSQL too. - # Dependent rows (files, user props, assets, collections, ...) are removed by - # their ON DELETE CASCADE foreign keys. - connection.execute( - sa.text( - """ + connection.execute(sa.text(""" DELETE FROM roms WHERE id NOT IN ( SELECT keep_id FROM ( @@ -47,9 +35,7 @@ def upgrade() -> None: GROUP BY platform_id, fs_name ) AS keepers ) - """ - ) - ) + """)) with op.batch_alter_table("roms", schema=None) as batch_op: batch_op.drop_index(INDEX_NAME, if_exists=True) diff --git a/backend/endpoints/roms/__init__.py b/backend/endpoints/roms/__init__.py index 9c73e9e7c..677efcd2d 100644 --- a/backend/endpoints/roms/__init__.py +++ b/backend/endpoints/roms/__init__.py @@ -28,6 +28,7 @@ from fastapi.responses import Response from fastapi_pagination import resolve_params from fastapi_pagination.limit_offset import LimitOffsetPage, LimitOffsetParams from pydantic import BaseModel, Field +from sqlalchemy.exc import IntegrityError from starlette.responses import FileResponse from config import ( @@ -1484,16 +1485,6 @@ async def update_rom( # Re-parse tags from the filename so region/language/revision/version/tags # stay in sync whenever the fs_name changes. if new_fs_name != rom.fs_name: - # (platform_id, fs_name) is unique, so reject a rename that would collide - # with another ROM on this platform before the DB update would fail. - if db_rom_handler.get_roms_by_fs_name( - platform_id=rom.platform_id, fs_names={new_fs_name} - ): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=f"A ROM named '{new_fs_name}' already exists on this platform", - ) - parsed_tags = fs_rom_handler.parse_tags(new_fs_name) cleaned_data.update( { @@ -1608,7 +1599,14 @@ async def update_rom( f"Updating {hl(cleaned_data.get('name', ''), color=BLUE)} [{hl(cleaned_data.get('fs_name', ''))}] with data {cleaned_data}" ) - db_rom_handler.update_rom(id, cleaned_data) + try: + db_rom_handler.update_rom(id, cleaned_data) + except IntegrityError as exc: + log.error(f"Failed to update ROM {id}: {exc}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update ROM {id}: {exc}", + ) from exc # Rename the file/folder if the name has changed should_update_fs = new_fs_name != rom.fs_name diff --git a/backend/endpoints/sockets/scan.py b/backend/endpoints/sockets/scan.py index 312b7b8da..9ccde5d05 100644 --- a/backend/endpoints/sockets/scan.py +++ b/backend/endpoints/sockets/scan.py @@ -271,9 +271,7 @@ async def _identify_rom( ) ) except IntegrityError: - # A concurrent scan already created this ROM ((platform_id, fs_name) - # is unique). That scan owns the full processing of this file, so - # skip it here instead of inserting a duplicate library entry. + # A concurrent scan already created this ROM, so skip it here. log.debug( f"Skipping {hl(fs_rom['fs_name'])}: already created by a concurrent scan" ) diff --git a/backend/models/rom.py b/backend/models/rom.py index 13610d26e..99bc81c2d 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -208,9 +208,7 @@ class Rom(BaseModel): libretro_id: Mapped[str | None] = mapped_column(String(length=64), default=None) __table_args__ = ( - # A platform folder can't hold two entries with the same name on disk, - # so (platform_id, fs_name) is unique. Enforcing it also stops racing - # scans from inserting the same ROM twice. + # Enforce unique fs name per platform to avoid duplicates Index("idx_roms_platform_id_fs_name", "platform_id", "fs_name", unique=True), # Covers the sibling_roms view self-join Index(