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:
@@ -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":
|
||||||
|
|||||||
Reference in New Issue
Block a user