fix(cli/db-reset): drive table list from SQLModel.metadata, not hardcoded

The hardcoded _DB_RESET_TABLES tuple had drifted — session_profile,
smtp_targets, and webhook_subscriptions were all missing, so
`decnet db-reset --i-know-what-im-doing drop-tables` silently left
them behind. Running it on a post-webhook install then letting
SQLModel.metadata.create_all() re-create tables produced a partial
schema: old rows survived, new columns didn't land, and endpoints
500'd on the missing columns (e.g. auto_disabled_at after the
circuit breaker merge).

Replace the hardcoded list with `SQLModel.metadata.sorted_tables`,
reversed for DROP safety (children first). Any future model addition
is auto-enrolled — no manual step, no more drift.

No behavior change on reset semantics; the SET FOREIGN_KEY_CHECKS=0
fence still covers any edge case the sort order misses.
This commit is contained in:
2026-04-24 16:31:10 -04:00
parent 2bcef50ac5
commit ba155b70e1

View File

@@ -8,26 +8,29 @@ from rich.table import Table
from .utils import console, log from .utils import console, log
_DB_RESET_TABLES: tuple[str, ...] = ( def _decnet_tables() -> tuple[str, ...]:
# Order matters for DROP TABLE: child FKs first. """Every DECNET-managed table, ordered child-first for DROP safety.
# - attacker_behavior FK-references attackers.
# - decky_shards FK-references swarm_hosts. Source is ``SQLModel.metadata.sorted_tables`` — the same registry that
# - topology_* children FK-reference topologies / lans / topology_deckies. drives ``create_all`` — so adding a new model automatically enrolls
"attacker_behavior", its table in ``db-reset`` with no manual step. (Previous hardcoded
"attackers", list drifted multiple times; ``webhook_subscriptions`` /
"logs", ``session_profile`` / ``smtp_targets`` all got missed.)
"bounty",
"state", ``sorted_tables`` returns parent-first (topological order that makes
"users", ``CREATE`` safe). For ``DROP`` we need the reverse: children first,
"decky_shards", so FK constraints drop before their parents. ``SET FOREIGN_KEY_CHECKS
"swarm_hosts", = 0`` below makes this order-insensitive for MySQL, but the reverse
"topology_status_events", order keeps the code honest for any backend that doesn't support
"topology_mutations", disabling the FK check.
"topology_edges", """
"topology_deckies", from sqlmodel import SQLModel
"lans", # Importing the models package registers every table on SQLModel.metadata.
"topologies", import decnet.web.db.models # noqa: F401
)
return tuple(
t.name for t in reversed(SQLModel.metadata.sorted_tables)
)
async def _db_reset_mysql_async(dsn: str, mode: str, confirm: bool) -> None: async def _db_reset_mysql_async(dsn: str, mode: str, confirm: bool) -> None:
@@ -39,10 +42,11 @@ async def _db_reset_mysql_async(dsn: str, mode: str, confirm: bool) -> None:
db_name = urlparse(dsn).path.lstrip("/") or "(default)" db_name = urlparse(dsn).path.lstrip("/") or "(default)"
engine = create_async_engine(dsn) engine = create_async_engine(dsn)
tables = _decnet_tables()
try: try:
rows: dict[str, int] = {} rows: dict[str, int] = {}
async with engine.connect() as conn: async with engine.connect() as conn:
for tbl in _DB_RESET_TABLES: for tbl in tables:
try: try:
result = await conn.execute(text(f"SELECT COUNT(*) FROM `{tbl}`")) # nosec B608 result = await conn.execute(text(f"SELECT COUNT(*) FROM `{tbl}`")) # nosec B608
rows[tbl] = result.scalar() or 0 rows[tbl] = result.scalar() or 0
@@ -65,7 +69,7 @@ async def _db_reset_mysql_async(dsn: str, mode: str, confirm: bool) -> None:
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.execute(text("SET FOREIGN_KEY_CHECKS = 0")) await conn.execute(text("SET FOREIGN_KEY_CHECKS = 0"))
for tbl in _DB_RESET_TABLES: for tbl in tables:
if rows.get(tbl, -1) < 0: if rows.get(tbl, -1) < 0:
continue continue
if mode == "truncate": if mode == "truncate":