@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Идемпотентная схема 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)
|
||||
Reference in New Issue
Block a user