Files
ds-galo/openspec/changes/fix-copy-edit-variant-sharing/design.md
T
iu 70ac12db10 chore(openspec): отметить задачи fix-copy-edit-variant-sharing выполненными
Все 9 задач закрыты; в design.md уточнено, что удалённые привязки
не осиротевают объекты: отредактированная версия живёт вместе с фото.
2026-10-21 21:01:33 +03:00

121 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
Нет.