~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/code/model/branchcollection.py

  • Committer: Launchpad Patch Queue Manager
  • Date: 2011-03-28 22:54:44 UTC
  • mfrom: (12660.1.6 bug-736008)
  • Revision ID: launchpad@pqm.canonical.com-20110328225444-65kxjlljsukfq5p2
[r=stevenk][bug=736008] Another optimisation attempt for
        Product:+code-index branch merge proposal counting

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
    LeftJoin,
20
20
    Or,
21
21
    Select,
 
22
    SQL,
22
23
    Union,
 
24
    With,
23
25
    )
24
26
from storm.info import ClassAlias
25
27
from zope.component import getUtility
81
83
    implements(IBranchCollection)
82
84
 
83
85
    def __init__(self, store=None, branch_filter_expressions=None,
84
 
                 tables=None, exclude_from_search=None):
 
86
                 tables=None, exclude_from_search=None,
 
87
                 asymmetric_filter_expressions=None, asymmetric_tables=None):
85
88
        """Construct a `GenericBranchCollection`.
86
89
 
87
90
        :param store: The store to look in for branches. If not specified,
93
96
        :param tables: A dict of Storm tables to the Join expression.  If an
94
97
            expression in branch_filter_expressions refers to a table, then
95
98
            that table *must* be in this list.
 
99
        :param asymmetric_filter_expressions: As per branch_filter_expressions
 
100
            but only applies to one side of reflexive joins.
 
101
        :param asymmetric_tables: As per tables, for
 
102
            asymmetric_filter_expressions.
96
103
        """
97
104
        self._store = store
98
105
        if branch_filter_expressions is None:
99
106
            branch_filter_expressions = []
100
 
        self._branch_filter_expressions = branch_filter_expressions
 
107
        self._branch_filter_expressions = list(branch_filter_expressions)
101
108
        if tables is None:
102
109
            tables = {}
103
110
        self._tables = tables
 
111
        if asymmetric_filter_expressions is None:
 
112
            asymmetric_filter_expressions = []
 
113
        self._asymmetric_filter_expressions = list(
 
114
            asymmetric_filter_expressions)
 
115
        if asymmetric_tables is None:
 
116
            asymmetric_tables = {}
 
117
        self._asymmetric_tables = asymmetric_tables
104
118
        if exclude_from_search is None:
105
119
            exclude_from_search = []
106
120
        self._exclude_from_search = exclude_from_search
132
146
            return self._store
133
147
 
134
148
    def _filterBy(self, expressions, table=None, join=None,
135
 
                  exclude_from_search=None):
136
 
        """Return a subset of this collection, filtered by 'expressions'."""
 
149
                  exclude_from_search=None, symmetric=True):
 
150
        """Return a subset of this collection, filtered by 'expressions'.
 
151
        
 
152
        :param symmetric: If True this filter will apply to both sides of merge
 
153
            proposal lookups and any other lookups that join Branch back onto
 
154
            Branch.
 
155
        """
137
156
        # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
138
157
        # for explicit 'tables' by harnessing Storm's table inference system.
139
158
        # See http://paste.ubuntu.com/118711/ for one way to do that.
140
 
        tables = self._tables.copy()
141
159
        if table is not None:
142
160
            if join is None:
143
161
                raise InvalidFilter("Cannot specify a table without a join.")
144
 
            tables[table] = join
 
162
        if expressions is None:
 
163
            expressions = []
 
164
        tables = self._tables.copy()
 
165
        asymmetric_tables = self._asymmetric_tables.copy()
 
166
        if symmetric:
 
167
            if table is not None:
 
168
                tables[table] = join
 
169
            symmetric_expr = self._branch_filter_expressions + expressions
 
170
            asymmetric_expr = list(self._asymmetric_filter_expressions)
 
171
        else:
 
172
            if table is not None:
 
173
                asymmetric_tables[table] = join
 
174
            symmetric_expr = list(self._branch_filter_expressions)
 
175
            asymmetric_expr = self._asymmetric_filter_expressions + expressions
145
176
        if exclude_from_search is None:
146
177
            exclude_from_search = []
147
 
        if expressions is None:
148
 
            expressions = []
149
178
        return self.__class__(
150
179
            self.store,
151
 
            self._branch_filter_expressions + expressions,
 
180
            symmetric_expr,
152
181
            tables,
153
 
            self._exclude_from_search + exclude_from_search)
 
182
            self._exclude_from_search + exclude_from_search,
 
183
            asymmetric_expr,
 
184
            asymmetric_tables)
154
185
 
155
186
    def _getBranchIdQuery(self):
156
187
        """Return a Storm 'Select' for the branch IDs in this collection."""
160
191
 
161
192
    def _getBranchExpressions(self):
162
193
        """Return the where expressions for this collection."""
163
 
        return (self._branch_filter_expressions
164
 
            + self._getBranchVisibilityExpression())
 
194
        return (self._branch_filter_expressions +
 
195
            self._asymmetric_filter_expressions +
 
196
            self._getBranchVisibilityExpression())
165
197
 
166
198
    def _getBranchVisibilityExpression(self, branch_class=None):
167
199
        """Return the where clauses for visibility."""
168
200
        return []
169
201
 
 
202
    def _getCandidateBranchesWith(self):
 
203
        """Return WITH clauses defining candidate branches.
 
204
        
 
205
        These are defined in terms of scope_branches which should be separately
 
206
        calculated.
 
207
        """
 
208
        return [
 
209
            With("candidate_branches", SQL("SELECT id from scope_branches"))]
 
210
 
170
211
    def getBranches(self, eager_load=False):
171
212
        """See `IBranchCollection`."""
172
 
        tables = [Branch] + self._tables.values()
 
213
        all_tables = set(
 
214
            self._tables.values() + self._asymmetric_tables.values())
 
215
        tables = [Branch] + list(all_tables)
173
216
        expressions = self._getBranchExpressions()
174
217
        resultset = self.store.using(*tables).find(Branch, *expressions)
175
218
        if not eager_load:
221
264
    def getMergeProposals(self, statuses=None, for_branches=None,
222
265
                          target_branch=None, merged_revnos=None):
223
266
        """See `IBranchCollection`."""
224
 
        Target = ClassAlias(Branch, "target")
225
 
        tables = [Branch] + self._tables.values() + [
226
 
            Join(BranchMergeProposal, And(
227
 
                Branch.id==BranchMergeProposal.source_branchID,
228
 
                *self._branch_filter_expressions)),
229
 
            Join(Target, Target.id==BranchMergeProposal.target_branchID)
230
 
            ]
231
 
        expressions = self._getBranchVisibilityExpression()
232
 
        expressions.extend(self._getBranchVisibilityExpression(Target))
 
267
        # teams = SQL("teams as (SELECT team from teamparticipation where person=%s)" % sqlvalues
 
268
        scope_tables = [Branch] + self._tables.values()
 
269
        scope_expressions = self._branch_filter_expressions
 
270
        select = self.store.using(*scope_tables).find(
 
271
            (Branch.id, Branch.private, Branch.ownerID), *scope_expressions)
 
272
        branches_query = select._get_select()
 
273
        with_expr = [With("scope_branches", branches_query)
 
274
            ] + self._getCandidateBranchesWith()
 
275
        expressions = [SQL("""
 
276
            source_branch IN (SELECT id FROM candidate_branches) AND
 
277
            target_branch IN (SELECT id FROM candidate_branches)""")]
 
278
        tables = [BranchMergeProposal]
 
279
        if self._asymmetric_filter_expressions:
 
280
            # Need to filter on Branch beyond the with constraints.
 
281
            expressions += self._asymmetric_filter_expressions
 
282
            expressions.append(
 
283
                BranchMergeProposal.source_branchID == Branch.id)
 
284
            tables.append(Branch)
 
285
            tables.extend(self._asymmetric_tables.values())
233
286
        if for_branches is not None:
234
287
            branch_ids = [branch.id for branch in for_branches]
235
288
            expressions.append(
243
296
        if statuses is not None:
244
297
            expressions.append(
245
298
                BranchMergeProposal.queue_status.is_in(statuses))
246
 
        return self.store.using(*tables).find(BranchMergeProposal, *expressions)
 
299
        return self.store.with_(with_expr).using(*tables).find(
 
300
            BranchMergeProposal, *expressions)
247
301
 
248
302
    def getMergeProposalsForPerson(self, person, status=None):
249
303
        """See `IBranchCollection`."""
420
474
 
421
475
    def ownedBy(self, person):
422
476
        """See `IBranchCollection`."""
423
 
        return self._filterBy([Branch.owner == person])
 
477
        return self._filterBy([Branch.owner == person], symmetric=False)
424
478
 
425
479
    def ownedByTeamMember(self, person):
426
480
        """See `IBranchCollection`."""
429
483
            where=TeamParticipation.personID==person.id)
430
484
        filter = [In(Branch.ownerID, subquery)]
431
485
 
432
 
        return self._filterBy(filter)
 
486
        return self._filterBy(filter, symmetric=False)
433
487
 
434
488
    def registeredBy(self, person):
435
489
        """See `IBranchCollection`."""
436
 
        return self._filterBy([Branch.registrant == person])
 
490
        return self._filterBy([Branch.registrant == person], symmetric=False)
437
491
 
438
492
    def relatedTo(self, person):
439
493
        """See `IBranchCollection`."""
444
498
                    Select(Branch.id, Branch.registrant == person),
445
499
                    Select(Branch.id,
446
500
                           And(BranchSubscription.person == person,
447
 
                               BranchSubscription.branch == Branch.id))))])
 
501
                               BranchSubscription.branch == Branch.id))))],
 
502
            symmetric=False)
448
503
 
449
504
    def _getExactMatch(self, search_term):
450
505
        """Return the exact branch that 'search_term' matches, or None."""
510
565
            [BranchSubscription.person == person],
511
566
            table=BranchSubscription,
512
567
            join=Join(BranchSubscription,
513
 
                      BranchSubscription.branch == Branch.id))
 
568
                      BranchSubscription.branch == Branch.id),
 
569
            symmetric=False)
514
570
 
515
571
    def targetedBy(self, person, since=None):
516
572
        """See `IBranchCollection`."""
521
577
            clauses,
522
578
            table=BranchMergeProposal,
523
579
            join=Join(BranchMergeProposal,
524
 
                      BranchMergeProposal.target_branch == Branch.id))
 
580
                      BranchMergeProposal.target_branch == Branch.id),
 
581
            symmetric=False)
525
582
 
526
583
    def visibleByUser(self, person):
527
584
        """See `IBranchCollection`."""
531
588
        if person is None:
532
589
            return AnonymousBranchCollection(
533
590
                self._store, self._branch_filter_expressions,
534
 
                self._tables, self._exclude_from_search)
 
591
                self._tables, self._exclude_from_search,
 
592
                self._asymmetric_filter_expressions, self._asymmetric_tables)
535
593
        return VisibleBranchCollection(
536
594
            person, self._store, self._branch_filter_expressions,
537
 
            self._tables, self._exclude_from_search)
 
595
            self._tables, self._exclude_from_search,
 
596
            self._asymmetric_filter_expressions, self._asymmetric_tables)
538
597
 
539
598
    def withBranchType(self, *branch_types):
540
 
        return self._filterBy([Branch.branch_type.is_in(branch_types)])
 
599
        return self._filterBy([Branch.branch_type.is_in(branch_types)],
 
600
            symmetric=False)
541
601
 
542
602
    def withLifecycleStatus(self, *statuses):
543
603
        """See `IBranchCollection`."""
544
 
        return self._filterBy([Branch.lifecycle_status.is_in(statuses)])
 
604
        return self._filterBy([Branch.lifecycle_status.is_in(statuses)],
 
605
            symmetric=False)
545
606
 
546
607
    def modifiedSince(self, epoch):
547
608
        """See `IBranchCollection`."""
548
 
        return self._filterBy([Branch.date_last_modified > epoch])
 
609
        return self._filterBy([Branch.date_last_modified > epoch],
 
610
            symmetric=False)
549
611
 
550
612
    def scannedSince(self, epoch):
551
613
        """See `IBranchCollection`."""
552
 
        return self._filterBy([Branch.last_scanned > epoch])
 
614
        return self._filterBy([Branch.last_scanned > epoch], symmetric=False)
553
615
 
554
616
 
555
617
class AnonymousBranchCollection(GenericBranchCollection):
556
618
    """Branch collection that only shows public branches."""
557
619
 
558
 
    def __init__(self, store=None, branch_filter_expressions=None,
559
 
                 tables=None, exclude_from_search=None):
560
 
        super(AnonymousBranchCollection, self).__init__(
561
 
            store=store,
562
 
            branch_filter_expressions=list(branch_filter_expressions),
563
 
            tables=tables, exclude_from_search=exclude_from_search)
564
 
 
565
620
    def _getBranchVisibilityExpression(self, branch_class=Branch):
566
621
        """Return the where clauses for visibility."""
567
622
        return [branch_class.private == False]
568
623
 
 
624
    def _getCandidateBranchesWith(self):
 
625
        """Return WITH clauses defining candidate branches.
 
626
        
 
627
        These are defined in terms of scope_branches which should be separately
 
628
        calculated.
 
629
        """
 
630
        # Anonymous users get public branches only.
 
631
        return [
 
632
            With("candidate_branches",
 
633
                SQL("select id from scope_branches where not private"))
 
634
            ]
 
635
 
569
636
 
570
637
class VisibleBranchCollection(GenericBranchCollection):
571
638
    """A branch collection that has special logic for visibility."""
572
639
 
573
640
    def __init__(self, user, store=None, branch_filter_expressions=None,
574
 
                 tables=None, exclude_from_search=None):
 
641
                 tables=None, exclude_from_search=None,
 
642
                 asymmetric_filter_expressions=None, asymmetric_tables=None):
575
643
        super(VisibleBranchCollection, self).__init__(
576
644
            store=store, branch_filter_expressions=branch_filter_expressions,
577
 
            tables=tables, exclude_from_search=exclude_from_search)
 
645
            tables=tables, exclude_from_search=exclude_from_search,
 
646
            asymmetric_filter_expressions=asymmetric_filter_expressions,
 
647
            asymmetric_tables=asymmetric_tables)
578
648
        self._user = user
579
649
        self._private_branch_ids = self._getPrivateBranchSubQuery()
580
650
 
581
651
    def _filterBy(self, expressions, table=None, join=None,
582
 
                  exclude_from_search=None):
583
 
        """Return a subset of this collection, filtered by 'expressions'."""
 
652
                  exclude_from_search=None, symmetric=True):
 
653
        """Return a subset of this collection, filtered by 'expressions'.
 
654
        
 
655
        :param symmetric: If True this filter will apply to both sides of merge
 
656
            proposal lookups and any other lookups that join Branch back onto
 
657
            Branch.
 
658
        """
584
659
        # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
585
660
        # for explicit 'tables' by harnessing Storm's table inference system.
586
661
        # See http://paste.ubuntu.com/118711/ for one way to do that.
587
 
        tables = self._tables.copy()
588
662
        if table is not None:
589
663
            if join is None:
590
664
                raise InvalidFilter("Cannot specify a table without a join.")
591
 
            tables[table] = join
 
665
        if expressions is None:
 
666
            expressions = []
 
667
        tables = self._tables.copy()
 
668
        asymmetric_tables = self._asymmetric_tables.copy()
 
669
        if symmetric:
 
670
            if table is not None:
 
671
                tables[table] = join
 
672
            symmetric_expr = self._branch_filter_expressions + expressions
 
673
            asymmetric_expr = list(self._asymmetric_filter_expressions)
 
674
        else:
 
675
            if table is not None:
 
676
                asymmetric_tables[table] = join
 
677
            symmetric_expr = list(self._branch_filter_expressions)
 
678
            asymmetric_expr = self._asymmetric_filter_expressions + expressions
592
679
        if exclude_from_search is None:
593
680
            exclude_from_search = []
594
 
        if expressions is None:
595
 
            expressions = []
596
681
        return self.__class__(
597
682
            self._user,
598
683
            self.store,
599
 
            self._branch_filter_expressions + expressions,
 
684
            symmetric_expr,
600
685
            tables,
601
 
            self._exclude_from_search + exclude_from_search)
 
686
            self._exclude_from_search + exclude_from_search,
 
687
            asymmetric_expr,
 
688
            asymmetric_tables)
602
689
 
603
690
    def _getPrivateBranchSubQuery(self):
604
691
        """Return a subquery to get the private branches the user can see.
649
736
                branch_class.id.is_in(self._private_branch_ids))
650
737
            return [public_or_private]
651
738
 
 
739
    def _getCandidateBranchesWith(self):
 
740
        """Return WITH clauses defining candidate branches.
 
741
        
 
742
        These are defined in terms of scope_branches which should be separately
 
743
        calculated.
 
744
        """
 
745
        person = self._user
 
746
        if person is None:
 
747
            # Really an anonymous sitation
 
748
            return [
 
749
                With("candidate_branches",
 
750
                    SQL("select id from scope_branches where not private"))
 
751
                ]
 
752
        return [
 
753
            With("teams", self.store.find(TeamParticipation.teamID,
 
754
                TeamParticipation.personID == person.id)._get_select()),
 
755
            With("private_branches", SQL("""
 
756
                SELECT scope_branches.id FROM scope_branches WHERE
 
757
                scope_branches.private AND ((scope_branches.owner in (select team from teams) OR
 
758
                    EXISTS(SELECT true from BranchSubscription, teams WHERE
 
759
                        branchsubscription.branch = scope_branches.id AND
 
760
                        branchsubscription.person = teams.team)))""")),
 
761
            With("candidate_branches", SQL("""
 
762
                (SELECT id FROM private_branches) UNION
 
763
                (select id FROM scope_branches WHERE not private)"""))
 
764
            ]
 
765
 
652
766
    def visibleByUser(self, person):
653
767
        """See `IBranchCollection`."""
654
768
        if person == self._user: