perf(files): batch tenant summaries
This commit is contained in:
@@ -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:
|
def _veto_group_delete(session, tenant_id: str, group_id: str) -> None:
|
||||||
from govoplan_files.backend.db.models import FileAsset, FileConnectorSpace, FileFolder, FileShare
|
from govoplan_files.backend.db.models import FileAsset, FileConnectorSpace, FileFolder, FileShare
|
||||||
|
|
||||||
@@ -189,6 +226,7 @@ manifest = ModuleManifest(
|
|||||||
route_factory=_files_router,
|
route_factory=_files_router,
|
||||||
role_templates=ROLE_TEMPLATES,
|
role_templates=ROLE_TEMPLATES,
|
||||||
tenant_summary_providers=(_tenant_summary,),
|
tenant_summary_providers=(_tenant_summary,),
|
||||||
|
tenant_summary_batch_providers=(_tenant_summary_batch,),
|
||||||
delete_veto_providers={"group": (_veto_group_delete,)},
|
delete_veto_providers={"group": (_veto_group_delete,)},
|
||||||
nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),),
|
nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),),
|
||||||
frontend=FrontendModule(
|
frontend=FrontendModule(
|
||||||
|
|||||||
108
tests/test_tenant_summary_batch.py
Normal file
108
tests/test_tenant_summary_batch.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user