@@ -0,0 +1,188 @@
|
||||
"""
|
||||
Добавление в 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}")
|
||||
Reference in New Issue
Block a user