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
1=== modified file 'lib/lp/code/model/branchcollection.py'
2--- lib/lp/code/model/branchcollection.py 2011-03-28 20:54:50 +0000
3+++ lib/lp/code/model/branchcollection.py 2011-03-28 22:49:01 +0000
4@@ -19,7 +19,9 @@
5 LeftJoin,
6 Or,
7 Select,
8+ SQL,
9 Union,
10+ With,
11 )
12 from storm.info import ClassAlias
13 from zope.component import getUtility
14@@ -81,7 +83,8 @@
15 implements(IBranchCollection)
16
17 def __init__(self, store=None, branch_filter_expressions=None,
18- tables=None, exclude_from_search=None):
19+ tables=None, exclude_from_search=None,
20+ asymmetric_filter_expressions=None, asymmetric_tables=None):
21 """Construct a `GenericBranchCollection`.
22
23 :param store: The store to look in for branches. If not specified,
24@@ -93,14 +96,25 @@
25 :param tables: A dict of Storm tables to the Join expression. If an
26 expression in branch_filter_expressions refers to a table, then
27 that table *must* be in this list.
28+ :param asymmetric_filter_expressions: As per branch_filter_expressions
29+ but only applies to one side of reflexive joins.
30+ :param asymmetric_tables: As per tables, for
31+ asymmetric_filter_expressions.
32 """
33 self._store = store
34 if branch_filter_expressions is None:
35 branch_filter_expressions = []
36- self._branch_filter_expressions = branch_filter_expressions
37+ self._branch_filter_expressions = list(branch_filter_expressions)
38 if tables is None:
39 tables = {}
40 self._tables = tables
41+ if asymmetric_filter_expressions is None:
42+ asymmetric_filter_expressions = []
43+ self._asymmetric_filter_expressions = list(
44+ asymmetric_filter_expressions)
45+ if asymmetric_tables is None:
46+ asymmetric_tables = {}
47+ self._asymmetric_tables = asymmetric_tables
48 if exclude_from_search is None:
49 exclude_from_search = []
50 self._exclude_from_search = exclude_from_search
51@@ -132,25 +146,42 @@
52 return self._store
53
54 def _filterBy(self, expressions, table=None, join=None,
55- exclude_from_search=None):
56- """Return a subset of this collection, filtered by 'expressions'."""
57+ exclude_from_search=None, symmetric=True):
58+ """Return a subset of this collection, filtered by 'expressions'.
59+
60+ :param symmetric: If True this filter will apply to both sides of merge
61+ proposal lookups and any other lookups that join Branch back onto
62+ Branch.
63+ """
64 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
65 # for explicit 'tables' by harnessing Storm's table inference system.
66 # See http://paste.ubuntu.com/118711/ for one way to do that.
67- tables = self._tables.copy()
68 if table is not None:
69 if join is None:
70 raise InvalidFilter("Cannot specify a table without a join.")
71- tables[table] = join
72+ if expressions is None:
73+ expressions = []
74+ tables = self._tables.copy()
75+ asymmetric_tables = self._asymmetric_tables.copy()
76+ if symmetric:
77+ if table is not None:
78+ tables[table] = join
79+ symmetric_expr = self._branch_filter_expressions + expressions
80+ asymmetric_expr = list(self._asymmetric_filter_expressions)
81+ else:
82+ if table is not None:
83+ asymmetric_tables[table] = join
84+ symmetric_expr = list(self._branch_filter_expressions)
85+ asymmetric_expr = self._asymmetric_filter_expressions + expressions
86 if exclude_from_search is None:
87 exclude_from_search = []
88- if expressions is None:
89- expressions = []
90 return self.__class__(
91 self.store,
92- self._branch_filter_expressions + expressions,
93+ symmetric_expr,
94 tables,
95- self._exclude_from_search + exclude_from_search)
96+ self._exclude_from_search + exclude_from_search,
97+ asymmetric_expr,
98+ asymmetric_tables)
99
100 def _getBranchIdQuery(self):
101 """Return a Storm 'Select' for the branch IDs in this collection."""
102@@ -160,16 +191,28 @@
103
104 def _getBranchExpressions(self):
105 """Return the where expressions for this collection."""
106- return (self._branch_filter_expressions
107- + self._getBranchVisibilityExpression())
108+ return (self._branch_filter_expressions +
109+ self._asymmetric_filter_expressions +
110+ self._getBranchVisibilityExpression())
111
112 def _getBranchVisibilityExpression(self, branch_class=None):
113 """Return the where clauses for visibility."""
114 return []
115
116+ def _getCandidateBranchesWith(self):
117+ """Return WITH clauses defining candidate branches.
118+
119+ These are defined in terms of scope_branches which should be separately
120+ calculated.
121+ """
122+ return [
123+ With("candidate_branches", SQL("SELECT id from scope_branches"))]
124+
125 def getBranches(self, eager_load=False):
126 """See `IBranchCollection`."""
127- tables = [Branch] + self._tables.values()
128+ all_tables = set(
129+ self._tables.values() + self._asymmetric_tables.values())
130+ tables = [Branch] + list(all_tables)
131 expressions = self._getBranchExpressions()
132 resultset = self.store.using(*tables).find(Branch, *expressions)
133 if not eager_load:
134@@ -221,15 +264,25 @@
135 def getMergeProposals(self, statuses=None, for_branches=None,
136 target_branch=None, merged_revnos=None):
137 """See `IBranchCollection`."""
138- Target = ClassAlias(Branch, "target")
139- tables = [Branch] + self._tables.values() + [
140- Join(BranchMergeProposal, And(
141- Branch.id==BranchMergeProposal.source_branchID,
142- *self._branch_filter_expressions)),
143- Join(Target, Target.id==BranchMergeProposal.target_branchID)
144- ]
145- expressions = self._getBranchVisibilityExpression()
146- expressions.extend(self._getBranchVisibilityExpression(Target))
147+ # teams = SQL("teams as (SELECT team from teamparticipation where person=%s)" % sqlvalues
148+ scope_tables = [Branch] + self._tables.values()
149+ scope_expressions = self._branch_filter_expressions
150+ select = self.store.using(*scope_tables).find(
151+ (Branch.id, Branch.private, Branch.ownerID), *scope_expressions)
152+ branches_query = select._get_select()
153+ with_expr = [With("scope_branches", branches_query)
154+ ] + self._getCandidateBranchesWith()
155+ expressions = [SQL("""
156+ source_branch IN (SELECT id FROM candidate_branches) AND
157+ target_branch IN (SELECT id FROM candidate_branches)""")]
158+ tables = [BranchMergeProposal]
159+ if self._asymmetric_filter_expressions:
160+ # Need to filter on Branch beyond the with constraints.
161+ expressions += self._asymmetric_filter_expressions
162+ expressions.append(
163+ BranchMergeProposal.source_branchID == Branch.id)
164+ tables.append(Branch)
165+ tables.extend(self._asymmetric_tables.values())
166 if for_branches is not None:
167 branch_ids = [branch.id for branch in for_branches]
168 expressions.append(
169@@ -243,7 +296,8 @@
170 if statuses is not None:
171 expressions.append(
172 BranchMergeProposal.queue_status.is_in(statuses))
173- return self.store.using(*tables).find(BranchMergeProposal, *expressions)
174+ return self.store.with_(with_expr).using(*tables).find(
175+ BranchMergeProposal, *expressions)
176
177 def getMergeProposalsForPerson(self, person, status=None):
178 """See `IBranchCollection`."""
179@@ -420,7 +474,7 @@
180
181 def ownedBy(self, person):
182 """See `IBranchCollection`."""
183- return self._filterBy([Branch.owner == person])
184+ return self._filterBy([Branch.owner == person], symmetric=False)
185
186 def ownedByTeamMember(self, person):
187 """See `IBranchCollection`."""
188@@ -429,11 +483,11 @@
189 where=TeamParticipation.personID==person.id)
190 filter = [In(Branch.ownerID, subquery)]
191
192- return self._filterBy(filter)
193+ return self._filterBy(filter, symmetric=False)
194
195 def registeredBy(self, person):
196 """See `IBranchCollection`."""
197- return self._filterBy([Branch.registrant == person])
198+ return self._filterBy([Branch.registrant == person], symmetric=False)
199
200 def relatedTo(self, person):
201 """See `IBranchCollection`."""
202@@ -444,7 +498,8 @@
203 Select(Branch.id, Branch.registrant == person),
204 Select(Branch.id,
205 And(BranchSubscription.person == person,
206- BranchSubscription.branch == Branch.id))))])
207+ BranchSubscription.branch == Branch.id))))],
208+ symmetric=False)
209
210 def _getExactMatch(self, search_term):
211 """Return the exact branch that 'search_term' matches, or None."""
212@@ -510,7 +565,8 @@
213 [BranchSubscription.person == person],
214 table=BranchSubscription,
215 join=Join(BranchSubscription,
216- BranchSubscription.branch == Branch.id))
217+ BranchSubscription.branch == Branch.id),
218+ symmetric=False)
219
220 def targetedBy(self, person, since=None):
221 """See `IBranchCollection`."""
222@@ -521,7 +577,8 @@
223 clauses,
224 table=BranchMergeProposal,
225 join=Join(BranchMergeProposal,
226- BranchMergeProposal.target_branch == Branch.id))
227+ BranchMergeProposal.target_branch == Branch.id),
228+ symmetric=False)
229
230 def visibleByUser(self, person):
231 """See `IBranchCollection`."""
232@@ -531,74 +588,104 @@
233 if person is None:
234 return AnonymousBranchCollection(
235 self._store, self._branch_filter_expressions,
236- self._tables, self._exclude_from_search)
237+ self._tables, self._exclude_from_search,
238+ self._asymmetric_filter_expressions, self._asymmetric_tables)
239 return VisibleBranchCollection(
240 person, self._store, self._branch_filter_expressions,
241- self._tables, self._exclude_from_search)
242+ self._tables, self._exclude_from_search,
243+ self._asymmetric_filter_expressions, self._asymmetric_tables)
244
245 def withBranchType(self, *branch_types):
246- return self._filterBy([Branch.branch_type.is_in(branch_types)])
247+ return self._filterBy([Branch.branch_type.is_in(branch_types)],
248+ symmetric=False)
249
250 def withLifecycleStatus(self, *statuses):
251 """See `IBranchCollection`."""
252- return self._filterBy([Branch.lifecycle_status.is_in(statuses)])
253+ return self._filterBy([Branch.lifecycle_status.is_in(statuses)],
254+ symmetric=False)
255
256 def modifiedSince(self, epoch):
257 """See `IBranchCollection`."""
258- return self._filterBy([Branch.date_last_modified > epoch])
259+ return self._filterBy([Branch.date_last_modified > epoch],
260+ symmetric=False)
261
262 def scannedSince(self, epoch):
263 """See `IBranchCollection`."""
264- return self._filterBy([Branch.last_scanned > epoch])
265+ return self._filterBy([Branch.last_scanned > epoch], symmetric=False)
266
267
268 class AnonymousBranchCollection(GenericBranchCollection):
269 """Branch collection that only shows public branches."""
270
271- def __init__(self, store=None, branch_filter_expressions=None,
272- tables=None, exclude_from_search=None):
273- super(AnonymousBranchCollection, self).__init__(
274- store=store,
275- branch_filter_expressions=list(branch_filter_expressions),
276- tables=tables, exclude_from_search=exclude_from_search)
277-
278 def _getBranchVisibilityExpression(self, branch_class=Branch):
279 """Return the where clauses for visibility."""
280 return [branch_class.private == False]
281
282+ def _getCandidateBranchesWith(self):
283+ """Return WITH clauses defining candidate branches.
284+
285+ These are defined in terms of scope_branches which should be separately
286+ calculated.
287+ """
288+ # Anonymous users get public branches only.
289+ return [
290+ With("candidate_branches",
291+ SQL("select id from scope_branches where not private"))
292+ ]
293+
294
295 class VisibleBranchCollection(GenericBranchCollection):
296 """A branch collection that has special logic for visibility."""
297
298 def __init__(self, user, store=None, branch_filter_expressions=None,
299- tables=None, exclude_from_search=None):
300+ tables=None, exclude_from_search=None,
301+ asymmetric_filter_expressions=None, asymmetric_tables=None):
302 super(VisibleBranchCollection, self).__init__(
303 store=store, branch_filter_expressions=branch_filter_expressions,
304- tables=tables, exclude_from_search=exclude_from_search)
305+ tables=tables, exclude_from_search=exclude_from_search,
306+ asymmetric_filter_expressions=asymmetric_filter_expressions,
307+ asymmetric_tables=asymmetric_tables)
308 self._user = user
309 self._private_branch_ids = self._getPrivateBranchSubQuery()
310
311 def _filterBy(self, expressions, table=None, join=None,
312- exclude_from_search=None):
313- """Return a subset of this collection, filtered by 'expressions'."""
314+ exclude_from_search=None, symmetric=True):
315+ """Return a subset of this collection, filtered by 'expressions'.
316+
317+ :param symmetric: If True this filter will apply to both sides of merge
318+ proposal lookups and any other lookups that join Branch back onto
319+ Branch.
320+ """
321 # NOTE: JonathanLange 2009-02-17: We might be able to avoid the need
322 # for explicit 'tables' by harnessing Storm's table inference system.
323 # See http://paste.ubuntu.com/118711/ for one way to do that.
324- tables = self._tables.copy()
325 if table is not None:
326 if join is None:
327 raise InvalidFilter("Cannot specify a table without a join.")
328- tables[table] = join
329+ if expressions is None:
330+ expressions = []
331+ tables = self._tables.copy()
332+ asymmetric_tables = self._asymmetric_tables.copy()
333+ if symmetric:
334+ if table is not None:
335+ tables[table] = join
336+ symmetric_expr = self._branch_filter_expressions + expressions
337+ asymmetric_expr = list(self._asymmetric_filter_expressions)
338+ else:
339+ if table is not None:
340+ asymmetric_tables[table] = join
341+ symmetric_expr = list(self._branch_filter_expressions)
342+ asymmetric_expr = self._asymmetric_filter_expressions + expressions
343 if exclude_from_search is None:
344 exclude_from_search = []
345- if expressions is None:
346- expressions = []
347 return self.__class__(
348 self._user,
349 self.store,
350- self._branch_filter_expressions + expressions,
351+ symmetric_expr,
352 tables,
353- self._exclude_from_search + exclude_from_search)
354+ self._exclude_from_search + exclude_from_search,
355+ asymmetric_expr,
356+ asymmetric_tables)
357
358 def _getPrivateBranchSubQuery(self):
359 """Return a subquery to get the private branches the user can see.
360@@ -649,6 +736,33 @@
361 branch_class.id.is_in(self._private_branch_ids))
362 return [public_or_private]
363
364+ def _getCandidateBranchesWith(self):
365+ """Return WITH clauses defining candidate branches.
366+
367+ These are defined in terms of scope_branches which should be separately
368+ calculated.
369+ """
370+ person = self._user
371+ if person is None:
372+ # Really an anonymous sitation
373+ return [
374+ With("candidate_branches",
375+ SQL("select id from scope_branches where not private"))
376+ ]
377+ return [
378+ With("teams", self.store.find(TeamParticipation.teamID,
379+ TeamParticipation.personID == person.id)._get_select()),
380+ With("private_branches", SQL("""
381+ SELECT scope_branches.id FROM scope_branches WHERE
382+ scope_branches.private AND ((scope_branches.owner in (select team from teams) OR
383+ EXISTS(SELECT true from BranchSubscription, teams WHERE
384+ branchsubscription.branch = scope_branches.id AND
385+ branchsubscription.person = teams.team)))""")),
386+ With("candidate_branches", SQL("""
387+ (SELECT id FROM private_branches) UNION
388+ (select id FROM scope_branches WHERE not private)"""))
389+ ]
390+
391 def visibleByUser(self, person):
392 """See `IBranchCollection`."""
393 if person == self._user:
394
395=== modified file 'lib/lp/code/model/tests/test_branchcollection.py'
396--- lib/lp/code/model/tests/test_branchcollection.py 2011-03-07 12:43:40 +0000
397+++ lib/lp/code/model/tests/test_branchcollection.py 2011-03-28 22:49:01 +0000
398@@ -746,8 +746,8 @@
399
400 def test_just_owned_branch_merge_proposals(self):
401 # If the collection only includes branches owned by a person, the
402- # getMergeProposals() will only return merge proposals for branches
403- # that are owned by that person.
404+ # getMergeProposals() will only return merge proposals for source
405+ # branches that are owned by that person.
406 person = self.factory.makePerson()
407 product = self.factory.makeProduct()
408 branch1 = self.factory.makeProductBranch(
409
410=== modified file 'versions.cfg'
411--- versions.cfg 2011-03-23 16:28:51 +0000
412+++ versions.cfg 2011-03-28 22:49:01 +0000
413@@ -72,7 +72,7 @@
414 Sphinx = 1.0.7
415 soupmatchers = 0.1r53
416 sourcecodegen = 0.6.9
417-storm = 0.18.0.99-lpwithnodatetime-r389
418+storm = 0.18.0.99-lpwithnodatetime-r391
419 testtools = 0.9.8
420 transaction = 1.0.0
421 Twisted = 10.2.0-4395fix-1-4909