Files
site/WESP_REL/app/schema_bootstrap.py
T
2026-07-17 12:57:18 +03:00

103 lines
3.3 KiB
Python
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.
"""
Идемпотентная схема WESP 2.0: недостающие таблицы (ORM) + недостающие колонки.
Используется:
- ревизией Alembic wesp_2_0;
- после каждого upgrade head в create_app — чтобы починить БД после stamp / частичного прогона.
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
from alembic.operations import Operations
def _load_legacy_align():
root = Path(__file__).resolve().parent.parent / "migrations" / "legacy_orm_schema_align.py"
spec = importlib.util.spec_from_file_location("legacy_orm_schema_align", root)
if spec is None or spec.loader is None:
raise RuntimeError(f"Не найден {root}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def _table_names_for_bind(*, reports_only: bool) -> set[str]:
from app import db # noqa: WPS433
import app.models # noqa: F401, WPS433
names: set[str] = set()
for mapper in db.Model.registry.mappers:
cls = mapper.class_
if cls.__dict__.get("__abstract__", False):
continue
bk = getattr(cls, "__bind_key__", None)
if reports_only:
if bk != "reports":
continue
else:
if bk == "reports":
continue
tab = getattr(cls, "__tablename__", None)
if tab:
names.add(tab)
return names
def run_wesp20_idempotent_sync(op: Operations, kw: dict) -> None:
from app import db # noqa: WPS433
import app.models # noqa: F401, WPS433
tag = kw.get("tag")
reports_only = tag == "reports"
bind = op.get_bind()
allowed = _table_names_for_bind(reports_only=reports_only)
# У Flask-SQLAlchemy отдельный MetaData на каждый bind; db.metadata — только основной.
meta = db.metadatas["reports"] if reports_only else db.metadatas[None]
from app.services.migration_pace import migration_pause, migration_pause_step
for table in meta.sorted_tables:
if table.name not in allowed:
continue
table.create(bind, checkfirst=True)
migration_pause_step(f"table:{table.name}")
_load_legacy_align().apply_legacy_orm_gaps(op, kw)
migration_pause("legacy_align")
if not reports_only:
repair_legacy_sync_client_snapshot_cursor(op)
migration_pause("sync_clients_repair")
def repair_legacy_sync_client_snapshot_cursor(op: Operations) -> int:
"""Старые узлы: personal_snapshot_cursor=0 при завершённом enqueue — новая логика ждёт cursor>=total."""
from sqlalchemy import inspect, text
from app.services.sync_manager import SNAPSHOT_MODELS
bind = op.get_bind()
if bind.dialect.name != "sqlite":
return 0
insp = inspect(bind)
if not insp.has_table("sync_clients"):
return 0
total = len(SNAPSHOT_MODELS)
result = bind.execute(
text(
"""
UPDATE sync_clients
SET personal_snapshot_cursor = :total
WHERE personal_snapshot_completed_at IS NOT NULL
AND COALESCE(personal_snapshot_cursor, 0) < :total
"""
),
{"total": total},
)
return int(getattr(result, "rowcount", 0) or 0)