""" Добавление в 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}")