Merge lp:~wgrant/launchpad/xref-buglinks into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 17782
Proposed branch: lp:~wgrant/launchpad/xref-buglinks
Merge into: lp:launchpad
Prerequisite: lp:~wgrant/launchpad/xref-model
Diff against target: 1243 lines (+555/-92)
20 files modified
database/schema/security.cfg (+8/-0)
lib/lp/answers/browser/tests/views.txt (+2/-2)
lib/lp/answers/model/question.py (+51/-14)
lib/lp/blueprints/model/specification.py (+32/-6)
lib/lp/blueprints/tests/test_specification.py (+17/-0)
lib/lp/bugs/browser/tests/test_bugtask.py (+5/-5)
lib/lp/bugs/doc/cve.txt (+3/-3)
lib/lp/bugs/model/bug.py (+57/-16)
lib/lp/bugs/model/bugcve.py (+3/-0)
lib/lp/bugs/model/bugtask.py (+11/-3)
lib/lp/bugs/model/bugtasksearch.py (+34/-9)
lib/lp/bugs/model/cve.py (+67/-34)
lib/lp/bugs/model/tests/test_bugtask.py (+16/-0)
lib/lp/bugs/model/tests/test_bugtasksearch.py (+21/-0)
lib/lp/bugs/tests/test_cve.py (+53/-0)
lib/lp/coop/answersbugs/model.py (+3/-0)
lib/lp/coop/answersbugs/tests/test_questionbug.py (+17/-0)
lib/lp/scripts/garbo.py (+63/-0)
lib/lp/scripts/tests/test_garbo.py (+88/-0)
lib/lp/services/xref/model.py (+4/-0)
To merge this branch: bzr merge lp:~wgrant/launchpad/xref-buglinks
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+272591@code.launchpad.net

Commit message

BugCve/QuestionBug/SpecificationBug -> XRef, part 1: write to both schemas, read from either.

Description of the change

BugCve/QuestionBug/SpecificationBug -> XRef, part 1: write to both schemas, read from either.

There's also a garbo job to backfill XRef, and a test-only feature flag to disable writing to the old schema.

There are a couple of XXXs about filling in XRef.creator, but the old schema doesn't store that data, and this branch is big enough as is. creator comes later.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve
Revision history for this message
William Grant (wgrant) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2015-09-18 01:31:50 +0000
3+++ database/schema/security.cfg 2015-10-01 01:43:11 +0000
4@@ -654,6 +654,7 @@
5 public.validpersoncache = SELECT
6 public.validpersonorteamcache = SELECT
7 public.wikiname = SELECT, INSERT
8+public.xref = SELECT, INSERT
9 type=user
10
11 [branchscanner]
12@@ -945,6 +946,7 @@
13 public.translationgroup = SELECT
14 public.validpersoncache = SELECT
15 public.validpersonorteamcache = SELECT
16+public.xref = SELECT, INSERT
17 type=user
18
19 [fiera]
20@@ -1295,6 +1297,7 @@
21 public.teamparticipation = SELECT
22 public.validpersoncache = SELECT
23 public.validpersonorteamcache = SELECT
24+public.xref = SELECT
25 type=user
26
27 [expire_questions]
28@@ -1436,6 +1439,7 @@
29 public.validpersoncache = SELECT
30 public.validpersonorteamcache = SELECT
31 public.wikiname = SELECT, INSERT
32+public.xref = SELECT, INSERT
33 type=group
34
35 [queued]
36@@ -1545,6 +1549,7 @@
37 public.teamparticipation = SELECT, INSERT
38 public.validpersoncache = SELECT
39 public.validpersonorteamcache = SELECT
40+public.xref = SELECT, INSERT
41 type=user
42
43 [process_accepted]
44@@ -1625,6 +1630,7 @@
45 public.teamparticipation = SELECT
46 public.validpersoncache = SELECT
47 public.validpersonorteamcache = SELECT
48+public.xref = SELECT
49 type=user
50
51 [personnotification]
52@@ -1835,6 +1841,7 @@
53 public.teamparticipation = SELECT
54 public.validpersoncache = SELECT
55 public.validpersonorteamcache = SELECT
56+public.xref = SELECT, INSERT
57 type=user
58
59 [mlist-sync]
60@@ -2337,6 +2344,7 @@
61 public.translationmessage = SELECT, DELETE
62 public.translationtemplateitem = SELECT, DELETE
63 public.webhookjob = SELECT, DELETE
64+public.xref = SELECT, INSERT
65 type=user
66
67 [garbo_daily]
68
69=== modified file 'lib/lp/answers/browser/tests/views.txt'
70--- lib/lp/answers/browser/tests/views.txt 2014-04-24 02:53:05 +0000
71+++ lib/lp/answers/browser/tests/views.txt 2015-10-01 01:43:11 +0000
72@@ -334,8 +334,8 @@
73 ... 'field.description': 'Bug description.'})
74 >>> request.method = 'POST'
75 >>> makebug = getMultiAdapter((question_three, request), name='+makebug')
76- >>> question_three.bugs.count() == 0
77- True
78+ >>> question_three.bugs
79+ []
80
81 >>> makebug.initialize()
82 >>> print question_three.bugs[0].title
83
84=== modified file 'lib/lp/answers/model/question.py'
85--- lib/lp/answers/model/question.py 2015-09-29 05:02:28 +0000
86+++ lib/lp/answers/model/question.py 2015-10-01 01:43:11 +0000
87@@ -94,12 +94,14 @@
88 IProductSet,
89 )
90 from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
91+from lp.services.database import bulk
92 from lp.services.database.constants import (
93 DEFAULT,
94 UTC_NOW,
95 )
96 from lp.services.database.datetimecol import UtcDateTimeCol
97 from lp.services.database.enumcol import EnumCol
98+from lp.services.database.interfaces import IStore
99 from lp.services.database.nl_search import nl_phrase_search
100 from lp.services.database.sqlbase import (
101 cursor,
102@@ -108,6 +110,7 @@
103 sqlvalues,
104 )
105 from lp.services.database.stormexpr import rank_by_fti
106+from lp.services.features import getFeatureFlag
107 from lp.services.mail.notificationrecipientset import NotificationRecipientSet
108 from lp.services.messages.interfaces.message import IMessage
109 from lp.services.messages.model.message import (
110@@ -119,6 +122,7 @@
111 from lp.services.worlddata.helpers import is_english_variant
112 from lp.services.worlddata.interfaces.language import ILanguage
113 from lp.services.worlddata.model.language import Language
114+from lp.services.xref.interfaces import IXRefSet
115
116
117 class notify_question_modified:
118@@ -210,8 +214,6 @@
119 subscribers = SQLRelatedJoin('Person',
120 joinColumn='question', otherColumn='person',
121 intermediateTable='QuestionSubscription', orderBy='name')
122- bugs = SQLRelatedJoin('Bug', joinColumn='question', otherColumn='bug',
123- intermediateTable='QuestionBug', orderBy='id')
124 messages = SQLMultipleJoin('QuestionMessage', joinColumn='question',
125 prejoins=['message'], orderBy=['QuestionMessage.id'])
126 reopenings = SQLMultipleJoin('QuestionReopening', orderBy='datecreated',
127@@ -660,14 +662,35 @@
128 self.status = new_status
129 return tktmsg
130
131+ @property
132+ def bugs(self):
133+ from lp.bugs.model.bug import Bug
134+ if getFeatureFlag('bugs.xref_buglinks.query'):
135+ bug_ids = [
136+ int(id) for _, id in getUtility(IXRefSet).findFrom(
137+ (u'question', unicode(self.id)), types=[u'bug'])]
138+ else:
139+ bug_ids = list(IStore(QuestionBug).find(
140+ QuestionBug,
141+ QuestionBug.question == self).values(QuestionBug.bugID))
142+ return list(sorted(
143+ bulk.load(Bug, bug_ids), key=operator.attrgetter('id')))
144+
145 # IBugLinkTarget implementation
146 def createBugLink(self, bug):
147 """See BugLinkTargetMixin."""
148- QuestionBug(question=self, bug=bug)
149+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
150+ QuestionBug(question=self, bug=bug)
151+ # XXX: Should set creator.
152+ getUtility(IXRefSet).create(
153+ {(u'question', unicode(self.id)): {(u'bug', unicode(bug.id)): {}}})
154
155 def deleteBugLink(self, bug):
156 """See BugLinkTargetMixin."""
157- Store.of(self).find(QuestionBug, question=self, bug=bug).remove()
158+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
159+ Store.of(self).find(QuestionBug, question=self, bug=bug).remove()
160+ getUtility(IXRefSet).delete(
161+ {(u'question', unicode(self.id)): [(u'bug', unicode(bug.id))]})
162
163 def setCommentVisibility(self, user, comment_number, visible):
164 """See `IQuestion`."""
165@@ -693,24 +716,38 @@
166 # This query joins to bugtasks that are not BugTaskStatus.INVALID
167 # because there are many bugtasks to one question. A question is
168 # included when BugTask.status IS NULL.
169- return Question.select("""
170- id in (SELECT Question.id
171- FROM Question
172+ if getFeatureFlag('bugs.xref_buglinks.query'):
173+ bugtask_join = """
174+ LEFT OUTER JOIN XRef ON (
175+ XRef.from_type = 'question'
176+ AND XRef.from_id_int = Question.id
177+ AND XRef.to_type = 'bug')
178+ LEFT OUTER JOIN BugTask ON (
179+ BugTask.bug = XRef.to_id_int
180+ AND BugTask.status != %s)
181+ """
182+ else:
183+ bugtask_join = """
184 LEFT OUTER JOIN QuestionBug
185 ON Question.id = QuestionBug.question
186- LEFT OUTER JOIN BugTask
187- ON QuestionBug.bug = BugTask.bug
188- AND BugTask.status != %s
189+ LEFT OUTER JOIN BugTask ON (
190+ BugTask.bug = QuestionBug.bug
191+ AND BugTask.status != %s)
192+ """
193+ return Question.select(("""
194+ id in (SELECT Question.id
195+ FROM Question
196+ %s
197 WHERE
198- Question.status IN (%s, %s)
199+ Question.status IN (%%s, %%s)
200 AND (Question.datelastresponse IS NULL
201 OR Question.datelastresponse < (CURRENT_TIMESTAMP
202- AT TIME ZONE 'UTC' - interval '%s days'))
203+ AT TIME ZONE 'UTC' - interval '%%s days'))
204 AND Question.datelastquery < (CURRENT_TIMESTAMP
205- AT TIME ZONE 'UTC' - interval '%s days')
206+ AT TIME ZONE 'UTC' - interval '%%s days')
207 AND Question.assignee IS NULL
208 AND BugTask.status IS NULL)
209- """ % sqlvalues(
210+ """ % bugtask_join) % sqlvalues(
211 BugTaskStatus.INVALID,
212 QuestionStatus.OPEN, QuestionStatus.NEEDSINFO,
213 days_before_expiration, days_before_expiration))
214
215=== modified file 'lib/lp/blueprints/model/specification.py'
216--- lib/lp/blueprints/model/specification.py 2015-09-28 07:57:17 +0000
217+++ lib/lp/blueprints/model/specification.py 2015-10-01 01:43:11 +0000
218@@ -11,6 +11,8 @@
219 'SpecificationSet',
220 ]
221
222+import operator
223+
224 from lazr.lifecycle.event import (
225 ObjectCreatedEvent,
226 ObjectModifiedEvent,
227@@ -86,6 +88,7 @@
228 from lp.registry.interfaces.person import validate_public_person
229 from lp.registry.interfaces.product import IProduct
230 from lp.registry.interfaces.productseries import IProductSeries
231+from lp.services.database import bulk
232 from lp.services.database.constants import (
233 DEFAULT,
234 UTC_NOW,
235@@ -99,12 +102,14 @@
236 SQLBase,
237 sqlvalues,
238 )
239+from lp.services.features import getFeatureFlag
240 from lp.services.mail.helpers import get_contact_email_addresses
241 from lp.services.propertycache import (
242 cachedproperty,
243 get_property_cache,
244 )
245 from lp.services.webapp.interfaces import ILaunchBag
246+from lp.services.xref.interfaces import IXRefSet
247
248
249 def recursive_blocked_query(user):
250@@ -239,9 +244,6 @@
251 sprints = SQLRelatedJoin('Sprint', orderBy='name',
252 joinColumn='specification', otherColumn='sprint',
253 intermediateTable='SprintSpecification')
254- bugs = SQLRelatedJoin('Bug',
255- joinColumn='specification', otherColumn='bug',
256- intermediateTable='SpecificationBug', orderBy='id')
257 spec_dependency_links = SQLMultipleJoin('SpecificationDependency',
258 joinColumn='specification', orderBy='id')
259
260@@ -791,14 +793,38 @@
261
262 return bool(self.subscription(person))
263
264+ @property
265+ def bugs(self):
266+ from lp.bugs.model.bug import Bug
267+ if getFeatureFlag('bugs.xref_buglinks.query'):
268+ bug_ids = [
269+ int(id) for _, id in getUtility(IXRefSet).findFrom(
270+ (u'specification', unicode(self.id)), types=[u'bug'])]
271+ else:
272+ bug_ids = list(IStore(SpecificationBug).find(
273+ SpecificationBug,
274+ SpecificationBug.specification == self).values(
275+ SpecificationBug.bugID))
276+ return list(sorted(
277+ bulk.load(Bug, bug_ids), key=operator.attrgetter('id')))
278+
279 def createBugLink(self, bug):
280 """See BugLinkTargetMixin."""
281- SpecificationBug(specification=self, bug=bug)
282+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
283+ SpecificationBug(specification=self, bug=bug)
284+ # XXX: Should set creator.
285+ getUtility(IXRefSet).create(
286+ {(u'specification', unicode(self.id)):
287+ {(u'bug', unicode(bug.id)): {}}})
288
289 def deleteBugLink(self, bug):
290 """See BugLinkTargetMixin."""
291- Store.of(self).find(
292- SpecificationBug, specification=self, bug=bug).remove()
293+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
294+ Store.of(self).find(
295+ SpecificationBug, specification=self, bug=bug).remove()
296+ getUtility(IXRefSet).delete(
297+ {(u'specification', unicode(self.id)):
298+ [(u'bug', unicode(bug.id))]})
299
300 # sprint linking
301 def linkSprint(self, sprint, user):
302
303=== modified file 'lib/lp/blueprints/tests/test_specification.py'
304--- lib/lp/blueprints/tests/test_specification.py 2015-09-28 07:39:28 +0000
305+++ lib/lp/blueprints/tests/test_specification.py 2015-10-01 01:43:11 +0000
306@@ -59,6 +59,7 @@
307 EditSpecificationByRelatedPeople,
308 ViewSpecification,
309 )
310+from lp.services.features.testing import FeatureFixture
311 from lp.services.propertycache import get_property_cache
312 from lp.services.webapp.authorization import check_permission
313 from lp.services.webapp.interaction import ANONYMOUS
314@@ -876,3 +877,19 @@
315 self.assertContentEqual([bug1], spec2.bugs)
316 self.assertContentEqual([spec2], bug1.specifications)
317 self.assertContentEqual([], bug2.specifications)
318+
319+
320+class TestBugLinksWithXRef(TestBugLinks):
321+
322+ def setUp(self):
323+ super(TestBugLinksWithXRef, self).setUp()
324+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
325+
326+
327+class TestBugLinksWithXRefAndNoOld(TestBugLinks):
328+
329+ def setUp(self):
330+ super(TestBugLinksWithXRefAndNoOld, self).setUp()
331+ self.useFixture(FeatureFixture({
332+ 'bugs.xref_buglinks.query': 'true',
333+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
334
335=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
336--- lib/lp/bugs/browser/tests/test_bugtask.py 2015-06-30 01:10:06 +0000
337+++ lib/lp/bugs/browser/tests/test_bugtask.py 2015-10-01 01:43:11 +0000
338@@ -125,7 +125,7 @@
339 0, 10, login_method=lambda: login(ADMIN_EMAIL))
340 # This may seem large: it is; there is easily another 25% fat in
341 # there.
342- self.assertThat(recorder1, HasQueryCount(LessThan(81)))
343+ self.assertThat(recorder1, HasQueryCount(LessThan(83)))
344 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
345
346 def test_rendered_query_counts_constant_with_attachments(self):
347@@ -136,7 +136,7 @@
348 lambda: self.getUserBrowser(url, person),
349 lambda: self.factory.makeBugAttachment(bug=task.bug),
350 1, 9, login_method=lambda: login(ADMIN_EMAIL))
351- self.assertThat(recorder1, HasQueryCount(LessThan(82)))
352+ self.assertThat(recorder1, HasQueryCount(LessThan(84)))
353 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
354
355 def makeLinkedBranchMergeProposal(self, sourcepackage, bug, owner):
356@@ -171,7 +171,7 @@
357 recorder1, recorder2 = record_two_runs(
358 lambda: self.getUserBrowser(url, owner),
359 make_merge_proposals, 0, 1)
360- self.assertThat(recorder1, HasQueryCount(LessThan(87)))
361+ self.assertThat(recorder1, HasQueryCount(LessThan(89)))
362 # Ideally this should be much fewer, but this tries to keep a win of
363 # removing more than half of these.
364 self.assertThat(
365@@ -217,7 +217,7 @@
366 lambda: self.getUserBrowser(url, person),
367 lambda: add_activity("description", self.factory.makePerson()),
368 1, 20, login_method=lambda: login(ADMIN_EMAIL))
369- self.assertThat(recorder1, HasQueryCount(LessThan(82)))
370+ self.assertThat(recorder1, HasQueryCount(LessThan(84)))
371 self.assertThat(recorder2, HasQueryCount(Equals(recorder1.count)))
372
373 def test_rendered_query_counts_constant_with_milestones(self):
374@@ -227,7 +227,7 @@
375
376 with celebrity_logged_in('admin'):
377 browses_under_limit = BrowsesWithQueryLimit(
378- 82, self.factory.makePerson())
379+ 84, self.factory.makePerson())
380
381 self.assertThat(bug, browses_under_limit)
382
383
384=== modified file 'lib/lp/bugs/doc/cve.txt'
385--- lib/lp/bugs/doc/cve.txt 2015-09-25 09:48:57 +0000
386+++ lib/lp/bugs/doc/cve.txt 2015-10-01 01:43:11 +0000
387@@ -75,16 +75,16 @@
388
389 Let's add the new CVE:
390
391- >>> b.cves.count()
392+ >>> len(b.cves)
393 1
394 >>> b.linkCVE(cve, no_priv)
395- >>> b.cves.count()
396+ >>> len(b.cves)
397 2
398
399 Ah, but that was a bad idea. Let's unlink it.
400
401 >>> b.unlinkCVE(cve, user=no_priv)
402- >>> b.cves.count()
403+ >>> len(b.cves)
404 1
405
406 Alternatively, we can link CVEs to bugs by looking for CVEs in a
407
408=== modified file 'lib/lp/bugs/model/bug.py'
409--- lib/lp/bugs/model/bug.py 2015-09-30 01:51:52 +0000
410+++ lib/lp/bugs/model/bug.py 2015-10-01 01:43:11 +0000
411@@ -99,11 +99,6 @@
412 from lp.app.interfaces.services import IService
413 from lp.app.model.launchpad import InformationTypeMixin
414 from lp.app.validators import LaunchpadValidationError
415-from lp.blueprints.model.specification import Specification
416-from lp.blueprints.model.specificationbug import SpecificationBug
417-from lp.blueprints.model.specificationsearch import (
418- get_specification_privacy_filter,
419- )
420 from lp.bugs.adapters.bug import convert_to_information_type
421 from lp.bugs.adapters.bugchange import (
422 BranchLinkedToBug,
423@@ -198,6 +193,7 @@
424 from lp.registry.model.pillar import pillar_sort_key
425 from lp.registry.model.teammembership import TeamParticipation
426 from lp.services.config import config
427+from lp.services.database import bulk
428 from lp.services.database.constants import UTC_NOW
429 from lp.services.database.datetimecol import UtcDateTimeCol
430 from lp.services.database.decoratedresultset import DecoratedResultSet
431@@ -208,6 +204,7 @@
432 sqlvalues,
433 )
434 from lp.services.database.stormbase import StormBase
435+from lp.services.features import getFeatureFlag
436 from lp.services.fields import DuplicateBug
437 from lp.services.helpers import shortlist
438 from lp.services.librarian.interfaces import ILibraryFileAliasSet
439@@ -234,6 +231,7 @@
440 from lp.services.webapp.publisher import (
441 get_raw_form_value_from_current_request,
442 )
443+from lp.services.xref.interfaces import IXRefSet
444
445
446 def snapshot_bug_params(bug_params):
447@@ -363,15 +361,7 @@
448 'BugMessage', joinColumn='bug', orderBy='index')
449 watches = SQLMultipleJoin(
450 'BugWatch', joinColumn='bug', orderBy=['bugtracker', 'remotebug'])
451- cves = SQLRelatedJoin('Cve', intermediateTable='BugCve',
452- orderBy='sequence', joinColumn='bug', otherColumn='cve')
453 duplicates = SQLMultipleJoin('Bug', joinColumn='duplicateof', orderBy='id')
454- specifications = SQLRelatedJoin(
455- 'Specification', joinColumn='bug', otherColumn='specification',
456- intermediateTable='SpecificationBug', orderBy='-datecreated')
457- questions = SQLRelatedJoin('Question', joinColumn='bug',
458- otherColumn='question', intermediateTable='QuestionBug',
459- orderBy='-datecreated')
460 linked_branches = SQLMultipleJoin(
461 'BugBranch', joinColumn='bug', orderBy='id')
462 date_last_message = UtcDateTimeCol(default=None)
463@@ -383,12 +373,63 @@
464 heat_last_updated = UtcDateTimeCol(default=None)
465 latest_patch_uploaded = UtcDateTimeCol(default=None)
466
467+ @property
468+ def cves(self):
469+ from lp.bugs.model.bugcve import BugCve
470+ from lp.bugs.model.cve import Cve
471+ if getFeatureFlag('bugs.xref_buglinks.query'):
472+ xref_cve_sequences = [
473+ sequence for _, sequence in getUtility(IXRefSet).findFrom(
474+ (u'bug', unicode(self.id)), types=[u'cve'])]
475+ expr = Cve.sequence.is_in(xref_cve_sequences)
476+ else:
477+ old_cve_ids = list(IStore(BugCve).find(
478+ BugCve,
479+ BugCve.bug == self).values(BugCve.cveID))
480+ expr = Cve.id.is_in(old_cve_ids)
481+ return list(sorted(
482+ IStore(Cve).find(Cve, expr), key=operator.attrgetter('sequence')))
483+
484+ @property
485+ def questions(self):
486+ from lp.answers.model.question import Question
487+ from lp.coop.answersbugs.model import QuestionBug
488+ if getFeatureFlag('bugs.xref_buglinks.query'):
489+ question_ids = [
490+ int(id) for _, id in getUtility(IXRefSet).findFrom(
491+ (u'bug', unicode(self.id)), types=[u'question'])]
492+ else:
493+ question_ids = list(IStore(QuestionBug).find(
494+ QuestionBug,
495+ QuestionBug.bug == self).values(QuestionBug.questionID))
496+ return list(sorted(
497+ bulk.load(Question, question_ids), key=operator.attrgetter('id')))
498+
499+ @property
500+ def specifications(self):
501+ from lp.blueprints.model.specification import Specification
502+ from lp.blueprints.model.specificationbug import SpecificationBug
503+ if getFeatureFlag('bugs.xref_buglinks.query'):
504+ spec_ids = [
505+ int(id) for _, id in getUtility(IXRefSet).findFrom(
506+ (u'bug', unicode(self.id)), types=[u'specification'])]
507+ else:
508+ spec_ids = list(IStore(SpecificationBug).find(
509+ SpecificationBug,
510+ SpecificationBug.bug == self).values(
511+ SpecificationBug.specificationID))
512+ return list(sorted(
513+ bulk.load(Specification, spec_ids), key=operator.attrgetter('id')))
514+
515 def getSpecifications(self, user):
516 """See `IBug`."""
517- return IStore(SpecificationBug).find(
518+ from lp.blueprints.model.specification import Specification
519+ from lp.blueprints.model.specificationsearch import (
520+ get_specification_privacy_filter,
521+ )
522+ return IStore(Specification).find(
523 Specification,
524- SpecificationBug.bugID == self.id,
525- SpecificationBug.specificationID == Specification.id,
526+ Specification.id.is_in(spec.id for spec in self.specifications),
527 *get_specification_privacy_filter(user))
528
529 @property
530
531=== modified file 'lib/lp/bugs/model/bugcve.py'
532--- lib/lp/bugs/model/bugcve.py 2015-09-25 10:15:37 +0000
533+++ lib/lp/bugs/model/bugcve.py 2015-10-01 01:43:11 +0000
534@@ -6,6 +6,8 @@
535
536 from sqlobject import ForeignKey
537
538+from lp.services.database.constants import UTC_NOW
539+from lp.services.database.datetimecol import UtcDateTimeCol
540 from lp.services.database.sqlbase import SQLBase
541
542
543@@ -17,3 +19,4 @@
544 # db field names
545 bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
546 cve = ForeignKey(dbName='cve', foreignKey='Cve', notNull=True)
547+ date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW)
548
549=== modified file 'lib/lp/bugs/model/bugtask.py'
550--- lib/lp/bugs/model/bugtask.py 2015-07-08 16:05:11 +0000
551+++ lib/lp/bugs/model/bugtask.py 2015-10-01 01:43:11 +0000
552@@ -139,10 +139,12 @@
553 SQLBase,
554 sqlvalues,
555 )
556+from lp.services.features import getFeatureFlag
557 from lp.services.helpers import shortlist
558 from lp.services.propertycache import get_property_cache
559 from lp.services.searchbuilder import any
560 from lp.services.webapp.interfaces import ILaunchBag
561+from lp.services.xref.interfaces import IXRefSet
562
563
564 def bugtask_sort_key(bugtask):
565@@ -1388,9 +1390,15 @@
566 from lp.bugs.model.bugbranch import BugBranch
567
568 bug_ids = set(bugtask.bugID for bugtask in bugtasks)
569- bug_ids_with_specifications = set(IStore(SpecificationBug).find(
570- SpecificationBug.bugID,
571- SpecificationBug.bugID.is_in(bug_ids)))
572+ if getFeatureFlag('bugs.xref_buglinks.query'):
573+ bug_ids_with_specifications = set(
574+ int(id) for _, id in getUtility(IXRefSet).findFromMany(
575+ [(u'bug', unicode(bug_id)) for bug_id in bug_ids],
576+ types=[u'specification']).keys())
577+ else:
578+ bug_ids_with_specifications = set(IStore(SpecificationBug).find(
579+ SpecificationBug.bugID,
580+ SpecificationBug.bugID.is_in(bug_ids)))
581 bug_ids_with_branches = set(IStore(BugBranch).find(
582 BugBranch.bugID, BugBranch.bugID.is_in(bug_ids)))
583 # Badging looks up milestones too : eager load into the storm cache.
584
585=== modified file 'lib/lp/bugs/model/bugtasksearch.py'
586--- lib/lp/bugs/model/bugtasksearch.py 2015-09-28 12:33:22 +0000
587+++ lib/lp/bugs/model/bugtasksearch.py 2015-10-01 01:43:11 +0000
588@@ -95,6 +95,7 @@
589 rank_by_fti,
590 Unnest,
591 )
592+from lp.services.features import getFeatureFlag
593 from lp.services.propertycache import get_property_cache
594 from lp.services.searchbuilder import (
595 all,
596@@ -103,6 +104,7 @@
597 not_equals,
598 NULL,
599 )
600+from lp.services.xref.model import XRef
601 from lp.soyuz.enums import PackagePublishingStatus
602 from lp.soyuz.model.publishing import SourcePackagePublishingHistory
603
604@@ -421,9 +423,18 @@
605 BugTaskFlat.productseries == None))
606
607 if params.has_cve:
608- extra_clauses.append(
609- BugTaskFlat.bug_id.is_in(
610- Select(BugCve.bugID, tables=[BugCve])))
611+ if getFeatureFlag('bugs.xref_buglinks.query'):
612+ where = [
613+ XRef.from_type == u'bug',
614+ XRef.from_id_int == BugTaskFlat.bug_id,
615+ XRef.to_type == u'cve',
616+ ]
617+ extra_clauses.append(Exists(Select(
618+ 1, tables=[XRef], where=And(*where))))
619+ else:
620+ extra_clauses.append(
621+ BugTaskFlat.bug_id.is_in(
622+ Select(BugCve.bugID, tables=[BugCve])))
623
624 if params.attachmenttype is not None:
625 if params.attachmenttype == BugAttachmentType.PATCH:
626@@ -1013,12 +1024,26 @@
627 linked_blueprints = params.linked_blueprints
628
629 def make_clause(blueprints=None):
630- where = [SpecificationBug.bugID == BugTaskFlat.bug_id]
631- if blueprints is not None:
632- where.append(
633- search_value_to_storm_where_condition(
634- SpecificationBug.specificationID, blueprints))
635- return Exists(Select(1, tables=[SpecificationBug], where=And(*where)))
636+ if getFeatureFlag('bugs.xref_buglinks.query'):
637+ where = [
638+ XRef.from_type == u'bug',
639+ XRef.from_id_int == BugTaskFlat.bug_id,
640+ XRef.to_type == u'specification',
641+ ]
642+ if blueprints is not None:
643+ where.append(
644+ search_value_to_storm_where_condition(
645+ XRef.to_id_int, blueprints))
646+ return Exists(Select(
647+ 1, tables=[XRef], where=And(*where)))
648+ else:
649+ where = [SpecificationBug.bugID == BugTaskFlat.bug_id]
650+ if blueprints is not None:
651+ where.append(
652+ search_value_to_storm_where_condition(
653+ SpecificationBug.specificationID, blueprints))
654+ return Exists(Select(
655+ 1, tables=[SpecificationBug], where=And(*where)))
656
657 if linked_blueprints is None:
658 return None
659
660=== modified file 'lib/lp/bugs/model/cve.py'
661--- lib/lp/bugs/model/cve.py 2015-09-28 07:57:17 +0000
662+++ lib/lp/bugs/model/cve.py 2015-10-01 01:43:11 +0000
663@@ -8,16 +8,16 @@
664 'CveSet',
665 ]
666
667-# SQL imports
668+import operator
669+
670 from sqlobject import (
671 SQLMultipleJoin,
672 SQLObjectNotFound,
673- SQLRelatedJoin,
674 StringCol,
675 )
676 from storm.expr import In
677 from storm.store import Store
678-# Zope
679+from zope.component import getUtility
680 from zope.interface import implementer
681
682 from lp.app.validators.cve import (
683@@ -34,12 +34,16 @@
684 from lp.bugs.model.bugcve import BugCve
685 from lp.bugs.model.buglinktarget import BugLinkTargetMixin
686 from lp.bugs.model.cvereference import CveReference
687-from lp.services.database.bulk import load_related
688+from lp.services.database import bulk
689 from lp.services.database.constants import UTC_NOW
690 from lp.services.database.datetimecol import UtcDateTimeCol
691 from lp.services.database.enumcol import EnumCol
692+from lp.services.database.interfaces import IStore
693 from lp.services.database.sqlbase import SQLBase
694 from lp.services.database.stormexpr import fti_search
695+from lp.services.features import getFeatureFlag
696+from lp.services.xref.interfaces import IXRefSet
697+from lp.services.xref.model import XRef
698
699
700 @implementer(ICve, IBugLinkTarget)
701@@ -54,10 +58,6 @@
702 datecreated = UtcDateTimeCol(notNull=True, default=UTC_NOW)
703 datemodified = UtcDateTimeCol(notNull=True, default=UTC_NOW)
704
705- # joins
706- bugs = SQLRelatedJoin('Bug', intermediateTable='BugCve',
707- joinColumn='cve', otherColumn='bug', orderBy='id')
708- bug_links = SQLMultipleJoin('BugCve', joinColumn='cve', orderBy='id')
709 references = SQLMultipleJoin(
710 'CveReference', joinColumn='cve', orderBy='id')
711
712@@ -75,6 +75,19 @@
713 def title(self):
714 return 'CVE-%s (%s)' % (self.sequence, self.status.title)
715
716+ @property
717+ def bugs(self):
718+ if getFeatureFlag('bugs.xref_buglinks.query'):
719+ bug_ids = [
720+ int(id) for _, id in getUtility(IXRefSet).findFrom(
721+ (u'cve', self.sequence), types=[u'bug'])]
722+ else:
723+ bug_ids = list(IStore(BugCve).find(
724+ BugCve,
725+ BugCve.cve == self).values(BugCve.bugID))
726+ return list(sorted(
727+ bulk.load(Bug, bug_ids), key=operator.attrgetter('id')))
728+
729 # CveReference's
730 def createReference(self, source, content, url=None):
731 """See ICveReference."""
732@@ -87,11 +100,18 @@
733
734 def createBugLink(self, bug):
735 """See BugLinkTargetMixin."""
736- BugCve(cve=self, bug=bug)
737+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
738+ BugCve(cve=self, bug=bug)
739+ # XXX: Should set creator.
740+ getUtility(IXRefSet).create(
741+ {(u'cve', self.sequence): {(u'bug', unicode(bug.id)): {}}})
742
743 def deleteBugLink(self, bug):
744 """See BugLinkTargetMixin."""
745- Store.of(self).find(BugCve, cve=self, bug=bug).remove()
746+ if not getFeatureFlag('bugs.xref_buglinks.write_old.disabled'):
747+ Store.of(self).find(BugCve, cve=self, bug=bug).remove()
748+ getUtility(IXRefSet).delete(
749+ {(u'cve', self.sequence): [(u'bug', unicode(bug.id))]})
750
751
752 @implementer(ICveSet)
753@@ -177,39 +197,52 @@
754
755 def getBugCvesForBugTasks(self, bugtasks, cve_mapper=None):
756 """See ICveSet."""
757- bugs = load_related(Bug, bugtasks, ('bugID', ))
758+ bugs = bulk.load_related(Bug, bugtasks, ('bugID', ))
759 if len(bugs) == 0:
760 return []
761- bug_ids = [bug.id for bug in bugs]
762-
763- # Do not use BugCve instances: Storm may need a very long time
764- # to look up the bugs and CVEs referenced by a BugCve instance
765- # when the +cve view of a distroseries is rendered: There may
766- # be a few thousand (bug, CVE) tuples, while the number of bugs
767- # and CVEs is in the order of hundred. It is much more efficient
768- # to retrieve just (bug_id, cve_id) from the BugCve table and
769- # to map this to (Bug, CVE) here, instead of letting Storm
770- # look up the CVE and bug for a BugCve instance, even if bugs
771- # and CVEs are bulk loaded.
772 store = Store.of(bugtasks[0])
773- bugcve_ids = store.find(
774- (BugCve.bugID, BugCve.cveID), In(BugCve.bugID, bug_ids))
775- bugcve_ids.order_by(BugCve.bugID, BugCve.cveID)
776- bugcve_ids = list(bugcve_ids)
777-
778- cve_ids = set(cve_id for bug_id, cve_id in bugcve_ids)
779- cves = store.find(Cve, In(Cve.id, list(cve_ids)))
780+
781+ if getFeatureFlag('bugs.xref_buglinks.query'):
782+ xrefs = getUtility(IXRefSet).findFromMany(
783+ [(u'bug', unicode(bug.id)) for bug in bugs], types=[u'cve'])
784+ bugcve_ids = set()
785+ for bug_key in xrefs:
786+ for cve_key in xrefs[bug_key]:
787+ bugcve_ids.add((int(bug_key[1]), cve_key[1]))
788+ else:
789+ # Do not use BugCve instances: Storm may need a very long time
790+ # to look up the bugs and CVEs referenced by a BugCve instance
791+ # when the +cve view of a distroseries is rendered: There may
792+ # be a few thousand (bug, CVE) tuples, while the number of bugs
793+ # and CVEs is in the order of hundred. It is much more efficient
794+ # to retrieve just (Bug.id, Cve.sequence) from the BugCve
795+ # table and to map this to (Bug, CVE) here, instead of
796+ # letting Storm look up the CVE and bug for a BugCve
797+ # instance, even if bugs and CVEs are bulk loaded.
798+ bug_ids = [bug.id for bug in bugs]
799+ bugcve_ids = store.find(
800+ (BugCve.bugID, Cve.sequence),
801+ Cve.id == BugCve.cveID, In(BugCve.bugID, bug_ids))
802+
803+ bugcve_ids = list(sorted(bugcve_ids))
804+
805+ cves = store.find(
806+ Cve, In(Cve.sequence, [seq for _, seq in bugcve_ids]))
807
808 if cve_mapper is None:
809- cvemap = dict((cve.id, cve) for cve in cves)
810+ cvemap = dict((cve.sequence, cve) for cve in cves)
811 else:
812- cvemap = dict((cve.id, cve_mapper(cve)) for cve in cves)
813+ cvemap = dict((cve.sequence, cve_mapper(cve)) for cve in cves)
814 bugmap = dict((bug.id, bug) for bug in bugs)
815 return [
816- (bugmap[bug_id], cvemap[cve_id])
817- for bug_id, cve_id in bugcve_ids
818+ (bugmap[bug_id], cvemap[cve_sequence])
819+ for bug_id, cve_sequence in bugcve_ids
820 ]
821
822 def getBugCveCount(self):
823 """See ICveSet."""
824- return BugCve.select().count()
825+ if getFeatureFlag('bugs.xref_buglinks.query'):
826+ return IStore(XRef).find(
827+ XRef, XRef.from_type == u'bug', XRef.to_type == u'cve').count()
828+ else:
829+ return BugCve.select().count()
830
831=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
832--- lib/lp/bugs/model/tests/test_bugtask.py 2015-09-28 17:38:45 +0000
833+++ lib/lp/bugs/model/tests/test_bugtask.py 2015-10-01 01:43:11 +0000
834@@ -523,6 +523,22 @@
835 ])
836
837
838+class TestBugTaskBadgesWithXRef(TestBugTaskBadges):
839+
840+ def setUp(self):
841+ super(TestBugTaskBadges, self).setUp()
842+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
843+
844+
845+class TestBugTaskBadgesWithXRefAndNoOld(TestBugTaskBadges):
846+
847+ def setUp(self):
848+ super(TestBugTaskBadges, self).setUp()
849+ self.useFixture(FeatureFixture({
850+ 'bugs.xref_buglinks.query': 'true',
851+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
852+
853+
854 class TestBugTaskPrivacy(TestCaseWithFactory):
855 """Verify that the bug is either private or public.
856
857
858=== modified file 'lib/lp/bugs/model/tests/test_bugtasksearch.py'
859--- lib/lp/bugs/model/tests/test_bugtasksearch.py 2015-09-28 12:33:22 +0000
860+++ lib/lp/bugs/model/tests/test_bugtasksearch.py 2015-10-01 01:43:11 +0000
861@@ -67,6 +67,7 @@
862 from lp.registry.model.person import Person
863 from lp.services.database.interfaces import IStore
864 from lp.services.database.sqlbase import convert_storm_clause_to_string
865+from lp.services.features.testing import FeatureFixture
866 from lp.services.searchbuilder import (
867 all,
868 any,
869@@ -419,6 +420,16 @@
870 BugBlueprintSearch.BUGS_WITHOUT_BLUEPRINTS))
871 self.assertSearchFinds(params, self.bugtasks[1:])
872
873+ def test_blueprints_linked_with_xref(self):
874+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
875+ self.test_blueprints_linked()
876+
877+ def test_blueprints_linked_with_xref_and_no_old(self):
878+ self.useFixture(FeatureFixture({
879+ 'bugs.xref_buglinks.query': 'true',
880+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
881+ self.test_blueprints_linked()
882+
883 def test_limit_search_to_one_bug(self):
884 # Search results can be limited to a given bug.
885 params = self.getBugTaskSearchParams(
886@@ -492,6 +503,16 @@
887 params = self.getBugTaskSearchParams(user=None, has_cve=True)
888 self.assertSearchFinds(params, self.bugtasks[:1])
889
890+ def test_has_cve_with_xref(self):
891+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
892+ self.test_has_cve()
893+
894+ def test_has_cve_with_xref_and_no_old(self):
895+ self.useFixture(FeatureFixture({
896+ 'bugs.xref_buglinks.query': 'true',
897+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
898+ self.test_has_cve()
899+
900 def test_sort_by_milestone_name(self):
901 expected = self.setUpMilestoneSorting()
902 params = self.getBugTaskSearchParams(
903
904=== modified file 'lib/lp/bugs/tests/test_cve.py'
905--- lib/lp/bugs/tests/test_cve.py 2015-09-28 07:39:28 +0000
906+++ lib/lp/bugs/tests/test_cve.py 2015-10-01 01:43:11 +0000
907@@ -7,6 +7,7 @@
908
909 from lp.bugs.interfaces.bugtasksearch import BugTaskSearchParams
910 from lp.bugs.interfaces.cve import ICveSet
911+from lp.services.features.testing import FeatureFixture
912 from lp.testing import (
913 login_person,
914 person_logged_in,
915@@ -77,6 +78,42 @@
916 u'CVE-2000-0004']
917 self.assertEqual(expected, cve_data)
918
919+ def test_getBugCveCount(self):
920+ login_person(self.factory.makePerson())
921+
922+ base = getUtility(ICveSet).getBugCveCount()
923+ bug1 = self.factory.makeBug()
924+ bug2 = self.factory.makeBug()
925+ cve1 = self.factory.makeCVE(sequence='2099-1234')
926+ cve2 = self.factory.makeCVE(sequence='2099-2468')
927+ self.assertEqual(base, getUtility(ICveSet).getBugCveCount())
928+ cve1.linkBug(bug1)
929+ self.assertEqual(base + 1, getUtility(ICveSet).getBugCveCount())
930+ cve1.linkBug(bug2)
931+ self.assertEqual(base + 2, getUtility(ICveSet).getBugCveCount())
932+ cve2.linkBug(bug1)
933+ self.assertEqual(base + 3, getUtility(ICveSet).getBugCveCount())
934+ cve1.unlinkBug(bug1)
935+ cve1.unlinkBug(bug2)
936+ cve2.unlinkBug(bug1)
937+ self.assertEqual(base, getUtility(ICveSet).getBugCveCount())
938+
939+
940+class TestCveSetWithXRef(TestCveSet):
941+
942+ def setUp(self):
943+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
944+ super(TestCveSetWithXRef, self).setUp()
945+
946+
947+class TestCveSetWithXRefAndNoOld(TestCveSet):
948+
949+ def setUp(self):
950+ self.useFixture(FeatureFixture({
951+ 'bugs.xref_buglinks.query': 'true',
952+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
953+ super(TestCveSetWithXRefAndNoOld, self).setUp()
954+
955
956 class TestBugLinks(TestCaseWithFactory):
957
958@@ -113,3 +150,19 @@
959 self.assertContentEqual([bug1], cve2.bugs)
960 self.assertContentEqual([cve2], bug1.cves)
961 self.assertContentEqual([], bug2.cves)
962+
963+
964+class TestBugLinksWithXRef(TestBugLinks):
965+
966+ def setUp(self):
967+ super(TestBugLinksWithXRef, self).setUp()
968+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
969+
970+
971+class TestBugLinksWithXRefAndNoOld(TestBugLinks):
972+
973+ def setUp(self):
974+ super(TestBugLinksWithXRefAndNoOld, self).setUp()
975+ self.useFixture(FeatureFixture({
976+ 'bugs.xref_buglinks.query': 'true',
977+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
978
979=== modified file 'lib/lp/coop/answersbugs/model.py'
980--- lib/lp/coop/answersbugs/model.py 2015-09-25 10:15:37 +0000
981+++ lib/lp/coop/answersbugs/model.py 2015-10-01 01:43:11 +0000
982@@ -9,6 +9,8 @@
983
984 from sqlobject import ForeignKey
985
986+from lp.services.database.constants import UTC_NOW
987+from lp.services.database.datetimecol import UtcDateTimeCol
988 from lp.services.database.sqlbase import SQLBase
989
990
991@@ -20,3 +22,4 @@
992 question = ForeignKey(
993 dbName='question', foreignKey='Question', notNull=True)
994 bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
995+ date_created = UtcDateTimeCol(notNull=True, default=UTC_NOW)
996
997=== modified file 'lib/lp/coop/answersbugs/tests/test_questionbug.py'
998--- lib/lp/coop/answersbugs/tests/test_questionbug.py 2015-09-29 05:04:44 +0000
999+++ lib/lp/coop/answersbugs/tests/test_questionbug.py 2015-10-01 01:43:11 +0000
1000@@ -1,6 +1,7 @@
1001 # Copyright 2015 Canonical Ltd. This software is licensed under the
1002 # GNU Affero General Public License version 3 (see the file LICENSE).
1003
1004+from lp.services.features.testing import FeatureFixture
1005 from lp.testing import (
1006 login_person,
1007 TestCaseWithFactory,
1008@@ -100,3 +101,19 @@
1009 self.assertFalse(bug.isSubscribed(question.owner))
1010 question.unlinkBug(bug)
1011 self.assertFalse(bug.isSubscribed(question.owner))
1012+
1013+
1014+class TestQuestionBugLinksWithXRef(TestQuestionBugLinks):
1015+
1016+ def setUp(self):
1017+ super(TestQuestionBugLinksWithXRef, self).setUp()
1018+ self.useFixture(FeatureFixture({'bugs.xref_buglinks.query': 'true'}))
1019+
1020+
1021+class TestQuestionBugLinksWithXRefAndNoOld(TestQuestionBugLinks):
1022+
1023+ def setUp(self):
1024+ super(TestQuestionBugLinksWithXRefAndNoOld, self).setUp()
1025+ self.useFixture(FeatureFixture({
1026+ 'bugs.xref_buglinks.query': 'true',
1027+ 'bugs.xref_buglinks.write_old.disabled': 'true'}))
1028
1029=== modified file 'lib/lp/scripts/garbo.py'
1030--- lib/lp/scripts/garbo.py 2015-07-22 07:09:12 +0000
1031+++ lib/lp/scripts/garbo.py 2015-10-01 01:43:11 +0000
1032@@ -12,6 +12,7 @@
1033 'save_garbo_job_state',
1034 ]
1035
1036+from collections import defaultdict
1037 from datetime import (
1038 datetime,
1039 timedelta,
1040@@ -81,6 +82,7 @@
1041 from lp.services.database.bulk import (
1042 create,
1043 dbify_value,
1044+ load_related,
1045 )
1046 from lp.services.database.constants import UTC_NOW
1047 from lp.services.database.interfaces import IMasterStore
1048@@ -1678,6 +1680,66 @@
1049 transaction.abort()
1050
1051
1052+class BugXRefMigrator(TunableLoop):
1053+ """Creates an XRef record for each former IBugLink."""
1054+
1055+ maximum_chunk_size = 5000
1056+
1057+ def __init__(self, log, abort_time=None):
1058+ super(BugXRefMigrator, self).__init__(log, abort_time)
1059+ self.start_at = 1
1060+ self.store = IMasterStore(Bug)
1061+
1062+ def findBugs(self):
1063+ if not getFeatureFlag('bugs.xref_buglinks.garbo.enabled'):
1064+ return EmptyResultSet()
1065+ return self.store.find(
1066+ Bug, Bug.id >= self.start_at).order_by(Bug.id)
1067+
1068+ def isDone(self):
1069+ return self.findBugs().is_empty()
1070+
1071+ def __call__(self, chunk_size):
1072+ # Grab a chunk of Bug IDs.
1073+ # Find all QuestionBugs, SpecificationBugs and BugCves for each
1074+ # of those bugs.
1075+ # Compose a list of link IDs that should exist.
1076+ # Perform a bulk XRef find for all of those.
1077+ # Create any missing.
1078+ from lp.blueprints.model.specificationbug import SpecificationBug
1079+ from lp.bugs.model.bugcve import BugCve
1080+ from lp.bugs.model.cve import Cve
1081+ from lp.coop.answersbugs.model import QuestionBug
1082+ from lp.services.xref.interfaces import IXRefSet
1083+ bug_ids = list(self.findBugs()[:chunk_size].values(Bug.id))
1084+ qbs = list(self.store.find(
1085+ QuestionBug, QuestionBug.bugID.is_in(bug_ids)))
1086+ sbs = list(self.store.find(
1087+ SpecificationBug, SpecificationBug.bugID.is_in(bug_ids)))
1088+ bcs = list(self.store.find(BugCve, BugCve.bugID.is_in(bug_ids)))
1089+ wanted = defaultdict(dict)
1090+ for qb in qbs:
1091+ wanted[(u'bug', unicode(qb.bugID))][
1092+ (u'question', unicode(qb.questionID))] = {
1093+ 'date_created': qb.date_created}
1094+ for sb in sbs:
1095+ wanted[(u'bug', unicode(sb.bugID))][
1096+ (u'specification', unicode(sb.specificationID))] = {}
1097+ load_related(Cve, bcs, ['cveID'])
1098+ for bc in bcs:
1099+ wanted[(u'bug', unicode(bc.bugID))][
1100+ (u'cve', unicode(bc.cve.sequence))] = {}
1101+ existing = getUtility(IXRefSet).findFromMany(wanted.keys())
1102+ needed = {
1103+ bug: {
1104+ other: meta for other, meta in others.iteritems()
1105+ if other not in existing.get(bug, {})}
1106+ for bug, others in wanted.iteritems() if others}
1107+ getUtility(IXRefSet).create(needed)
1108+ self.start_at = bug_ids[-1] + 1
1109+ transaction.commit()
1110+
1111+
1112 class FrequentDatabaseGarbageCollector(BaseDatabaseGarbageCollector):
1113 """Run every 5 minutes.
1114
1115@@ -1711,6 +1773,7 @@
1116 tunable_loops = [
1117 BugHeatUpdater,
1118 BugWatchScheduler,
1119+ BugXRefMigrator,
1120 DuplicateSessionPruner,
1121 RevisionCachePruner,
1122 UnusedSessionPruner,
1123
1124=== modified file 'lib/lp/scripts/tests/test_garbo.py'
1125--- lib/lp/scripts/tests/test_garbo.py 2015-07-22 07:09:12 +0000
1126+++ lib/lp/scripts/tests/test_garbo.py 2015-10-01 01:43:11 +0000
1127@@ -1433,6 +1433,94 @@
1128 for person in people_enf_true:
1129 _assert_enf_by_person(person, True)
1130
1131+ def test_BugXRefMigrator(self):
1132+ from testtools.matchers import (
1133+ Equals,
1134+ Is,
1135+ MatchesDict,
1136+ Not,
1137+ )
1138+
1139+ from lp.bugs.model.bug import Bug
1140+ from lp.bugs.model.bugcve import BugCve
1141+ from lp.blueprints.model.specificationbug import SpecificationBug
1142+ from lp.coop.answersbugs.model import QuestionBug
1143+ from lp.services.database.interfaces import IStore
1144+ from lp.services.xref.interfaces import IXRefSet
1145+
1146+ switch_dbuser('testadmin')
1147+ self.useFixture(FeatureFixture(
1148+ {'bugs.xref_buglinks.garbo.enabled': 'on'}))
1149+ store = IStore(Bug)
1150+
1151+ # The first bug has a spec and a question.
1152+ bug1 = self.factory.makeBug()
1153+ spec1 = self.factory.makeSpecification()
1154+ sb1 = SpecificationBug(specification=spec1, bug=bug1)
1155+ store.add(sb1)
1156+ question1 = self.factory.makeQuestion()
1157+ qb1 = QuestionBug(question=question1, bug=bug1)
1158+ store.add(qb1)
1159+
1160+ # A second bug has a question and a CVE.
1161+ bug2 = self.factory.makeBug()
1162+ question2 = self.factory.makeQuestion()
1163+ qb2 = QuestionBug(question=question2, bug=bug2)
1164+ store.add(qb2)
1165+ cve2 = self.factory.makeCVE(sequence='2099-1234')
1166+ bc2 = BugCve(bug=bug2, cve=cve2)
1167+ store.add(bc2)
1168+
1169+ # Bug the third is all alone.
1170+ bug3 = self.factory.makeBug()
1171+
1172+ # Bug four has just a spec.
1173+ bug4 = self.factory.makeBug()
1174+ spec4 = self.factory.makeSpecification()
1175+ sb4 = SpecificationBug(specification=spec4, bug=bug4)
1176+ store.add(sb4)
1177+
1178+ # Initially the new XRef table has no links for the bugs.
1179+ self.assertEqual(
1180+ {},
1181+ getUtility(IXRefSet).findFromMany(
1182+ (u'bug', unicode(bug.id)) for bug in (bug1, bug2, bug3, bug4)))
1183+
1184+ # Garbo fills in links for each QuestionBug, SpecificationBug
1185+ # and BugCve.
1186+ self.runHourly()
1187+ matches_expected = MatchesDict({
1188+ (u'bug', unicode(bug1.id)): MatchesDict({
1189+ (u'specification', unicode(spec1.id)): MatchesDict({
1190+ 'metadata': Is(None), 'creator': Is(None),
1191+ 'date_created': Not(Is(None))}),
1192+ (u'question', unicode(question1.id)): MatchesDict({
1193+ 'metadata': Is(None), 'creator': Is(None),
1194+ 'date_created': Equals(qb1.date_created)}),
1195+ }),
1196+ (u'bug', unicode(bug2.id)): MatchesDict({
1197+ (u'question', unicode(question2.id)): MatchesDict({
1198+ 'metadata': Is(None), 'creator': Is(None),
1199+ 'date_created': Equals(qb2.date_created)}),
1200+ (u'cve', cve2.sequence): MatchesDict({
1201+ 'metadata': Is(None), 'creator': Is(None),
1202+ 'date_created': Not(Is(None))}),
1203+ }),
1204+ (u'bug', unicode(bug4.id)): MatchesDict({
1205+ (u'specification', unicode(spec4.id)): MatchesDict({
1206+ 'metadata': Is(None), 'creator': Is(None),
1207+ 'date_created': Not(Is(None))}),
1208+ }),
1209+ })
1210+ self.assertThat(
1211+ getUtility(IXRefSet).findFromMany(
1212+ (u'bug', unicode(bug.id)) for bug in (bug1, bug2, bug3, bug4)),
1213+ matches_expected)
1214+
1215+ # A second run is harmless.
1216+ self.runHourly()
1217+
1218+
1219
1220 class TestGarboTasks(TestCaseWithFactory):
1221 layer = LaunchpadZopelessLayer
1222
1223=== modified file 'lib/lp/services/xref/model.py'
1224--- lib/lp/services/xref/model.py 2015-09-28 23:37:26 +0000
1225+++ lib/lp/services/xref/model.py 2015-10-01 01:43:11 +0000
1226@@ -5,6 +5,7 @@
1227
1228 __metaclass__ = type
1229 __all__ = [
1230+ "XRef",
1231 "XRefSet",
1232 ]
1233
1234@@ -37,6 +38,9 @@
1235
1236 The to_id_int and from_id_int columns exist for efficient SQL joins.
1237 They are set automatically when the ID looks like an integer.
1238+
1239+ NOTE: This should rarely be used directly. Prefer IXRefSet unless
1240+ porting an old query.
1241 """
1242
1243 __storm_table__ = 'XRef'