o
    "i1                     @   s|   d Z ddlmZ ddlmZ ddlZddlmZmZ e	e
ZG dd dZded	ed
efddZded	ed
efddZdS )a  
Tenant Schema Management for PostgreSQL Multi-Tenancy

Implements schema-per-tenant isolation pattern:
- Each tenant gets a dedicated PostgreSQL schema
- Shared tables (tenants, users) remain in public schema
- Tenant-specific data isolated in tenant schemas
    )text)SessionN)OptionalListc                
   @   s"  e Zd ZdZg dZg dZededefddZ	ede
d	edefd
dZede
dedefddZed!de
dededefddZed"de
dee fddZedd Zede
dedefddZede
dededefddZede
dee fddZede
dedee fdd ZdS )#TenantSchemaManagerz?
    Manages PostgreSQL schemas for multi-tenant isolation
    )tenantsusersalembic_versionrolesmenus
user_roles
role_menusrole_permissionscasbin_rule
audit_logspermission_audit_logsmodel_providersmodels)agentschat_messagesknowledge_basesknowledge_categoriesknowledge_documentsknowledge_tagsdocument_tagsdocument_vectorsdocument_versionsknowledge_qaknowledge_metadata_fieldsdocument_metadata_valuesdata_modelsdata_model_fieldstag_categoriestag_auto_rulestax_documents	tenant_idreturnc                 C   s
   d|  S )z
        Get schema name for a tenant

        Args:
            tenant_id: Tenant ID

        Returns:
            Schema name (e.g., 'tenant_1')
        tenant_ )r%   r(   r(   </lsinfo/ai/hellotax_ai/base_platform/app/db/tenant_schema.pyget_schema_name<   s   
z#TenantSchemaManager.get_schema_namedbschema_namec              
   C   sZ   z|  tdd|i}| duW S  ty, } ztd|  W Y d}~dS d}~ww )z
        Check if a schema exists

        Args:
            db: Database session
            schema_name: Schema name to check

        Returns:
            True if schema exists, False otherwise
        zOSELECT schema_name FROM information_schema.schemata WHERE schema_name = :schemaschemaNz!Error checking schema existence: F)executer   fetchone	Exceptionloggererror)r+   r,   resulter(   r(   r)   schema_existsI   s   z!TenantSchemaManager.schema_existsc              
   C   s   t |}z,t | |rtd| d W dS | td| d |   td|  W dS  tyS } zt	d| d|  | 
  W Y d	}~d
S d	}~ww )z
        Create a new tenant schema

        Args:
            db: Database session
            tenant_id: Tenant ID

        Returns:
            True if successful, False otherwise
        Schema z already existsTzCREATE SCHEMA IF NOT EXISTS ""zCreated schema: zError creating schema : NF)r   r*   r5   r1   infor.   r   commitr0   r2   rollbackr+   r%   r,   r4   r(   r(   r)   create_schema_   s   
z!TenantSchemaManager.create_schemaTcascadec              
   C   s   t |}z4t | |std| d W dS |rdnd}| td| d|  |   td|  W dS  t	y[ } zt
d	| d
|  |   W Y d}~dS d}~ww )z
        Delete a tenant schema

        Args:
            db: Database session
            tenant_id: Tenant ID
            cascade: If True, drop all objects in schema

        Returns:
            True if successful, False otherwise
        r6   z does not existTCASCADERESTRICTzDROP SCHEMA IF EXISTS "z" zDeleted schema: zError deleting schema r8   NF)r   r*   r5   r1   warningr.   r   r:   r9   r0   r2   r;   )r+   r%   r>   r,   cascade_clauser4   r(   r(   r)   delete_schema   s    
z!TenantSchemaManager.delete_schemaNc              
   C   s   z-|rt |}| td| d td| d W dS | td td W dS  tyB } z	td|   d}~ww )	z
        Set PostgreSQL search_path for tenant isolation

        Args:
            db: Database session
            tenant_id: Tenant ID (None for public schema only)
        zSET search_path TO "z	", publiczSet search_path to: z, publiczSET search_path TO publiczSet search_path to: publiczError setting search_path: N)r   r*   r.   r   r1   debugr0   r2   r<   r(   r(   r)   set_search_path   s   	
z#TenantSchemaManager.set_search_pathc                  C   st   ddl m}  ddl}ddl}ddl}dd | jj D }dd |D }tt	t
j| }|r8tdd	| |S )
a  
        Return schema-less tenant table definitions from SQLAlchemy metadata.

        We explicitly target schema-less tables here and later translate them
        into the tenant schema. This avoids false positives from same-named
        tables already existing in `public`.
        r   BaseNc                 S   s&   g | ]}|j tjv r|jd u r|qS N)namer   TENANT_TABLESr-   .0tabler(   r(   r)   
<listcomp>   s
    zDTenantSchemaManager.get_tenant_table_definitions.<locals>.<listcomp>c                 S   s   h | ]}|j qS r(   )rI   rK   r(   r(   r)   	<setcomp>   s    zCTenantSchemaManager.get_tenant_table_definitions.<locals>.<setcomp>zISkipped tenant table definitions not registered as schema-less tables: %sz, )app.db.baserG   app.models.agentapp.models.knowledge_baseapp.models.data_modelmetadatatablesvaluessortedsetr   rJ   r1   rD   join)rG   apptenant_tablesregistered_table_namesmissing_table_namesr(   r(   r)   get_tenant_table_definitions   s   	
z0TenantSchemaManager.get_tenant_table_definitionsc              
   C   s   ddl m} t|}t }|std dS z#|  jd|id}|j	j
||dd |   td	t|| W dS  ty] } ztd
| d|  |   W Y d}~dS d}~ww )zR
        Create all tenant-scoped tables inside the target tenant schema.
        r   rF   z9No tenant table definitions found for schema provisioningFN)schema_translate_mapT)bindrU   
checkfirstz+Ensured %s tenant tables exist in schema %sz'Error creating tenant tables in schema r8   )rP   rG   r   r*   r^   r1   r2   
connectionexecution_optionsrT   
create_allr:   r9   lenr0   r;   )r+   r%   rG   r,   r[   tenant_bindr4   r(   r(   r)   create_tenant_tables   s8   

z(TenantSchemaManager.create_tenant_tablessource_tenant_idtarget_tenant_idc           	      C   s   t |}t |}zEt | |sW dS | tdd|i}dd |D }|D ]}| td| d| d| d| d		 q'|   td
| d|  W dS  tyn } zt	d|  | 
  W Y d}~dS d}~ww )aQ  
        Clone schema structure from one tenant to another
        (useful for creating new tenants based on a template)

        Args:
            db: Database session
            source_tenant_id: Source tenant ID
            target_tenant_id: Target tenant ID

        Returns:
            True if successful, False otherwise
        Fz
                    SELECT table_name
                    FROM information_schema.tables
                    WHERE table_schema = :schema
                    AND table_type = 'BASE TABLE'
                r-   c                 S   s   g | ]}|d  qS )r   r(   )rL   rowr(   r(   r)   rN     s    z>TenantSchemaManager.clone_schema_structure.<locals>.<listcomp>z#
                    CREATE TABLE "z"."z"
                    (LIKE "z!" INCLUDING ALL)
                zCloned schema structure from z to Tz Error cloning schema structure: N)r   r*   r=   r.   r   r:   r1   r9   r0   r2   r;   )	r+   rh   ri   source_schematarget_schemar3   rU   rM   r4   r(   r(   r)   clone_schema_structure   s:   


z*TenantSchemaManager.clone_schema_structurec              
   C   s   z%|  td}g }|D ]}|d }t|dd}|||d q|W S  tyA } ztd|  g W  Y d}~S d}~ww )z
        List all tenant schemas

        Args:
            db: Database session

        Returns:
            List of schema info dicts
        z
                    SELECT schema_name
                    FROM information_schema.schemata
                    WHERE schema_name LIKE 'tenant_%'
                    ORDER BY schema_name
                r   r'    )r%   r,   zError listing tenant schemas: N)r.   r   intreplaceappendr0   r1   r2   )r+   r3   schemasrj   r,   r%   r4   r(   r(   r)   list_tenant_schemas.  s$   	
z'TenantSchemaManager.list_tenant_schemasc              
   C   sn   t |}z| tdd|i}| }|r|W S dW S  ty6 } ztd|  W Y d}~dS d}~ww )z
        Get the size of a tenant schema in bytes

        Args:
            db: Database session
            tenant_id: Tenant ID

        Returns:
            Size in bytes, or None if error
        z
                    SELECT SUM(pg_total_relation_size(quote_ident(schemaname) || '.' || quote_ident(tablename)))
                    FROM pg_tables
                    WHERE schemaname = :schema
                r-   r   zError getting schema size: N)r   r*   r.   r   scalarr0   r1   r2   )r+   r%   r,   r3   sizer4   r(   r(   r)   get_schema_sizeS  s   
	z#TenantSchemaManager.get_schema_size)TrH   )__name__
__module____qualname____doc__PUBLIC_TABLESrJ   staticmethodro   strr*   r   boolr5   r=   rC   r   rE   r^   rg   rm   r   dictrs   rv   r(   r(   r(   r)   r      s0    !
"2$ r   r+   r%   r&   c              
   C   sz   zt | |s
W dS t | |sW dS td|  W dS  ty< } ztd|  |   W Y d}~dS d}~ww )a8  
    Provision a complete tenant schema with all tables

    This should be called when a new tenant is created.
    It creates the schema and runs migrations to create all tables.

    Args:
        db: Database session
        tenant_id: Tenant ID

    Returns:
        True if successful, False otherwise
    Fz(Provisioned tenant schema for tenant_id=Tz"Error provisioning tenant schema: N)r   r=   rg   r1   r9   r0   r2   r;   )r+   r%   r4   r(   r(   r)   provision_tenant_schemas  s   r   c                 C   s   t j| |ddS )z
    Deprovision a tenant schema (delete all data)

    This should be called when a tenant is deleted.

    Args:
        db: Database session
        tenant_id: Tenant ID

    Returns:
        True if successful, False otherwise
    T)r>   )r   rC   )r+   r%   r(   r(   r)   deprovision_tenant_schema  s   r   )rz   
sqlalchemyr   sqlalchemy.ormr   loggingtypingr   r   	getLoggerrw   r1   r   ro   r~   r   r   r(   r(   r(   r)   <module>   s    
  d