# Design ## Context См. `proposal.md` — Why. Ограничения, важные для подхода: - `Store.Clone` (Postgres) и `SQLiteStore.Clone` (SQLite) копируют привязки `photo_objects` одной вставкой `INSERT ... SELECT` без фильтра по ролям, поэтому копия наследует и обычные варианты, и edit-варианты. - Обычные варианты действительно должны разделяться: воркер создаёт их через `createObject(ctx, siblings, ...)` , где `siblings` — все фото, делящие оригинал (`PhotosByOriginalObject`). Edit-варианты, наоборот, создаются с владельцем `[]string{photoID}` и привязаны только к одному фото. - Параметры правок лежат в колонке `photos.edit_params` и при клонировании не переносятся: `INSERT INTO photos (...)` в `Clone` её не включает. То есть рассинхрон возникает именно на уровне привязок объектов. - `ResetEdit` удаляет и параметры, и привязки edit-объектов (`DeleteObjectBindings`). Значит, у фото с пустыми `edit_params` edit-привязок быть не должно — кроме как последствия копирования. - Выдача URL смотрит только на привязки (`attachURLs`), а фронтенд выбирает `edit_thumb_url ?? thumb_url`. Привязка — источник правды о том, какие варианты есть у фото; чинить выдачу URL вместо привязки нельзя. - `ClaimGarbage` собирает готовые объекты без привязок, поэтому удаление привязки само по себе освобождает объект — отдельная уборка не нужна. ## Goals / Non-Goals **Goals:** - Копия фото не наследует отредактированную версию: показывается исходник, параметры правок пусты. - Postgres и SQLite ведут себя одинаково, список исключаемых ролей задан один раз. - Уже существующие копии в базе перестают показывать отредактированную версию без ручного вмешательства. **Non-Goals:** - Не переносим параметры правок на копию: правка принадлежит фото, а не копии. - Не рендерим отредактированную версию заново для копии. - Не меняем поведение переноса (`MoveBatch`) и разделяемых обычных вариантов. - Не трогаем клиентов и HTTP API: формат ответа не меняется. ## Decisions ### 1. Фильтровать роли в `Clone`, а не в выдаче URL Копирование исключает edit-роли из переносимых привязок: ``` INSERT INTO photo_objects (photo_id, object_id, role) SELECT $1, object_id, role FROM photo_objects WHERE photo_id = $2 AND role <> ALL($3) ``` - **Альтернатива: прятать edit-URL, если у фото нет `edit_params`.** Отклонено: привязка осталась бы в базе, GC считал бы объект живым, а модель и привязка расходились бы. Привязка — источник правды. - **Альтернатива: копировать и параметры, и готовую версию.** Отклонено: меняет семантику «правка принадлежит фото» и требует отдельного рендера для копии. ### 2. Список edit-ролей — в одном месте Роли уже объявлены в `backend/internal/photos/edit_render.go` (`RoleEdit`, `RoleEditThumb`, `RoleEditMedium`, `RoleEditPreview`), оба хранилища находятся в том же пакете. Добавляется общий список ролей и хелпер проверки, чтобы Postgres и SQLite не расходились и в SQL не попадали строковые литералы. - **Альтернатива: писать роли литералами в каждом `Clone`.** Отклонено: список размножится, добавление новой edit-роли забудет одно из хранилищ. ### 3. Разовая чистка существующих копий — миграцией В базе могут остаться привязки `edit*` у фото с пустыми `edit_params` — это и есть следы бага копирования. Чистка выполняется миграцией в обоих диалектах (Postgres и SQLite нумеруются независимо): ``` DELETE FROM photo_objects WHERE role IN ('edit', 'edit_thumb', 'edit_medium', 'edit_preview') AND photo_id IN (SELECT id FROM photos WHERE edit_params IS NULL OR edit_params = '{}') ``` - **Альтернатива: чистить лениво при чтении.** Отклонено: правило прячется в двух местах и не отражается в данных. - **Альтернатива: ничего не чистить.** Отклонено: старые копии продолжат показывать отредактированную версию, и расхождение останется в данных. - Удалённые привязки не осиротевают объекты: отредактированная версия остаётся привязана к своему фото, поэтому её объекты живут, пока живёт фото, и обычный `ClaimGarbage` уберёт их вместе с ним. ### 4. Проверка — в существующем тесте копирования `backend/internal/photos/copy_test.go` уже проверяет, что копия делит объекты с оригиналом. Добавляется сценарий: после рендера правок копия получает обычные варианты, но не edit-объекты, и её `edit_params` пусты. ## Risks / Trade-offs - [Копии, созданные до фикса, продолжат показывать правки до применения миграции] → Миграция чистит привязки у фото с пустыми `edit_params`; после неё поведение единообразно. - [Пользователь может потерять «неожиданно» отредактированную копию] → Копия без параметров правок всё равно не позволяла их править осмысленно: первая новая правка перезаписывала обработку с нуля. - [Опечатка в списке ролей оставит edit-привязку в копии] → Список ролей берётся из общих констант, а тест проверяет конкретные роли. - [Миграция удалит привязки у фото, которые потеряли `edit_params` не из-за копирования] → Таких фото нет: `ResetEdit` удаляет и параметры, и привязки одновременно. ## Migration Plan 1. Миграция Postgres (`000016_*`) и SQLite (`000008_*`) удаляют «чужие» edit-привязки. Обе идемпотентны (`DELETE` без `IF EXISTS` по данным). 2. Изменения в `Clone` — только код, схема не меняется. 3. Откат: миграции `down` возвращают удалённые привязки не могут — откат выполняется только откатом кода; правки фото при этом сохраняются у исходных фото. ## Open Questions Нет.