fix: preserve legacy monitoring migration data
This commit is contained in:
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
from sqlalchemy import Column, DateTime, MetaData, String, Table, inspect, select, text
|
||||
from sqlalchemy.engine import Connection
|
||||
|
||||
from app.models import Base
|
||||
from app.models import AlertEvent, Device, NotificationOutbox, PingRecord
|
||||
|
||||
|
||||
REVISION = "20260803_reliability_monitoring"
|
||||
@@ -23,6 +23,40 @@ _DEFAULTS_FOR_EXISTING_ROWS = {
|
||||
"notification_attempts": "0",
|
||||
}
|
||||
|
||||
_REVISION_COLUMNS = {
|
||||
"devices": (
|
||||
Device.__table__,
|
||||
{
|
||||
"probe_packets_per_round",
|
||||
"offline_consecutive_rounds",
|
||||
"degraded_window_rounds",
|
||||
"degraded_loss_percent",
|
||||
"recovery_consecutive_clean_rounds",
|
||||
},
|
||||
),
|
||||
"ping_records": (
|
||||
PingRecord.__table__,
|
||||
{
|
||||
"sent_count",
|
||||
"received_count",
|
||||
"packet_loss_percent",
|
||||
"average_rtt_ms",
|
||||
"is_valid",
|
||||
"failure_reason",
|
||||
},
|
||||
),
|
||||
"alert_events": (
|
||||
AlertEvent.__table__,
|
||||
{
|
||||
"notification_attempts",
|
||||
"last_notification_error",
|
||||
"related_event_id",
|
||||
"previous_status",
|
||||
"current_status",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _quote(connection: Connection, identifier: str) -> str:
|
||||
"""Quote one database identifier through the active dialect."""
|
||||
@@ -33,15 +67,15 @@ def _add_missing_columns(connection: Connection) -> None:
|
||||
"""Add only newly required nullable/defaulted columns to existing tables."""
|
||||
inspector = inspect(connection)
|
||||
existing_tables = set(inspector.get_table_names())
|
||||
for table in Base.metadata.sorted_tables:
|
||||
if table.name not in existing_tables:
|
||||
for table_name, (table, column_names) in _REVISION_COLUMNS.items():
|
||||
if table_name not in existing_tables:
|
||||
continue
|
||||
|
||||
existing_columns = {
|
||||
column["name"] for column in inspector.get_columns(table.name)
|
||||
column["name"] for column in inspector.get_columns(table_name)
|
||||
}
|
||||
for column in table.columns:
|
||||
if column.name in existing_columns:
|
||||
if column.name not in column_names or column.name in existing_columns:
|
||||
continue
|
||||
|
||||
column_type = connection.dialect.type_compiler.process(column.type)
|
||||
@@ -50,10 +84,13 @@ def _add_missing_columns(connection: Connection) -> None:
|
||||
if default is not None:
|
||||
if connection.dialect.name == "sqlite" and default == "TRUE":
|
||||
default = "1"
|
||||
definition = f"{definition} DEFAULT {default}"
|
||||
if column.name == "is_valid":
|
||||
definition = f"{definition} NOT NULL DEFAULT {default}"
|
||||
else:
|
||||
definition = f"{definition} DEFAULT {default}"
|
||||
connection.execute(
|
||||
text(
|
||||
f"ALTER TABLE {_quote(connection, table.name)} "
|
||||
f"ALTER TABLE {_quote(connection, table_name)} "
|
||||
f"ADD COLUMN {definition}"
|
||||
)
|
||||
)
|
||||
@@ -62,32 +99,40 @@ def _add_missing_columns(connection: Connection) -> None:
|
||||
def _create_missing_indexes(connection: Connection) -> None:
|
||||
"""Create new performance indexes for tables that predate this revision."""
|
||||
inspector = inspect(connection)
|
||||
for table in Base.metadata.sorted_tables:
|
||||
existing_indexes = {
|
||||
index["name"] for index in inspector.get_indexes(table.name)
|
||||
}
|
||||
for index in table.indexes:
|
||||
if index.name not in existing_indexes:
|
||||
index.create(bind=connection)
|
||||
table = PingRecord.__table__
|
||||
if table.name not in inspector.get_table_names():
|
||||
return
|
||||
existing_indexes = {
|
||||
index["name"] for index in inspector.get_indexes(table.name)
|
||||
}
|
||||
for index in table.indexes:
|
||||
if index.name not in existing_indexes:
|
||||
index.create(bind=connection)
|
||||
|
||||
|
||||
def _upgrade_postgresql_alert_type(connection: Connection) -> None:
|
||||
"""Permit the degraded alert value for databases using PostgreSQL enums."""
|
||||
if connection.dialect.name == "postgresql":
|
||||
enum_name = AlertEvent.__table__.c.alert_type.type.name
|
||||
connection.execute(
|
||||
text("ALTER TYPE alerttypeenum ADD VALUE IF NOT EXISTS 'degraded'")
|
||||
text(
|
||||
f"ALTER TYPE {_quote(connection, enum_name)} "
|
||||
"ADD VALUE IF NOT EXISTS 'degraded'"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def run_reliability_migration(connection: Connection) -> None:
|
||||
"""Apply this revision once while retaining all existing monitoring data.
|
||||
|
||||
This runner is deliberately idempotent: it creates only absent tables,
|
||||
columns, and indexes, then writes one immutable revision marker. It never
|
||||
issues DROP, DELETE, UPDATE, or data-copy statements.
|
||||
This runner is deliberately idempotent: it creates only this revision's
|
||||
outbox table, missing revision columns, and indexes, then writes one
|
||||
immutable revision marker. It never issues DROP, DELETE, UPDATE, or
|
||||
data-copy statements. The application owns global metadata creation after
|
||||
this function returns.
|
||||
"""
|
||||
_migration_metadata.create_all(bind=connection)
|
||||
Base.metadata.create_all(bind=connection)
|
||||
NotificationOutbox.__table__.create(bind=connection, checkfirst=True)
|
||||
|
||||
already_applied = connection.execute(
|
||||
select(_schema_migrations.c.revision).where(
|
||||
|
||||
Reference in New Issue
Block a user