# /home/Autopartes/pos/tenant_db.py """Tenant DB connection manager with pooling. Uses psycopg2.pool.ThreadedConnectionPool for both master and tenant DBs. Connections are returned to the pool on .close() via a thin wrapper — this keeps the rest of the codebase unchanged. """ import psycopg2 from psycopg2 import pool from config import MASTER_DB_URL, TENANT_DB_URL_TEMPLATE # ─── Pools ───────────────────────────────────── _master_pool = None _tenant_pools = {} def _get_master_pool(): """Lazy-initialize master DB connection pool.""" global _master_pool if _master_pool is None: _master_pool = pool.ThreadedConnectionPool( minconn=2, maxconn=10, dsn=MASTER_DB_URL ) return _master_pool def _get_tenant_pool(db_name): """Lazy-initialize tenant DB connection pool by db_name.""" global _tenant_pools if db_name not in _tenant_pools: dsn = TENANT_DB_URL_TEMPLATE.format(db_name=db_name) _tenant_pools[db_name] = pool.ThreadedConnectionPool( minconn=2, maxconn=10, dsn=dsn ) return _tenant_pools[db_name] class _PooledConnection: """Thin wrapper that delegates all attribute access to the real psycopg2 connection, but returns it to the pool on .close(). """ __slots__ = ('_conn', '_pool') def __init__(self, conn, db_pool): self._conn = conn self._pool = db_pool def __getattr__(self, name): return getattr(self._conn, name) def close(self): try: self._pool.putconn(self._conn) except Exception: # If pool is already closed, fall back to real close self._conn.close() def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): self.close() # ─── Public API ──────────────────────────────── def get_master_conn(): """Get a pooled connection to the master DB.""" p = _get_master_pool() return _PooledConnection(p.getconn(), p) def get_tenant_conn(tenant_id): """Get a pooled connection to a tenant's DB.""" master = get_master_conn() cur = master.cursor() cur.execute( "SELECT db_name FROM tenants WHERE id = %s AND is_active = true", (tenant_id,) ) row = cur.fetchone() cur.close() master.close() if not row: raise ValueError(f"Tenant {tenant_id} not found or inactive") db_name = row[0] p = _get_tenant_pool(db_name) return _PooledConnection(p.getconn(), p) def get_tenant_conn_by_dbname(db_name): """Get a pooled connection to a tenant DB directly by name.""" p = _get_tenant_pool(db_name) return _PooledConnection(p.getconn(), p)