From 8d2d7a4fd095459a5bc9589d6cf126c9874ffa5a Mon Sep 17 00:00:00 2001 From: Daniel Alley Date: Tue, 17 Feb 2026 17:19:29 -0500 Subject: [PATCH] Add a test for the postgresql parameter limit workaround Assisted By: Claude Code (cherry picked from commit cd6685e96f0edfc3e725f4dc1e2029fb46437e1a) --- pulpcore/tests/unit/models/test_repository.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/pulpcore/tests/unit/models/test_repository.py b/pulpcore/tests/unit/models/test_repository.py index 621a86373dd..e947c8442bc 100644 --- a/pulpcore/tests/unit/models/test_repository.py +++ b/pulpcore/tests/unit/models/test_repository.py @@ -788,3 +788,87 @@ def test_batch_operations_preserve_correctness(repository, db): assert rvcd_qs.get(count_type=RepositoryVersionContentDetails.PRESENT).count == 40 assert rvcd_qs.filter(count_type=RepositoryVersionContentDetails.ADDED).first() is None assert rvcd_qs.get(count_type=RepositoryVersionContentDetails.REMOVED).count == 60 + + +def test_postgresql_parameter_limit(db, repository): + """ + Test repository operations with >65535 content units to verify PostgreSQL parameter limit + workaround. + + PostgreSQL limits queries to 65535 parameters. This test verifies that content, added(), + and removed() all handle >65535 items correctly. + + Queries MUST be evaluated via .iterator() because psycopg3 uses client-side binding for + regular queries (inlining params into the SQL string, bypassing the limit) but server-side + binding for server-side cursors (.iterator()), which enforces the 65,535 parameter cap. + Without .iterator() the test passes even when the fix is absent. + """ + # Create 66000 content units (exceeds PostgreSQL's 65535 parameter limit) + large_content_set = [Content(pulp_type="core.content") for _ in range(66000)] + Content.objects.bulk_create(large_content_set, batch_size=2000) + large_pks = sorted([c.pk for c in large_content_set]) + + version0 = repository.latest_version() + + # Test 1: Add >65535 content units - tests added() and content with >65535 items + with repository.new_version() as version1: + version1.add_content(Content.objects.filter(pk__in=large_pks)) + + # Verify content_ids exceeds the PostgreSQL parameter limit threshold + assert isinstance(version1.content_ids, list) + assert len(version1.content_ids) >= 65535 + + # Test the content property with >65535 items + # .iterator() forces a server-side cursor — the only way to reliably trigger the + # 65,535-parameter limit in psycopg3. + content_pks = set(version1.content.values_list("pk", flat=True).iterator()) + assert len(content_pks) == 66000 + + # Test the added() method with >65535 items + added_pks = set(version1.added(base_version=version0).values_list("pk", flat=True).iterator()) + assert len(added_pks) == 66000 # Critical: added() must handle >65535 items + + # Test the removed() method returns nothing (nothing was removed) + removed_pks = set( + version1.removed(base_version=version0).values_list("pk", flat=True).iterator() + ) + assert len(removed_pks) == 0 + + # Verify RepositoryVersionContentDetails + rvcd_qs = RepositoryVersionContentDetails.objects.filter( + repository_version=version1, content_type="core.content" + ) + assert rvcd_qs.get(count_type=RepositoryVersionContentDetails.PRESENT).count == 66000 + assert rvcd_qs.get(count_type=RepositoryVersionContentDetails.ADDED).count == 66000 + assert rvcd_qs.filter(count_type=RepositoryVersionContentDetails.REMOVED).first() is None + + # Test 2: Remove >65535 content units - tests removed() with >65535 items + with repository.new_version() as version2: + version2.remove_content(Content.objects.filter(pk__in=large_pks)) + + # Test the content property returns nothing (all content was removed) + content_pks = set(version2.content.values_list("pk", flat=True).iterator()) + assert len(content_pks) == 0 + + # Test the added() method returns nothing (nothing was added) + added_pks = set(version2.added(base_version=version1).values_list("pk", flat=True).iterator()) + assert len(added_pks) == 0 + + # Test the removed() method with >65535 items + removed_pks = set( + version2.removed(base_version=version1).values_list("pk", flat=True).iterator() + ) + assert len(removed_pks) == 66000 # Critical: removed() must handle >65535 items + + # Verify RepositoryVersionContentDetails + rvcd_qs = RepositoryVersionContentDetails.objects.filter( + repository_version=version2, content_type="core.content" + ) + assert rvcd_qs.filter(count_type=RepositoryVersionContentDetails.PRESENT).first() is None + assert rvcd_qs.filter(count_type=RepositoryVersionContentDetails.ADDED).first() is None + assert rvcd_qs.get(count_type=RepositoryVersionContentDetails.REMOVED).count == 66000 + + # Verify we can iterate and fetch content without errors + first_100 = list(version1.content[:100].values_list("pk", flat=True)) + assert len(first_100) == 100 + assert all(pk in large_pks for pk in first_100)