diff --git a/autobot-slm-backend/main.py b/autobot-slm-backend/main.py index ede5c7419..28b0a4b28 100644 --- a/autobot-slm-backend/main.py +++ b/autobot-slm-backend/main.py @@ -76,6 +76,59 @@ logger = logging.getLogger(__name__) +def _check_tablename_collisions() -> None: + """Detect shared tablenames across both SQLAlchemy Base MetaData objects (#1878). + + The SLM backend uses two independent ``DeclarativeBase`` subclasses that each + target a **different** PostgreSQL database: + + * ``models.database.Base`` — SLM admin models (nodes, deployments, …) + → connects to the main SLM database (``settings.database_url``) + * ``user_management.models.base.Base`` — User management models (users, roles, …) + → connects to the ``slm_users`` database (``SLM_USERS_DATABASE_URL``) + + Because each Base has its own ``MetaData`` object, SQLAlchemy cannot enforce + cross-Base uniqueness. A developer who accidentally assigns the same + ``__tablename__`` to models from both bases will get no compile-time error; the + table will simply be created in the wrong database and FK references will silently + break, exactly as happened with the ``users`` / ``slm_users`` incident (#1854). + + This function logs a WARNING for every overlapping tablename so that such + regressions are surfaced immediately at startup rather than discovered later via + mysterious query failures. + + Note: This does NOT raise because several names (e.g. ``roles``, ``audit_logs``) + are intentionally shared between the two databases for independent domain purposes. + The warning is the signal; renaming is the developer's responsibility. + """ + # Import after path is set up so this function is safe to call early in lifespan. + import user_management.models # noqa: F401 — registers all UM models with UMBase + from models.database import Base as SLMBase + from user_management.models.base import Base as UMBase + + slm_tables: set[str] = set(SLMBase.metadata.tables.keys()) + um_tables: set[str] = set(UMBase.metadata.tables.keys()) + collisions: set[str] = slm_tables & um_tables + + if collisions: + sorted_names = sorted(collisions) + logger.warning( + "Tablename overlap detected between SLM Base and UserManagement Base — " + "%d shared name(s): %s. " + "These names refer to tables in different databases, but sharing names " + "increases the risk of future model misplacement. " + "See GitHub issue #1878.", + len(sorted_names), + sorted_names, + ) + else: + logger.info( + "Tablename collision check passed — %d SLM tables, %d UM tables, 0 shared names", + len(slm_tables), + len(um_tables), + ) + + async def _init_user_management_tables() -> None: """Create user management tables (SSO, organizations, users, etc.) (#921). @@ -131,6 +184,10 @@ async def lifespan(app: FastAPI): logger.info("Starting SLM Backend v1.0.0") logger.info("Debug mode: %s", settings.debug) + # Validate that the two Base MetaData objects share no tablenames (#1878). + # Must run before create_all / migrations so conflicts are caught immediately. + _check_tablename_collisions() + # Create base tables first, then apply incremental migrations await db_service.initialize() await _init_user_management_tables()