Return a copy with relations not referenced by ``fields()`` removed from a flat values projection. A filter like ``filter(project__id=...)`` auto-joins ``project``; without this, a flat ``values()`` call leaks every Project column even though only one main-m
(
self,
source_model: type["Model"],
select_related: list[str],
)
| 738 | ) |
| 739 | |
| 740 | def with_projection_exclusions( |
| 741 | self, |
| 742 | source_model: type["Model"], |
| 743 | select_related: list[str], |
| 744 | ) -> "ExcludableItems": |
| 745 | """ |
| 746 | Return a copy with relations not referenced by ``fields()`` removed |
| 747 | from a flat values projection. |
| 748 | |
| 749 | A filter like ``filter(project__id=...)`` auto-joins ``project``; |
| 750 | without this, a flat ``values()`` call leaks every Project column |
| 751 | even though only one main-model field was requested. Returns the |
| 752 | original instance unchanged when ``fields()`` was never called - |
| 753 | the leak is a values-projection problem, not an ORM-load problem. |
| 754 | |
| 755 | :param source_model: model from which relation paths are rooted |
| 756 | :type source_model: type[Model] |
| 757 | :param select_related: paths joined into the query (dunder strings) |
| 758 | :type select_related: list[str] |
| 759 | :return: new :class:`ExcludableItems` with implicit excludes added, |
| 760 | or ``self`` when no ``fields()`` call had any effect |
| 761 | :rtype: ExcludableItems |
| 762 | """ |
| 763 | if self.include_entry_count() == 0: |
| 764 | return self |
| 765 | |
| 766 | excludable = ExcludableItems.from_excludable(self) |
| 767 | |
| 768 | # If fields() never names the source model, project only its PK. |
| 769 | if not excludable._referenced_at(source_model, ()): |
| 770 | excludable.get(source_model).pk_only = True |
| 771 | |
| 772 | for path in select_related: |
| 773 | parts = tuple(path.split("__")) |
| 774 | # Snapshot which prefixes carry a fields() reference before any |
| 775 | # exclusions are added below - the exclude additions would |
| 776 | # otherwise show up as "referenced" on the next iteration. |
| 777 | referenced = [ |
| 778 | excludable._referenced_at(source_model, parts[: d + 1]) |
| 779 | for d in range(len(parts)) |
| 780 | ] |
| 781 | # Walk deepest-first so kept_deeper accumulates inward. Each |
| 782 | # segment still needs its own exclude when not kept, because |
| 783 | # ReverseAliasResolver only consults the immediate parent. |
| 784 | kept_deeper = False |
| 785 | for i in reversed(range(len(parts))): |
| 786 | kept_deeper = kept_deeper or referenced[i] |
| 787 | segment = parts[i] |
| 788 | parent_alias, parent_model = excludable._resolve_path( |
| 789 | source_model, parts[:i] |
| 790 | ) |
| 791 | parent_exc = excludable.get(parent_model, alias=parent_alias) |
| 792 | if parent_exc.is_explicitly_included(segment): |
| 793 | continue |
| 794 | if kept_deeper: |
| 795 | # Intermediate relation: keep the join but project |
| 796 | # PK-only when this segment was never explicitly named. |
| 797 | if not referenced[i]: |
no test coverage detected