fix(maf): harden phase 5 approval safety

This commit is contained in:
董宏凯 committed 2026-08-02 14:32:49 +08:00
1 parent f82363d81f
commit a55992e31c
11 files changed
+489 -146

No files matched your search

@@ -58,27 +58,70 @@ def _create_lease_table_sql(conn_or_dialect: Any) -> str:
"""
def _add_missing_columns(conn: Any, *, inspector: Any, execute: Any) -> None:
"""Complete a partially created table without rewriting existing data.
def validate_lease_table_schema(inspector: Any) -> list[str]:
"""Return every lease-table contract violation found by SQLAlchemy."""
The migration is additive. A missing primary key or action foreign-key
column cannot be safely added to a populated table, so fail loudly rather
than creating a lease table that the arbiter could misinterpret.
"""
if TABLE not in inspector.get_table_names():
return [f"missing table: {TABLE}"]
columns = {column["name"] for column in inspector.get_columns(TABLE)}
required = {"target_key", "action_id", "lease_expires_at", "created_at", "updated_at"}
missing = required - columns
if not missing:
return
if {"target_key", "action_id"} & missing:
raise RuntimeError(
f"{TABLE} is incomplete ({sorted(missing)}); recreate it with the deployment migration"
required_columns = ("target_key", "action_id", "lease_expires_at", "created_at", "updated_at")
columns = {column["name"]: column for column in inspector.get_columns(TABLE)}
issues: list[str] = []
for name in required_columns:
column = columns.get(name)
if column is None:
issues.append(f"missing column: {name}")
elif column.get("nullable") is not False:
issues.append(f"nullable column: {name}")
primary_key = inspector.get_pk_constraint(TABLE).get("constrained_columns") or []
if list(primary_key) != ["target_key"]:
issues.append(f"primary key must be (target_key), found {list(primary_key)}")
foreign_key_ok = False
for foreign_key in inspector.get_foreign_keys(TABLE):
options = foreign_key.get("options") or {}
if (
list(foreign_key.get("constrained_columns") or []) == ["action_id"]
and foreign_key.get("referred_table") == "maf_roundtable_actions"
and list(foreign_key.get("referred_columns") or []) == ["id"]
and str(options.get("ondelete") or "").upper() == "CASCADE"
):
foreign_key_ok = True
break
if not foreign_key_ok:
issues.append(
"missing foreign key: action_id -> maf_roundtable_actions.id ON DELETE CASCADE"
)
timestamp_type = _timestamp_sql(conn)
for column in ("lease_expires_at", "created_at", "updated_at"):
if column in missing:
execute(f"ALTER TABLE {TABLE} ADD COLUMN {column} {timestamp_type}")
unique_action_id = any(
list(constraint.get("column_names") or []) == ["action_id"]
for constraint in inspector.get_unique_constraints(TABLE)
)
# Some dialects expose a named UNIQUE constraint only as a unique index.
unique_action_id = unique_action_id or any(
bool(index.get("unique")) and list(index.get("column_names") or []) == ["action_id"]
for index in inspector.get_indexes(TABLE)
)
if not unique_action_id:
issues.append("missing unique constraint: action_id")
indexes = {index.get("name"): list(index.get("column_names") or []) for index in inspector.get_indexes(TABLE)}
for name, column in ((ACTION_INDEX, "action_id"), (EXPIRY_INDEX, "lease_expires_at")):
if name not in indexes:
issues.append(f"missing index: {name}")
elif indexes[name] != [column]:
issues.append(f"invalid index: {name} must cover ({column})")
return issues
def _table_has_rows(conn: Any, text: Any) -> bool:
return conn.execute(text(f"SELECT 1 FROM {TABLE} LIMIT 1")).first() is not None
def _create_indexes(execute: Any) -> None:
execute(f"CREATE INDEX {ACTION_INDEX} ON {TABLE} (action_id)")
execute(f"CREATE INDEX {EXPIRY_INDEX} ON {TABLE} (lease_expires_at)")
async def _migrate(dry_run: bool) -> int:
@@ -111,22 +154,53 @@ def _migrate_sync(conn: Any, *, dry_run: bool) -> None:
if TABLE not in inspector.get_table_names():
logger.info("Creating %s", TABLE)
execute(_create_lease_table_sql(conn))
_create_indexes(execute)
if dry_run:
execute(f"CREATE INDEX {ACTION_INDEX} ON {TABLE} (action_id)")
execute(f"CREATE INDEX {EXPIRY_INDEX} ON {TABLE} (lease_expires_at)")
return
inspector = inspect(conn)
else:
logger.info("Table %s already exists", TABLE)
_add_missing_columns(conn, inspector=inspector, execute=execute)
if not dry_run:
issues = validate_lease_table_schema(inspector)
constraint_issues = [
issue for issue in issues
if not issue.startswith("missing index:")
]
if constraint_issues:
has_rows = _table_has_rows(conn, text)
detail = "; ".join(constraint_issues)
if has_rows:
raise RuntimeError(
f"{TABLE} has data but its lease safety schema is incomplete: {detail}. "
"Back up and manually rebuild the table after operational review."
)
logger.warning(
"%s is empty but incomplete; rebuilding it with the current full schema: %s",
TABLE,
detail,
)
execute(f"DROP TABLE {TABLE}")
execute(_create_lease_table_sql(conn))
_create_indexes(execute)
if dry_run:
return
inspector = inspect(conn)
else:
for index_name, column in (
(ACTION_INDEX, "action_id"),
(EXPIRY_INDEX, "lease_expires_at"),
):
if f"missing index: {index_name}" in issues:
execute(f"CREATE INDEX {index_name} ON {TABLE} ({column})")
if dry_run:
return
inspector = inspect(conn)
indexes = {index.get("name") for index in inspector.get_indexes(TABLE)}
if ACTION_INDEX not in indexes:
execute(f"CREATE INDEX {ACTION_INDEX} ON {TABLE} (action_id)")
if EXPIRY_INDEX not in indexes:
execute(f"CREATE INDEX {EXPIRY_INDEX} ON {TABLE} (lease_expires_at)")
final_issues = validate_lease_table_schema(inspector)
if final_issues:
raise RuntimeError(
f"{TABLE} migration did not produce the required lease schema: "
+ "; ".join(final_issues)
)
def main() -> int: