"""表存在性审计:`baseline_generated.sql` 声明的表 vs MySQL 实际表。 与另两个工具互补: - `tools/audit_constraints.py`:唯一键 ↔ 文档基线、ORM 映射 ↔ 库列名; - `tools/schema_fingerprint.py`:字段、索引与外键的结构指纹; - 本脚本:表名集合与列存在性。 凭据来自 `.env` 的 `MYSQL_DSN`,不在脚本内硬编码口令。 """ from __future__ import annotations import re import sys from pathlib import Path from sqlalchemy import create_engine, text ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from app.core.config import get_settings # noqa: E402 SQL_FILE = ROOT / "alembic" / "baseline_generated.sql" # baseline_generated.sql 只覆盖基线表;以下平台增量表由 Alembic 迁移创建。 INCREMENTAL_TABLES = { "agent_run", "domain_event_outbox", "request_idempotency", "config_release", "platform_config_item", "model_endpoint_config", "model_routing_rule", "model_routing_fallback", "prompt_template_version", "outbox_delivery", "svc_conversation_session", "api_request_receipt", "offsite_fund_mail", "offsite_mail_cursor", "offsite_fund_attachment", "offsite_fund_document", "offsite_execution_plan_task", "offsite_rule_result", "offsite_query_record", "offsite_notification", } def expected_tables() -> set[str]: content = SQL_FILE.read_text(encoding="utf-8") return set(re.findall(r"CREATE TABLE `?([A-Za-z0-9_]+)`?", content)) def main() -> int: engine = create_engine(get_settings().mysql_dsn.replace("mysql+asyncmy", "mysql+pymysql")) with engine.connect() as connection: actual = set( connection.execute( text( "SELECT TABLE_NAME FROM information_schema.TABLES " "WHERE TABLE_SCHEMA = DATABASE()" ) ) .scalars() .all() ) - {"alembic_version"} expected = expected_tables() | INCREMENTAL_TABLES missing = expected - actual unexpected = actual - expected if missing or unexpected: print(f"schema audit FAILED: missing={sorted(missing)} unexpected={sorted(unexpected)}") return 1 rows = connection.execute( text( "SELECT TABLE_NAME, COLUMN_NAME FROM information_schema.COLUMNS " "WHERE TABLE_SCHEMA = DATABASE()" ) ).all() engine.dispose() column_counts: dict[str, int] = {} for table, _column in rows: column_counts[table] = column_counts.get(table, 0) + 1 without_columns = sorted(table for table in expected if not column_counts.get(table)) if without_columns: print(f"schema audit FAILED: tables without columns: {without_columns}") return 1 print(f"schema audit passed: {len(expected)} business tables, no missing or unexpected tables") return 0 if __name__ == "__main__": raise SystemExit(main())