#73: GET /admin/scrape/freshness — last_success/age/freshness/status для 5 источников (kn/objective/nspd/nspd_geo/cadastre) + overall_status (worst по critical). Prod-verified имена таблиц, FILTER/COALESCE корректны, CAST(:d AS interval). +4 теста. #83: README — GitHub→Forgejo CI/CD, секция 6 subagents, ссылки перемаплены. #72: docs/Schema_Migrations_vs_Alembic.md — _schema_migrations паттерн (не Alembic), рекомендация снести пустой alembic-скелет (отдельный PR). Closes #73 Closes #83 Closes #72
12 KiB
Миграции схемы БД: _schema_migrations, не Alembic
Decision-doc по issue #72 (B5-2). Дата: 2026-06-13. Статус: ACCEPTED.
Снапшот — источник правды по решениям живёт в Obsidian vault (
decisions/). Здесь — версионированная в репозитории копия рядом с кодом, на который она ссылается.
TL;DR
- Схема прод-БД развивается через идемпотентные
data/sql/NN_*.sql, которыеdeploy.yml(Forgejo Actions) прогоняет автоматически и ровно один раз каждый, отмечая применённые в таблице-трекере_schema_migrations. - Alembic в проде НЕ используется.
backend/alembic/versions/содержит только.gitkeep— за всё время не создано ни одной ревизии.alembic upgrade headна проде — no-op (нет ревизий), аalembic revision --autogenerateрассинхронизирован с реальной схемой (модели вapp/models/описывают лишь крошечную часть из ~50 таблиц). - Рекомендация (принята): удалить пустой Alembic-скелет (
backend/alembic/,backend/alembic.ini, depalembicвpyproject.toml) — он не несёт функции и вводит в заблуждение («есть Alembic → значит миграции через него»). Если в будущем понадобится ORM-driven migration tool — переустановить осознанно. - Формулировка из тела issue про «синхронизацию состояния Alembic с прод»
(
alembic current/alembic stamp head/ catch-up migration) неактуальна: синхронизировать нечего — Alembic никогда не был источником схемы.
Как реально работают миграции
Артефакт: data/sql/NN_*.sql
~118 файлов (на 2026-06-13), нумерованных NN_описание.sql (01_…, 144_…).
Каждый — самодостаточный idempotent DDL/ETL-фрагмент:
-- 144_audit_log.sql
-- Deploy: auto-applied deploy.yml через _schema_migrations (ровно один раз NN=144).
-- Idempotent: CREATE TABLE/INDEX IF NOT EXISTS + guarded DO-блок для CHECK.
BEGIN;
CREATE TABLE IF NOT EXISTS audit_log ( ... );
COMMENT ON TABLE audit_log IS '...';
COMMIT;
Правила для нового файла:
- Имя — следующий свободный номер + snake_case описание:
NNN_<что>.sql. Порядок применения = лексикографическая сортировкаls -1 | sort, поэтому зануляй ширину при необходимости (089_…vs89_…сортируются по-разному — придерживайся существующей ширины в каталоге). - Идемпотентность —
CREATE … IF NOT EXISTS,ADD COLUMN IF NOT EXISTS,CREATE OR REPLACE VIEW,ON CONFLICT DO NOTHING, guardedDO $$ … $$-блоки для того, что не поддерживаетIF NOT EXISTS(CHECK-констрейнты, ENUM-значения). Файл может выполниться повторно вручную без вреда (хотя deploy и так пропустит уже применённый). - psycopg v3 / SQLAlchemy — в коде приложения только
CAST(:x AS type), никогда:x::type(см..claude/rules/backend.md). В самих.sql-файлах, которые гоняетpsql,::type-каст допустим — это чистый psql, не через SQLAlchemy bind-params. - Транзакционность — оборачивай связанные изменения в
BEGIN; … COMMIT;.psql -v ON_ERROR_STOP=on: при ошибке деплой падает (exit 1) и контейнеры НЕ обновляются (схема и образы остаются консистентны).
Трекер: таблица _schema_migrations
CREATE TABLE IF NOT EXISTS _schema_migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
filename — basename файла (144_audit_log.sql). Применённость = наличие строки.
Раннер: .forgejo/workflows/deploy.yml
После git reset --hard origin/main и compose pull, ДО compose up -d:
# 1) гарантировать трекер
psql … -c "CREATE TABLE IF NOT EXISTS _schema_migrations ( filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW() );"
# 2) применить каждый ещё-не-применённый файл в порядке сортировки
for sql_file in $(ls -1 data/sql/*.sql 2>/dev/null | sort); do
fname=$(basename "$sql_file")
applied=$(psql … -tAc "SELECT COUNT(*) FROM _schema_migrations WHERE filename='$fname'")
if [ "$applied" = "0" ]; then
echo "→ Applying migration: $fname"
psql … -v ON_ERROR_STOP=on < "$sql_file" \
|| { echo "FAILED on migration: $fname"; exit 1; }
psql … -c "INSERT INTO _schema_migrations (filename) VALUES ('$fname') ON CONFLICT DO NOTHING;"
else
echo "✓ Already applied: $fname"
fi
done
Ключевые свойства:
- Только
*.sqlтрекаются/применяются.data/sql/*.pyи*.sh(ad-hoc загрузчики, напр.02_load_all_quarters.sh,40_scrape_history.py) раннер игнорирует — они запускаются вручную. - Идемпотентность на двух уровнях: трекер пропускает уже применённое; сам SQL безопасен к повторному прогону (на случай ручного запуска или восстановления).
- Fail-fast: первый упавший файл останавливает деплой, образы не катятся.
- Триггер деплоя: путь
data/sql/**вon.push.pathsdeploy.yml(правка толькоdocs/**деплой НЕ триггерит — см..claude/rules/deploy.md).
Bootstrap (исторический, разовый)
scripts/bootstrap_schema_migrations.sh сидировал _schema_migrations всеми
файлами, уже применёнными на проде ДО введения auto-apply (PR #150) — чтобы первый
auto-deploy не пытался переиграть всю историю. Запускался один раз вручную через
SSH-туннель. Для новых файлов больше не нужен — deploy.yml сам ведёт трекер.
Почему raw SQL, а не Alembic
data/sql/NN_*.sql (используется) |
Alembic (скелет, не используется) | |
|---|---|---|
| Что описывает | Реальную прод-схему (~50 таблиц, partitions, GIST, materialized views, PostGIS, FDW, роли, гранты) | Только Base.metadata из app/models/ — крошечная часть (1 модуль parcel) |
| PostGIS / big views / partitions | Нативно — пишем DDL как есть | autogenerate их не видит/ломает (geoalchemy типы, CREATE MATERIALIZED VIEW, partitioning) |
| Идемпотентность | Встроена (IF NOT EXISTS, guarded DO) |
Линейная цепочка ревизий, down/up |
| Источник истины | Файлы в репе + _schema_migrations |
alembic_version (в проде таблицы нет) |
| Применение | deploy.yml авто, fail-fast |
Не подключён к деплою |
Проект на старте — data-heavy: партиционированный rosreestr_deals (~7M строк),
PostGIS-геометрии, materialized views, cross-DB FDW в tradein, роли/гранты. Это
deployment artifacts, которые естественно описывать прямым DDL. ORM-модели
покрывают лишь endpoints приложения, а не аналитический слой. Alembic autogenerate
на такой схеме генерирует мусор (drop неизвестных ему объектов) — поэтому им и не
пользовались.
Граница: что куда
data/sql/NN_*.sql— ВСЁ изменение схемы прод-БД сейчас идёт сюда:- новые таблицы / колонки / индексы / констрейнты;
- materialized views и обычные views (
v_*,mv_*); - partitions
rosreestr_deals; - роли, гранты, FDW-bootstrap;
- data-backfill, который должен прогоняться ровно один раз.
- Владелец правок:
database-expertsubagent (см..claude/rules/sql.md).
- Alembic — НЕ используется. (Гипотетически: если бы в будущем понадобилась чистая ORM-driven эволюция небольшого набора app-таблиц с автогенерацией — это был бы кандидат. Сейчас такой потребности нет, и смешение двух раннеров на одной схеме опаснее единого пути.)
Рекомендация по пустому Alembic-скелету
Удалить, чтобы убрать ложный сигнал «миграции через Alembic»:
backend/alembic/(env.py, script.py.mako, versions/.gitkeep)backend/alembic.ini- dep
alembic>=1.13.0изbackend/pyproject.toml+ пересборка лок-файла (cd backend && uv lock) - таргеты
make migrate(alembic upgrade head) иmake migration NAME=…(alembic revision --autogenerate) в корневомMakefile— оба зовут Alembic и сейчас вводят в заблуждение (на проде upgrade — no-op, autogenerate рассинхронизирован со схемой). Заменить наmake migrate-statusповерх_schema_migrationsлибо удалить.
Это правка
pyproject.toml+ удаление файлов подbackend/— выполняется отдельным PR (chore(backend): drop unused Alembic skeleton), вне scope чисто документационного #72, чтобы не смешивать docs-only изменение с правкой зависимостей/Dockerfile-кэша. Здесь — зафиксированное решение; реализацию сноса делает backend-engineer отдельным PR после approve этого decision-doc.
Альтернатива (если решат оставить Alembic «на будущее»): держать скелет, но
добавить в backend/alembic/README явную пометку «NOT WIRED — схема через
data/sql + _schema_migrations, см. docs/Schema_Migrations_vs_Alembic.md»,
чтобы новый разработчик не пытался alembic upgrade head.
Ссылки
- Раннер:
.forgejo/workflows/deploy.yml(шаг "Apply pending SQL migrations") - Bootstrap:
scripts/bootstrap_schema_migrations.sh - Конвенции SQL/деплоя:
.claude/rules/sql.md,.claude/rules/deploy.md,.claude/rules/backend.md - Скелет (к удалению):
backend/alembic/,backend/alembic.ini