A class to abstract away join promotion problems for complex filter conditions.
| 2806 | |
| 2807 | |
| 2808 | class JoinPromoter: |
| 2809 | """ |
| 2810 | A class to abstract away join promotion problems for complex filter |
| 2811 | conditions. |
| 2812 | """ |
| 2813 | |
| 2814 | def __init__(self, connector, num_children, negated): |
| 2815 | self.connector = connector |
| 2816 | self.negated = negated |
| 2817 | if self.negated: |
| 2818 | if connector == AND: |
| 2819 | self.effective_connector = OR |
| 2820 | else: |
| 2821 | self.effective_connector = AND |
| 2822 | else: |
| 2823 | self.effective_connector = self.connector |
| 2824 | self.num_children = num_children |
| 2825 | # Maps of table alias to how many times it is seen as required for |
| 2826 | # inner and/or outer joins. |
| 2827 | self.votes = Counter() |
| 2828 | |
| 2829 | def __repr__(self): |
| 2830 | return ( |
| 2831 | f"{self.__class__.__qualname__}(connector={self.connector!r}, " |
| 2832 | f"num_children={self.num_children!r}, negated={self.negated!r})" |
| 2833 | ) |
| 2834 | |
| 2835 | def add_votes(self, votes): |
| 2836 | """ |
| 2837 | Add single vote per item to self.votes. Parameter can be any |
| 2838 | iterable. |
| 2839 | """ |
| 2840 | self.votes.update(votes) |
| 2841 | |
| 2842 | def update_join_types(self, query): |
| 2843 | """ |
| 2844 | Change join types so that the generated query is as efficient as |
| 2845 | possible, but still correct. So, change as many joins as possible |
| 2846 | to INNER, but don't make OUTER joins INNER if that could remove |
| 2847 | results from the query. |
| 2848 | """ |
| 2849 | to_promote = set() |
| 2850 | to_demote = set() |
| 2851 | # The effective_connector is used so that NOT (a AND b) is treated |
| 2852 | # similarly to (a OR b) for join promotion. |
| 2853 | for table, votes in self.votes.items(): |
| 2854 | # We must use outer joins in OR case when the join isn't contained |
| 2855 | # in all of the joins. Otherwise the INNER JOIN itself could remove |
| 2856 | # valid results. Consider the case where a model with rel_a and |
| 2857 | # rel_b relations is queried with rel_a__col=1 | rel_b__col=2. Now, |
| 2858 | # if rel_a join doesn't produce any results is null (for example |
| 2859 | # reverse foreign key or null value in direct foreign key), and |
| 2860 | # there is a matching row in rel_b with col=2, then an INNER join |
| 2861 | # to rel_a would remove a valid match from the query. So, we need |
| 2862 | # to promote any existing INNER to LOUTER (it is possible this |
| 2863 | # promotion in turn will be demoted later on). |
| 2864 | if self.effective_connector == OR and votes < self.num_children: |
| 2865 | to_promote.add(table) |