perf(access): batch tenant summary counts

This commit is contained in:
2026-07-30 01:15:16 +02:00
parent b6c2c89adf
commit 6295cfa840
2 changed files with 153 additions and 2 deletions

View File

@@ -42,6 +42,69 @@ class SqlAccessAdministration(AccessAdministration):
"active_api_keys": int(active_api_keys), "active_api_keys": int(active_api_keys),
} }
def tenant_counts_many(
self,
session: object,
tenant_ids: Sequence[str],
) -> Mapping[str, Mapping[str, int]]:
ids = tuple(dict.fromkeys(str(tenant_id) for tenant_id in tenant_ids if tenant_id))
if not ids:
return {}
db = _session(session)
counts: dict[str, dict[str, int]] = {
tenant_id: {
"users": 0,
"active_users": 0,
"groups": 0,
"api_keys": 0,
"active_api_keys": 0,
}
for tenant_id in ids
}
user_rows = (
db.query(
User.tenant_id,
func.count(User.id),
func.coalesce(
func.sum(case((User.is_active.is_(True), 1), else_=0)),
0,
),
)
.filter(User.tenant_id.in_(ids))
.group_by(User.tenant_id)
.all()
)
for tenant_id, users, active_users in user_rows:
counts[tenant_id]["users"] = int(users)
counts[tenant_id]["active_users"] = int(active_users)
group_rows = (
db.query(Group.tenant_id, func.count(Group.id))
.filter(Group.tenant_id.in_(ids))
.group_by(Group.tenant_id)
.all()
)
for tenant_id, groups in group_rows:
counts[tenant_id]["groups"] = int(groups)
api_key_rows = (
db.query(
ApiKey.tenant_id,
func.count(ApiKey.id),
func.coalesce(
func.sum(case((ApiKey.revoked_at.is_(None), 1), else_=0)),
0,
),
)
.filter(ApiKey.tenant_id.in_(ids))
.group_by(ApiKey.tenant_id)
.all()
)
for tenant_id, api_keys, active_api_keys in api_key_rows:
counts[tenant_id]["api_keys"] = int(api_keys)
counts[tenant_id]["active_api_keys"] = int(active_api_keys)
return counts
def system_account_count(self, session: object) -> int: def system_account_count(self, session: object) -> int:
db = _session(session) db = _session(session)
return db.query(Account).count() return db.query(Account).count()

View File

@@ -1,10 +1,12 @@
from __future__ import annotations from __future__ import annotations
import unittest import unittest
from datetime import UTC, datetime
from sqlalchemy import create_engine from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from govoplan_access.backend.administration import SqlAccessAdministration
from govoplan_access.backend.api.v1.admin_common import ( from govoplan_access.backend.api.v1.admin_common import (
_accounts_by_user_id, _accounts_by_user_id,
_group_member_ids_by_group_id, _group_member_ids_by_group_id,
@@ -13,7 +15,7 @@ from govoplan_access.backend.api.v1.admin_common import (
_roles_by_user_id, _roles_by_user_id,
_tenant_role_assignment_counts, _tenant_role_assignment_counts,
) )
from govoplan_access.backend.db.models import Account, Group, GroupRoleAssignment, Role, User, UserGroupMembership, UserRoleAssignment from govoplan_access.backend.db.models import Account, ApiKey, Group, GroupRoleAssignment, Role, User, UserGroupMembership, UserRoleAssignment
from govoplan_core.db.base import Base from govoplan_core.db.base import Base
@@ -56,6 +58,92 @@ class AdminBatchHelperTests(unittest.TestCase):
self.assertEqual([item.id for item in roles_by_user[user.id]], [role.id]) self.assertEqual([item.id for item in roles_by_user[user.id]], [role.id])
self.assertEqual(role_counts, {role.id: (1, 1)}) self.assertEqual(role_counts, {role.id: (1, 1)})
def test_tenant_counts_many_uses_three_grouped_queries(self) -> None:
accounts = [
Account(
id=f"account-{index}",
email=f"user-{index}@example.test",
normalized_email=f"user-{index}@example.test",
)
for index in range(3)
]
users = [
User(
id="user-1",
tenant_id="tenant-1",
account_id=accounts[0].id,
email=accounts[0].email,
),
User(
id="user-2",
tenant_id="tenant-1",
account_id=accounts[1].id,
email=accounts[1].email,
is_active=False,
),
User(
id="user-3",
tenant_id="tenant-2",
account_id=accounts[2].id,
email=accounts[2].email,
),
]
self.session.add_all(
[
*accounts,
*users,
Group(id="group-1", tenant_id="tenant-1", slug="one", name="One"),
Group(id="group-2", tenant_id="tenant-2", slug="two", name="Two"),
ApiKey(
id="key-1",
tenant_id="tenant-1",
user_id="user-1",
name="Active",
prefix="active",
key_hash="hash-1",
),
ApiKey(
id="key-2",
tenant_id="tenant-1",
user_id="user-2",
name="Revoked",
prefix="revoked",
key_hash="hash-2",
revoked_at=datetime.now(UTC),
),
]
)
self.session.commit()
query_count = 0
def count_query(*_args: object) -> None:
nonlocal query_count
query_count += 1
event.listen(self.engine, "before_cursor_execute", count_query)
try:
counts = SqlAccessAdministration().tenant_counts_many(
self.session,
["tenant-1", "tenant-2", "tenant-empty"],
)
finally:
event.remove(self.engine, "before_cursor_execute", count_query)
self.assertEqual(3, query_count)
self.assertEqual(
{
"users": 2,
"active_users": 1,
"groups": 1,
"api_keys": 2,
"active_api_keys": 1,
},
counts["tenant-1"],
)
self.assertEqual(1, counts["tenant-2"]["users"])
self.assertEqual(0, counts["tenant-empty"]["users"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()