Merge lp:~lifeless/launchpad/bug-736008 into lp:launchpad
- bug-736008
- Merge into devel
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 | ||||
Related bugs: |
|
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.
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 |
After discussing my concerns with Robert on IRC, I'm happy with this branch.