Closes #23032: Make GraphQL custom field N+1 regression test deterministic (#23034)

Fixes #23032
This commit is contained in:
Martin Hauser 2026-08-27 17:07:06 +02:00 committed by GitHub
parent aa1d49d0f5
commit 1374fde4aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 17 additions and 15 deletions

View File

@ -43,6 +43,12 @@ from utilities.testing import APITestCase, TestCase
from virtualization.models import VirtualMachine
def get_primary_table_queries(queries, model):
"""Return the SQL of captured queries that read from the model's table as the primary relation."""
table = connection.ops.quote_name(model._meta.db_table)
return [q['sql'] for q in queries if f'FROM {table}' in q['sql']]
class CustomFieldTestCase(TestCase):
@classmethod
@ -1568,20 +1574,9 @@ class CustomFieldAPITestCase(APITestCase):
def test_graphql_selection_field_list_query_is_not_n_plus_one(self):
self.add_permissions('dcim.view_site')
Site.objects.bulk_create([Site(name=f'Site {i}', slug=f'site-{i}') for i in range(3, 13)])
query = '{ site_list { custom_fields } }'
Site.objects.bulk_create([Site(name=f'Site {i}', slug=f'site-{i}') for i in range(3, 8)])
# Prime process-level caches (e.g. ContentType) outside the measured request.
self.client.post(reverse('graphql'), data={'query': query}, format='json', **self.header)
with CaptureQueriesContext(connection) as ctx:
response = self.client.post(reverse('graphql'), data={'query': query}, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
data = json.loads(response.content)
self.assertNotIn('errors', data)
self.assertEqual(len(data['data']['site_list']), 7)
baseline_query_count = len(ctx.captured_queries)
Site.objects.bulk_create([Site(name=f'Site {i}', slug=f'site-{i}') for i in range(8, 13)])
with CaptureQueriesContext(connection) as ctx:
response = self.client.post(reverse('graphql'), data={'query': query}, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
@ -1589,9 +1584,16 @@ class CustomFieldAPITestCase(APITestCase):
self.assertNotIn('errors', data)
self.assertEqual(len(data['data']['site_list']), 12)
# The capture window also holds one-off request queries, so assert on per-table counts, not the total.
site_queries = get_primary_table_queries(ctx.captured_queries, Site)
self.assertEqual(
len(ctx.captured_queries), baseline_query_count,
"custom_fields label resolution should not scale with the number of objects returned"
len(site_queries), 1,
f'custom_field_data must be fetched by the site list query itself, got {site_queries}'
)
custom_field_queries = get_primary_table_queries(ctx.captured_queries, CustomField)
self.assertEqual(
len(custom_field_queries), 1,
f'custom field definitions must be fetched once per request, got {custom_field_queries}'
)
def test_get_for_model_select_related_choice_set(self):