Merge lp:~wgrant/launchpad/bugtaskflat-db into lp:launchpad/db-devel

Proposed by William Grant on 2012-03-08
Status: Merged
Approved by: William Grant on 2012-03-30
Approved revision: no longer in the source branch.
Merged at revision: 11489
Proposed branch: lp:~wgrant/launchpad/bugtaskflat-db
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~wgrant/launchpad/ap-delete
Diff against target: 1103 lines (+971/-0)
3 files modified
database/schema/patch-2209-16-0.sql (+546/-0)
database/schema/security.cfg (+19/-0)
lib/lp/bugs/tests/test_bugtaskflat_triggers.py (+406/-0)
To merge this branch: bzr merge lp:~wgrant/launchpad/bugtaskflat-db
Reviewer Review Type Date Requested Status
j.c.sackett (community) code 2012-03-29 Approve on 2012-03-29
Richard Harding (community) code* 2012-03-29 Approve on 2012-03-29
Stuart Bishop db 2012-03-08 Approve on 2012-03-29
Robert Collins db 2012-03-29 Pending
Review via email: mp+96520@code.launchpad.net

Commit Message

Create BugTaskFlat, a flattened version of Bug, BugTask, and some other bits and pieces.

Description of the Change

Bug filtering and sorting is split awkwardly across Bug and BugTask, causing fairly universally terrible performance. Further, access checks extend to joins on BugSubscription. Experiments have shown that putting most of the sorting and filtering criteria into a single table improves speed vastly.

This branch introduces a new table, BugTaskFlat, comprising relevant fields from Bug and BugTask, Product.active and arrays of relevant records from AccessPolicyArtifact and AccessArtifactGrant. This allows most bug searches (including access checks) to be answered from a simple scan down a sort index, filtering inline as the records are retrieved. More selective queries can be optimised by bitmap index operations once we work out what indexes are interesting.

The table is maintained by a fairly impressive array of triggers and helper functions, but there is also a master function which is the canonical definition of BugTaskFlat and can correct rows to match reality. So the table is fairly disposable and easy to regenerate if we need to for any reason. There are reasonably thorough tests of these triggers.

The triggers have one hole: Product.active. Because products can have hilariously large numbers of bugs, updating BugTaskFlat.active inside a request is infeasible. It will probably be updated by a job later.

I've omitted foreign keys from the table. This seems to be a significant performance enhancement, and is fine from an integrity perspective because the table is only updated by triggers from FKed tables, and is disposable and cheap to regenerate if things end up going wrong. I also make use of arrays which can't be FKed in current PostgreSQL releases.

To post a comment you must log in.
William Grant (wgrant) wrote :

I suspect that a later iteration will include a tag array in some form, but with PostgreSQL 8.4 it gets a bit awkward, and it needs lots of testing to see which method is best (an array of strings, or an array of integers as interned strings, a fake tsvector, etc.). This is a good start.

Also, access_policies and access_grants are null if the bug is public, as they don't really make sense. When the bug is private they're always (possibly empty) arrays.

Stuart Bishop (stub) wrote :

I guess we need a bug files about the Product.active hole. I agree it can't be done with triggers (well... we could schedule a celary job from a trigger).

Without foreign key references to the Person table, all those references will need special casing in the people merge code. We probably want a bug tracking this too, as we will need to rebuild BugTaskFlat after adding that code. Or perhaps the job that handles Product.active will maintain this too?

=== added file 'database/schema/patch-2209-16-0.sql'

+CREATE INDEX bugtaskflat__bugtask__idx ON BugTaskFlat USING btree (bugtask);

This index is redundant - a UNIQUE index gets created on bugtask with the PRIMARY KEY.

+CREATE INDEX

Lots of other indexes for searching and reporting. I'm not going to second guess which columns should be DESC or not, and that things are structured the way they are to support the queries you need. My eyeballs don't pick up any naming errors or cut'n'paste glitches.

+ ELSIF new_flat_row != old_flat_row THEN

I think this comparison will fail if the rows are identical except for the order of the elements in the access_policies and access_grants arrays. A spurious update will be harmless here, but the return value will be incorrect. I think we need to guarantee the access cache helpers return the elements in a stable order.

=== added file 'lib/lp/bugs/tests/test_bugtaskflat_triggers.py'

I haven't reviewed the tests. I can do the code portion later if nobody beats me too it.

review: Approve (db)
Richard Harding (rharding) wrote :

Thanks William. From a code standpoint this looks ok. It's hard to say that all cases and corners are covered but it appears very well thought out and run through. I'd definitely expect the integration parts in the future exercising the current tests around the actual searching to help find any large holes in moving to this new flattened table setup.

review: Approve (code*)
William Grant (wgrant) wrote :

On 29/03/12 23:48, Stuart Bishop wrote:
> Review: Approve db
>
> I guess we need a bug files about the Product.active hole. I agree it
> can't be done with triggers (well... we could schedule a celary job
> from a trigger).
>
> Without foreign key references to the Person table, all those
> references will need special casing in the people merge code. We
> probably want a bug tracking this too, as we will need to rebuild
> BugTaskFlat after adding that code. Or perhaps the job that handles
> Product.active will maintain this too?

No need for special-casing -- person merging will update the FKs on the
base tables, and the usual triggers will update BugTaskFlat. Person
merging doesn't need to know it exists.

> === added file 'database/schema/patch-2209-16-0.sql'
>
>
> +CREATE INDEX bugtaskflat__bugtask__idx ON BugTaskFlat USING btree
> (bugtask);
>
> This index is redundant - a UNIQUE index gets created on bugtask with
> the PRIMARY KEY.

Good point.

> +CREATE INDEX
>
> Lots of other indexes for searching and reporting. I'm not going to
> second guess which columns should be DESC or not, and that things are
> structured the way they are to support the queries you need. My
> eyeballs don't pick up any naming errors or cut'n'paste glitches.
>
>
> + ELSIF new_flat_row != old_flat_row THEN
>
> I think this comparison will fail if the rows are identical except
> for the order of the elements in the access_policies and
> access_grants arrays. A spurious update will be harmless here, but
> the return value will be incorrect. I think we need to guarantee the
> access cache helpers return the elements in a stable order.

Indeed. I'll add an order.

> === added file 'lib/lp/bugs/tests/test_bugtaskflat_triggers.py'
>
> I haven't reviewed the tests. I can do the code portion later if
> nobody beats me too it.
>

j.c.sackett (jcsackett) wrote :

The tests seem pretty solid--everything I would worry about seems covered. This looks fine to land from my perspective.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'database/schema/patch-2209-16-0.sql'
2--- database/schema/patch-2209-16-0.sql 1970-01-01 00:00:00 +0000
3+++ database/schema/patch-2209-16-0.sql 2012-03-30 00:02:34 +0000
4@@ -0,0 +1,546 @@
5+-- Copyright 2012 Canonical Ltd. This software is licensed under the
6+-- GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+SET client_min_messages=ERROR;
9+
10+CREATE TABLE BugTaskFlat (
11+ bugtask integer PRIMARY KEY,
12+ bug integer NOT NULL,
13+ datecreated timestamp without time zone,
14+ duplicateof integer,
15+ bug_owner integer NOT NULL,
16+ fti ts2.tsvector,
17+ information_type integer NOT NULL,
18+ date_last_updated timestamp without time zone NOT NULL,
19+ heat integer NOT NULL,
20+ product integer,
21+ productseries integer,
22+ distribution integer,
23+ distroseries integer,
24+ sourcepackagename integer,
25+ status integer NOT NULL,
26+ importance integer NOT NULL,
27+ assignee integer,
28+ milestone integer,
29+ owner integer NOT NULL,
30+ active boolean NOT NULL,
31+ access_policies integer[],
32+ access_grants integer[]
33+);
34+
35+
36+-- Non-target-specific filters
37+CREATE INDEX bugtaskflat__bug__idx ON BugTaskFlat USING btree (bug);
38+
39+CREATE INDEX bugtaskflat__bug_owner__idx
40+ ON BugTaskFlat USING btree (bug_owner);
41+CREATE INDEX bugtaskflat__owner__idx
42+ ON BugTaskFlat USING btree (owner);
43+CREATE INDEX bugtaskflat__assignee__idx
44+ ON BugTaskFlat USING btree (assignee);
45+CREATE INDEX bugtaskflat__milestone__idx
46+ ON BugTaskFlat USING btree (milestone);
47+
48+CREATE INDEX bugtaskflat__fti__idx ON BugTaskFlat USING gist (fti);
49+
50+
51+-- Distribution-wide sorts
52+CREATE INDEX
53+ bugtaskflat__distribution__date_last_updated__idx
54+ ON bugtaskflat
55+ USING btree (distribution, date_last_updated)
56+ WHERE distribution IS NOT NULL;
57+CREATE INDEX
58+ bugtaskflat__distribution__datecreated__idx
59+ ON bugtaskflat
60+ USING btree (distribution, datecreated)
61+ WHERE distribution IS NOT NULL;
62+CREATE INDEX
63+ bugtaskflat__distribution__heat__bugtask__idx
64+ ON bugtaskflat
65+ USING btree (distribution, heat, bugtask DESC)
66+ WHERE distribution IS NOT NULL;
67+CREATE INDEX
68+ bugtaskflat__distribution__importance__bugtask__idx
69+ ON bugtaskflat
70+ USING btree (distribution, importance, bugtask DESC)
71+ WHERE distribution IS NOT NULL;
72+CREATE INDEX
73+ bugtaskflat__distribution__status__bugtask__idx
74+ ON bugtaskflat
75+ USING btree (distribution, status, bugtask DESC)
76+ WHERE distribution IS NOT NULL;
77+
78+-- DSP or packageless sorts
79+CREATE INDEX
80+ bugtaskflat__distribution__spn__date_last_updated__idx
81+ ON bugtaskflat
82+ USING btree (distribution, sourcepackagename, date_last_updated)
83+ WHERE distribution IS NOT NULL;
84+CREATE INDEX
85+ bugtaskflat__distribution__spn__datecreated__idx
86+ ON bugtaskflat
87+ USING btree (distribution, sourcepackagename, datecreated)
88+ WHERE distribution IS NOT NULL;
89+CREATE INDEX
90+ bugtaskflat__distribution__spn__heat__bug__idx
91+ ON bugtaskflat
92+ USING btree (distribution, sourcepackagename, heat, bug DESC)
93+ WHERE distribution IS NOT NULL;
94+CREATE INDEX
95+ bugtaskflat__distribution__spn__importance__bug__idx
96+ ON bugtaskflat
97+ USING btree (distribution, sourcepackagename, importance, bug DESC)
98+ WHERE distribution IS NOT NULL;
99+CREATE INDEX
100+ bugtaskflat__distribution__spn__status__bug__idx
101+ ON bugtaskflat
102+ USING btree (distribution, sourcepackagename, status, bug DESC)
103+ WHERE distribution IS NOT NULL;
104+
105+
106+-- DistroSeries-wide sorts
107+CREATE INDEX
108+ bugtaskflat__distroseries__date_last_updated__idx
109+ ON bugtaskflat
110+ USING btree (distroseries, date_last_updated)
111+ WHERE distroseries IS NOT NULL;
112+CREATE INDEX
113+ bugtaskflat__distroseries__datecreated__idx
114+ ON bugtaskflat
115+ USING btree (distroseries, datecreated)
116+ WHERE distroseries IS NOT NULL;
117+CREATE INDEX
118+ bugtaskflat__distroseries__heat__bugtask__idx
119+ ON bugtaskflat
120+ USING btree (distroseries, heat, bugtask DESC)
121+ WHERE distroseries IS NOT NULL;
122+CREATE INDEX
123+ bugtaskflat__distroseries__importance__bugtask__idx
124+ ON bugtaskflat
125+ USING btree (distroseries, importance, bugtask DESC)
126+ WHERE distroseries IS NOT NULL;
127+CREATE INDEX
128+ bugtaskflat__distroseries__status__bugtask__idx
129+ ON bugtaskflat
130+ USING btree (distroseries, status, bugtask DESC)
131+ WHERE distroseries IS NOT NULL;
132+
133+-- SP or packageless sorts
134+CREATE INDEX
135+ bugtaskflat__distroseries__spn__date_last_updated__idx
136+ ON bugtaskflat
137+ USING btree (distroseries, sourcepackagename, date_last_updated)
138+ WHERE distroseries IS NOT NULL;
139+CREATE INDEX
140+ bugtaskflat__distroseries__spn__datecreated__idx
141+ ON bugtaskflat
142+ USING btree (distroseries, sourcepackagename, datecreated)
143+ WHERE distroseries IS NOT NULL;
144+CREATE INDEX
145+ bugtaskflat__distroseries__spn__heat__bug__idx
146+ ON bugtaskflat
147+ USING btree (distroseries, sourcepackagename, heat, bug DESC)
148+ WHERE distroseries IS NOT NULL;
149+CREATE INDEX
150+ bugtaskflat__distroseries__spn__importance__bug__idx
151+ ON bugtaskflat
152+ USING btree (distroseries, sourcepackagename, importance, bug DESC)
153+ WHERE distroseries IS NOT NULL;
154+CREATE INDEX
155+ bugtaskflat__distroseries__spn__status__bug__idx
156+ ON bugtaskflat
157+ USING btree (distroseries, sourcepackagename, status, bug DESC)
158+ WHERE distroseries IS NOT NULL;
159+
160+
161+-- Product sorts
162+CREATE INDEX
163+ bugtaskflat__product__date_last_updated__idx
164+ ON bugtaskflat
165+ USING btree (product, date_last_updated)
166+ WHERE product IS NOT NULL;
167+CREATE INDEX
168+ bugtaskflat__product__datecreated__idx
169+ ON bugtaskflat
170+ USING btree (product, datecreated)
171+ WHERE product IS NOT NULL;
172+CREATE INDEX
173+ bugtaskflat__product__heat__bug__idx
174+ ON bugtaskflat
175+ USING btree (product, heat, bug DESC)
176+ WHERE product IS NOT NULL;
177+CREATE INDEX
178+ bugtaskflat__product__importance__bug__idx
179+ ON bugtaskflat
180+ USING btree (product, importance, bug DESC)
181+ WHERE product IS NOT NULL;
182+CREATE INDEX
183+ bugtaskflat__product__status__bug__idx
184+ ON bugtaskflat
185+ USING btree (product, status, bug DESC)
186+ WHERE product IS NOT NULL;
187+
188+-- ProductSeries sorts
189+CREATE INDEX
190+ bugtaskflat__productseries__date_last_updated__idx
191+ ON bugtaskflat
192+ USING btree (productseries, date_last_updated)
193+ WHERE productseries IS NOT NULL;
194+CREATE INDEX
195+ bugtaskflat__productseries__datecreated__idx
196+ ON bugtaskflat
197+ USING btree (productseries, datecreated)
198+ WHERE productseries IS NOT NULL;
199+CREATE INDEX
200+ bugtaskflat__productseries__heat__bug__idx
201+ ON bugtaskflat
202+ USING btree (productseries, heat, bug DESC)
203+ WHERE productseries IS NOT NULL;
204+CREATE INDEX
205+ bugtaskflat__productseries__importance__bug__idx
206+ ON bugtaskflat
207+ USING btree (productseries, importance, bug DESC)
208+ WHERE productseries IS NOT NULL;
209+CREATE INDEX
210+ bugtaskflat__productseries__status__bug__idx
211+ ON bugtaskflat
212+ USING btree (productseries, status, bug DESC)
213+ WHERE productseries IS NOT NULL;
214+
215+
216+-- Update helpers
217+
218+CREATE OR REPLACE FUNCTION bug_build_access_cache(bug_id integer,
219+ information_type integer)
220+ RETURNS record
221+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
222+ AS $$
223+DECLARE
224+ _access_artifact integer;
225+ _access_policies integer[];
226+ _access_grants integer[];
227+ cache record;
228+BEGIN
229+ -- If the bug is private, grab the access control information.
230+ -- If the bug is public, access_policies and access_grants are NULL.
231+ -- 3 == EMBARGOEDSECURITY, 4 == USERDATA, 5 == PROPRIETARY
232+ IF information_type IN (3, 4, 5) THEN
233+ SELECT id INTO _access_artifact
234+ FROM accessartifact
235+ WHERE bug = bug_id;
236+ -- We have to do the order in a subquery until 9.0 (8.4 doesn't
237+ -- support ordering within an aggregate).
238+ SELECT COALESCE(array_agg(policy), ARRAY[]::integer[])
239+ INTO _access_policies
240+ FROM (
241+ SELECT policy FROM
242+ accesspolicyartifact
243+ WHERE artifact = _access_artifact
244+ ORDER BY policy) AS policies;
245+ SELECT COALESCE(array_agg(grantee), ARRAY[]::integer[])
246+ INTO _access_grants
247+ FROM (
248+ SELECT grantee FROM
249+ accessartifactgrant
250+ WHERE artifact = _access_artifact
251+ ORDER BY grantee) AS grantees;
252+ END IF;
253+ cache := (_access_policies, _access_grants);
254+ RETURN cache;
255+END;
256+$$;
257+
258+COMMENT ON FUNCTION bug_build_access_cache(bug_id integer,
259+ information_type integer) IS
260+ 'Build an access cache for the given bug. Returns '
261+ '({AccessPolicyArtifact.policy}, {AccessArtifactGrant.grantee}) '
262+ 'for private bugs, or (NULL, NULL) for public ones.';
263+
264+
265+CREATE OR REPLACE FUNCTION bugtask_flatten(task_id integer, check_only boolean)
266+ RETURNS boolean
267+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
268+ AS $$
269+DECLARE
270+ bug_row Bug%ROWTYPE;
271+ task_row BugTask%ROWTYPE;
272+ old_flat_row BugTaskFlat%ROWTYPE;
273+ new_flat_row BugTaskFlat%ROWTYPE;
274+ _product_active boolean;
275+ _access_policies integer[];
276+ _access_grants integer[];
277+BEGIN
278+ -- This is the master function to update BugTaskFlat, but there are
279+ -- maintenance triggers and jobs on the involved tables that update
280+ -- it directly. Any changes here probably require a corresponding
281+ -- change in other trigger functions.
282+
283+ SELECT * INTO task_row FROM BugTask WHERE id = task_id;
284+ SELECT * INTO old_flat_row FROM BugTaskFlat WHERE bugtask = task_id;
285+
286+ -- If the task doesn't exist, ensure that there's no flat row.
287+ IF task_row.id IS NULL THEN
288+ IF old_flat_row.bugtask IS NOT NULL THEN
289+ IF NOT check_only THEN
290+ DELETE FROM BugTaskFlat WHERE bugtask = task_id;
291+ END IF;
292+ RETURN FALSE;
293+ ELSE
294+ RETURN TRUE;
295+ END IF;
296+ END IF;
297+
298+ SELECT * FROM bug INTO bug_row WHERE id = task_row.bug;
299+
300+ -- If it's a product(series) task, we must consult the active flag.
301+ IF task_row.product IS NOT NULL THEN
302+ SELECT product.active INTO _product_active
303+ FROM product WHERE product.id = task_row.product LIMIT 1;
304+ ELSIF task_row.productseries IS NOT NULL THEN
305+ SELECT product.active INTO _product_active
306+ FROM
307+ product
308+ JOIN productseries ON productseries.product = product.id
309+ WHERE productseries.id = task_row.productseries LIMIT 1;
310+ END IF;
311+
312+ SELECT policies, grants
313+ INTO _access_policies, _access_grants
314+ FROM bug_build_access_cache(bug_row.id, bug_row.information_type)
315+ AS (policies integer[], grants integer[]);
316+
317+ -- Compile the new flat row.
318+ SELECT task_row.id, bug_row.id, task_row.datecreated,
319+ bug_row.duplicateof, bug_row.owner, bug_row.fti,
320+ bug_row.information_type, bug_row.date_last_updated,
321+ bug_row.heat, task_row.product, task_row.productseries,
322+ task_row.distribution, task_row.distroseries,
323+ task_row.sourcepackagename, task_row.status,
324+ task_row.importance, task_row.assignee,
325+ task_row.milestone, task_row.owner,
326+ COALESCE(_product_active, TRUE),
327+ _access_policies,
328+ _access_grants
329+ INTO new_flat_row;
330+
331+ -- Calculate the necessary updates.
332+ IF old_flat_row.bugtask IS NULL THEN
333+ IF NOT check_only THEN
334+ INSERT INTO BugTaskFlat VALUES (new_flat_row.*);
335+ END IF;
336+ RETURN FALSE;
337+ ELSIF new_flat_row != old_flat_row THEN
338+ IF NOT check_only THEN
339+ UPDATE BugTaskFlat SET
340+ bug = new_flat_row.bug,
341+ datecreated = new_flat_row.datecreated,
342+ duplicateof = new_flat_row.duplicateof,
343+ bug_owner = new_flat_row.bug_owner,
344+ fti = new_flat_row.fti,
345+ information_type = new_flat_row.information_type,
346+ date_last_updated = new_flat_row.date_last_updated,
347+ heat = new_flat_row.heat,
348+ product = new_flat_row.product,
349+ productseries = new_flat_row.productseries,
350+ distribution = new_flat_row.distribution,
351+ distroseries = new_flat_row.distroseries,
352+ sourcepackagename = new_flat_row.sourcepackagename,
353+ status = new_flat_row.status,
354+ importance = new_flat_row.importance,
355+ assignee = new_flat_row.assignee,
356+ milestone = new_flat_row.milestone,
357+ owner = new_flat_row.owner,
358+ active = new_flat_row.active,
359+ access_policies = new_flat_row.access_policies,
360+ access_grants = new_flat_row.access_grants
361+ WHERE bugtask = new_flat_row.bugtask;
362+ END IF;
363+ RETURN FALSE;
364+ ELSE
365+ RETURN TRUE;
366+ END IF;
367+END;
368+$$;
369+
370+COMMENT ON FUNCTION bugtask_flatten(task_id integer, check_only boolean) IS
371+ 'Create or update a BugTaskFlat row from the source tables. Returns '
372+ 'whether the row was up to date. If check_only is true, the row is not '
373+ 'brought up to date.';
374+
375+
376+CREATE OR REPLACE FUNCTION bug_flatten_access(bug_id integer)
377+ RETURNS void
378+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
379+ AS $$
380+DECLARE
381+ _information_type integer;
382+ _access_policies integer[];
383+ _access_grants integer[];
384+BEGIN
385+ SELECT information_type FROM bug INTO _information_type WHERE id = bug_id;
386+ SELECT policies, grants
387+ INTO _access_policies, _access_grants
388+ FROM bug_build_access_cache(bug_id, _information_type)
389+ AS (policies integer[], grants integer[]);
390+ UPDATE bugtaskflat
391+ SET
392+ access_policies = _access_policies,
393+ access_grants = _access_grants
394+ WHERE bug = bug_id;
395+ RETURN;
396+END;
397+$$;
398+
399+COMMENT ON FUNCTION bug_flatten_access(bug_id integer) IS
400+ 'Recalculate the access cache on a bug''s flattened tasks.';
401+
402+
403+CREATE OR REPLACE FUNCTION accessartifact_flatten_bug(artifact_id integer)
404+ RETURNS void
405+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
406+ AS $$
407+DECLARE
408+ bug_id integer;
409+BEGIN
410+ SELECT bug INTO bug_id FROM accessartifact WHERE id = artifact_id;
411+ IF bug_id IS NOT NULL THEN
412+ PERFORM bug_flatten_access(bug_id);
413+ END IF;
414+ RETURN;
415+END;
416+$$;
417+
418+COMMENT ON FUNCTION accessartifact_flatten_bug(artifact_id integer) IS
419+ 'If the access artifact is a bug, update the access cache on its '
420+ 'flattened tasks.';
421+
422+
423+
424+-- BugTask trigger.
425+
426+CREATE OR REPLACE FUNCTION bugtask_maintain_bugtaskflat_trig() RETURNS trigger
427+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
428+ AS $$
429+BEGIN
430+ IF TG_OP = 'INSERT' THEN
431+ PERFORM bugtask_flatten(NEW.id, FALSE);
432+ ELSIF TG_OP = 'UPDATE' THEN
433+ IF NEW.bug != OLD.bug THEN
434+ RAISE EXCEPTION 'cannot move bugtask to a different bug';
435+ ELSIF (NEW.product IS DISTINCT FROM OLD.product
436+ OR NEW.productseries IS DISTINCT FROM OLD.productseries) THEN
437+ -- product.active may differ. Do a full update.
438+ PERFORM bugtask_flatten(NEW.id, FALSE);
439+ ELSIF (
440+ NEW.datecreated IS DISTINCT FROM OLD.datecreated
441+ OR NEW.product IS DISTINCT FROM OLD.product
442+ OR NEW.productseries IS DISTINCT FROM OLD.productseries
443+ OR NEW.distribution IS DISTINCT FROM OLD.distribution
444+ OR NEW.distroseries IS DISTINCT FROM OLD.distroseries
445+ OR NEW.sourcepackagename IS DISTINCT FROM OLD.sourcepackagename
446+ OR NEW.status IS DISTINCT FROM OLD.status
447+ OR NEW.importance IS DISTINCT FROM OLD.importance
448+ OR NEW.assignee IS DISTINCT FROM OLD.assignee
449+ OR NEW.milestone IS DISTINCT FROM OLD.milestone
450+ OR NEW.owner IS DISTINCT FROM OLD.owner) THEN
451+ -- Otherwise just update the columns from bugtask.
452+ -- Access policies and grants may have changed due to target
453+ -- transitions, but an earlier trigger will already have
454+ -- mirrored them to all relevant flat tasks.
455+ UPDATE BugTaskFlat SET
456+ datecreated = NEW.datecreated,
457+ product = NEW.product,
458+ productseries = NEW.productseries,
459+ distribution = NEW.distribution,
460+ distroseries = NEW.distroseries,
461+ sourcepackagename = NEW.sourcepackagename,
462+ status = NEW.status,
463+ importance = NEW.importance,
464+ assignee = NEW.assignee,
465+ milestone = NEW.milestone,
466+ owner = NEW.owner
467+ WHERE bugtask = NEW.id;
468+ END IF;
469+ ELSIF TG_OP = 'DELETE' THEN
470+ PERFORM bugtask_flatten(OLD.id, FALSE);
471+ END IF;
472+ RETURN NULL;
473+END;
474+$$;
475+
476+-- z so they happen after fti and access updates.
477+CREATE TRIGGER z_bugtask_maintain_bugtaskflat_trigger
478+ AFTER INSERT OR UPDATE OR DELETE ON bugtask
479+ FOR EACH ROW EXECUTE PROCEDURE bugtask_maintain_bugtaskflat_trig();
480+
481+
482+
483+-- Bug trigger. Only UPDATE, since on INSERT or DELETE there are no tasks.
484+
485+CREATE OR REPLACE FUNCTION bug_maintain_bugtaskflat_trig() RETURNS trigger
486+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
487+ AS $$
488+BEGIN
489+ IF (
490+ NEW.duplicateof IS DISTINCT FROM OLD.duplicateof
491+ OR NEW.owner IS DISTINCT FROM OLD.owner
492+ OR NEW.fti IS DISTINCT FROM OLD.fti
493+ OR NEW.information_type IS DISTINCT FROM OLD.information_type
494+ OR NEW.date_last_updated IS DISTINCT FROM OLD.date_last_updated
495+ OR NEW.heat IS DISTINCT FROM OLD.heat) THEN
496+ UPDATE bugtaskflat
497+ SET
498+ duplicateof = NEW.duplicateof,
499+ bug_owner = NEW.owner,
500+ fti = NEW.fti,
501+ information_type = NEW.information_type,
502+ date_last_updated = NEW.date_last_updated,
503+ heat = NEW.heat
504+ WHERE bug = OLD.id;
505+ END IF;
506+
507+ IF NEW.information_type IS DISTINCT FROM OLD.information_type THEN
508+ PERFORM bug_flatten_access(OLD.id);
509+ END IF;
510+ RETURN NULL;
511+END;
512+$$;
513+
514+-- z so they happen after fti and access updates.
515+CREATE TRIGGER z_bug_maintain_bugtaskflat_trigger
516+ AFTER UPDATE ON bug
517+ FOR EACH ROW EXECUTE PROCEDURE bug_maintain_bugtaskflat_trig();
518+
519+
520+
521+-- Shared AccessPolicyArtifact and AccessArtifactGrant trigger.
522+
523+CREATE OR REPLACE FUNCTION accessartifact_maintain_bugtaskflat_trig()
524+ RETURNS trigger
525+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
526+ AS $$
527+BEGIN
528+ IF TG_OP = 'INSERT' THEN
529+ PERFORM accessartifact_flatten_bug(NEW.artifact);
530+ ELSIF TG_OP = 'UPDATE' THEN
531+ PERFORM accessartifact_flatten_bug(NEW.artifact);
532+ IF OLD.artifact != NEW.artifact THEN
533+ PERFORM accessartifact_flatten_bug(OLD.artifact);
534+ END IF;
535+ ELSIF TG_OP = 'DELETE' THEN
536+ PERFORM accessartifact_flatten_bug(OLD.artifact);
537+ END IF;
538+ RETURN NULL;
539+END;
540+$$;
541+
542+CREATE TRIGGER accesspolicyartifact_maintain_bugtaskflat_trigger
543+ AFTER INSERT OR UPDATE OR DELETE ON accesspolicyartifact
544+ FOR EACH ROW EXECUTE PROCEDURE accessartifact_maintain_bugtaskflat_trig();
545+
546+CREATE TRIGGER accessartifactgrant_maintain_bugtaskflat_trigger
547+ AFTER INSERT OR UPDATE OR DELETE ON accessartifactgrant
548+ FOR EACH ROW EXECUTE PROCEDURE accessartifact_maintain_bugtaskflat_trig();
549+
550+INSERT INTO LaunchpadDatabaseRevision VALUES (2209, 16, 0);
551
552=== modified file 'database/schema/security.cfg'
553--- database/schema/security.cfg 2012-03-26 05:50:55 +0000
554+++ database/schema/security.cfg 2012-03-30 00:02:34 +0000
555@@ -11,13 +11,17 @@
556
557 [public]
558 type=group
559+public.accessartifact_flatten_bug(integer) =
560 public.activity() = EXECUTE
561 public.add_test_openid_identifier(integer) = EXECUTE
562 public.alllocks =
563 public.assert_patch_applied(integer, integer, integer) = EXECUTE
564+public.bug_build_access_cache(integer, integer) =
565+public.bug_flatten_access(integer) =
566 public.bug_mirror_legacy_access(integer) =
567 public.bug_update_latest_patch_uploaded(integer) =
568 public.bugnotificationarchive =
569+public.bugtask_flatten(integer, boolean) =
570 public.calculate_bug_heat(integer) = EXECUTE
571 public.cursor_fetch(refcursor, integer) = EXECUTE
572 public.databasediskutilization =
573@@ -344,6 +348,7 @@
574 public.bug = SELECT
575 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
576 public.bugtask = SELECT
577+public.bugtaskflat = SELECT
578 public.buildfarmjob = SELECT
579 public.distribution = SELECT
580 public.distributionsourcepackagecache = SELECT, INSERT, UPDATE, DELETE
581@@ -433,6 +438,7 @@
582 groups=script
583 public.bug = SELECT
584 public.bugtask = SELECT, UPDATE
585+public.bugtaskflat = SELECT
586 public.libraryfilealias = SELECT, INSERT
587 public.libraryfilecontent = SELECT, INSERT
588 public.milestone = SELECT, INSERT
589@@ -577,6 +583,7 @@
590 public.bugsubscriptionfiltertag = SELECT
591 public.bugtag = SELECT
592 public.bugtask = SELECT, INSERT, UPDATE
593+public.bugtaskflat = SELECT
594 public.bugtracker = SELECT, INSERT
595 public.bugtrackeralias = SELECT
596 public.bugtrackerperson = SELECT, INSERT
597@@ -694,6 +701,7 @@
598 groups=script
599 public.binarypackagename = SELECT
600 public.bugtask = SELECT, UPDATE
601+public.bugtaskflat = SELECT
602 public.distribution = SELECT
603 public.distroseries = SELECT
604 public.potemplate = SELECT, UPDATE
605@@ -850,6 +858,7 @@
606 public.bugsubscriptionfiltertag = SELECT
607 public.bugtag = SELECT
608 public.bugtask = SELECT, UPDATE
609+public.bugtaskflat = SELECT
610 public.bugtracker = SELECT, INSERT
611 public.bugtrackeralias = SELECT, INSERT
612 public.bugwatch = SELECT, INSERT
613@@ -1128,6 +1137,7 @@
614 public.bugsubscriptionfilterstatus = SELECT, INSERT, UPDATE, DELETE
615 public.bugsubscriptionfiltertag = SELECT, INSERT, UPDATE, DELETE
616 public.bugtask = SELECT, INSERT, UPDATE, DELETE
617+public.bugtaskflat = SELECT
618 public.bugtracker = SELECT, INSERT, UPDATE, DELETE
619 public.bugtrackeralias = SELECT, INSERT, UPDATE, DELETE
620 public.bugwatch = SELECT, INSERT, UPDATE, DELETE
621@@ -1217,6 +1227,7 @@
622 public.bug = SELECT
623 public.bugaffectsperson = SELECT, INSERT, UPDATE, DELETE
624 public.bugtask = SELECT
625+public.bugtaskflat = SELECT
626 public.distribution = SELECT
627 public.emailaddress = SELECT
628 public.faq = SELECT
629@@ -1279,6 +1290,7 @@
630 public.bugsubscriptionfiltertag = SELECT
631 public.bugtag = SELECT
632 public.bugtask = SELECT, UPDATE
633+public.bugtaskflat = SELECT
634 public.bugtracker = SELECT, INSERT
635 public.bugtrackeralias = SELECT, INSERT
636 public.bugwatch = SELECT, INSERT
637@@ -1382,6 +1394,7 @@
638 public.bugsubscriptionfiltertag = SELECT
639 public.bugtag = SELECT
640 public.bugtask = SELECT, UPDATE
641+public.bugtaskflat = SELECT
642 public.bugtracker = SELECT, INSERT
643 public.bugtrackeralias = SELECT, INSERT
644 public.bugwatch = SELECT, INSERT
645@@ -1488,6 +1501,7 @@
646 public.bugsubscriptionfiltertag = SELECT, INSERT
647 public.bugtag = SELECT
648 public.bugtask = SELECT, INSERT, UPDATE
649+public.bugtaskflat = SELECT
650 public.bugwatch = SELECT
651 public.component = SELECT
652 public.distribution = SELECT, UPDATE
653@@ -1668,6 +1682,7 @@
654 public.bugsubscriptionfiltertag = SELECT, INSERT, UPDATE, DELETE
655 public.bugtag = SELECT, INSERT, DELETE
656 public.bugtask = SELECT, INSERT, UPDATE
657+public.bugtaskflat = SELECT
658 public.bugtracker = SELECT, INSERT
659 public.bugtrackeralias = SELECT, INSERT
660 public.bugwatch = SELECT, INSERT
661@@ -1833,6 +1848,7 @@
662 public.bugbranch = SELECT
663 public.bugsubscription = SELECT
664 public.bugtask = SELECT
665+public.bugtaskflat = SELECT
666 public.codereviewmessage = SELECT, INSERT
667 public.codereviewvote = SELECT, INSERT
668 public.diff = SELECT, INSERT
669@@ -1924,6 +1940,7 @@
670 public.bugsubscriptionfiltertag = SELECT, INSERT
671 public.bugtag = SELECT
672 public.bugtask = SELECT, INSERT, UPDATE
673+public.bugtaskflat = SELECT
674 public.bugtracker = SELECT, INSERT
675 public.bugtrackeralias = SELECT
676 public.bugwatch = SELECT, INSERT
677@@ -1998,6 +2015,7 @@
678 public.bugsubscriptionfilter = SELECT, UPDATE, DELETE
679 public.bugsummary = SELECT
680 public.bugtask = SELECT, UPDATE
681+public.bugtaskflat = SELECT
682 public.bugtracker = SELECT, UPDATE
683 public.bugtrackerperson = SELECT, UPDATE
684 public.bugwatch = SELECT, UPDATE
685@@ -2122,6 +2140,7 @@
686 public.bugsummary_rollup_journal(integer) = EXECUTE
687 public.bugtag = SELECT
688 public.bugtask = SELECT, UPDATE
689+public.bugtaskflat = SELECT
690 public.bugwatch = SELECT, UPDATE
691 public.bugwatchactivity = SELECT, DELETE
692 public.codeimportevent = SELECT, DELETE
693
694=== added file 'lib/lp/bugs/tests/test_bugtaskflat_triggers.py'
695--- lib/lp/bugs/tests/test_bugtaskflat_triggers.py 1970-01-01 00:00:00 +0000
696+++ lib/lp/bugs/tests/test_bugtaskflat_triggers.py 2012-03-30 00:02:34 +0000
697@@ -0,0 +1,406 @@
698+# Copyright 2012 Canonical Ltd. This software is licensed under the
699+# GNU Affero General Public License version 3 (see the file LICENSE).
700+
701+__metaclass__ = type
702+
703+from collections import namedtuple
704+from contextlib import contextmanager
705+
706+from testtools.matchers import MatchesStructure
707+from zope.component import getUtility
708+from zope.security.proxy import removeSecurityProxy
709+
710+from lp.bugs.interfaces.bugtask import BugTaskStatus
711+from lp.bugs.model.bug import Bug
712+from lp.registry.enums import InformationType
713+from lp.registry.interfaces.accesspolicy import (
714+ IAccessArtifactSource,
715+ IAccessArtifactGrantSource,
716+ IAccessPolicyArtifactSource,
717+ IAccessPolicySource,
718+ )
719+from lp.services.database.lpstorm import IStore
720+from lp.services.features.testing import FeatureFixture
721+from lp.testing import (
722+ login_person,
723+ person_logged_in,
724+ TestCaseWithFactory,
725+ )
726+from lp.testing.dbuser import dbuser
727+from lp.testing.layers import DatabaseFunctionalLayer
728+
729+BUGTASKFLAT_COLUMNS = (
730+ 'bugtask',
731+ 'bug',
732+ 'datecreated',
733+ 'duplicateof',
734+ 'bug_owner',
735+ 'fti',
736+ 'information_type',
737+ 'date_last_updated',
738+ 'heat',
739+ 'product',
740+ 'productseries',
741+ 'distribution',
742+ 'distroseries',
743+ 'sourcepackagename',
744+ 'status',
745+ 'importance',
746+ 'assignee',
747+ 'milestone',
748+ 'owner',
749+ 'active',
750+ 'access_policies',
751+ 'access_grants',
752+ )
753+
754+BugTaskFlat = namedtuple('BugTaskFlat', BUGTASKFLAT_COLUMNS)
755+
756+
757+class BugTaskFlatTestMixin(TestCaseWithFactory):
758+
759+ def setUp(self):
760+ super(BugTaskFlatTestMixin, self).setUp()
761+ self.useFixture(FeatureFixture(
762+ {'disclosure.allow_multipillar_private_bugs.enabled': 'true'}))
763+
764+ def checkFlattened(self, bugtask, check_only=True):
765+ if hasattr(bugtask, 'id'):
766+ bugtask = bugtask.id
767+ result = IStore(Bug).execute(
768+ "SELECT bugtask_flatten(?, ?)", (bugtask, check_only))
769+ return result.get_one()[0]
770+
771+ def assertFlattened(self, bugtask):
772+ # Assert that the BugTask is correctly represented in
773+ # BugTaskFlat.
774+ self.assertIs(True, self.checkFlattened(bugtask))
775+
776+ def assertFlattens(self, bugtask):
777+ # Assert that the BugTask isn't correctly represented in
778+ # BugTaskFlat, but a call to bugtask_flatten fixes it.
779+ self.assertFalse(self.checkFlattened(bugtask))
780+ self.checkFlattened(bugtask, check_only=False)
781+ self.assertTrue(self.checkFlattened(bugtask))
782+
783+ def getBugTaskFlat(self, bugtask):
784+ if hasattr(bugtask, 'id'):
785+ bugtask = bugtask.id
786+ assert bugtask is not None
787+ result = IStore(Bug).execute(
788+ "SELECT %s FROM bugtaskflat WHERE bugtask = ?"
789+ % ', '.join(BUGTASKFLAT_COLUMNS), (bugtask,)).get_one()
790+ if result is not None:
791+ result = BugTaskFlat(*result)
792+ return result
793+
794+ def makeLoggedInTask(self, private=False):
795+ owner = self.factory.makePerson()
796+ login_person(owner)
797+ bug = self.factory.makeBug(private=private, owner=owner)
798+ return bug.default_bugtask
799+
800+ @contextmanager
801+ def bugtaskflat_is_deleted(self, bugtask):
802+ old_row = self.getBugTaskFlat(bugtask)
803+ self.assertFlattened(bugtask)
804+ self.assertIsNot(None, old_row)
805+ yield
806+ new_row = self.getBugTaskFlat(bugtask)
807+ self.assertFlattened(bugtask)
808+ self.assertIs(None, new_row)
809+
810+ @contextmanager
811+ def bugtaskflat_is_updated(self, bugtask, expected_fields):
812+ old_row = self.getBugTaskFlat(bugtask)
813+ self.assertFlattened(bugtask)
814+ yield
815+ new_row = self.getBugTaskFlat(bugtask)
816+ self.assertFlattened(bugtask)
817+ changed_fields = [
818+ field for field in BugTaskFlat._fields
819+ if getattr(old_row, field) != getattr(new_row, field)]
820+ self.assertEqual(expected_fields, changed_fields)
821+
822+ @contextmanager
823+ def bugtaskflat_is_identical(self, bugtask):
824+ old_row = self.getBugTaskFlat(bugtask)
825+ self.assertFlattened(bugtask)
826+ yield
827+ new_row = self.getBugTaskFlat(bugtask)
828+ self.assertFlattened(bugtask)
829+ self.assertEqual(old_row, new_row)
830+
831+
832+class TestBugTaskFlatten(BugTaskFlatTestMixin):
833+
834+ layer = DatabaseFunctionalLayer
835+
836+ def test_create(self):
837+ # bugtask_flatten() returns true if the BugTaskFlat is missing,
838+ # and optionally creates it.
839+ task = self.factory.makeBugTask()
840+ self.assertTrue(self.checkFlattened(task))
841+ with dbuser('testadmin'):
842+ IStore(Bug).execute(
843+ "DELETE FROM BugTaskFlat WHERE bugtask = ?", (task.id,))
844+ self.assertFlattens(task)
845+
846+ def test_update(self):
847+ # bugtask_flatten() returns true if the BugTaskFlat is out of
848+ # date, and optionally updates it.
849+ task = self.factory.makeBugTask()
850+ self.assertTrue(self.checkFlattened(task))
851+ with dbuser('testadmin'):
852+ IStore(Bug).execute(
853+ "UPDATE BugTaskFlat SET status = ? WHERE bugtask = ?",
854+ (BugTaskStatus.UNKNOWN.value, task.id))
855+ self.assertFlattens(task)
856+
857+ def test_delete(self):
858+ # bugtask_flatten() returns true if the BugTaskFlat exists but
859+ # the task doesn't, and optionally deletes it.
860+ self.assertTrue(self.checkFlattened(200))
861+ with dbuser('testadmin'):
862+ IStore(Bug).execute(
863+ "INSERT INTO bugtaskflat "
864+ "(bug, bugtask, bug_owner, information_type, "
865+ " date_last_updated, heat, status, importance, owner, "
866+ " active) "
867+ "VALUES "
868+ "(1, 200, 1, 1, "
869+ " current_timestamp at time zone 'UTC', 999, 1, 1, 1, true);")
870+ self.assertFlattens(200)
871+
872+ def test_values(self):
873+ task = self.factory.makeBugTask()
874+ with person_logged_in(task.product.owner):
875+ task.transitionToAssignee(self.factory.makePerson())
876+ task.transitionToMilestone(
877+ self.factory.makeMilestone(product=task.product),
878+ task.product.owner)
879+ task.bug.markAsDuplicate(self.factory.makeBug())
880+ flat = self.getBugTaskFlat(task)
881+ self.assertThat(
882+ flat,
883+ MatchesStructure.byEquality(
884+ bugtask=task.id,
885+ bug=task.bug.id,
886+ datecreated=task.datecreated.replace(tzinfo=None),
887+ duplicateof=task.bug.duplicateof.id,
888+ bug_owner=task.bug.owner.id,
889+ information_type=task.bug.information_type.value,
890+ date_last_updated=task.bug.date_last_updated.replace(
891+ tzinfo=None),
892+ heat=task.bug.heat,
893+ product=task.product.id,
894+ productseries=None,
895+ distribution=None,
896+ distroseries=None,
897+ sourcepackagename=None,
898+ status=task.status.value,
899+ importance=task.importance.value,
900+ assignee=task.assignee.id,
901+ milestone=task.milestone.id,
902+ owner=task.owner.id,
903+ active=task.product.active,
904+ access_policies=None,
905+ access_grants=None))
906+ self.assertIsNot(None, flat.fti)
907+
908+ def test_productseries_target(self):
909+ ps = self.factory.makeProductSeries()
910+ task = self.factory.makeBugTask(target=ps)
911+ flat = self.getBugTaskFlat(task)
912+ self.assertThat(
913+ flat,
914+ MatchesStructure.byEquality(
915+ product=None, productseries=ps.id, distribution=None,
916+ distroseries=None, sourcepackagename=None, active=True))
917+
918+ def test_distributionsourcepackage_target(self):
919+ dsp = self.factory.makeDistributionSourcePackage()
920+ task = self.factory.makeBugTask(target=dsp)
921+ flat = self.getBugTaskFlat(task)
922+ self.assertThat(
923+ flat,
924+ MatchesStructure.byEquality(
925+ product=None, productseries=None,
926+ distribution=dsp.distribution.id, distroseries=None,
927+ sourcepackagename=dsp.sourcepackagename.id, active=True))
928+
929+ def test_sourcepackage_target(self):
930+ sp = self.factory.makeSourcePackage()
931+ task = self.factory.makeBugTask(target=sp)
932+ flat = self.getBugTaskFlat(task)
933+ self.assertThat(
934+ flat,
935+ MatchesStructure.byEquality(
936+ product=None, productseries=None, distribution=None,
937+ distroseries=sp.distroseries.id,
938+ sourcepackagename=sp.sourcepackagename.id, active=True))
939+
940+ def test_product_active_flag_respected(self):
941+ # A bugtask created on a product or productseries respects the
942+ # product's active flag. Note that there are no triggers to
943+ # handle this change, as the number of changes can be too large.
944+ # A job will be used instead.
945+ p = self.factory.makeProduct()
946+ removeSecurityProxy(p).active = False
947+ ps = self.factory.makeProductSeries(product=p)
948+ ptask = self.factory.makeBugTask(target=p)
949+ pstask = self.factory.makeBugTask(target=ps)
950+ self.assertEqual(False, self.getBugTaskFlat(ptask).active)
951+ self.assertEqual(False, self.getBugTaskFlat(pstask).active)
952+
953+ def test_public_access_cache_is_null(self):
954+ # access_policies and access_grants for a public bug are NULL.
955+ bugtask = self.makeLoggedInTask()
956+ flat = self.getBugTaskFlat(bugtask.id)
957+ self.assertIs(None, flat.access_policies)
958+ self.assertIs(None, flat.access_grants)
959+
960+ def test_private_access_cache_is_set(self):
961+ # access_policies and access_grants for a private bug are
962+ # mirrored appropriately.
963+ bugtask = self.makeLoggedInTask(private=True)
964+ flat = self.getBugTaskFlat(bugtask.id)
965+ [policy] = getUtility(IAccessPolicySource).find(
966+ [(bugtask.pillar, InformationType.USERDATA)])
967+ self.assertContentEqual([policy.id], flat.access_policies)
968+ self.assertContentEqual(
969+ [p.id for p in bugtask.bug.getDirectSubscribers()],
970+ flat.access_grants)
971+
972+
973+class TestBugTaskFlatTriggers(BugTaskFlatTestMixin):
974+
975+ layer = DatabaseFunctionalLayer
976+
977+ def test_bugtask_create(self):
978+ # Triggers maintain BugTaskFlat when a task is created.
979+ task = self.factory.makeBugTask()
980+ self.assertFlattened(task)
981+
982+ def test_bugtask_delete(self):
983+ # Triggers maintain BugTaskFlat when a task is deleted.
984+ task = self.makeLoggedInTask()
985+ # We need a second task before it will let us delete the first.
986+ self.factory.makeBugTask(bug=task.bug)
987+ with self.bugtaskflat_is_deleted(task):
988+ task.delete()
989+
990+ def test_bugtask_change(self):
991+ # Triggers maintain BugTaskFlat when a task is changed.
992+ task = self.makeLoggedInTask()
993+ with self.bugtaskflat_is_updated(task, ['status']):
994+ task.transitionToStatus(BugTaskStatus.UNKNOWN, task.owner)
995+
996+ def test_bugtask_change_unflattened(self):
997+ # Some fields on BugTask aren't mirrored, so don't trigger updates.
998+ task = self.makeLoggedInTask()
999+ with self.bugtaskflat_is_identical(task):
1000+ task.bugwatch = self.factory.makeBugWatch()
1001+
1002+ def test_bug_change(self):
1003+ # Triggers maintain BugTaskFlat when a bug is changed
1004+ task = self.makeLoggedInTask()
1005+ with self.bugtaskflat_is_updated(task, ['information_type']):
1006+ removeSecurityProxy(task.bug).information_type = (
1007+ InformationType.UNEMBARGOEDSECURITY)
1008+
1009+ def test_bug_make_private(self):
1010+ # Triggers maintain BugTaskFlat when a bug is made private.
1011+ task = self.makeLoggedInTask()
1012+ with self.bugtaskflat_is_updated(
1013+ task, ['information_type', 'access_policies', 'access_grants']):
1014+ removeSecurityProxy(task.bug).information_type = (
1015+ InformationType.USERDATA)
1016+
1017+ def test_bug_make_public(self):
1018+ # Triggers maintain BugTaskFlat when a bug is made public.
1019+ task = self.makeLoggedInTask(private=True)
1020+ with self.bugtaskflat_is_updated(
1021+ task, [
1022+ 'information_type', 'date_last_updated', 'heat',
1023+ 'access_policies', 'access_grants']):
1024+ task.bug.setPrivate(False, task.owner)
1025+
1026+ def test_bug_change_unflattened(self):
1027+ # Some fields on Bug aren't mirrored, so don't trigger updates.
1028+ task = self.makeLoggedInTask()
1029+ with self.bugtaskflat_is_identical(task):
1030+ removeSecurityProxy(task.bug).who_made_private = task.owner
1031+
1032+ def test_accessartifactgrant_create(self):
1033+ # Creating an AccessArtifactGrant updates the relevant bugs.
1034+ task = self.makeLoggedInTask(private=True)
1035+ [artifact] = getUtility(IAccessArtifactSource).find([task.bug])
1036+ with self.bugtaskflat_is_updated(task, ['access_grants']):
1037+ self.factory.makeAccessArtifactGrant(artifact=artifact)
1038+
1039+ def test_accessartifactgrant_update(self):
1040+ # Updating an AccessArtifactGrant updates the relevant bugs.
1041+ # Person merge is the main use case here.
1042+ task = self.makeLoggedInTask(private=True)
1043+ [artifact] = getUtility(IAccessArtifactSource).find([task.bug])
1044+ grant = self.factory.makeAccessArtifactGrant(artifact=artifact)
1045+ with self.bugtaskflat_is_updated(task, ['access_grants']):
1046+ removeSecurityProxy(grant).grantee = self.factory.makePerson()
1047+
1048+ def test_accessartifactgrant_delete(self):
1049+ # Deleting an AccessArtifactGrant updates the relevant bugs.
1050+ task = self.makeLoggedInTask(private=True)
1051+ [artifact] = getUtility(IAccessArtifactSource).find([task.bug])
1052+ self.factory.makeAccessArtifactGrant(artifact=artifact)
1053+ with self.bugtaskflat_is_updated(task, ['access_grants']):
1054+ getUtility(IAccessArtifactGrantSource).revokeByArtifact(
1055+ [artifact])
1056+
1057+ def test_accesspolicyartifact_create(self):
1058+ # Creating an AccessPolicyArtifact updates the relevant bugtasks.
1059+ task = self.makeLoggedInTask(private=True)
1060+ [artifact] = getUtility(IAccessArtifactSource).find([task.bug])
1061+ with self.bugtaskflat_is_updated(task, ['access_policies']):
1062+ self.factory.makeAccessPolicyArtifact(artifact=artifact)
1063+
1064+ def test_accesspolicyartifact_update(self):
1065+ # Updating an AccessPolicyArtifact updates the relevant bugs.
1066+ # There are currently no users of this, but it still works.
1067+ task = self.makeLoggedInTask(private=True)
1068+ [artifact] = getUtility(IAccessArtifactSource).find([task.bug])
1069+ link = self.factory.makeAccessPolicyArtifact(artifact=artifact)
1070+ with self.bugtaskflat_is_updated(task, ['access_policies']):
1071+ removeSecurityProxy(link).policy = self.factory.makeAccessPolicy()
1072+
1073+ def test_accesspolicyartifact_delete(self):
1074+ # Deleting an AccessPolicyArtifact updates the relevant bugtasks.
1075+ task = self.makeLoggedInTask(private=True)
1076+ [artifact] = getUtility(IAccessArtifactSource).find([task.bug])
1077+ self.factory.makeAccessPolicyArtifact(artifact=artifact)
1078+ with self.bugtaskflat_is_updated(task, ['access_policies']):
1079+ getUtility(IAccessPolicyArtifactSource).deleteByArtifact(
1080+ [artifact])
1081+
1082+ def test_access_create_public(self):
1083+ # Creating a grant or policy link on a public bug has no effect.
1084+ # The access caches remain null.
1085+ task = self.makeLoggedInTask()
1086+ with self.bugtaskflat_is_identical(task):
1087+ [artifact] = getUtility(IAccessArtifactSource).ensure([task.bug])
1088+ self.factory.makeAccessPolicyArtifact(artifact=artifact)
1089+ self.factory.makeAccessArtifactGrant(artifact=artifact)
1090+
1091+ def test_accessartifact_delete(self):
1092+ # Deleting an AccessArtifact removes the corresponding
1093+ # AccessArtifactGrant and AccessPolicyArtifact rows. Even though
1094+ # it's hopefully impossible for a private bug to not have an
1095+ # AccessArtifact, access_policies and access_grants are empty
1096+ # lists, not NULL.
1097+ task = self.makeLoggedInTask(private=True)
1098+ with self.bugtaskflat_is_updated(
1099+ task, ['access_policies', 'access_grants']):
1100+ getUtility(IAccessArtifactSource).delete([task.bug])
1101+ flat = self.getBugTaskFlat(task.id)
1102+ self.assertEqual([], flat.access_policies)
1103+ self.assertEqual([], flat.access_grants)

Subscribers

People subscribed via source and target branches

to status/vote changes: