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

189 lines
5.5 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.
"""
Добавление в SQLite недостающих колонок по ORM-моделям (легаси → рефактор).
Вызывается из единой миграции wesp_2_0 на каждом bind отдельно (recipes / reports).
Не создаёт таблицы и не меняет типы существующих колонок.
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect as sa_inspect
from sqlalchemy.sql import sqltypes
def _bool_server_default(col: sa.Column) -> sa.TextClause:
if col.default is None:
return sa.text("0")
arg = getattr(col.default, "arg", False)
if callable(arg):
return sa.text("0")
return sa.text("1" if arg else "0")
def _int_server_default(col: sa.Column) -> str:
if col.default is None:
return "0"
arg = getattr(col.default, "arg", None)
if arg is None or callable(arg):
return "0"
try:
return str(int(arg))
except (TypeError, ValueError):
return "0"
def _float_server_default(col: sa.Column) -> str:
if col.default is None:
return "0"
arg = getattr(col.default, "arg", None)
if arg is None or callable(arg):
return "0"
try:
return str(float(arg))
except (TypeError, ValueError):
return "0"
def _is_bool_type(t: sa.types.TypeEngine) -> bool:
return isinstance(t, sqltypes.Boolean)
def _is_int_type(t: sa.types.TypeEngine) -> bool:
return isinstance(t, sqltypes.Integer)
def _is_float_type(t: sa.types.TypeEngine) -> bool:
return isinstance(t, (sqltypes.Float, sqltypes.REAL))
def _is_stringy_type(t: sa.types.TypeEngine) -> bool:
return isinstance(t, (sqltypes.String, sqltypes.Text, sqltypes.Unicode, sqltypes.UnicodeText))
def _is_datetime_type(t: sa.types.TypeEngine) -> bool:
return isinstance(t, sqltypes.DateTime)
def _add_column_sqlite_safe(op, table: str, col: sa.Column) -> None:
typ = col.type
name = col.name
# В SQLAlchemy по умолчанию nullable=None/True; только явный False — NOT NULL в DDL.
if col.nullable is not False:
op.add_column(table, sa.Column(name, typ, nullable=True))
return
if col.server_default is not None:
op.add_column(
table,
sa.Column(name, typ, nullable=False, server_default=col.server_default),
)
return
if _is_bool_type(typ):
op.add_column(
table,
sa.Column(name, typ, nullable=False, server_default=_bool_server_default(col)),
)
return
if _is_stringy_type(typ):
op.add_column(
table,
sa.Column(name, typ, nullable=False, server_default=""),
)
return
if _is_int_type(typ):
op.add_column(
table,
sa.Column(
name,
typ,
nullable=False,
server_default=_int_server_default(col),
),
)
return
if _is_float_type(typ):
op.add_column(
table,
sa.Column(
name,
typ,
nullable=False,
server_default=_float_server_default(col),
),
)
return
if _is_datetime_type(typ):
op.add_column(table, sa.Column(name, typ, nullable=True))
bind = op.get_bind()
insp = sa_inspect(bind)
cols_now = {c["name"] for c in insp.get_columns(table)}
parts: list[str] = []
for key in ("created_at", "updated_at"):
if key in cols_now and key != name:
parts.append(f'"{key}"')
if parts:
expr = "COALESCE(" + ", ".join(parts) + ", CURRENT_TIMESTAMP)"
else:
expr = "CURRENT_TIMESTAMP"
op.execute(
sa.text(f'UPDATE "{table}" SET "{name}" = {expr} WHERE "{name}" IS NULL')
)
with op.batch_alter_table(table) as batch:
batch.alter_column(name, existing_type=typ, nullable=False)
return
op.add_column(table, sa.Column(name, typ, nullable=True))
def apply_legacy_orm_gaps(op, kw: dict) -> None:
tag = kw.get("tag")
reports_only = tag == "reports"
from app import db # noqa: WPS433 — внутри миграции, после загрузки приложения в env
import app.models # noqa: F401, WPS433
bind = op.get_bind()
insp = sa_inspect(bind)
for mapper in db.Model.registry.mappers:
cls = mapper.class_
bk = getattr(cls, "__bind_key__", None)
if reports_only:
if bk != "reports":
continue
else:
if bk == "reports":
continue
# db.Model (Flask-SQLAlchemy) задаёт __abstract__ = True; наследники остаются конкретными.
if cls.__dict__.get("__abstract__", False):
continue
tab = getattr(cls, "__tablename__", None)
if not tab:
continue
if not insp.has_table(tab):
continue
existing = {c["name"] for c in insp.get_columns(tab)}
for col in cls.__table__.columns:
if col.primary_key:
continue
if col.key in existing:
continue
_add_column_sqlite_safe(op, tab, col)
existing.add(col.key)
from app.services.migration_pace import migration_pause_step
migration_pause_step(f"column:{tab}.{col.key}")