Files
boc/landvex-admin-backend/app/db/session.py
T
Bernt aee0f09db8 landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar
- Vision: Identify-modell, FAISS, OCR alla testade
- API: Alla 7 integrationstester passerade
- Upplösare: Entitetsupplösning verifierad
2026-07-05 06:41:32 +00:00

152 lines
4.6 KiB
Python

"""
LandveX Admin Backend — Database Session Management
Async SQLAlchemy session handling with tenant context
"""
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Optional
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy import text
from app.core.config import settings
from app.core.exceptions import TenantSchemaException
# Create async engine
engine = create_async_engine(
settings.DATABASE_URL,
pool_size=settings.DATABASE_POOL_SIZE,
max_overflow=settings.DATABASE_MAX_OVERFLOW,
pool_pre_ping=True,
echo=settings.DEBUG,
)
# Create async session factory
AsyncSessionLocal = async_sessionmaker(
engine,
class_=AsyncSession,
expire_on_commit=False,
autoflush=False,
autocommit=False,
)
async def get_db() -> AsyncGenerator[AsyncSession, None]:
"""Dependency to get database session."""
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
@asynccontextmanager
async def get_db_context() -> AsyncGenerator[AsyncSession, None]:
"""Context manager for database sessions."""
async with AsyncSessionLocal() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
class TenantContext:
"""Context manager for tenant schema isolation.
Uses PostgreSQL row-level security and schema search path
to ensure data isolation between tenants.
"""
def __init__(self, session: AsyncSession, tenant_schema: Optional[str] = None):
self.session = session
self.tenant_schema = tenant_schema or settings.DEFAULT_TENANT_SCHEMA
self._original_search_path = None
async def __aenter__(self):
"""Set tenant context on database connection."""
# Set search path to tenant schema first, then public
await self.session.execute(
text(f'SET search_path TO "{self.tenant_schema}", public')
)
# Set application context for RLS policies
await self.session.execute(
text("SELECT set_config('app.current_tenant', :tenant, true)"),
{"tenant": self.tenant_schema}
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Reset tenant context."""
# Reset search path
await self.session.execute(text('SET search_path TO public'))
# Clear application context
await self.session.execute(
text("SELECT set_config('app.current_tenant', '', true)")
)
return False
async def set_tenant_schema(session: AsyncSession, tenant_schema: str) -> None:
"""Set the tenant schema for the current session."""
try:
await session.execute(
text(f'SET search_path TO "{tenant_schema}", public')
)
await session.execute(
text("SELECT set_config('app.current_tenant', :tenant, true)"),
{"tenant": tenant_schema}
)
except Exception as e:
raise TenantSchemaException(
tenant_id=tenant_schema,
operation="set_schema",
original_error=str(e)
)
async def reset_tenant_schema(session: AsyncSession) -> None:
"""Reset schema to default."""
await session.execute(text('SET search_path TO public'))
await session.execute(
text("SELECT set_config('app.current_tenant', '', true)")
)
async def create_tenant_schema(schema_name: str) -> None:
"""Create a new schema for a tenant."""
async with engine.begin() as conn:
# Create schema if not exists
await conn.execute(text(f'CREATE SCHEMA IF NOT EXISTS "{schema_name}"'))
# Grant usage
await conn.execute(text(f'GRANT USAGE ON SCHEMA "{schema_name}" TO PUBLIC'))
async def drop_tenant_schema(schema_name: str) -> None:
"""Drop a tenant schema."""
async with engine.begin() as conn:
await conn.execute(text(f'DROP SCHEMA IF EXISTS "{schema_name}" CASCADE'))
async def schema_exists(schema_name: str) -> bool:
"""Check if a schema exists."""
async with engine.connect() as conn:
result = await conn.execute(
text(
"SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = :schema)"
),
{"schema": schema_name}
)
return result.scalar()