diff --git a/netx_api/models/port_traffic.py b/netx_api/models/port_traffic.py index 4920371..9161a5b 100644 --- a/netx_api/models/port_traffic.py +++ b/netx_api/models/port_traffic.py @@ -3,13 +3,20 @@ from __future__ import annotations from datetime import datetime from uuid import uuid4 -from sqlalchemy import BigInteger, Boolean, DateTime, Float, ForeignKey, Integer, LargeBinary, String, Text, UniqueConstraint +from sqlalchemy import BigInteger, Boolean, DateTime, Float, ForeignKey, Integer, LargeBinary, String, Text, UniqueConstraint, event from sqlalchemy.orm import Mapped, mapped_column, relationship from ..db import Base from ..timeutil import utcnow_naive from ._types import JsonType as _JsonType + +def _mirror_device_task_id(target: object) -> None: + """Keep legacy task_id column aligned with device_id (brownfield NOT NULL).""" + did = str(getattr(target, "device_id", None) or getattr(target, "task_id", None) or "") + target.device_id = did # type: ignore[attr-defined] + target.task_id = did # type: ignore[attr-defined] + class PortTrafficDevice(Base): """Per-NE port traffic monitoring config (device-centric).""" @@ -49,18 +56,12 @@ class PortTrafficSeries(Base): id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex) device_id: Mapped[str] = mapped_column(String(64), default="", index=True) + # Legacy column retained for brownfield DBs that still enforce NOT NULL task_id. + task_id: Mapped[str] = mapped_column(String(64), default="") title: Mapped[str] = mapped_column(String(256), default="") status: Mapped[str] = mapped_column(String(32), default="active", index=True) # active|disabled created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive) - @property - def task_id(self) -> str: - return str(self.device_id or "") - - @task_id.setter - def task_id(self, value: str) -> None: - self.device_id = str(value or "") - class PortTrafficTarget(Base): """Monitored interface under a device monitoring config.""" @@ -69,6 +70,8 @@ class PortTrafficTarget(Base): id: Mapped[str] = mapped_column(String(64), primary_key=True, default=lambda: uuid4().hex) device_id: Mapped[str] = mapped_column(String(64), default="", index=True) + # Legacy column retained for brownfield DBs that still enforce NOT NULL task_id. + task_id: Mapped[str] = mapped_column(String(64), default="") series_id: Mapped[str] = mapped_column(String(64), default="", index=True) source: Mapped[str] = mapped_column(String(32), default="managed", index=True) target_id: Mapped[str] = mapped_column(String(128), index=True) # NE id @@ -83,13 +86,17 @@ class PortTrafficTarget(Base): last_sample_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=utcnow_naive) - @property - def task_id(self) -> str: - return str(self.device_id or "") - @task_id.setter - def task_id(self, value: str) -> None: - self.device_id = str(value or "") +@event.listens_for(PortTrafficSeries, "before_insert") +@event.listens_for(PortTrafficSeries, "before_update") +def _sync_series_task_id(mapper, connection, target) -> None: # noqa: ANN001 + _mirror_device_task_id(target) + + +@event.listens_for(PortTrafficTarget, "before_insert") +@event.listens_for(PortTrafficTarget, "before_update") +def _sync_target_task_id(mapper, connection, target) -> None: # noqa: ANN001 + _mirror_device_task_id(target) class PortTrafficSample(Base): diff --git a/netx_api/port_traffic_migrate.py b/netx_api/port_traffic_migrate.py index c2fe92a..50df6d7 100644 --- a/netx_api/port_traffic_migrate.py +++ b/netx_api/port_traffic_migrate.py @@ -63,6 +63,27 @@ def ensure_port_traffic_series_schema(conn) -> None: conn.exec_driver_sql( "ALTER TABLE port_traffic_series ADD COLUMN IF NOT EXISTS task_id VARCHAR(64) DEFAULT ''" ) + # Older installs created task_id as NOT NULL before the device-centric rename. + # Keep both columns populated and relax NOT NULL so ORM inserts that only set + # device_id (or only task_id) do not 500. + for sql in ( + """ + UPDATE port_traffic_series + SET device_id = task_id + WHERE COALESCE(device_id, '') = '' AND COALESCE(task_id, '') <> '' + """, + """ + UPDATE port_traffic_series + SET task_id = device_id + WHERE COALESCE(task_id, '') = '' AND COALESCE(device_id, '') <> '' + """, + "ALTER TABLE port_traffic_series ALTER COLUMN task_id DROP NOT NULL", + "ALTER TABLE port_traffic_series ALTER COLUMN task_id SET DEFAULT ''", + ): + try: + conn.exec_driver_sql(sql) + except Exception: + pass conn.exec_driver_sql( "CREATE INDEX IF NOT EXISTS ix_port_traffic_series_device_id ON port_traffic_series (device_id)" ) @@ -79,6 +100,24 @@ def ensure_port_traffic_series_schema(conn) -> None: conn.exec_driver_sql( "ALTER TABLE port_traffic_target ADD COLUMN IF NOT EXISTS task_id VARCHAR(64) DEFAULT ''" ) + for sql in ( + """ + UPDATE port_traffic_target + SET device_id = task_id + WHERE COALESCE(device_id, '') = '' AND COALESCE(task_id, '') <> '' + """, + """ + UPDATE port_traffic_target + SET task_id = device_id + WHERE COALESCE(task_id, '') = '' AND COALESCE(device_id, '') <> '' + """, + "ALTER TABLE port_traffic_target ALTER COLUMN task_id DROP NOT NULL", + "ALTER TABLE port_traffic_target ALTER COLUMN task_id SET DEFAULT ''", + ): + try: + conn.exec_driver_sql(sql) + except Exception: + pass conn.exec_driver_sql( "CREATE INDEX IF NOT EXISTS ix_port_traffic_target_series_id ON port_traffic_target (series_id)" )