~launchpad-pqm/launchpad/devel

« back to all changes in this revision

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

  • Committer: Robert Collins
  • Date: 2011-03-28 00:27:00 UTC
  • mto: This revision was merged to the branch mainline in revision 12681.
  • Revision ID: robert@canonical.com-20110328002700-leau4oo4i0ttme5d
Switch to scoped WITH for BranchMergeProposal lookups, which requires splitting BranchCollection filters into symmetric and asymmetric to permit collapsing the common clauses and getting efficient(ish) private branch handling.

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
83
85
    implements(IBranchCollection)
84
86
 
85
87
    def __init__(self, store=None, branch_filter_expressions=None,
86
 
                 tables=None, exclude_from_search=None):
 
88
                 tables=None, exclude_from_search=None,
 
89
                 asymmetric_filter_expressions=None, asymmetric_tables=None):
87
90
        """Construct a `GenericBranchCollection`.
88
91
 
89
92
        :param store: The store to look in for branches. If not specified,
95
98
        :param tables: A dict of Storm tables to the Join expression.  If an
96
99
            expression in branch_filter_expressions refers to a table, then
97
100
            that table *must* be in this list.
 
101
        :param asymmetric_filter_expressions: As per branch_filter_expressions
 
102
            but only applies to one side of reflexive joins.
 
103
        :param asymmetric_tables: As per tables, for
 
104
            asymmetric_filter_expressions.
98
105
        """
99
106
        self._store = store
100
107
        if branch_filter_expressions is None:
101
108
            branch_filter_expressions = []
102
 
        self._branch_filter_expressions = branch_filter_expressions
 
109
        self._branch_filter_expressions = list(branch_filter_expressions)
103
110
        if tables is None:
104
111
            tables = {}
105
112
        self._tables = tables
 
113
        if asymmetric_filter_expressions is None:
 
114
            asymmetric_filter_expressions = []
 
115
        self._asymmetric_filter_expressions = list(
 
116
            asymmetric_filter_expressions)
 
117
        if asymmetric_tables is None:
 
118
            asymmetric_tables = {}
 
119
        self._asymmetric_tables = asymmetric_tables
106
120
        if exclude_from_search is None:
107
121
            exclude_from_search = []
108
122
        self._exclude_from_search = exclude_from_search
134
148
            return self._store
135
149
 
136
150
    def _filterBy(self, expressions, table=None, join=None,
137
 
                  exclude_from_search=None):
138
 
        """Return a subset of this collection, filtered by 'expressions'."""
 
151
                  exclude_from_search=None, symmetric=True):
 
152
        """Return a subset of this collection, filtered by 'expressions'.
 
153
        
 
154
        :param symmetric: If True this filter will apply to both sides of merge
 
155
            proposal lookups and any other lookups that join Branch back onto
 
156
            Branch.
 
157
        """
139
158
        # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
140
159
        # for explicit 'tables' by harnessing Storm's table inference system.
141
160
        # See http://paste.ubuntu.com/118711/ for one way to do that.
142
 
        tables = self._tables.copy()
143
161
        if table is not None:
144
162
            if join is None:
145
163
                raise InvalidFilter("Cannot specify a table without a join.")
146
 
            tables[table] = join
 
164
        if expressions is None:
 
165
            expressions = []
 
166
        tables = self._tables.copy()
 
167
        asymmetric_tables = self._asymmetric_tables.copy()
 
168
        if symmetric:
 
169
            if table is not None:
 
170
                tables[table] = join
 
171
            symmetric_expr = self._branch_filter_expressions + expressions
 
172
            asymmetric_expr = list(self._asymmetric_filter_expressions)
 
173
        else:
 
174
            if table is not None:
 
175
                asymmetric_tables[table] = join
 
176
            symmetric_expr = list(self._branch_filter_expressions)
 
177
            asymmetric_expr = self._asymmetric_filter_expressions + expressions
147
178
        if exclude_from_search is None:
148
179
            exclude_from_search = []
149
 
        if expressions is None:
150
 
            expressions = []
151
180
        return self.__class__(
152
181
            self.store,
153
 
            self._branch_filter_expressions + expressions,
 
182
            symmetric_expr,
154
183
            tables,
155
 
            self._exclude_from_search + exclude_from_search)
 
184
            self._exclude_from_search + exclude_from_search,
 
185
            asymmetric_expr,
 
186
            asymmetric_tables)
156
187
 
157
188
    def _getBranchIdQuery(self):
158
189
        """Return a Storm 'Select' for the branch IDs in this collection."""
162
193
 
163
194
    def _getBranchExpressions(self):
164
195
        """Return the where expressions for this collection."""
165
 
        return (self._branch_filter_expressions
166
 
            + self._getBranchVisibilityExpression())
 
196
        return (self._branch_filter_expressions +
 
197
            self._asymmetric_filter_expressions +
 
198
            self._getBranchVisibilityExpression())
167
199
 
168
200
    def _getBranchVisibilityExpression(self, branch_class=None):
169
201
        """Return the where clauses for visibility."""
170
202
        return []
171
203
 
 
204
    def _getCandidateBranchesWith(self):
 
205
        """Return WITH clauses defining candidate branches.
 
206
        
 
207
        These are defined in terms of scope_branches which should be separately
 
208
        calculated.
 
209
        """
 
210
        return [
 
211
            With("candidate_branches", SQL("SELECT id from scope_branches"))]
 
212
 
172
213
    def getBranches(self, eager_load=False):
173
214
        """See `IBranchCollection`."""
174
215
        tables = [Branch] + self._tables.values()
223
264
    def getMergeProposals(self, statuses=None, for_branches=None,
224
265
                          target_branch=None, merged_revnos=None):
225
266
        """See `IBranchCollection`."""
226
 
        Target = ClassAlias(Branch, "target")
227
 
        tables = [Branch] + self._tables.values() + [
228
 
            Join(BranchMergeProposal, And(
229
 
                Branch.id==BranchMergeProposal.source_branchID,
230
 
                *self._branch_filter_expressions)),
231
 
            Join(Target, Target.id==BranchMergeProposal.target_branchID)
232
 
            ]
233
 
        expressions = self._getBranchVisibilityExpression()
234
 
        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())
235
286
        if for_branches is not None:
236
287
            branch_ids = [branch.id for branch in for_branches]
237
288
            expressions.append(
245
296
        if statuses is not None:
246
297
            expressions.append(
247
298
                BranchMergeProposal.queue_status.is_in(statuses))
248
 
        return self.store.using(*tables).find(BranchMergeProposal, *expressions)
 
299
        return self.store.with_(with_expr).using(*tables).find(
 
300
            BranchMergeProposal, *expressions)
249
301
 
250
302
    def getMergeProposalsForPerson(self, person, status=None):
251
303
        """See `IBranchCollection`."""
422
474
 
423
475
    def ownedBy(self, person):
424
476
        """See `IBranchCollection`."""
425
 
        return self._filterBy([Branch.owner == person])
 
477
        return self._filterBy([Branch.owner == person], symmetric=False)
426
478
 
427
479
    def ownedByTeamMember(self, person):
428
480
        """See `IBranchCollection`."""
431
483
            where=TeamParticipation.personID==person.id)
432
484
        filter = [In(Branch.ownerID, subquery)]
433
485
 
434
 
        return self._filterBy(filter)
 
486
        return self._filterBy(filter, symmetric=False)
435
487
 
436
488
    def registeredBy(self, person):
437
489
        """See `IBranchCollection`."""
438
 
        return self._filterBy([Branch.registrant == person])
 
490
        return self._filterBy([Branch.registrant == person], symmetric=False)
439
491
 
440
492
    def relatedTo(self, person):
441
493
        """See `IBranchCollection`."""
446
498
                    Select(Branch.id, Branch.registrant == person),
447
499
                    Select(Branch.id,
448
500
                           And(BranchSubscription.person == person,
449
 
                               BranchSubscription.branch == Branch.id))))])
 
501
                               BranchSubscription.branch == Branch.id))))],
 
502
            symmetric=False)
450
503
 
451
504
    def _getExactMatch(self, search_term):
452
505
        """Return the exact branch that 'search_term' matches, or None."""
512
565
            [BranchSubscription.person == person],
513
566
            table=BranchSubscription,
514
567
            join=Join(BranchSubscription,
515
 
                      BranchSubscription.branch == Branch.id))
 
568
                      BranchSubscription.branch == Branch.id),
 
569
            symmetric=False)
516
570
 
517
571
    def targetedBy(self, person, since=None):
518
572
        """See `IBranchCollection`."""
523
577
            clauses,
524
578
            table=BranchMergeProposal,
525
579
            join=Join(BranchMergeProposal,
526
 
                      BranchMergeProposal.target_branch == Branch.id))
 
580
                      BranchMergeProposal.target_branch == Branch.id),
 
581
            symmetric=False)
527
582
 
528
583
    def visibleByUser(self, person):
529
584
        """See `IBranchCollection`."""
533
588
        if person is None:
534
589
            return AnonymousBranchCollection(
535
590
                self._store, self._branch_filter_expressions,
536
 
                self._tables, self._exclude_from_search)
 
591
                self._tables, self._exclude_from_search,
 
592
                self._asymmetric_filter_expressions, self._asymmetric_tables)
537
593
        return VisibleBranchCollection(
538
594
            person, self._store, self._branch_filter_expressions,
539
 
            self._tables, self._exclude_from_search)
 
595
            self._tables, self._exclude_from_search,
 
596
            self._asymmetric_filter_expressions, self._asymmetric_tables)
540
597
 
541
598
    def withBranchType(self, *branch_types):
542
 
        return self._filterBy([Branch.branch_type.is_in(branch_types)])
 
599
        return self._filterBy([Branch.branch_type.is_in(branch_types)],
 
600
            symmetric=False)
543
601
 
544
602
    def withLifecycleStatus(self, *statuses):
545
603
        """See `IBranchCollection`."""
546
 
        return self._filterBy([Branch.lifecycle_status.is_in(statuses)])
 
604
        return self._filterBy([Branch.lifecycle_status.is_in(statuses)],
 
605
            symmetric=False)
547
606
 
548
607
    def modifiedSince(self, epoch):
549
608
        """See `IBranchCollection`."""
550
 
        return self._filterBy([Branch.date_last_modified > epoch])
 
609
        return self._filterBy([Branch.date_last_modified > epoch],
 
610
            symmetric=False)
551
611
 
552
612
    def scannedSince(self, epoch):
553
613
        """See `IBranchCollection`."""
554
 
        return self._filterBy([Branch.last_scanned > epoch])
 
614
        return self._filterBy([Branch.last_scanned > epoch], symmetric=False)
555
615
 
556
616
 
557
617
class AnonymousBranchCollection(GenericBranchCollection):
558
618
    """Branch collection that only shows public branches."""
559
619
 
560
 
    def __init__(self, store=None, branch_filter_expressions=None,
561
 
                 tables=None, exclude_from_search=None):
562
 
        super(AnonymousBranchCollection, self).__init__(
563
 
            store=store,
564
 
            branch_filter_expressions=list(branch_filter_expressions),
565
 
            tables=tables, exclude_from_search=exclude_from_search)
566
 
 
567
620
    def _getBranchVisibilityExpression(self, branch_class=Branch):
568
621
        """Return the where clauses for visibility."""
569
622
        return [branch_class.private == False]
570
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
 
571
636
 
572
637
class VisibleBranchCollection(GenericBranchCollection):
573
638
    """A branch collection that has special logic for visibility."""
574
639
 
575
640
    def __init__(self, user, store=None, branch_filter_expressions=None,
576
 
                 tables=None, exclude_from_search=None):
 
641
                 tables=None, exclude_from_search=None,
 
642
                 asymmetric_filter_expressions=None, asymmetric_tables=None):
577
643
        super(VisibleBranchCollection, self).__init__(
578
644
            store=store, branch_filter_expressions=branch_filter_expressions,
579
 
            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)
580
648
        self._user = user
581
649
        self._private_branch_ids = self._getPrivateBranchSubQuery()
582
650
 
583
651
    def _filterBy(self, expressions, table=None, join=None,
584
 
                  exclude_from_search=None):
585
 
        """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
        """
586
659
        # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
587
660
        # for explicit 'tables' by harnessing Storm's table inference system.
588
661
        # See http://paste.ubuntu.com/118711/ for one way to do that.
589
 
        tables = self._tables.copy()
590
662
        if table is not None:
591
663
            if join is None:
592
664
                raise InvalidFilter("Cannot specify a table without a join.")
593
 
            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
594
679
        if exclude_from_search is None:
595
680
            exclude_from_search = []
596
 
        if expressions is None:
597
 
            expressions = []
598
681
        return self.__class__(
599
682
            self._user,
600
683
            self.store,
601
 
            self._branch_filter_expressions + expressions,
 
684
            symmetric_expr,
602
685
            tables,
603
 
            self._exclude_from_search + exclude_from_search)
 
686
            self._exclude_from_search + exclude_from_search,
 
687
            asymmetric_expr,
 
688
            asymmetric_tables)
604
689
 
605
690
    def _getPrivateBranchSubQuery(self):
606
691
        """Return a subquery to get the private branches the user can see.
651
736
                branch_class.id.is_in(self._private_branch_ids))
652
737
            return [public_or_private]
653
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.team,
 
754
                TeamParticipation.personID == person)._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
 
654
766
    def visibleByUser(self, person):
655
767
        """See `IBranchCollection`."""
656
768
        if person == self._user: