diff --git a/src/govoplan_files/backend/manifest.py b/src/govoplan_files/backend/manifest.py index 45e1de3..0711e27 100644 --- a/src/govoplan_files/backend/manifest.py +++ b/src/govoplan_files/backend/manifest.py @@ -140,6 +140,43 @@ def _tenant_summary(session, tenant_id: str) -> dict[str, int]: } +def _tenant_summary_batch(session, tenant_ids) -> dict[str, dict[str, int]]: + from sqlalchemy import func + + from govoplan_files.backend.db.models import FileAsset, FileConnectorCredential, FileConnectorPolicy, FileConnectorProfile, FileConnectorSpace + + ids = tuple(dict.fromkeys(str(tenant_id) for tenant_id in tenant_ids if tenant_id)) + if not ids: + return {} + counts: dict[str, dict[str, int]] = { + tenant_id: { + "files": 0, + "connector_credentials": 0, + "connector_policies": 0, + "connector_profiles": 0, + "connector_spaces": 0, + } + for tenant_id in ids + } + models = ( + ("files", FileAsset), + ("connector_credentials", FileConnectorCredential), + ("connector_policies", FileConnectorPolicy), + ("connector_profiles", FileConnectorProfile), + ("connector_spaces", FileConnectorSpace), + ) + for count_key, model in models: + rows = ( + session.query(model.tenant_id, func.count(model.id)) + .filter(model.tenant_id.in_(ids)) + .group_by(model.tenant_id) + .all() + ) + for tenant_id, count in rows: + counts[tenant_id][count_key] = int(count) + return counts + + def _veto_group_delete(session, tenant_id: str, group_id: str) -> None: from govoplan_files.backend.db.models import FileAsset, FileConnectorSpace, FileFolder, FileShare @@ -189,6 +226,7 @@ manifest = ModuleManifest( route_factory=_files_router, role_templates=ROLE_TEMPLATES, tenant_summary_providers=(_tenant_summary,), + tenant_summary_batch_providers=(_tenant_summary_batch,), delete_veto_providers={"group": (_veto_group_delete,)}, nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),), frontend=FrontendModule( diff --git a/tests/test_tenant_summary_batch.py b/tests/test_tenant_summary_batch.py new file mode 100644 index 0000000..980d48d --- /dev/null +++ b/tests/test_tenant_summary_batch.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import unittest + +from sqlalchemy import create_engine, event +from sqlalchemy.orm import Session + +from govoplan_access.backend.db.models import Account, Group, User +from govoplan_core.core.change_sequence import ChangeSequenceEntry +from govoplan_core.db.base import Base +from govoplan_files.backend.db.models import ( + FileAsset, + FileConnectorCredential, + FileConnectorPolicy, + FileConnectorProfile, + FileConnectorSpace, +) +from govoplan_files.backend.manifest import _tenant_summary_batch + + +class FilesTenantSummaryBatchTests(unittest.TestCase): + def test_batch_summary_uses_one_grouped_query_per_owned_table(self) -> None: + engine = create_engine("sqlite+pysqlite:///:memory:") + Base.metadata.create_all( + engine, + tables=[ + Account.__table__, + User.__table__, + Group.__table__, + FileAsset.__table__, + FileConnectorCredential.__table__, + FileConnectorPolicy.__table__, + FileConnectorProfile.__table__, + FileConnectorSpace.__table__, + ChangeSequenceEntry.__table__, + ], + ) + try: + with Session(engine) as session: + session.add_all( + [ + FileAsset( + id="file-1", + tenant_id="tenant-1", + owner_type="tenant", + display_path="/one.txt", + filename="one.txt", + ), + FileConnectorCredential( + id="credential-1", + tenant_id="tenant-1", + label="Credential", + ), + FileConnectorPolicy( + id="policy-1", + tenant_id="tenant-1", + scope_type="tenant", + ), + FileConnectorProfile( + id="profile-1", + tenant_id="tenant-1", + label="Profile", + provider="webdav", + ), + FileConnectorSpace( + id="space-1", + tenant_id="tenant-1", + owner_type="tenant", + label="Space", + connector_profile_id="profile-1", + provider="webdav", + ), + ] + ) + session.commit() + query_count = 0 + + def count_query(*_args: object) -> None: + nonlocal query_count + query_count += 1 + + event.listen(engine, "before_cursor_execute", count_query) + try: + counts = _tenant_summary_batch( + session, + ["tenant-1", "tenant-empty"], + ) + finally: + event.remove(engine, "before_cursor_execute", count_query) + + self.assertEqual(5, query_count) + self.assertEqual( + { + "files": 1, + "connector_credentials": 1, + "connector_policies": 1, + "connector_profiles": 1, + "connector_spaces": 1, + }, + counts["tenant-1"], + ) + self.assertEqual(0, counts["tenant-empty"]["files"]) + finally: + engine.dispose() + + +if __name__ == "__main__": + unittest.main()