Merge lp:~lifeless/launchpad/bug-736008 into lp:launchpad

Proposed by Robert Collins
Status: Merged
Approved by: Robert Collins
Approved revision: no longer in the source branch.
Merged at revision: 12681
Proposed branch: lp:~lifeless/launchpad/bug-736008
Merge into: lp:launchpad
Diff against target: 421 lines (+169/-55)
3 files modified
lib/lp/code/model/branchcollection.py (+166/-52)
lib/lp/code/model/tests/test_branchcollection.py (+2/-2)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~lifeless/launchpad/bug-736008
Reviewer Review Type Date Requested Status
Steve Kowalik (community) Approve
Review via email: mp+55028@code.launchpad.net

Commit message

[r=stevenk][bug=736008] Another optimisation attempt for Product:+code-index branch merge proposal counting

Description of the change

Take 3 at fixing this bug: separate clauses that should apply to both source and target branches and those for just the source branch. Then use with to tell sql we're looking for common constraints on both source and target. This lowers the reported cost from 50K to 20K even for launchpad, on qastaging.

There is a new storm needed as part of this - I hadn't made select statements usable in WITH clauses before.

To post a comment you must log in.
Revision history for this message
Steve Kowalik (stevenk) wrote :

After discussing my concerns with Robert on IRC, I'm happy with this branch.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/model/branchcollection.py'
--- lib/lp/code/model/branchcollection.py 2011-03-28 20:54:50 +0000
+++ lib/lp/code/model/branchcollection.py 2011-03-28 22:49:01 +0000
@@ -19,7 +19,9 @@
19 LeftJoin,19 LeftJoin,
20 Or,20 Or,
21 Select,21 Select,
22 SQL,
22 Union,23 Union,
24 With,
23 )25 )
24from storm.info import ClassAlias26from storm.info import ClassAlias
25from zope.component import getUtility27from zope.component import getUtility
@@ -81,7 +83,8 @@
81 implements(IBranchCollection)83 implements(IBranchCollection)
8284
83 def __init__(self, store=None, branch_filter_expressions=None,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 """Construct a `GenericBranchCollection`.88 """Construct a `GenericBranchCollection`.
8689
87 :param store: The store to look in for branches. If not specified,90 :param store: The store to look in for branches. If not specified,
@@ -93,14 +96,25 @@
93 :param tables: A dict of Storm tables to the Join expression. If an96 :param tables: A dict of Storm tables to the Join expression. If an
94 expression in branch_filter_expressions refers to a table, then97 expression in branch_filter_expressions refers to a table, then
95 that table *must* be in this list.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 self._store = store104 self._store = store
98 if branch_filter_expressions is None:105 if branch_filter_expressions is None:
99 branch_filter_expressions = []106 branch_filter_expressions = []
100 self._branch_filter_expressions = branch_filter_expressions107 self._branch_filter_expressions = list(branch_filter_expressions)
101 if tables is None:108 if tables is None:
102 tables = {}109 tables = {}
103 self._tables = tables110 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 if exclude_from_search is None:118 if exclude_from_search is None:
105 exclude_from_search = []119 exclude_from_search = []
106 self._exclude_from_search = exclude_from_search120 self._exclude_from_search = exclude_from_search
@@ -132,25 +146,42 @@
132 return self._store146 return self._store
133147
134 def _filterBy(self, expressions, table=None, join=None,148 def _filterBy(self, expressions, table=None, join=None,
135 exclude_from_search=None):149 exclude_from_search=None, symmetric=True):
136 """Return a subset of this collection, filtered by 'expressions'."""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 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need156 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
138 # for explicit 'tables' by harnessing Storm's table inference system.157 # for explicit 'tables' by harnessing Storm's table inference system.
139 # See http://paste.ubuntu.com/118711/ for one way to do that.158 # See http://paste.ubuntu.com/118711/ for one way to do that.
140 tables = self._tables.copy()
141 if table is not None:159 if table is not None:
142 if join is None:160 if join is None:
143 raise InvalidFilter("Cannot specify a table without a join.")161 raise InvalidFilter("Cannot specify a table without a join.")
144 tables[table] = join162 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 if exclude_from_search is None:176 if exclude_from_search is None:
146 exclude_from_search = []177 exclude_from_search = []
147 if expressions is None:
148 expressions = []
149 return self.__class__(178 return self.__class__(
150 self.store,179 self.store,
151 self._branch_filter_expressions + expressions,180 symmetric_expr,
152 tables,181 tables,
153 self._exclude_from_search + exclude_from_search)182 self._exclude_from_search + exclude_from_search,
183 asymmetric_expr,
184 asymmetric_tables)
154185
155 def _getBranchIdQuery(self):186 def _getBranchIdQuery(self):
156 """Return a Storm 'Select' for the branch IDs in this collection."""187 """Return a Storm 'Select' for the branch IDs in this collection."""
@@ -160,16 +191,28 @@
160191
161 def _getBranchExpressions(self):192 def _getBranchExpressions(self):
162 """Return the where expressions for this collection."""193 """Return the where expressions for this collection."""
163 return (self._branch_filter_expressions194 return (self._branch_filter_expressions +
164 + self._getBranchVisibilityExpression())195 self._asymmetric_filter_expressions +
196 self._getBranchVisibilityExpression())
165197
166 def _getBranchVisibilityExpression(self, branch_class=None):198 def _getBranchVisibilityExpression(self, branch_class=None):
167 """Return the where clauses for visibility."""199 """Return the where clauses for visibility."""
168 return []200 return []
169201
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 def getBranches(self, eager_load=False):211 def getBranches(self, eager_load=False):
171 """See `IBranchCollection`."""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 expressions = self._getBranchExpressions()216 expressions = self._getBranchExpressions()
174 resultset = self.store.using(*tables).find(Branch, *expressions)217 resultset = self.store.using(*tables).find(Branch, *expressions)
175 if not eager_load:218 if not eager_load:
@@ -221,15 +264,25 @@
221 def getMergeProposals(self, statuses=None, for_branches=None,264 def getMergeProposals(self, statuses=None, for_branches=None,
222 target_branch=None, merged_revnos=None):265 target_branch=None, merged_revnos=None):
223 """See `IBranchCollection`."""266 """See `IBranchCollection`."""
224 Target = ClassAlias(Branch, "target")267 # teams = SQL("teams as (SELECT team from teamparticipation where person=%s)" % sqlvalues
225 tables = [Branch] + self._tables.values() + [268 scope_tables = [Branch] + self._tables.values()
226 Join(BranchMergeProposal, And(269 scope_expressions = self._branch_filter_expressions
227 Branch.id==BranchMergeProposal.source_branchID,270 select = self.store.using(*scope_tables).find(
228 *self._branch_filter_expressions)),271 (Branch.id, Branch.private, Branch.ownerID), *scope_expressions)
229 Join(Target, Target.id==BranchMergeProposal.target_branchID)272 branches_query = select._get_select()
230 ]273 with_expr = [With("scope_branches", branches_query)
231 expressions = self._getBranchVisibilityExpression()274 ] + self._getCandidateBranchesWith()
232 expressions.extend(self._getBranchVisibilityExpression(Target))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 if for_branches is not None:286 if for_branches is not None:
234 branch_ids = [branch.id for branch in for_branches]287 branch_ids = [branch.id for branch in for_branches]
235 expressions.append(288 expressions.append(
@@ -243,7 +296,8 @@
243 if statuses is not None:296 if statuses is not None:
244 expressions.append(297 expressions.append(
245 BranchMergeProposal.queue_status.is_in(statuses))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)
247301
248 def getMergeProposalsForPerson(self, person, status=None):302 def getMergeProposalsForPerson(self, person, status=None):
249 """See `IBranchCollection`."""303 """See `IBranchCollection`."""
@@ -420,7 +474,7 @@
420474
421 def ownedBy(self, person):475 def ownedBy(self, person):
422 """See `IBranchCollection`."""476 """See `IBranchCollection`."""
423 return self._filterBy([Branch.owner == person])477 return self._filterBy([Branch.owner == person], symmetric=False)
424478
425 def ownedByTeamMember(self, person):479 def ownedByTeamMember(self, person):
426 """See `IBranchCollection`."""480 """See `IBranchCollection`."""
@@ -429,11 +483,11 @@
429 where=TeamParticipation.personID==person.id)483 where=TeamParticipation.personID==person.id)
430 filter = [In(Branch.ownerID, subquery)]484 filter = [In(Branch.ownerID, subquery)]
431485
432 return self._filterBy(filter)486 return self._filterBy(filter, symmetric=False)
433487
434 def registeredBy(self, person):488 def registeredBy(self, person):
435 """See `IBranchCollection`."""489 """See `IBranchCollection`."""
436 return self._filterBy([Branch.registrant == person])490 return self._filterBy([Branch.registrant == person], symmetric=False)
437491
438 def relatedTo(self, person):492 def relatedTo(self, person):
439 """See `IBranchCollection`."""493 """See `IBranchCollection`."""
@@ -444,7 +498,8 @@
444 Select(Branch.id, Branch.registrant == person),498 Select(Branch.id, Branch.registrant == person),
445 Select(Branch.id,499 Select(Branch.id,
446 And(BranchSubscription.person == person,500 And(BranchSubscription.person == person,
447 BranchSubscription.branch == Branch.id))))])501 BranchSubscription.branch == Branch.id))))],
502 symmetric=False)
448503
449 def _getExactMatch(self, search_term):504 def _getExactMatch(self, search_term):
450 """Return the exact branch that 'search_term' matches, or None."""505 """Return the exact branch that 'search_term' matches, or None."""
@@ -510,7 +565,8 @@
510 [BranchSubscription.person == person],565 [BranchSubscription.person == person],
511 table=BranchSubscription,566 table=BranchSubscription,
512 join=Join(BranchSubscription,567 join=Join(BranchSubscription,
513 BranchSubscription.branch == Branch.id))568 BranchSubscription.branch == Branch.id),
569 symmetric=False)
514570
515 def targetedBy(self, person, since=None):571 def targetedBy(self, person, since=None):
516 """See `IBranchCollection`."""572 """See `IBranchCollection`."""
@@ -521,7 +577,8 @@
521 clauses,577 clauses,
522 table=BranchMergeProposal,578 table=BranchMergeProposal,
523 join=Join(BranchMergeProposal,579 join=Join(BranchMergeProposal,
524 BranchMergeProposal.target_branch == Branch.id))580 BranchMergeProposal.target_branch == Branch.id),
581 symmetric=False)
525582
526 def visibleByUser(self, person):583 def visibleByUser(self, person):
527 """See `IBranchCollection`."""584 """See `IBranchCollection`."""
@@ -531,74 +588,104 @@
531 if person is None:588 if person is None:
532 return AnonymousBranchCollection(589 return AnonymousBranchCollection(
533 self._store, self._branch_filter_expressions,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 return VisibleBranchCollection(593 return VisibleBranchCollection(
536 person, self._store, self._branch_filter_expressions,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)
538597
539 def withBranchType(self, *branch_types):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)
541601
542 def withLifecycleStatus(self, *statuses):602 def withLifecycleStatus(self, *statuses):
543 """See `IBranchCollection`."""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)
545606
546 def modifiedSince(self, epoch):607 def modifiedSince(self, epoch):
547 """See `IBranchCollection`."""608 """See `IBranchCollection`."""
548 return self._filterBy([Branch.date_last_modified > epoch])609 return self._filterBy([Branch.date_last_modified > epoch],
610 symmetric=False)
549611
550 def scannedSince(self, epoch):612 def scannedSince(self, epoch):
551 """See `IBranchCollection`."""613 """See `IBranchCollection`."""
552 return self._filterBy([Branch.last_scanned > epoch])614 return self._filterBy([Branch.last_scanned > epoch], symmetric=False)
553615
554616
555class AnonymousBranchCollection(GenericBranchCollection):617class AnonymousBranchCollection(GenericBranchCollection):
556 """Branch collection that only shows public branches."""618 """Branch collection that only shows public branches."""
557619
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 def _getBranchVisibilityExpression(self, branch_class=Branch):620 def _getBranchVisibilityExpression(self, branch_class=Branch):
566 """Return the where clauses for visibility."""621 """Return the where clauses for visibility."""
567 return [branch_class.private == False]622 return [branch_class.private == False]
568623
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
569636
570class VisibleBranchCollection(GenericBranchCollection):637class VisibleBranchCollection(GenericBranchCollection):
571 """A branch collection that has special logic for visibility."""638 """A branch collection that has special logic for visibility."""
572639
573 def __init__(self, user, store=None, branch_filter_expressions=None,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 super(VisibleBranchCollection, self).__init__(643 super(VisibleBranchCollection, self).__init__(
576 store=store, branch_filter_expressions=branch_filter_expressions,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 self._user = user648 self._user = user
579 self._private_branch_ids = self._getPrivateBranchSubQuery()649 self._private_branch_ids = self._getPrivateBranchSubQuery()
580650
581 def _filterBy(self, expressions, table=None, join=None,651 def _filterBy(self, expressions, table=None, join=None,
582 exclude_from_search=None):652 exclude_from_search=None, symmetric=True):
583 """Return a subset of this collection, filtered by 'expressions'."""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 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need659 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
585 # for explicit 'tables' by harnessing Storm's table inference system.660 # for explicit 'tables' by harnessing Storm's table inference system.
586 # See http://paste.ubuntu.com/118711/ for one way to do that.661 # See http://paste.ubuntu.com/118711/ for one way to do that.
587 tables = self._tables.copy()
588 if table is not None:662 if table is not None:
589 if join is None:663 if join is None:
590 raise InvalidFilter("Cannot specify a table without a join.")664 raise InvalidFilter("Cannot specify a table without a join.")
591 tables[table] = join665 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 if exclude_from_search is None:679 if exclude_from_search is None:
593 exclude_from_search = []680 exclude_from_search = []
594 if expressions is None:
595 expressions = []
596 return self.__class__(681 return self.__class__(
597 self._user,682 self._user,
598 self.store,683 self.store,
599 self._branch_filter_expressions + expressions,684 symmetric_expr,
600 tables,685 tables,
601 self._exclude_from_search + exclude_from_search)686 self._exclude_from_search + exclude_from_search,
687 asymmetric_expr,
688 asymmetric_tables)
602689
603 def _getPrivateBranchSubQuery(self):690 def _getPrivateBranchSubQuery(self):
604 """Return a subquery to get the private branches the user can see.691 """Return a subquery to get the private branches the user can see.
@@ -649,6 +736,33 @@
649 branch_class.id.is_in(self._private_branch_ids))736 branch_class.id.is_in(self._private_branch_ids))
650 return [public_or_private]737 return [public_or_private]
651738
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 def visibleByUser(self, person):766 def visibleByUser(self, person):
653 """See `IBranchCollection`."""767 """See `IBranchCollection`."""
654 if person == self._user:768 if person == self._user:
655769
=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
--- lib/lp/code/model/tests/test_branchcollection.py 2011-03-07 12:43:40 +0000
+++ lib/lp/code/model/tests/test_branchcollection.py 2011-03-28 22:49:01 +0000
@@ -746,8 +746,8 @@
746746
747 def test_just_owned_branch_merge_proposals(self):747 def test_just_owned_branch_merge_proposals(self):
748 # If the collection only includes branches owned by a person, the748 # If the collection only includes branches owned by a person, the
749 # getMergeProposals() will only return merge proposals for branches749 # getMergeProposals() will only return merge proposals for source
750 # that are owned by that person.750 # branches that are owned by that person.
751 person = self.factory.makePerson()751 person = self.factory.makePerson()
752 product = self.factory.makeProduct()752 product = self.factory.makeProduct()
753 branch1 = self.factory.makeProductBranch(753 branch1 = self.factory.makeProductBranch(
754754
=== modified file 'versions.cfg'
--- versions.cfg 2011-03-23 16:28:51 +0000
+++ versions.cfg 2011-03-28 22:49:01 +0000
@@ -72,7 +72,7 @@
72Sphinx = 1.0.772Sphinx = 1.0.7
73soupmatchers = 0.1r5373soupmatchers = 0.1r53
74sourcecodegen = 0.6.974sourcecodegen = 0.6.9
75storm = 0.18.0.99-lpwithnodatetime-r38975storm = 0.18.0.99-lpwithnodatetime-r391
76testtools = 0.9.876testtools = 0.9.8
77transaction = 1.0.077transaction = 1.0.0
78Twisted = 10.2.0-4395fix-1-490978Twisted = 10.2.0-4395fix-1-4909