Копирование фото переносит и отредактированную версию, хотя правка принадлежит фото, чьи правки её породили. Изменение описывает фильтр edit-ролей в Clone, разовую чистку копий миграцией и дельту требования «Совместные объекты».
120 lines
8.2 KiB
Markdown
120 lines
8.2 KiB
Markdown
# 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
|
||
|
||
Нет.
|