Merge lp:~adeuring/launchpad/bug-675595 into lp:launchpad

Proposed by Abel Deuring
Status: Merged
Approved by: Graham Binns
Approved revision: no longer in the source branch.
Merged at revision: 11973
Proposed branch: lp:~adeuring/launchpad/bug-675595
Merge into: lp:launchpad
Diff against target: 434 lines (+183/-22)
7 files modified
lib/lp/bugs/interfaces/bugtarget.py (+6/-3)
lib/lp/bugs/interfaces/bugtask.py (+11/-7)
lib/lp/bugs/model/bugtarget.py (+4/-4)
lib/lp/bugs/model/bugtask.py (+10/-2)
lib/lp/bugs/tests/test_bugtarget.py (+108/-1)
lib/lp/bugs/tests/test_bugtask_search.py (+37/-2)
lib/lp/registry/model/person.py (+7/-3)
To merge this branch: bzr merge lp:~adeuring/launchpad/bug-675595
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+41595@code.launchpad.net

Description of the change

This branch is a first step to fix bug 675595:
BugTaskSet.getAssignedMilestonesFromSearch() can call BugTaskSet._search()
directly and retrieve the milestones with one SQL query instead of two.

It adds an optional parameter "prejoins" to BugTaskSet.search()
and to HasBugs.searchTasks().

BugTaskSet.search() already prejoined a number of tables in order to
reduce the total query count; in certain cases it makes sense to
prejoin other tables too.

tests:

./bin/test -vvt test_bugtarget -t test_bugtask_search

no lint

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/interfaces/bugtarget.py'
2--- lib/lp/bugs/interfaces/bugtarget.py 2010-10-21 16:40:10 +0000
3+++ lib/lp/bugs/interfaces/bugtarget.py 2010-11-23 13:40:38 +0000
4@@ -82,7 +82,8 @@
5 "omit_duplicates": copy_field(IBugTaskSearch['omit_dupes']),
6 "omit_targeted": copy_field(IBugTaskSearch['omit_targeted']),
7 "status_upstream": copy_field(IBugTaskSearch['status_upstream']),
8- "milestone_assignment": copy_field(IBugTaskSearch['milestone_assignment']),
9+ "milestone_assignment": copy_field(
10+ IBugTaskSearch['milestone_assignment']),
11 "milestone": copy_field(IBugTaskSearch['milestone']),
12 "component": copy_field(IBugTaskSearch['component']),
13 "nominated_for": Reference(schema=Interface),
14@@ -232,7 +233,7 @@
15 hardware_owner_is_subscribed_to_bug=False,
16 hardware_is_linked_to_bug=False, linked_branches=None,
17 structural_subscriber=None, modified_since=None,
18- created_since=None):
19+ created_since=None, prejoins=[]):
20 """Search the IBugTasks reported on this entity.
21
22 :search_params: a BugTaskSearchParams object
23@@ -337,8 +338,10 @@
24 :istargeted: Is there a fix targeted to this series?
25 :sourcepackage: The sourcepackage to which the fix would be targeted.
26 :assignee: An IPerson, or None if no assignee.
27- :status: A BugTaskStatus dbschema item, or None, if series is not targeted.
28+ :status: A BugTaskStatus dbschema item, or None, if series is not
29+ targeted.
30 """
31+
32 def __init__(self, series, istargeted=False, sourcepackage=None,
33 assignee=None, status=None):
34 self.series = series
35
36=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
37--- lib/lp/bugs/interfaces/bugtask.py 2010-11-03 16:50:05 +0000
38+++ lib/lp/bugs/interfaces/bugtask.py 2010-11-23 13:40:38 +0000
39@@ -667,10 +667,11 @@
40 underlying bug for this bugtask.
41
42 This method was documented as being required here so that
43- MentorshipOffers could happen on IBugTask. If that was the sole reason
44- then this method should be deletable. When we move to context-less bug
45- presentation (where the bug is at /bugs/n?task=ubuntu) then we can
46- eliminate this if it is no longer useful.
47+ MentorshipOffers could happen on IBugTask. If that was the sole
48+ reason then this method should be deletable. When we move to
49+ context-less bug presentation (where the bug is at
50+ /bugs/n?task=ubuntu) then we can eliminate this if it is no
51+ longer useful.
52 """
53
54 @mutator_for(milestone)
55@@ -1387,15 +1388,18 @@
56 Only BugTasks that the user has access to will be returned.
57 """
58
59- def search(params, *args):
60+ def search(params, *args, **kwargs):
61 """Search IBugTasks with the given search parameters.
62
63 Note: only use this method of BugTaskSet if you want to query
64 tasks across multiple IBugTargets; otherwise, use the
65 IBugTarget's searchTasks() method.
66
67- :search_params: a BugTaskSearchParams object
68- :args: any number of BugTaskSearchParams objects
69+ :param search_params: a BugTaskSearchParams object
70+ :param args: any number of BugTaskSearchParams objects
71+ :param prejoins: (keyword) A sequence of tuples
72+ (table, table_join) which should be pre-joined in addition
73+ to the default prejoins.
74
75 If more than one BugTaskSearchParams is given, return the union of
76 IBugTasks which match any of them, with the results ordered by the
77
78=== modified file 'lib/lp/bugs/model/bugtarget.py'
79--- lib/lp/bugs/model/bugtarget.py 2010-10-21 16:40:10 +0000
80+++ lib/lp/bugs/model/bugtarget.py 2010-11-23 13:40:38 +0000
81@@ -72,6 +72,7 @@
82 All `IHasBugs` implementations should inherit from this class
83 or from `BugTargetBase`.
84 """
85+
86 def searchTasks(self, search_params, user=None,
87 order_by=None, search_text=None,
88 status=None,
89@@ -93,7 +94,7 @@
90 hardware_owner_is_affected_by_bug=False,
91 hardware_owner_is_subscribed_to_bug=False,
92 hardware_is_linked_to_bug=False, linked_branches=None,
93- modified_since=None, created_since=None):
94+ modified_since=None, created_since=None, prejoins=[]):
95 """See `IHasBugs`."""
96 if status is None:
97 # If no statuses are supplied, default to the
98@@ -109,9 +110,10 @@
99 del kwargs['self']
100 del kwargs['user']
101 del kwargs['search_params']
102+ del kwargs['prejoins']
103 search_params = BugTaskSearchParams.fromSearchForm(user, **kwargs)
104 self._customizeSearchParams(search_params)
105- return BugTaskSet().search(search_params)
106+ return BugTaskSet().search(search_params, prejoins=prejoins)
107
108 def _customizeSearchParams(self, search_params):
109 """Customize `search_params` for a specific target."""
110@@ -328,7 +330,6 @@
111 self.project.recalculateBugHeatCache()
112
113
114-
115 class OfficialBugTagTargetMixin:
116 """See `IOfficialBugTagTarget`.
117
118@@ -441,4 +442,3 @@
119 'IDistribution instance or an IProduct instance.')
120
121 target = property(target, _settarget, doc=target.__doc__)
122-
123
124=== modified file 'lib/lp/bugs/model/bugtask.py'
125--- lib/lp/bugs/model/bugtask.py 2010-11-18 13:09:22 +0000
126+++ lib/lp/bugs/model/bugtask.py 2010-11-23 13:40:38 +0000
127@@ -2277,6 +2277,9 @@
128 :param _noprejoins: Private internal parameter to BugTaskSet which
129 disables all use of prejoins : consolidated from code paths that
130 claim they were inefficient and unwanted.
131+ :param prejoins: A sequence of tuples (table, table_join) which
132+ which should be pre-joined in addition to the default prejoins.
133+ This parameter has no effect if _noprejoins is True.
134 """
135 # Prevent circular import problems.
136 from lp.registry.model.product import Product
137@@ -2286,6 +2289,7 @@
138 prejoins = []
139 resultrow = BugTask
140 else:
141+ requested_joins = kwargs.get('prejoins', [])
142 prejoins = [
143 (Bug, LeftJoin(Bug, BugTask.bug == Bug.id)),
144 (Product, LeftJoin(Product, BugTask.product == Product.id)),
145@@ -2293,8 +2297,12 @@
146 LeftJoin(
147 SourcePackageName,
148 BugTask.sourcepackagename == SourcePackageName.id)),
149- ]
150- resultrow = (BugTask, Bug, Product, SourcePackageName, )
151+ ] + requested_joins
152+ resultrow = (BugTask, Bug, Product, SourcePackageName)
153+ additional_result_objects = [
154+ table for table, join in requested_joins
155+ if table not in resultrow]
156+ resultrow = resultrow + tuple(additional_result_objects)
157 return self._search(resultrow, prejoins, params, *args)
158
159 def searchBugIds(self, params):
160
161=== modified file 'lib/lp/bugs/tests/test_bugtarget.py'
162--- lib/lp/bugs/tests/test_bugtarget.py 2010-10-04 19:50:45 +0000
163+++ lib/lp/bugs/tests/test_bugtarget.py 2010-11-23 13:40:38 +0000
164@@ -15,8 +15,11 @@
165 __all__ = []
166
167 import random
168+from testtools.matchers import Equals
169 import unittest
170
171+from storm.expr import LeftJoin
172+from storm.store import Store
173 from zope.component import getUtility
174
175 from canonical.launchpad.testing.systemdocs import (
176@@ -28,15 +31,24 @@
177 from canonical.testing.layers import LaunchpadFunctionalLayer
178 from lp.bugs.interfaces.bug import CreateBugParams
179 from lp.bugs.interfaces.bugtask import (
180+ BugTaskSearchParams,
181 BugTaskStatus,
182 IBugTaskSet,
183 )
184+from lp.bugs.model.bugtask import BugTask
185 from lp.registry.interfaces.distribution import (
186 IDistribution,
187 IDistributionSet,
188 )
189 from lp.registry.interfaces.product import IProductSet
190 from lp.registry.interfaces.projectgroup import IProjectGroupSet
191+from lp.registry.model.milestone import Milestone
192+from lp.testing import (
193+ person_logged_in,
194+ StormStatementRecorder,
195+ TestCaseWithFactory,
196+ )
197+from lp.testing.matchers import HasQueryCount
198
199
200 def bugtarget_filebug(bugtarget, summary, status=None):
201@@ -176,6 +188,101 @@
202 test.globs['question_target'] = ubuntu.getSourcePackage('mozilla-firefox')
203
204
205+class TestBugTargetSearchTasks(TestCaseWithFactory):
206+ """Tests of IHasBugs.searchTasks()."""
207+
208+ layer = LaunchpadFunctionalLayer
209+
210+ def setUp(self):
211+ super(TestBugTargetSearchTasks, self).setUp()
212+ self.bug = self.factory.makeBug()
213+ self.target = self.bug.default_bugtask.target
214+ self.milestone = self.factory.makeMilestone(product=self.target)
215+ with person_logged_in(self.target.owner):
216+ self.bug.default_bugtask.transitionToMilestone(
217+ self.milestone, self.target.owner)
218+ self.store = Store.of(self.bug)
219+ self.store.flush()
220+ self.store.invalidate()
221+
222+ def test_preload_other_objects(self):
223+ # We can prejoin objects in calls of searchTasks().
224+
225+ # Without prejoining the table Milestone, accessing the
226+ # BugTask property milestone requires an extra query.
227+ with StormStatementRecorder() as recorder:
228+ params = BugTaskSearchParams(user=None)
229+ found_tasks = self.target.searchTasks(params)
230+ found_tasks[0].milestone
231+ self.assertThat(recorder, HasQueryCount(Equals(2)))
232+
233+ # When we prejoin Milestone, the milestone of our bugtask is
234+ # already loaded during the main search query.
235+ self.store.invalidate()
236+ with StormStatementRecorder() as recorder:
237+ params = BugTaskSearchParams(user=None)
238+ prejoins = [(Milestone,
239+ LeftJoin(Milestone,
240+ BugTask.milestone == Milestone.id))]
241+ found_tasks = self.target.searchTasks(params, prejoins=prejoins)
242+ found_tasks[0].milestone
243+ self.assertThat(recorder, HasQueryCount(Equals(1)))
244+
245+ def test_preload_other_objects_for_person_search_no_params_passed(self):
246+ # We can prejoin objects in calls of Person.searchTasks().
247+ owner = self.bug.owner
248+ with StormStatementRecorder() as recorder:
249+ found_tasks = owner.searchTasks(None, user=None)
250+ found_tasks[0].milestone
251+ self.assertThat(recorder, HasQueryCount(Equals(2)))
252+
253+ self.store.invalidate()
254+ with StormStatementRecorder() as recorder:
255+ prejoins = [(Milestone,
256+ LeftJoin(Milestone,
257+ BugTask.milestone == Milestone.id))]
258+ found_tasks = owner.searchTasks(
259+ None, user=None, prejoins=prejoins)
260+ found_tasks[0].milestone
261+ self.assertThat(recorder, HasQueryCount(Equals(1)))
262+
263+ def test_preload_other_objects_for_person_search_no_keywords_passed(self):
264+ # We can prejoin objects in calls of Person.searchTasks().
265+ owner = self.bug.owner
266+ params = BugTaskSearchParams(user=None, owner=owner)
267+ with StormStatementRecorder() as recorder:
268+ found_tasks = owner.searchTasks(params)
269+ found_tasks[0].milestone
270+ self.assertThat(recorder, HasQueryCount(Equals(2)))
271+
272+ self.store.invalidate()
273+ with StormStatementRecorder() as recorder:
274+ prejoins = [(Milestone,
275+ LeftJoin(Milestone,
276+ BugTask.milestone == Milestone.id))]
277+ found_tasks = owner.searchTasks(params, prejoins=prejoins)
278+ found_tasks[0].milestone
279+ self.assertThat(recorder, HasQueryCount(Equals(1)))
280+
281+ def test_preload_other_objects_for_person_search_keywords_passed(self):
282+ # We can prejoin objects in calls of Person.searchTasks().
283+ owner = self.bug.owner
284+ params = BugTaskSearchParams(user=None, owner=owner)
285+ with StormStatementRecorder() as recorder:
286+ found_tasks = owner.searchTasks(params, order_by=BugTask.id)
287+ found_tasks[0].milestone
288+ self.assertThat(recorder, HasQueryCount(Equals(2)))
289+
290+ self.store.invalidate()
291+ with StormStatementRecorder() as recorder:
292+ prejoins = [(Milestone,
293+ LeftJoin(Milestone,
294+ BugTask.milestone == Milestone.id))]
295+ found_tasks = owner.searchTasks(params, prejoins=prejoins)
296+ found_tasks[0].milestone
297+ self.assertThat(recorder, HasQueryCount(Equals(1)))
298+
299+
300 def test_suite():
301 """Return the `IBugTarget` TestSuite."""
302 suite = unittest.TestSuite()
303@@ -195,7 +302,6 @@
304 layer=LaunchpadFunctionalLayer)
305 suite.addTest(test)
306
307-
308 setUpMethods.remove(sourcePackageForQuestionSetUp)
309 setUpMethods.append(sourcePackageSetUp)
310 setUpMethods.append(projectSetUp)
311@@ -206,4 +312,5 @@
312 layer=LaunchpadFunctionalLayer)
313 suite.addTest(test)
314
315+ suite.addTest(unittest.TestLoader().loadTestsFromName(__name__))
316 return suite
317
318=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
319--- lib/lp/bugs/tests/test_bugtask_search.py 2010-11-12 17:42:43 +0000
320+++ lib/lp/bugs/tests/test_bugtask_search.py 2010-11-23 13:40:38 +0000
321@@ -10,10 +10,14 @@
322 from new import classobj
323 import pytz
324 import sys
325+from testtools.matchers import Equals
326 import unittest
327
328 from zope.component import getUtility
329
330+from storm.expr import Join
331+from storm.store import Store
332+
333 from canonical.launchpad.searchbuilder import (
334 all,
335 any,
336@@ -31,6 +35,7 @@
337 BugTaskStatus,
338 IBugTaskSet,
339 )
340+from lp.bugs.model.bugtask import BugTask
341 from lp.registry.interfaces.distribution import IDistribution
342 from lp.registry.interfaces.distributionsourcepackage import (
343 IDistributionSourcePackage,
344@@ -39,10 +44,13 @@
345 from lp.registry.interfaces.person import IPersonSet
346 from lp.registry.interfaces.product import IProduct
347 from lp.registry.interfaces.sourcepackage import ISourcePackage
348+from lp.registry.model.person import Person
349 from lp.testing import (
350 person_logged_in,
351+ StormStatementRecorder,
352 TestCaseWithFactory,
353 )
354+from lp.testing.matchers import HasQueryCount
355
356
357 class SearchTestBase:
358@@ -907,13 +915,40 @@
359 def setUp(self):
360 super(PreloadBugtaskTargets, self).setUp()
361
362- def runSearch(self, params, *args):
363+ def runSearch(self, params, *args, **kw):
364 """Run BugTaskSet.search() and preload bugtask target objects."""
365- return list(self.bugtask_set.search(params, *args, _noprejoins=False))
366+ return list(self.bugtask_set.search(
367+ params, *args, _noprejoins=False, **kw))
368
369 def resultValuesForBugtasks(self, expected_bugtasks):
370 return expected_bugtasks
371
372+ def test_preload_additional_objects(self):
373+ # It is possible to join additional tables in the search query
374+ # in order to load related Storm objects during the query.
375+ store = Store.of(self.bugtasks[0])
376+ store.invalidate()
377+
378+ # If we do not prejoin the owner, two queries a run
379+ # in order to retrieve the owner of the bugtask.
380+ with StormStatementRecorder() as recorder:
381+ params = self.getBugTaskSearchParams(user=None)
382+ found_tasks = self.runSearch(params)
383+ found_tasks[0].owner
384+ self.assertTrue(len(recorder.statements) > 1)
385+
386+ # If we join the table person on bugtask.owner == person.id
387+ # the owner object is loaded in the query that retrieves the
388+ # bugtasks.
389+ store.invalidate()
390+ with StormStatementRecorder() as recorder:
391+ params = self.getBugTaskSearchParams(user=None)
392+ found_tasks = self.runSearch(
393+ params,
394+ prejoins=[(Person, Join(Person, BugTask.owner == Person.id))])
395+ found_tasks[0].owner
396+ self.assertThat(recorder, HasQueryCount(Equals(1)))
397+
398
399 class NoPreloadBugtaskTargets(MultipleParams):
400 """Do not preload bug targets during a BugTaskSet.search() query."""
401
402=== modified file 'lib/lp/registry/model/person.py'
403--- lib/lp/registry/model/person.py 2010-11-18 12:05:34 +0000
404+++ lib/lp/registry/model/person.py 2010-11-23 13:40:38 +0000
405@@ -882,6 +882,7 @@
406
407 def searchTasks(self, search_params, *args, **kwargs):
408 """See `IHasBugs`."""
409+ prejoins = kwargs.pop('prejoins', [])
410 if search_params is None and len(args) == 0:
411 # this method is called via webapi directly
412 # calling this method on a Person object directly via the
413@@ -896,15 +897,18 @@
414 # method, see docstring of
415 # `lazr.restful.declarations.webservice_error()`
416 raise e
417- return getUtility(IBugTaskSet).search(*search_params)
418+ return getUtility(IBugTaskSet).search(
419+ *search_params, prejoins=prejoins)
420 if len(kwargs) > 0:
421 # if keyword arguments are supplied, use the deault
422 # implementation in HasBugsBase.
423- return HasBugsBase.searchTasks(self, search_params, **kwargs)
424+ return HasBugsBase.searchTasks(
425+ self, search_params, prejoins=prejoins, **kwargs)
426 else:
427 # Otherwise pass all positional arguments to the
428 # implementation in BugTaskSet.
429- return getUtility(IBugTaskSet).search(search_params, *args)
430+ return getUtility(IBugTaskSet).search(
431+ search_params, *args, prejoins=prejoins)
432
433 def getProjectsAndCategoriesContributedTo(self, limit=5):
434 """See `IPerson`."""