Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES/+base-version-content-filter.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Added a ``base_version`` filter to the content list endpoints. When combined with
``repository_version_added`` or ``repository_version_removed``, it returns the net set of content
added or removed between two arbitrary repository versions instead of only the single-step
difference against the filtered version's immediate predecessor. The diff is computed database-side
via ``unnest`` subqueries over each version's ``content_ids``, avoiding loading the arrays into
memory or passing them as per-query parameters.
31 changes: 22 additions & 9 deletions pulpcore/app/models/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,13 +1000,26 @@ def get_content(self, content_qs=None):
content_ids = self.content_ids
if len(content_ids) >= 65535:
# Workaround for PostgreSQL's limit on the number of parameters in a query
content_ids = (
RepositoryVersion.objects.filter(pk=self.pk)
.annotate(cids=Func(F("content_ids"), function="unnest"))
.values_list("cids", flat=True)
)
content_ids = self.content_ids_subquery()
return content_qs.filter(pk__in=content_ids)

def content_ids_subquery(self):
"""
Return this version's ``content_ids`` as a database-side ``unnest`` subquery.

Using a subquery keeps the content unit UUIDs inside PostgreSQL instead of loading the
whole array into Python and passing each UUID as a bound query parameter. This avoids the
per-query parameter limit and the memory/serialization cost for large repository versions.

Returns:
django.db.models.QuerySet: A values queryset yielding the content unit UUIDs.
"""
return (
RepositoryVersion.objects.filter(pk=self.pk)
.annotate(cids=Func(F("content_ids"), function="unnest"))
.values_list("cids", flat=True)
)

@property
def content(self):
"""
Expand Down Expand Up @@ -1119,8 +1132,8 @@ def added(self, base_version=None):
if not base_version:
return Content.objects.filter(version_memberships__version_added=self)

return Content.objects.filter(pk__in=self.content_ids).exclude(
pk__in=base_version.content_ids
return Content.objects.filter(pk__in=self.content_ids_subquery()).exclude(
pk__in=base_version.content_ids_subquery()
)

def removed(self, base_version=None):
Expand All @@ -1134,8 +1147,8 @@ def removed(self, base_version=None):
if not base_version:
return Content.objects.filter(version_memberships__version_removed=self)

return Content.objects.filter(pk__in=base_version.content_ids).exclude(
pk__in=self.content_ids
return Content.objects.filter(pk__in=base_version.content_ids_subquery()).exclude(
pk__in=self.content_ids_subquery()
)

def contains(self, content):
Expand Down
6 changes: 6 additions & 0 deletions pulpcore/app/viewsets/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
ContentAddedRepositoryVersionFilter,
ContentRemovedRepositoryVersionFilter,
ContentRepositoryVersionFilter,
RepositoryVersionBaseFilter,
)


Expand Down Expand Up @@ -125,6 +126,10 @@ class ContentFilter(BaseFilterSet):
Return Content which was added in this repository version.
repository_version_removed:
Return Content which was removed from this repository version.
base_version:
When combined with repository_version_added / repository_version_removed, compute the
net difference relative to this base repository version instead of the filtered
version's immediate predecessor. Has no effect on its own.
orphaned_for:
Return Content which has been orphaned for a given number of minutes;
-1 uses ORPHAN_PROTECTION_TIME value.
Expand All @@ -135,6 +140,7 @@ class ContentFilter(BaseFilterSet):
repository_version = ContentRepositoryVersionFilter()
repository_version_added = ContentAddedRepositoryVersionFilter()
repository_version_removed = ContentRemovedRepositoryVersionFilter()
base_version = RepositoryVersionBaseFilter()
orphaned_for = OrphanedFilter(
help_text="Minutes Content has been orphaned for. -1 uses ORPHAN_PROTECTION_TIME."
)
Expand Down
51 changes: 49 additions & 2 deletions pulpcore/app/viewsets/custom_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,53 @@ def filter(self, qs, value):
"""
raise NotImplementedError()

def get_base_version(self, field_name="base_version"):
"""
Resolve the companion ``base_version`` filter, if the request supplied one.

The added/removed filters use this to diff against an *arbitrary* base repository
version instead of the filtered version's immediate predecessor.

Args:
field_name (string): The name of the companion base-version filter on the filterset.

Returns:
pulpcore.app.models.RepositoryVersion or None: The resolved base version, or None
when no base version was supplied.
"""
if self.parent is None:
return None
base_value = self.parent.form.cleaned_data.get(field_name)
if not base_value:
return None
return self.get_repository_version(base_value)


class RepositoryVersionBaseFilter(RepoVersionHrefPrnFilter):
"""
Companion filter that designates the base repository version for
``repository_version_added`` / ``repository_version_removed`` diffs.

On its own this filter does not alter the queryset. It is consumed by the added/removed
filters to compute the net difference between two arbitrary repository versions, rather than
the single-step difference against the filtered version's immediate predecessor.
"""

def __init__(self, *args, **kwargs):
kwargs.setdefault(
"help_text",
_(
"Repository Version referenced by HREF/PRN to use as the base for "
"repository_version_added / repository_version_removed. When set, added/removed "
"content is computed relative to this version instead of the immediate predecessor."
),
)
super().__init__(*args, **kwargs)

def filter(self, qs, value):
# No-op on its own; the value is consumed by the added/removed filters.
return qs


class RepositoryVersionFilter(RepoVersionHrefPrnFilter):
"""
Expand Down Expand Up @@ -251,7 +298,7 @@ def filter(self, qs, value):
return qs

repo_version = self.get_repository_version(value)
return qs.filter(pk__in=repo_version.added())
return qs.filter(pk__in=repo_version.added(base_version=self.get_base_version()))


class ContentRemovedRepositoryVersionFilter(RepoVersionHrefPrnFilter):
Expand All @@ -273,7 +320,7 @@ def filter(self, qs, value):
return qs

repo_version = self.get_repository_version(value)
return qs.filter(pk__in=repo_version.removed())
return qs.filter(pk__in=repo_version.removed(base_version=self.get_base_version()))


class CharInFilter(BaseInFilter, CharFilter):
Expand Down
Loading