1
0
mirror of https://github.com/quay/quay.git synced 2026-01-27 18:42:52 +03:00
Files
quay/data/model/test/test_organization.py
jbpratt c3aaa87c47 fix(organization): optimize get_organization_member_set to accept IDs directly (PROJQUAY-900) (#4918)
Eliminate N+1 lazy-load queries by accepting user IDs instead of User objects.

## Problem

When checking organization membership for permissions, the old code triggered
a lazy-load query for EACH permission just to extract the user ID:

```python
# Old caller pattern - triggers N lazy-loads!
users_filter = {perm.user for perm in repo_perms}
org_members = get_organization_member_set(org, users_filter=users_filter)
```

For an endpoint returning 50 permissions, this caused 50 extra SELECT queries.

## Solution

Accept user IDs directly via `user_ids_filter` parameter:

```python
# New pattern - no lazy-loads!
user_ids_filter = {perm.user_id for perm in repo_perms}
org_members = get_organization_member_set(org, user_ids_filter=user_ids_filter)
```

The `user_id` foreign key field is already populated on the model - accessing
it doesn't require a database query.

## Changes

- Renamed `users_filter` → `user_ids_filter` parameter
- Accept set of integer IDs instead of User objects
- Updated 6 call sites in permission_models_pre_oci.py and prototype.py
- Added comprehensive test coverage

## Performance Impact

For get_repo_permissions_by_user with 50 permissions:
- Before: 50 lazy-load queries
- After: 0 queries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Signed-off-by: Brady Pratt <bpratt@redhat.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 14:17:08 -06:00

178 lines
6.5 KiB
Python

import pytest
from playhouse.test_utils import assert_query_count
from data.model.organization import (
create_organization,
get_organization,
get_organization_member_set,
get_organizations,
is_org_admin,
)
from data.model.team import add_user_to_team, get_organization_team
from data.model.user import (
create_robot,
create_user,
get_user,
mark_namespace_for_deletion,
)
from data.queue import WorkQueue
from test.fixtures import *
@pytest.mark.parametrize(
"deleted",
[
(True),
(False),
],
)
def test_get_organizations(deleted, initialized_db):
# Delete an org.
deleted_org = get_organization("sellnsmall")
queue = WorkQueue("testgcnamespace", lambda db: db.transaction())
mark_namespace_for_deletion(deleted_org, [], queue)
orgs = get_organizations(deleted=deleted)
assert orgs
deleted_found = [org for org in orgs if org.id == deleted_org.id]
assert bool(deleted_found) == deleted
def test_is_org_admin(initialized_db):
user = get_user("devtable")
org = get_organization("sellnsmall")
assert is_org_admin(user, org) is True
class TestGetOrganizationMemberSet:
"""Tests for get_organization_member_set with user_ids_filter parameter."""
def test_returns_all_members_without_filter(self, initialized_db):
"""Test that without a filter, all org members are returned."""
org = get_organization("buynlarge")
members = get_organization_member_set(org)
# devtable created buynlarge, so should be a member
assert "devtable" in members
def test_user_ids_filter_returns_matching_members(self, initialized_db):
"""Test that user_ids_filter correctly filters to only matching members."""
org = get_organization("buynlarge")
user = get_user("devtable")
# Filter to only devtable's ID
members = get_organization_member_set(org, user_ids_filter={user.id})
assert members == {"devtable"}
def test_user_ids_filter_with_non_member_returns_empty(self, initialized_db):
"""Test that filtering by a non-member ID returns empty set."""
org = get_organization("buynlarge")
freshuser = get_user("freshuser")
# freshuser is not a member of buynlarge
members = get_organization_member_set(org, user_ids_filter={freshuser.id})
assert members == set()
def test_empty_user_ids_filter_returns_empty_set(self, initialized_db):
"""Test that an empty user_ids_filter returns empty set immediately."""
org = get_organization("buynlarge")
# Empty filter should short-circuit and return empty set
members = get_organization_member_set(org, user_ids_filter=set())
assert members == set()
def test_user_ids_filter_with_multiple_ids(self, initialized_db):
"""Test filtering with multiple user IDs."""
org = get_organization("buynlarge")
devtable = get_user("devtable")
freshuser = get_user("freshuser")
# Add freshuser to buynlarge's owners team
owners_team = get_organization_team("buynlarge", "owners")
add_user_to_team(freshuser, owners_team)
# Filter to both users
members = get_organization_member_set(org, user_ids_filter={devtable.id, freshuser.id})
assert "devtable" in members
assert "freshuser" in members
def test_include_robots_false_excludes_robots(self, initialized_db):
"""Test that include_robots=False excludes robot accounts."""
org = get_organization("buynlarge")
# Create a robot for the org and add to a team
robot, _ = create_robot("testrob", org)
owners_team = get_organization_team("buynlarge", "owners")
add_user_to_team(robot, owners_team)
# Without include_robots, robot should not be in the set
members = get_organization_member_set(org, include_robots=False)
assert robot.username not in members
# With include_robots, robot should be in the set
members_with_robots = get_organization_member_set(org, include_robots=True)
assert robot.username in members_with_robots
def test_user_ids_filter_with_robot_id_and_include_robots(self, initialized_db):
"""Test that user_ids_filter works correctly with robot IDs."""
org = get_organization("buynlarge")
# Create a robot and add to team
robot, _ = create_robot("filterrob", org)
owners_team = get_organization_team("buynlarge", "owners")
add_user_to_team(robot, owners_team)
# Filter to robot ID with include_robots=True
members = get_organization_member_set(org, include_robots=True, user_ids_filter={robot.id})
assert members == {robot.username}
# Filter to robot ID with include_robots=False should return empty
members_no_robots = get_organization_member_set(
org, include_robots=False, user_ids_filter={robot.id}
)
assert members_no_robots == set()
def test_user_ids_filter_executes_single_query(self, initialized_db):
"""
Verify that user_ids_filter results in a single query regardless of filter size.
This is the key optimization: by accepting user IDs directly instead of User
objects, callers can use perm.user_id (already loaded) instead of perm.user
(triggers lazy-load), eliminating N+1 queries.
"""
admin_user = get_user("devtable")
org = create_organization("querytestorg", "querytest@example.com", admin_user)
owners_team = get_organization_team("querytestorg", "owners")
# Create 50 users and add them to the org
user_ids = {admin_user.id}
for i in range(50):
user = create_user(
username=f"querytest_user_{i}",
password="password",
email=f"querytest_{i}@example.com",
auto_verify=True,
)
add_user_to_team(user, owners_team)
user_ids.add(user.id)
# Key assertion: 51 user IDs in filter should still be 1 query
with assert_query_count(1):
members = get_organization_member_set(org, user_ids_filter=user_ids)
assert len(members) == 51
def test_no_filter_executes_single_query(self, initialized_db):
"""Verify get_organization_member_set without filter uses single query."""
org = get_organization("buynlarge")
with assert_query_count(1):
members = get_organization_member_set(org)
assert len(members) > 0