diff --git a/backend/app/tasks/scan.py b/backend/app/tasks/scan.py index b7a8e91..78f67b4 100644 --- a/backend/app/tasks/scan.py +++ b/backend/app/tasks/scan.py @@ -618,43 +618,44 @@ async def handle_directory_rename(old_dirpath: str, new_dirpath: str) -> dict: ) return {"status": "cross_root", "discarded": n} - # Same-root: prefix-rewrite. Use parameterised raw SQL so the - # SUBSTRING + concat happens server-side in one shot; iterating - # in Python would mean N row updates. - params = { - "new_prefix": new_prefix, - "old_prefix": old_prefix, - "off": len(old_prefix) + 1, - "old_pat": old_prefix + "/%", - } - photos_res = await session.execute( - text( - "UPDATE photos SET filepath = :new_prefix || SUBSTRING(filepath FROM :off) " - "WHERE filepath LIKE :old_pat" - ), - params, - ) - folders_res = await session.execute( - text( - "UPDATE folders SET path = CASE " - "WHEN path = :old_prefix THEN :new_prefix " - "ELSE :new_prefix || SUBSTRING(path FROM :off) END " - "WHERE path = :old_prefix OR path LIKE :old_pat" - ), - params, - ) - source_roots_res = await session.execute( - text( - "UPDATE source_roots SET path = :new_prefix WHERE path = :old_prefix" - ), - params, - ) + # Same-root: iterate the matching rows in Python and rewrite + # the prefix attribute-side. We tried a single UPDATE … SET … + # SUBSTRING(... FROM LENGTH(:old)+1) raw-SQL approach but + # asyncpg miscategorises the LENGTH() result and rejects it + # as "$2: int (expected str)". The PATCH /folders/{id} + # endpoint already loops in Python for the same reason — match + # its pattern. Folder renames are rare and typically span ≤1k + # photos, so per-row UPDATEs are fine. + old_pat = old_prefix + "/%" + photos = (await session.execute( + select(Photo).where(Photo.filepath.like(old_pat)) + )).scalars().all() + for p in photos: + p.filepath = new_prefix + p.filepath[len(old_prefix):] + + folders = (await session.execute( + select(Folder).where( + or_( + Folder.path == old_prefix, + Folder.path.like(old_pat), + ) + ) + )).scalars().all() + for f in folders: + f.path = new_prefix if f.path == old_prefix else \ + new_prefix + f.path[len(old_prefix):] + + source_roots = (await session.execute( + select(SourceRoot).where(SourceRoot.path == old_prefix) + )).scalars().all() + for sr in source_roots: + sr.path = new_prefix await session.commit() return { "status": "renamed", - "photos": photos_res.rowcount or 0, - "folders": folders_res.rowcount or 0, - "source_roots": source_roots_res.rowcount or 0, + "photos": len(photos), + "folders": len(folders), + "source_roots": len(source_roots), }