# Design ## Context См. `proposal.md` — Why. Текущее состояние, определяющее подход: - Каждый домен имеет конкретный `Store` с полем `*pgxpool.Pool` и SQL диалекта Postgres. Всего ~72 метода: `auth(9)`, `albums(7)`, `members(8)`, `photos(20)`, `photos/objects(15)`, `social(6)`, `notifications(3)`, `sharelinks(4)`. - Postgres-специфика: `FOR UPDATE SKIP LOCKED`, `ANY($1)`, `make_interval`/`power`/`least`, `now()`, `timestamptz`, `jsonb`/`::jsonb`, `ON CONFLICT`, `RETURNING`. - Кросс-доменные зависимости уже выражены интерфейсами (`AlbumAccess`, `PhotoLookup`, `Notifier`, `MemberLookup`, `AlbumReader`); свои store сервисы берут конкретными. - Сборка `CGO_ENABLED=0`; model: один экземпляр приложения на инсталляцию. - Ограничение: SQLite допускает один писатель, много читателей (WAL); один файл на один экземпляр приложения. ## Goals / Non-Goals **Goals:** - Ввести интерфейсы репозиториев по доменам и вторую реализацию на SQLite с паритетом. - Сохранить текущее поведение PostgreSQL по умолчанию и не менять сервисную логику. - Обеспечить атомарность объектов, GC и очереди вариантов на обоих бэкендах. - Гарантировать паритет общим контрактным набором тестов. **Non-Goals:** - Горизонтальное масштабирование и несколько экземпляров на один файл SQLite. - Миграция данных между PostgreSQL и SQLite. - Изменение наблюдаемого поведения API/спек (см. `proposal.md`, изменений требований нет). - Изменение слоя хранения (см. `local-fs-storage-backend`). ## Decisions ### D1. Параллельные реализации, а не слой диалекта Выбрано (по решению пользователя): Postgres-store не трогаем, пишем отдельные SQLite-store. Интерфейс `Repository` становится швом между доменом и хранилищем. - Альтернатива: единая логика + `Dialect` — отвергнута: правки во всех существующих store и риск регрессии Postgres ради экономии дублирования. - Цена: каждая будущая фича реализуется дважды; снижается контрактными тестами (D9). ### D2. Именование и размещение Интерфейс домена — `Repository`; существующий pgx-тип остаётся `Store` (`NewStore(pool)`), SQLite-тип — `SQLiteStore` и живёт в том же пакете (`photos/sqlite_store.go` и т.п.). - В том же пакете — доступ к `models` и sentinel-ошибкам без циклов импорта и без перемещения существующего кода. - Сервисы типизируются интерфейсом (`albums.Repository` и т.д.); кросс-доменные интерфейсы удовлетворяются любой реализацией. ### D3. Драйвер SQLite — `modernc.org/sqlite` Чистый Go, совместим с `CGO_ENABLED=0`. - Альтернатива `mattn/go-sqlite3` (CGO) — отвергнута, ломает модель сборки. ### D4. Роль `$1`-плейсхолдеров Плейсхолдеры остаются `$1`: и pgx, и `modernc` понимают именованные параметры. Поскольку реализации раздельные, каждая пишет свой SQL; отдельный слой диалекта не нужен. ### D5. Отдельный набор миграций для SQLite `migrations/sqlite/` рядом с Postgres-набором, драйвер `golang-migrate/database/sqlite`. Отображение типов: `timestamptz`→TEXT, `jsonb`→TEXT, `bigint`→INTEGER, `double precision`→REAL, `now()`→`strftime('%Y-%m-%dT%H:%M:%fZ','now')`, `REFERENCES ... ON DELETE CASCADE` и `CHECK` сохраняются. - Альтернатива: переносимый единый набор — отвергнута, тянет переписывание обоих. ### D6. Настройка соединения и конкурентность `journal_mode=WAL`, `busy_timeout=5000`, `foreign_keys=ON` через DSN-параметры `_pragma`. Писатель один; читатели — параллельно. - `ANY($1)` переписывается в `IN (?, ...)` с построением списка в Go; батчи малы. - `FOR UPDATE`/`SKIP LOCKED` заменяются транзакцией с немедленной блокировкой записи (`BEGIN IMMEDIATE`); порядок «проверить состояние — вставить/заявку» сохраняется. ### D7. Атомарность жизненного цикла объектов `BindObject` и claim-методы (`ClaimGarbage`, `ClaimStalePending`, `ClaimVariantJobs`) выполняются в `BEGIN IMMEDIATE`-транзакции, что сериализует запись и воспроизводит инварианты Postgres-версии (нельзя привязать объект, уходящий в GC; нельзя удалить используемый; одинаковая роль без дублей). ### D8. Backoff и `now()` `FailVariantJob` считает задержку в Go (`min(300, 10*2^attempts)`) вместо `make_interval/power/least`. Время пишется/читается через хелперы преобразования `time.Time` <-> RFC3339 TEXT; запросы сравнения используют `datetime('now')`/значения Go, а не неявное приведение драйвера. ### D9. Контрактные тесты на интерфейс Один набор тестов на каждый `Repository`, прогоняемый на обоих бэкендах: Postgres под `TEST_DATABASE_URL` (пропуск без него), SQLite на временном файле (всегда). Это механизм удержания паритета при параллельных реализациях. ### D10. Фабрика и сборка зависимостей `store.NewPostgres(pool)` и `store.NewSQLite(db)` возвращают бандл репозиториев; `app.New` принимает бандл и `storage.Backend`. Выбор бэкенда — на старте по `DB_BACKEND=postgres|sqlite` (дефолт `postgres`), путь — `SQLITE_PATH`. - Альтернатива: ветвление внутри `app.New` по типу пула — отвергнута, размазывает выбор. ### D11. Маппинг ошибок и `RETURNING` `schema`-уровневые sentinel-ошибки домена сохраняются; каждая реализация сама отображает `pgx.ErrNoRows`/`sql.ErrNoRows` в sentinel. `RETURNING` используется и в SQLite (>=3.35), поэтому вставки с возвратом `created_at` переносятся один-в-один. ## Risks / Trade-offs - [Дрейф параллельных реализаций] → общий контрактный набор тестов на интерфейс (D9). - [Пропускная способность записи SQLite под многими пользователями] → WAL, `busy_timeout`, один писатель; документируем как режим малой инсталляции. - [Потеря валидации `jsonb`] → хранение как TEXT, проверка JSON при необходимости в Go. - [Ошибки преобразования времени/JSON между бэкендами] → хелперы + контрактные тесты. - [Расхождение миграций] → симметричные наборы, прогон `up` на обоих в тестах. - [Лимит параметров SQLite в `IN (...)`] → малые батчи (`WORKER_BATCH`), построение списка в Go. ## Migration Plan 1. Ввести интерфейсы, не меняя Postgres-реализации и дефолт `postgres`. 2. Добавить SQLite-реализации, миграции и фабрику; включение — `DB_BACKEND=sqlite`. 3. Существующие развёртывания не меняются; откат — вернуть `DB_BACKEND=postgres`. 4. Перенос данных между бэкендами не автоматизируется и вне рамок изменения. ## Open Questions - Нужен ли SQLite-специфичный поиск меток (например, FTS) или достаточно текущих запросов с точным совпадением/префиксом.