Merge lp:~stub/launchpad/db-cleanups into lp:launchpad/db-devel

Proposed by Stuart Bishop
Status: Merged
Approved by: Stuart Bishop
Approved revision: no longer in the source branch.
Merged at revision: 11209
Proposed branch: lp:~stub/launchpad/db-cleanups
Merge into: lp:launchpad/db-devel
Prerequisite: lp:~stub/launchpad/pending-db-changes
Diff against target: 26240 lines (+13764/-3660)
4 files modified
database/schema/Makefile (+2/-18)
database/schema/launchpad-2209-00-0.sql (+13757/-1521)
database/schema/patch-2209-00-0.sql (+3/-0)
database/schema/trusted.sql (+2/-2121)
To merge this branch: bzr merge lp:~stub/launchpad/db-cleanups
Reviewer Review Type Date Requested Status
Stuart Bishop (community) db Approve
Review via email: mp+84781@code.launchpad.net

Commit message

[r=stub][no-qa] New database baseline

Description of the change

New database baseline.

To post a comment you must log in.
Revision history for this message
Stuart Bishop (stub) :
review: Approve (db)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/Makefile'
2--- database/schema/Makefile 2011-10-06 09:38:10 +0000
3+++ database/schema/Makefile 2011-12-08 10:11:32 +0000
4@@ -64,9 +64,9 @@
5 # on production. It is generated using newbaseline.py in
6 # bzr+ssh://devpad.canonical.com/code/stub/dbascripts
7 #
8-REV=2208
9+REV=2209
10 BASELINE=launchpad-${REV}-00-0.sql
11-MD5SUM=12a258f8651ae3bba0c96ec8e62be1dd launchpad-2208-00-0.sql
12+MD5SUM=cc7a493c924196409a22392a16443d52 launchpad-2209-00-0.sql
13
14 default: all
15
16@@ -116,26 +116,10 @@
17 @ echo
18 @ echo "* Creating database \"$(EMPTY_DBNAME)\"."
19 @ ${CREATEDB} template0 ${EMPTY_DBNAME}
20- @ if ! `createlang -l ${EMPTY_DBNAME} | grep -qs plpythonu`; then \
21- echo "* Installing PL/PythonU"; \
22- createlang -d ${EMPTY_DBNAME} plpythonu; \
23- fi
24- @ if ! `createlang -l ${EMPTY_DBNAME} | grep -qs plpgsql`; then \
25- echo "* Installing PL/PgSQL"; \
26- createlang -d ${EMPTY_DBNAME} plpgsql; \
27- fi
28- @ echo "* Creating todrop schema"
29- @ psql -d ${EMPTY_DBNAME} -q -c "CREATE SCHEMA todrop;"
30- @ echo "* Creating functions"
31- @ psql -d ${EMPTY_DBNAME} -f trusted.sql | grep : | cat
32- @ echo "* Installing tsearch2 into ts2 schema"
33- @ ${PYTHON} fti.py -q --setup-only -d ${EMPTY_DBNAME}
34 @ echo "* Loading base database schema"
35 @ psql -d ${EMPTY_DBNAME} -f ${BASELINE} | grep : | cat
36 @ echo "* Patching the database schema"
37 @ ${PYTHON} upgrade.py -d ${EMPTY_DBNAME}
38- @ echo "* Setting up full text indexes"
39- @ ${PYTHON} fti.py -q -d ${EMPTY_DBNAME}
40 @ echo "* Security setup"
41 @ ${PYTHON} security.py -q -d ${EMPTY_DBNAME}
42 @ echo "* Disabling autovacuum"
43
44=== renamed file 'database/schema/patch-2208-00-0.sql' => 'database/schema/archive/patch-2208-00-0.sql'
45=== renamed file 'database/schema/patch-2208-01-0.sql' => 'database/schema/archive/patch-2208-01-0.sql'
46=== renamed file 'database/schema/patch-2208-01-1.sql' => 'database/schema/archive/patch-2208-01-1.sql'
47=== renamed file 'database/schema/patch-2208-01-2.sql' => 'database/schema/archive/patch-2208-01-2.sql'
48=== renamed file 'database/schema/patch-2208-01-3.sql' => 'database/schema/archive/patch-2208-01-3.sql'
49=== renamed file 'database/schema/patch-2208-02-0.sql' => 'database/schema/archive/patch-2208-02-0.sql'
50=== renamed file 'database/schema/patch-2208-03-0.sql' => 'database/schema/archive/patch-2208-03-0.sql'
51=== renamed file 'database/schema/patch-2208-04-0.sql' => 'database/schema/archive/patch-2208-04-0.sql'
52=== renamed file 'database/schema/patch-2208-05-0.sql' => 'database/schema/archive/patch-2208-05-0.sql'
53=== renamed file 'database/schema/patch-2208-07-0.sql' => 'database/schema/archive/patch-2208-07-0.sql'
54=== renamed file 'database/schema/patch-2208-08-0.sql' => 'database/schema/archive/patch-2208-08-0.sql'
55=== renamed file 'database/schema/patch-2208-08-1.sql' => 'database/schema/archive/patch-2208-08-1.sql'
56=== renamed file 'database/schema/patch-2208-08-2.sql' => 'database/schema/archive/patch-2208-08-2.sql'
57=== renamed file 'database/schema/patch-2208-08-3.sql' => 'database/schema/archive/patch-2208-08-3.sql'
58=== renamed file 'database/schema/patch-2208-09-0.sql' => 'database/schema/archive/patch-2208-09-0.sql'
59=== renamed file 'database/schema/patch-2208-10-0.sql' => 'database/schema/archive/patch-2208-10-0.sql'
60=== renamed file 'database/schema/patch-2208-12-0.sql' => 'database/schema/archive/patch-2208-12-0.sql'
61=== renamed file 'database/schema/patch-2208-13-0.sql' => 'database/schema/archive/patch-2208-13-0.sql'
62=== renamed file 'database/schema/patch-2208-14-0.sql' => 'database/schema/archive/patch-2208-14-0.sql'
63=== renamed file 'database/schema/patch-2208-15-0.sql' => 'database/schema/archive/patch-2208-15-0.sql'
64=== renamed file 'database/schema/patch-2208-16-0.sql' => 'database/schema/archive/patch-2208-16-0.sql'
65=== renamed file 'database/schema/patch-2208-17-0.sql' => 'database/schema/archive/patch-2208-17-0.sql'
66=== renamed file 'database/schema/patch-2208-18-0.sql' => 'database/schema/archive/patch-2208-18-0.sql'
67=== renamed file 'database/schema/patch-2208-19-0.sql' => 'database/schema/archive/patch-2208-19-0.sql'
68=== renamed file 'database/schema/patch-2208-20-0.sql' => 'database/schema/archive/patch-2208-20-0.sql'
69=== renamed file 'database/schema/patch-2208-21-0.sql' => 'database/schema/archive/patch-2208-21-0.sql'
70=== renamed file 'database/schema/patch-2208-22-0.sql' => 'database/schema/archive/patch-2208-22-0.sql'
71=== renamed file 'database/schema/patch-2208-23-0.sql' => 'database/schema/archive/patch-2208-23-0.sql'
72=== renamed file 'database/schema/patch-2208-24-0.sql' => 'database/schema/archive/patch-2208-24-0.sql'
73=== renamed file 'database/schema/patch-2208-25-0.sql' => 'database/schema/archive/patch-2208-25-0.sql'
74=== renamed file 'database/schema/patch-2208-26-0.sql' => 'database/schema/archive/patch-2208-26-0.sql'
75=== renamed file 'database/schema/patch-2208-27-0.sql' => 'database/schema/archive/patch-2208-27-0.sql'
76=== renamed file 'database/schema/patch-2208-28-0.sql' => 'database/schema/archive/patch-2208-28-0.sql'
77=== renamed file 'database/schema/patch-2208-28-1.sql' => 'database/schema/archive/patch-2208-28-1.sql'
78=== renamed file 'database/schema/patch-2208-29-0.sql' => 'database/schema/archive/patch-2208-29-0.sql'
79=== renamed file 'database/schema/patch-2208-30-0.sql' => 'database/schema/archive/patch-2208-30-0.sql'
80=== renamed file 'database/schema/patch-2208-31-0.sql' => 'database/schema/archive/patch-2208-31-0.sql'
81=== renamed file 'database/schema/patch-2208-32-0.sql' => 'database/schema/archive/patch-2208-32-0.sql'
82=== renamed file 'database/schema/patch-2208-33-0.sql' => 'database/schema/archive/patch-2208-33-0.sql'
83=== renamed file 'database/schema/patch-2208-34-0.sql' => 'database/schema/archive/patch-2208-34-0.sql'
84=== renamed file 'database/schema/patch-2208-35-0.sql' => 'database/schema/archive/patch-2208-35-0.sql'
85=== renamed file 'database/schema/patch-2208-36-0.sql' => 'database/schema/archive/patch-2208-36-0.sql'
86=== renamed file 'database/schema/patch-2208-37-0.sql' => 'database/schema/archive/patch-2208-37-0.sql'
87=== renamed file 'database/schema/patch-2208-38-0.sql' => 'database/schema/archive/patch-2208-38-0.sql'
88=== renamed file 'database/schema/patch-2208-38-1.sql' => 'database/schema/archive/patch-2208-38-1.sql'
89=== renamed file 'database/schema/patch-2208-39-0.sql' => 'database/schema/archive/patch-2208-39-0.sql'
90=== renamed file 'database/schema/patch-2208-40-0.sql' => 'database/schema/archive/patch-2208-40-0.sql'
91=== renamed file 'database/schema/patch-2208-41-0.sql' => 'database/schema/archive/patch-2208-41-0.sql'
92=== renamed file 'database/schema/patch-2208-42-0.sql' => 'database/schema/archive/patch-2208-42-0.sql'
93=== renamed file 'database/schema/patch-2208-43-0.sql' => 'database/schema/archive/patch-2208-43-0.sql'
94=== renamed file 'database/schema/patch-2208-44-0.sql' => 'database/schema/archive/patch-2208-44-0.sql'
95=== renamed file 'database/schema/patch-2208-45-0.sql' => 'database/schema/archive/patch-2208-45-0.sql'
96=== renamed file 'database/schema/patch-2208-46-0.sql' => 'database/schema/archive/patch-2208-46-0.sql'
97=== renamed file 'database/schema/patch-2208-47-0.sql' => 'database/schema/archive/patch-2208-47-0.sql'
98=== renamed file 'database/schema/patch-2208-48-0.sql' => 'database/schema/archive/patch-2208-48-0.sql'
99=== renamed file 'database/schema/patch-2208-49-0.sql' => 'database/schema/archive/patch-2208-49-0.sql'
100=== renamed file 'database/schema/patch-2208-50-0.sql' => 'database/schema/archive/patch-2208-50-0.sql'
101=== renamed file 'database/schema/patch-2208-51-0.sql' => 'database/schema/archive/patch-2208-51-0.sql'
102=== renamed file 'database/schema/patch-2208-52-0.sql' => 'database/schema/archive/patch-2208-52-0.sql'
103=== renamed file 'database/schema/patch-2208-53-0.sql' => 'database/schema/archive/patch-2208-53-0.sql'
104=== renamed file 'database/schema/patch-2208-54-0.sql' => 'database/schema/archive/patch-2208-54-0.sql'
105=== renamed file 'database/schema/patch-2208-55-0.sql' => 'database/schema/archive/patch-2208-55-0.sql'
106=== renamed file 'database/schema/patch-2208-56-0.sql' => 'database/schema/archive/patch-2208-56-0.sql'
107=== renamed file 'database/schema/patch-2208-57-0.sql' => 'database/schema/archive/patch-2208-57-0.sql'
108=== renamed file 'database/schema/patch-2208-57-1.sql' => 'database/schema/archive/patch-2208-57-1.sql'
109=== renamed file 'database/schema/patch-2208-58-0.sql' => 'database/schema/archive/patch-2208-58-0.sql'
110=== renamed file 'database/schema/patch-2208-59-0.sql' => 'database/schema/archive/patch-2208-59-0.sql'
111=== renamed file 'database/schema/patch-2208-59-1.sql' => 'database/schema/archive/patch-2208-59-1.sql'
112=== renamed file 'database/schema/patch-2208-59-2.sql' => 'database/schema/archive/patch-2208-59-2.sql'
113=== renamed file 'database/schema/patch-2208-60-0.sql' => 'database/schema/archive/patch-2208-60-0.sql'
114=== renamed file 'database/schema/patch-2208-60-1.sql' => 'database/schema/archive/patch-2208-60-1.sql'
115=== renamed file 'database/schema/patch-2208-61-0.sql' => 'database/schema/archive/patch-2208-61-0.sql'
116=== renamed file 'database/schema/patch-2208-62-0.sql' => 'database/schema/archive/patch-2208-62-0.sql'
117=== renamed file 'database/schema/patch-2208-63-0.sql' => 'database/schema/archive/patch-2208-63-0.sql'
118=== renamed file 'database/schema/patch-2208-63-1.sql' => 'database/schema/archive/patch-2208-63-1.sql'
119=== renamed file 'database/schema/patch-2208-63-2.sql' => 'database/schema/archive/patch-2208-63-2.sql'
120=== renamed file 'database/schema/patch-2208-63-3.sql' => 'database/schema/archive/patch-2208-63-3.sql'
121=== renamed file 'database/schema/patch-2208-63-4.sql' => 'database/schema/archive/patch-2208-63-4.sql'
122=== renamed file 'database/schema/patch-2208-64-0.sql' => 'database/schema/archive/patch-2208-64-0.sql'
123=== renamed file 'database/schema/patch-2208-65-0.sql' => 'database/schema/archive/patch-2208-65-0.sql'
124=== renamed file 'database/schema/patch-2208-65-1.sql' => 'database/schema/archive/patch-2208-65-1.sql'
125=== renamed file 'database/schema/patch-2208-65-2.sql' => 'database/schema/archive/patch-2208-65-2.sql'
126=== renamed file 'database/schema/patch-2208-67-0.sql' => 'database/schema/archive/patch-2208-67-0.sql'
127=== renamed file 'database/schema/patch-2208-68-0.sql' => 'database/schema/archive/patch-2208-68-0.sql'
128=== renamed file 'database/schema/patch-2208-69-0.sql' => 'database/schema/archive/patch-2208-69-0.sql'
129=== renamed file 'database/schema/patch-2208-70-0.sql' => 'database/schema/archive/patch-2208-70-0.sql'
130=== renamed file 'database/schema/patch-2208-71-0.sql' => 'database/schema/archive/patch-2208-71-0.sql'
131=== renamed file 'database/schema/patch-2208-72-0.sql' => 'database/schema/archive/patch-2208-72-0.sql'
132=== renamed file 'database/schema/patch-2208-73-0.sql' => 'database/schema/archive/patch-2208-73-0.sql'
133=== renamed file 'database/schema/patch-2208-73-1.sql' => 'database/schema/archive/patch-2208-73-1.sql'
134=== renamed file 'database/schema/patch-2208-73-2.sql' => 'database/schema/archive/patch-2208-73-2.sql'
135=== renamed file 'database/schema/patch-2208-74-0.sql' => 'database/schema/archive/patch-2208-74-0.sql'
136=== renamed file 'database/schema/patch-2208-75-0.sql' => 'database/schema/archive/patch-2208-75-0.sql'
137=== renamed file 'database/schema/patch-2208-75-1.sql' => 'database/schema/archive/patch-2208-75-1.sql'
138=== renamed file 'database/schema/patch-2208-76-0.sql' => 'database/schema/archive/patch-2208-76-0.sql'
139=== renamed file 'database/schema/patch-2208-76-1.sql' => 'database/schema/archive/patch-2208-76-1.sql'
140=== renamed file 'database/schema/patch-2208-76-2.sql' => 'database/schema/archive/patch-2208-76-2.sql'
141=== renamed file 'database/schema/patch-2208-76-3.sql' => 'database/schema/archive/patch-2208-76-3.sql'
142=== renamed file 'database/schema/patch-2208-76-4.sql' => 'database/schema/archive/patch-2208-76-4.sql'
143=== renamed file 'database/schema/patch-2208-77-0.sql' => 'database/schema/archive/patch-2208-77-0.sql'
144=== renamed file 'database/schema/patch-2208-78-0.sql' => 'database/schema/archive/patch-2208-78-0.sql'
145=== renamed file 'database/schema/patch-2208-78-1.sql' => 'database/schema/archive/patch-2208-78-1.sql'
146=== renamed file 'database/schema/patch-2208-78-2.sql' => 'database/schema/archive/patch-2208-78-2.sql'
147=== renamed file 'database/schema/patch-2208-79-0.sql' => 'database/schema/archive/patch-2208-79-0.sql'
148=== renamed file 'database/schema/patch-2208-79-1.sql' => 'database/schema/archive/patch-2208-79-1.sql'
149=== renamed file 'database/schema/patch-2208-80-1.sql' => 'database/schema/archive/patch-2208-80-1.sql'
150=== renamed file 'database/schema/patch-2208-81-1.sql' => 'database/schema/archive/patch-2208-81-1.sql'
151=== renamed file 'database/schema/patch-2208-82-1.sql' => 'database/schema/archive/patch-2208-82-1.sql'
152=== renamed file 'database/schema/patch-2208-83-1.sql' => 'database/schema/archive/patch-2208-83-1.sql'
153=== renamed file 'database/schema/patch-2208-83-2.sql' => 'database/schema/archive/patch-2208-83-2.sql'
154=== renamed file 'database/schema/patch-2208-84-1.sql' => 'database/schema/archive/patch-2208-84-1.sql'
155=== renamed file 'database/schema/patch-2208-85-1.sql' => 'database/schema/archive/patch-2208-85-1.sql'
156=== renamed file 'database/schema/patch-2208-87-1.sql' => 'database/schema/archive/patch-2208-87-1.sql'
157=== renamed file 'database/schema/patch-2208-87-2.sql' => 'database/schema/archive/patch-2208-87-2.sql'
158=== renamed file 'database/schema/patch-2208-88-1.sql' => 'database/schema/archive/patch-2208-88-1.sql'
159=== renamed file 'database/schema/patch-2208-89-1.sql' => 'database/schema/archive/patch-2208-89-1.sql'
160=== renamed file 'database/schema/patch-2208-90-1.sql' => 'database/schema/archive/patch-2208-90-1.sql'
161=== renamed file 'database/schema/patch-2208-91-1.sql' => 'database/schema/archive/patch-2208-91-1.sql'
162=== renamed file 'database/schema/patch-2208-92-1.sql' => 'database/schema/archive/patch-2208-92-1.sql'
163=== renamed file 'database/schema/patch-2208-92-2.sql' => 'database/schema/archive/patch-2208-92-2.sql'
164=== renamed file 'database/schema/patch-2208-93-0.sql' => 'database/schema/archive/patch-2208-93-0.sql'
165=== renamed file 'database/schema/patch-2208-93-1.sql' => 'database/schema/archive/patch-2208-93-1.sql'
166=== renamed file 'database/schema/patch-2208-93-2.sql' => 'database/schema/archive/patch-2208-93-2.sql'
167=== renamed file 'database/schema/launchpad-2208-00-0.sql' => 'database/schema/launchpad-2209-00-0.sql'
168--- database/schema/launchpad-2208-00-0.sql 2011-01-31 11:10:39 +0000
169+++ database/schema/launchpad-2209-00-0.sql 2011-12-08 10:11:32 +0000
170@@ -1,16 +1,65 @@
171--- Generated Tue Aug 17 10:52:11 2010 UTC
172+-- Generated Tue Dec 6 20:57:32 2011 UTC
173
174 SET client_min_messages TO ERROR;
175-
176-
177+SET statement_timeout = 0;
178 SET client_encoding = 'UTF8';
179 SET standard_conforming_strings = off;
180 SET check_function_bodies = false;
181 SET client_min_messages = warning;
182 SET escape_string_warning = off;
183
184+CREATE SCHEMA todrop;
185+
186+
187+CREATE SCHEMA ts2;
188+
189+
190+CREATE PROCEDURAL LANGUAGE plpgsql;
191+
192+
193+CREATE PROCEDURAL LANGUAGE plpythonu;
194+
195+
196 SET search_path = public, pg_catalog;
197
198+CREATE TYPE debversion;
199+
200+
201+CREATE FUNCTION debversionin(cstring) RETURNS debversion
202+ LANGUAGE internal IMMUTABLE STRICT
203+ AS $$textin$$;
204+
205+
206+CREATE FUNCTION debversionout(debversion) RETURNS cstring
207+ LANGUAGE internal IMMUTABLE STRICT
208+ AS $$textout$$;
209+
210+
211+CREATE FUNCTION debversionrecv(internal) RETURNS debversion
212+ LANGUAGE internal STABLE STRICT
213+ AS $$textrecv$$;
214+
215+
216+CREATE FUNCTION debversionsend(debversion) RETURNS bytea
217+ LANGUAGE internal STABLE STRICT
218+ AS $$textsend$$;
219+
220+
221+CREATE TYPE debversion (
222+ INTERNALLENGTH = variable,
223+ INPUT = debversionin,
224+ OUTPUT = debversionout,
225+ RECEIVE = debversionrecv,
226+ SEND = debversionsend,
227+ CATEGORY = 'S',
228+ ALIGNMENT = int4,
229+ STORAGE = extended
230+);
231+
232+
233+COMMENT ON TYPE debversion IS 'Debian package version number';
234+
235+
236 CREATE TYPE pgstattuple_type AS (
237 table_len bigint,
238 tuple_count bigint,
239@@ -23,38 +72,4489 @@
240 free_percent double precision
241 );
242
243+
244+SET search_path = ts2, pg_catalog;
245+
246+CREATE DOMAIN gtsq AS text;
247+
248+
249+CREATE DOMAIN gtsvector AS pg_catalog.gtsvector;
250+
251+
252+CREATE TYPE statinfo AS (
253+ word text,
254+ ndoc integer,
255+ nentry integer
256+);
257+
258+
259+CREATE TYPE tokenout AS (
260+ tokid integer,
261+ token text
262+);
263+
264+
265+CREATE TYPE tokentype AS (
266+ tokid integer,
267+ alias text,
268+ descr text
269+);
270+
271+
272+CREATE TYPE tsdebug AS (
273+ ts_name text,
274+ tok_type text,
275+ description text,
276+ token text,
277+ dict_name text[],
278+ tsvector pg_catalog.tsvector
279+);
280+
281+
282+CREATE DOMAIN tsquery AS pg_catalog.tsquery;
283+
284+
285+CREATE DOMAIN tsvector AS pg_catalog.tsvector;
286+
287+
288+SET search_path = public, pg_catalog;
289+
290+CREATE FUNCTION activity() RETURNS SETOF pg_stat_activity
291+ LANGUAGE sql SECURITY DEFINER
292+ SET search_path TO public
293+ AS $$
294+ SELECT
295+ datid, datname, procpid, usesysid, usename,
296+ CASE
297+ WHEN current_query LIKE '<IDLE>%'
298+ OR current_query LIKE 'autovacuum:%'
299+ THEN current_query
300+ ELSE
301+ '<HIDDEN>'
302+ END AS current_query,
303+ waiting, xact_start, query_start,
304+ backend_start, client_addr, client_port
305+ FROM pg_catalog.pg_stat_activity;
306+$$;
307+
308+
309+COMMENT ON FUNCTION activity() IS 'SECURITY DEFINER wrapper around pg_stat_activity allowing unprivileged users to access most of its information.';
310+
311+
312+CREATE FUNCTION add_test_openid_identifier(account_ integer) RETURNS boolean
313+ LANGUAGE plpgsql SECURITY DEFINER
314+ SET search_path TO public
315+ AS $$
316+BEGIN
317+ -- The generated OpenIdIdentifier is not a valid OpenId Identity URL
318+ -- and does not match tokens generated by the Canonical SSO. They
319+ -- are only useful to the test suite, and access to this stored
320+ -- procedure on production does not allow you to compromise
321+ -- accounts.
322+ INSERT INTO OpenIdIdentifier (identifier, account)
323+ VALUES ('test' || CAST(account_ AS text), account_);
324+ RETURN TRUE;
325+EXCEPTION
326+ WHEN unique_violation THEN
327+ RETURN FALSE;
328+END;
329+$$;
330+
331+
332+COMMENT ON FUNCTION add_test_openid_identifier(account_ integer) IS 'Add an OpenIdIdentifier to an account that can be used to login in the test environment. These identifiers are not usable on production or staging.';
333+
334+
335+CREATE FUNCTION assert_patch_applied(major integer, minor integer, patch integer) RETURNS boolean
336+ LANGUAGE plpythonu STABLE
337+ AS $$
338+ rv = plpy.execute("""
339+ SELECT * FROM LaunchpadDatabaseRevision
340+ WHERE major=%d AND minor=%d AND patch=%d
341+ """ % (major, minor, patch))
342+ if len(rv) == 0:
343+ raise Exception(
344+ 'patch-%d-%02d-%d not applied.' % (major, minor, patch))
345+ else:
346+ return True
347+$$;
348+
349+
350+COMMENT ON FUNCTION assert_patch_applied(major integer, minor integer, patch integer) IS 'Raise an exception if the given database patch has not been applied.';
351+
352+
353+CREATE FUNCTION bug_maintain_bug_summary() RETURNS trigger
354+ LANGUAGE plpgsql SECURITY DEFINER
355+ SET search_path TO public
356+ AS $$
357+BEGIN
358+ -- There is no INSERT logic, as a bug will not have any summary
359+ -- information until BugTask rows have been attached.
360+ IF TG_OP = 'UPDATE' THEN
361+ IF OLD.duplicateof IS DISTINCT FROM NEW.duplicateof
362+ OR OLD.private IS DISTINCT FROM NEW.private
363+ OR (OLD.latest_patch_uploaded IS NULL)
364+ <> (NEW.latest_patch_uploaded IS NULL) THEN
365+ PERFORM unsummarise_bug(OLD);
366+ PERFORM summarise_bug(NEW);
367+ END IF;
368+
369+ ELSIF TG_OP = 'DELETE' THEN
370+ PERFORM unsummarise_bug(OLD);
371+ END IF;
372+
373+ PERFORM bug_summary_flush_temp_journal();
374+ RETURN NULL; -- Ignored - this is an AFTER trigger
375+END;
376+$$;
377+
378+
379+COMMENT ON FUNCTION bug_maintain_bug_summary() IS 'AFTER trigger on bug maintaining the bugs summaries in bugsummary.';
380+
381+
382+CREATE FUNCTION valid_bug_name(text) RETURNS boolean
383+ LANGUAGE plpythonu IMMUTABLE STRICT
384+ AS $_$
385+ import re
386+ name = args[0]
387+ pat = r"^[a-z][a-z0-9+\.\-]+$"
388+ if re.match(pat, name):
389+ return 1
390+ return 0
391+$_$;
392+
393+
394+COMMENT ON FUNCTION valid_bug_name(text) IS 'validate a bug name
395+
396+ As per valid_name, except numeric-only names are not allowed (including
397+ names that look like floats).';
398+
399+
400 SET default_tablespace = '';
401
402 SET default_with_oids = false;
403
404-CREATE TABLE revision (
405- id integer NOT NULL,
406- date_created timestamp without time zone DEFAULT timezone('UTC'::text, ('now'::text)::timestamp(6) with time zone) NOT NULL,
407- log_body text NOT NULL,
408- revision_author integer NOT NULL,
409- gpgkey integer,
410- revision_id text NOT NULL,
411- revision_date timestamp without time zone,
412- karma_allocated boolean DEFAULT false
413-);
414-ALTER TABLE ONLY revision ALTER COLUMN revision_author SET STATISTICS 500;
415-ALTER TABLE ONLY revision ALTER COLUMN revision_date SET STATISTICS 500;
416+CREATE TABLE bug (
417+ id integer NOT NULL,
418+ datecreated timestamp without time zone DEFAULT timezone('UTC'::text, ('now'::text)::timestamp(6) with time zone) NOT NULL,
419+ name text,
420+ title text NOT NULL,
421+ description text NOT NULL,
422+ owner integer NOT NULL,
423+ duplicateof integer,
424+ fti ts2.tsvector,
425+ private boolean DEFAULT false NOT NULL,
426+ security_related boolean DEFAULT false NOT NULL,
427+ date_last_updated timestamp without time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
428+ date_made_private timestamp without time zone,
429+ who_made_private integer,
430+ date_last_message timestamp without time zone,
431+ number_of_duplicates integer DEFAULT 0 NOT NULL,
432+ message_count integer DEFAULT 0 NOT NULL,
433+ users_affected_count integer DEFAULT 0,
434+ users_unaffected_count integer DEFAULT 0,
435+ heat integer DEFAULT 0 NOT NULL,
436+ heat_last_updated timestamp without time zone,
437+ latest_patch_uploaded timestamp without time zone,
438+ access_policy integer,
439+ CONSTRAINT notduplicateofself CHECK ((NOT (id = duplicateof))),
440+ CONSTRAINT sane_description CHECK (((ltrim(description) <> ''::text) AND (char_length(description) <= 50000))),
441+ CONSTRAINT valid_bug_name CHECK (valid_bug_name(name))
442+);
443+
444+
445+COMMENT ON TABLE bug IS 'A software bug that requires fixing. This particular bug may be linked to one or more products or source packages to identify the location(s) that this bug is found.';
446+
447+
448+COMMENT ON COLUMN bug.name IS 'A lowercase name uniquely identifying the bug';
449+
450+
451+COMMENT ON COLUMN bug.description IS 'A detailed description of the bug. Initially this will be set to the contents of the initial email or bug filing comment, but later it can be edited to give a more accurate description of the bug itself rather than the symptoms observed by the reporter.';
452+
453+
454+COMMENT ON COLUMN bug.private IS 'Is this bug private? If so, only explicit subscribers will be able to see it';
455+
456+
457+COMMENT ON COLUMN bug.security_related IS 'Is this bug a security issue?';
458+
459+
460+COMMENT ON COLUMN bug.date_last_message IS 'When the last BugMessage was attached to this Bug. Maintained by a trigger on the BugMessage table.';
461+
462+
463+COMMENT ON COLUMN bug.number_of_duplicates IS 'The number of bugs marked as duplicates of this bug, populated by a trigger after setting the duplicateof of bugs.';
464+
465+
466+COMMENT ON COLUMN bug.message_count IS 'The number of messages (currently just comments) on this bugbug, maintained by the set_bug_message_count_t trigger.';
467+
468+
469+COMMENT ON COLUMN bug.users_affected_count IS 'The number of users affected by this bug, maintained by the set_bug_users_affected_count_t trigger.';
470+
471+
472+COMMENT ON COLUMN bug.heat IS 'The relevance of this bug. This value is computed periodically using bug_affects_person and other bug values.';
473+
474+
475+COMMENT ON COLUMN bug.heat_last_updated IS 'The time this bug''s heat was last updated, or NULL if the heat has never yet been updated.';
476+
477+
478+COMMENT ON COLUMN bug.latest_patch_uploaded IS 'The time when the most recent patch has been attached to this bug or NULL if no patches are attached';
479+
480+
481+CREATE FUNCTION bug_row(bug_id integer) RETURNS bug
482+ LANGUAGE sql STABLE
483+ AS $_$
484+ SELECT * FROM Bug WHERE id=$1;
485+$_$;
486+
487+
488+COMMENT ON FUNCTION bug_row(bug_id integer) IS 'Helper for manually testing functions requiring a bug row as input. eg. SELECT * FROM bugsummary_tags(bug_row(1))';
489+
490+
491+CREATE TABLE bugsummary (
492+ id integer NOT NULL,
493+ count integer DEFAULT 0 NOT NULL,
494+ product integer,
495+ productseries integer,
496+ distribution integer,
497+ distroseries integer,
498+ sourcepackagename integer,
499+ viewed_by integer,
500+ tag text,
501+ status integer NOT NULL,
502+ milestone integer,
503+ importance integer NOT NULL,
504+ has_patch boolean NOT NULL,
505+ fixed_upstream boolean NOT NULL,
506+ CONSTRAINT bugtask_assignment_checks CHECK (CASE WHEN (product IS NOT NULL) THEN ((((productseries IS NULL) AND (distribution IS NULL)) AND (distroseries IS NULL)) AND (sourcepackagename IS NULL)) WHEN (productseries IS NOT NULL) THEN (((distribution IS NULL) AND (distroseries IS NULL)) AND (sourcepackagename IS NULL)) WHEN (distribution IS NOT NULL) THEN (distroseries IS NULL) WHEN (distroseries IS NOT NULL) THEN true ELSE false END)
507+);
508+
509+
510+CREATE FUNCTION bug_summary_dec(bugsummary) RETURNS void
511+ LANGUAGE sql
512+ AS $_$
513+ -- We own the row reference, so in the absence of bugs this cannot
514+ -- fail - just decrement the row.
515+ UPDATE BugSummary SET count = count + $1.count
516+ WHERE
517+ ((product IS NULL AND $1.product IS NULL)
518+ OR product = $1.product)
519+ AND ((productseries IS NULL AND $1.productseries IS NULL)
520+ OR productseries = $1.productseries)
521+ AND ((distribution IS NULL AND $1.distribution IS NULL)
522+ OR distribution = $1.distribution)
523+ AND ((distroseries IS NULL AND $1.distroseries IS NULL)
524+ OR distroseries = $1.distroseries)
525+ AND ((sourcepackagename IS NULL AND $1.sourcepackagename IS NULL)
526+ OR sourcepackagename = $1.sourcepackagename)
527+ AND ((viewed_by IS NULL AND $1.viewed_by IS NULL)
528+ OR viewed_by = $1.viewed_by)
529+ AND ((tag IS NULL AND $1.tag IS NULL)
530+ OR tag = $1.tag)
531+ AND status = $1.status
532+ AND ((milestone IS NULL AND $1.milestone IS NULL)
533+ OR milestone = $1.milestone)
534+ AND importance = $1.importance
535+ AND has_patch = $1.has_patch
536+ AND fixed_upstream = $1.fixed_upstream;
537+$_$;
538+
539+
540+COMMENT ON FUNCTION bug_summary_dec(bugsummary) IS 'UPSERT into bugsummary incrementing one row';
541+
542+
543+CREATE FUNCTION bug_summary_flush_temp_journal() RETURNS void
544+ LANGUAGE plpgsql
545+ AS $$
546+DECLARE
547+ d bugsummary%ROWTYPE;
548+BEGIN
549+ -- may get called even though no summaries were made (for simplicity in the
550+ -- callers)
551+ PERFORM ensure_bugsummary_temp_journal();
552+ FOR d IN SELECT * FROM bugsummary_temp_journal LOOP
553+ PERFORM bugsummary_journal_ins(d);
554+ END LOOP;
555+ TRUNCATE bugsummary_temp_journal;
556+END;
557+$$;
558+
559+
560+COMMENT ON FUNCTION bug_summary_flush_temp_journal() IS 'flush the temporary bugsummary journal into the bugsummary table';
561+
562+
563+CREATE FUNCTION bug_summary_inc(d bugsummary) RETURNS void
564+ LANGUAGE plpgsql
565+ AS $_$
566+BEGIN
567+ -- Shameless adaption from postgresql manual
568+ LOOP
569+ -- first try to update the row
570+ UPDATE BugSummary SET count = count + d.count
571+ WHERE
572+ ((product IS NULL AND $1.product IS NULL)
573+ OR product = $1.product)
574+ AND ((productseries IS NULL AND $1.productseries IS NULL)
575+ OR productseries = $1.productseries)
576+ AND ((distribution IS NULL AND $1.distribution IS NULL)
577+ OR distribution = $1.distribution)
578+ AND ((distroseries IS NULL AND $1.distroseries IS NULL)
579+ OR distroseries = $1.distroseries)
580+ AND ((sourcepackagename IS NULL AND $1.sourcepackagename IS NULL)
581+ OR sourcepackagename = $1.sourcepackagename)
582+ AND ((viewed_by IS NULL AND $1.viewed_by IS NULL)
583+ OR viewed_by = $1.viewed_by)
584+ AND ((tag IS NULL AND $1.tag IS NULL)
585+ OR tag = $1.tag)
586+ AND status = $1.status
587+ AND ((milestone IS NULL AND $1.milestone IS NULL)
588+ OR milestone = $1.milestone)
589+ AND importance = $1.importance
590+ AND has_patch = $1.has_patch
591+ AND fixed_upstream = $1.fixed_upstream;
592+ IF found THEN
593+ RETURN;
594+ END IF;
595+ -- not there, so try to insert the key
596+ -- if someone else inserts the same key concurrently,
597+ -- we could get a unique-key failure
598+ BEGIN
599+ INSERT INTO BugSummary(
600+ count, product, productseries, distribution,
601+ distroseries, sourcepackagename, viewed_by, tag,
602+ status, milestone,
603+ importance, has_patch, fixed_upstream)
604+ VALUES (
605+ d.count, d.product, d.productseries, d.distribution,
606+ d.distroseries, d.sourcepackagename, d.viewed_by, d.tag,
607+ d.status, d.milestone,
608+ d.importance, d.has_patch, d.fixed_upstream);
609+ RETURN;
610+ EXCEPTION WHEN unique_violation THEN
611+ -- do nothing, and loop to try the UPDATE again
612+ END;
613+ END LOOP;
614+END;
615+$_$;
616+
617+
618+COMMENT ON FUNCTION bug_summary_inc(d bugsummary) IS 'UPSERT into bugsummary incrementing one row';
619+
620+
621+CREATE FUNCTION bug_summary_temp_journal_ins(d bugsummary) RETURNS void
622+ LANGUAGE plpgsql
623+ AS $$
624+BEGIN
625+ INSERT INTO BugSummary_Temp_Journal(
626+ count, product, productseries, distribution,
627+ distroseries, sourcepackagename, viewed_by, tag,
628+ status, milestone, importance, has_patch, fixed_upstream)
629+ VALUES (
630+ d.count, d.product, d.productseries, d.distribution,
631+ d.distroseries, d.sourcepackagename, d.viewed_by, d.tag,
632+ d.status, d.milestone, d.importance, d.has_patch, d.fixed_upstream);
633+ RETURN;
634+END;
635+$$;
636+
637+
638+COMMENT ON FUNCTION bug_summary_temp_journal_ins(d bugsummary) IS 'Insert a BugSummary into the temporary journal';
639+
640+
641+CREATE FUNCTION bug_update_heat_copy_to_bugtask() RETURNS trigger
642+ LANGUAGE plpgsql SECURITY DEFINER
643+ SET search_path TO public
644+ AS $$
645+BEGIN
646+ IF NEW.heat != OLD.heat THEN
647+ UPDATE bugtask SET heat=NEW.heat WHERE bugtask.bug=NEW.id;
648+ END IF;
649+ RETURN NULL; -- Ignored - this is an AFTER trigger
650+END;
651+$$;
652+
653+
654+COMMENT ON FUNCTION bug_update_heat_copy_to_bugtask() IS 'Copies bug heat to bugtasks when the bug is changed. Runs on UPDATE only because INSERTs do not have bugtasks at the point of insertion.';
655+
656+
657+CREATE FUNCTION bug_update_latest_patch_uploaded(integer) RETURNS void
658+ LANGUAGE plpgsql SECURITY DEFINER
659+ SET search_path TO public
660+ AS $_$
661+BEGIN
662+ UPDATE bug SET latest_patch_uploaded =
663+ (SELECT max(message.datecreated)
664+ FROM message, bugattachment
665+ WHERE bugattachment.message=message.id AND
666+ bugattachment.bug=$1 AND
667+ bugattachment.type=1)
668+ WHERE bug.id=$1;
669+END;
670+$_$;
671+
672+
673+CREATE FUNCTION bug_update_latest_patch_uploaded_on_delete() RETURNS trigger
674+ LANGUAGE plpgsql SECURITY DEFINER
675+ SET search_path TO public
676+ AS $$
677+BEGIN
678+ PERFORM bug_update_latest_patch_uploaded(OLD.bug);
679+ RETURN NULL; -- Ignored - this is an AFTER trigger
680+END;
681+$$;
682+
683+
684+CREATE FUNCTION bug_update_latest_patch_uploaded_on_insert_update() RETURNS trigger
685+ LANGUAGE plpgsql SECURITY DEFINER
686+ SET search_path TO public
687+ AS $$
688+BEGIN
689+ PERFORM bug_update_latest_patch_uploaded(NEW.bug);
690+ RETURN NULL; -- Ignored - this is an AFTER trigger
691+END;
692+$$;
693+
694+
695+CREATE FUNCTION bugmessage_copy_owner_from_message() RETURNS trigger
696+ LANGUAGE plpgsql SECURITY DEFINER
697+ SET search_path TO public
698+ AS $$
699+BEGIN
700+ IF TG_OP = 'INSERT' THEN
701+ IF NEW.owner is NULL THEN
702+ UPDATE BugMessage
703+ SET owner = Message.owner FROM
704+ Message WHERE
705+ Message.id = NEW.message AND
706+ BugMessage.id = NEW.id;
707+ END IF;
708+ ELSIF NEW.message != OLD.message THEN
709+ UPDATE BugMessage
710+ SET owner = Message.owner FROM
711+ Message WHERE
712+ Message.id = NEW.message AND
713+ BugMessage.id = NEW.id;
714+ END IF;
715+ RETURN NULL; -- Ignored - this is an AFTER trigger
716+END;
717+$$;
718+
719+
720+COMMENT ON FUNCTION bugmessage_copy_owner_from_message() IS 'Copies the message owner into bugmessage when bugmessage changes.';
721+
722+
723+CREATE FUNCTION bugsubscription_maintain_bug_summary() RETURNS trigger
724+ LANGUAGE plpgsql SECURITY DEFINER
725+ SET search_path TO public
726+ AS $$
727+BEGIN
728+ -- This trigger only works if we are inserting, updating or deleting
729+ -- a single row per statement.
730+ IF TG_OP = 'INSERT' THEN
731+ IF NOT (bug_row(NEW.bug)).private THEN
732+ -- Public subscriptions are not aggregated.
733+ RETURN NEW;
734+ END IF;
735+ IF TG_WHEN = 'BEFORE' THEN
736+ PERFORM unsummarise_bug(bug_row(NEW.bug));
737+ ELSE
738+ PERFORM summarise_bug(bug_row(NEW.bug));
739+ END IF;
740+ PERFORM bug_summary_flush_temp_journal();
741+ RETURN NEW;
742+ ELSIF TG_OP = 'DELETE' THEN
743+ IF NOT (bug_row(OLD.bug)).private THEN
744+ -- Public subscriptions are not aggregated.
745+ RETURN OLD;
746+ END IF;
747+ IF TG_WHEN = 'BEFORE' THEN
748+ PERFORM unsummarise_bug(bug_row(OLD.bug));
749+ ELSE
750+ PERFORM summarise_bug(bug_row(OLD.bug));
751+ END IF;
752+ PERFORM bug_summary_flush_temp_journal();
753+ RETURN OLD;
754+ ELSE
755+ IF (OLD.person IS DISTINCT FROM NEW.person
756+ OR OLD.bug IS DISTINCT FROM NEW.bug) THEN
757+ IF TG_WHEN = 'BEFORE' THEN
758+ IF (bug_row(OLD.bug)).private THEN
759+ -- Public subscriptions are not aggregated.
760+ PERFORM unsummarise_bug(bug_row(OLD.bug));
761+ END IF;
762+ IF OLD.bug <> NEW.bug AND (bug_row(NEW.bug)).private THEN
763+ -- Public subscriptions are not aggregated.
764+ PERFORM unsummarise_bug(bug_row(NEW.bug));
765+ END IF;
766+ ELSE
767+ IF (bug_row(OLD.bug)).private THEN
768+ -- Public subscriptions are not aggregated.
769+ PERFORM summarise_bug(bug_row(OLD.bug));
770+ END IF;
771+ IF OLD.bug <> NEW.bug AND (bug_row(NEW.bug)).private THEN
772+ -- Public subscriptions are not aggregated.
773+ PERFORM summarise_bug(bug_row(NEW.bug));
774+ END IF;
775+ END IF;
776+ END IF;
777+ PERFORM bug_summary_flush_temp_journal();
778+ RETURN NEW;
779+ END IF;
780+END;
781+$$;
782+
783+
784+COMMENT ON FUNCTION bugsubscription_maintain_bug_summary() IS 'AFTER trigger on bugsubscription maintaining the bugs summaries in bugsummary.';
785+
786+
787+CREATE FUNCTION bugsummary_journal_ins(d bugsummary) RETURNS void
788+ LANGUAGE plpgsql
789+ AS $$
790+BEGIN
791+ IF d.count <> 0 THEN
792+ INSERT INTO BugSummaryJournal (
793+ count, product, productseries, distribution,
794+ distroseries, sourcepackagename, viewed_by, tag,
795+ status, milestone,
796+ importance, has_patch, fixed_upstream)
797+ VALUES (
798+ d.count, d.product, d.productseries, d.distribution,
799+ d.distroseries, d.sourcepackagename, d.viewed_by, d.tag,
800+ d.status, d.milestone,
801+ d.importance, d.has_patch, d.fixed_upstream);
802+ END IF;
803+END;
804+$$;
805+
806+
807+COMMENT ON FUNCTION bugsummary_journal_ins(d bugsummary) IS 'Add an entry into BugSummaryJournal';
808+
809+
810+CREATE FUNCTION bugsummary_locations(bug_row bug) RETURNS SETOF bugsummary
811+ LANGUAGE plpgsql
812+ AS $$
813+BEGIN
814+ IF BUG_ROW.duplicateof IS NOT NULL THEN
815+ RETURN;
816+ END IF;
817+ RETURN QUERY
818+ SELECT
819+ CAST(NULL AS integer) AS id,
820+ CAST(1 AS integer) AS count,
821+ product, productseries, distribution, distroseries,
822+ sourcepackagename, person AS viewed_by, tag, status, milestone,
823+ importance,
824+ BUG_ROW.latest_patch_uploaded IS NOT NULL AS has_patch,
825+ (EXISTS (
826+ SELECT TRUE FROM BugTask AS RBT
827+ WHERE
828+ RBT.bug = tasks.bug
829+ -- This would just be 'RBT.id <> tasks.id', except
830+ -- that the records from tasks are summaries and not
831+ -- real bugtasks, and do not have an id.
832+ AND (RBT.product IS DISTINCT FROM tasks.product
833+ OR RBT.productseries
834+ IS DISTINCT FROM tasks.productseries
835+ OR RBT.distribution IS DISTINCT FROM tasks.distribution
836+ OR RBT.distroseries IS DISTINCT FROM tasks.distroseries
837+ OR RBT.sourcepackagename
838+ IS DISTINCT FROM tasks.sourcepackagename)
839+ -- Flagged as INVALID, FIXCOMMITTED or FIXRELEASED
840+ -- via a bugwatch, or FIXCOMMITTED or FIXRELEASED on
841+ -- the product.
842+ AND ((bugwatch IS NOT NULL AND status IN (17, 25, 30))
843+ OR (bugwatch IS NULL AND product IS NOT NULL
844+ AND status IN (25, 30))))
845+ )::boolean AS fixed_upstream
846+ FROM bugsummary_tasks(BUG_ROW) AS tasks
847+ JOIN bugsummary_tags(BUG_ROW) AS bug_tags ON TRUE
848+ LEFT OUTER JOIN bugsummary_viewers(BUG_ROW) AS bug_viewers ON TRUE;
849+END;
850+$$;
851+
852+
853+COMMENT ON FUNCTION bugsummary_locations(bug_row bug) IS 'Calculate what BugSummary rows should exist for a given Bug.';
854+
855+
856+CREATE FUNCTION bugsummary_rollup_journal(batchsize integer DEFAULT NULL::integer) RETURNS void
857+ LANGUAGE plpgsql SECURITY DEFINER
858+ SET search_path TO public
859+ AS $$
860+DECLARE
861+ d bugsummary%ROWTYPE;
862+ max_id integer;
863+BEGIN
864+ -- Lock so we don't content with other invokations of this
865+ -- function. We can happily lock the BugSummary table for writes
866+ -- as this function is the only thing that updates that table.
867+ -- BugSummaryJournal remains unlocked so nothing should be blocked.
868+ LOCK TABLE BugSummary IN ROW EXCLUSIVE MODE;
869+
870+ IF batchsize IS NULL THEN
871+ SELECT MAX(id) INTO max_id FROM BugSummaryJournal;
872+ ELSE
873+ SELECT MAX(id) INTO max_id FROM (
874+ SELECT id FROM BugSummaryJournal ORDER BY id LIMIT batchsize
875+ ) AS Whatever;
876+ END IF;
877+
878+ FOR d IN
879+ SELECT
880+ NULL as id,
881+ SUM(count),
882+ product,
883+ productseries,
884+ distribution,
885+ distroseries,
886+ sourcepackagename,
887+ viewed_by,
888+ tag,
889+ status,
890+ milestone,
891+ importance,
892+ has_patch,
893+ fixed_upstream
894+ FROM BugSummaryJournal
895+ WHERE id <= max_id
896+ GROUP BY
897+ product, productseries, distribution, distroseries,
898+ sourcepackagename, viewed_by, tag, status, milestone,
899+ importance, has_patch, fixed_upstream
900+ HAVING sum(count) <> 0
901+ LOOP
902+ IF d.count < 0 THEN
903+ PERFORM bug_summary_dec(d);
904+ ELSIF d.count > 0 THEN
905+ PERFORM bug_summary_inc(d);
906+ END IF;
907+ END LOOP;
908+
909+ -- Clean out any counts we reduced to 0.
910+ DELETE FROM BugSummary WHERE count=0;
911+ -- Clean out the journal entries we have handled.
912+ DELETE FROM BugSummaryJournal WHERE id <= max_id;
913+END;
914+$$;
915+
916+
917+COMMENT ON FUNCTION bugsummary_rollup_journal(batchsize integer) IS 'Collate and migrate rows from BugSummaryJournal to BugSummary';
918+
919+
920+CREATE FUNCTION valid_name(text) RETURNS boolean
921+ LANGUAGE plpythonu IMMUTABLE STRICT
922+ AS $$
923+ import re
924+ name = args[0]
925+ pat = r"^[a-z0-9][a-z0-9\+\.\-]*\Z"
926+ if re.match(pat, name):
927+ return 1
928+ return 0
929+$$;
930+
931+
932+COMMENT ON FUNCTION valid_name(text) IS 'validate a name.
933+
934+ Names must contain only lowercase letters, numbers, ., & -. They
935+ must start with an alphanumeric. They are ASCII only. Names are useful
936+ for mneumonic identifiers such as nicknames and as URL components.
937+ This specification is the same as the Debian product naming policy.
938+
939+ Note that a valid name might be all integers, so there is a possible
940+ namespace conflict if URL traversal is possible by name as well as id.';
941+
942+
943+CREATE TABLE bugtag (
944+ id integer NOT NULL,
945+ bug integer NOT NULL,
946+ tag text NOT NULL,
947+ CONSTRAINT valid_tag CHECK (valid_name(tag))
948+);
949+
950+
951+COMMENT ON TABLE bugtag IS 'Attaches simple text tags to a bug.';
952+
953+
954+COMMENT ON COLUMN bugtag.bug IS 'The bug the tags is attached to.';
955+
956+
957+COMMENT ON COLUMN bugtag.tag IS 'The text representation of the tag.';
958+
959+
960+CREATE FUNCTION bugsummary_tags(bug_row bug) RETURNS SETOF bugtag
961+ LANGUAGE sql STABLE
962+ AS $_$
963+ SELECT * FROM BugTag WHERE BugTag.bug = $1.id
964+ UNION ALL
965+ SELECT NULL::integer, $1.id, NULL::text;
966+$_$;
967+
968+
969+COMMENT ON FUNCTION bugsummary_tags(bug_row bug) IS 'Return (bug, tag) for all tags + (bug, NULL::text)';
970+
971+
972+CREATE TABLE bugtask (
973+ id integer NOT NULL,
974+ bug integer NOT NULL,
975+ product integer,
976+ distribution integer,
977+ distroseries integer,
978+ sourcepackagename integer,
979+ binarypackagename integer,
980+ status integer NOT NULL,
981+ importance integer DEFAULT 5 NOT NULL,
982+ assignee integer,
983+ date_assigned timestamp without time zone,
984+ datecreated timestamp without time zone DEFAULT timezone('UTC'::text, ('now'::text)::timestamp(6) with time zone),
985+ owner integer NOT NULL,
986+ milestone integer,
987+ bugwatch integer,
988+ fti ts2.tsvector,
989+ targetnamecache text,
990+ date_confirmed timestamp without time zone,
991+ date_inprogress timestamp without time zone,
992+ date_closed timestamp without time zone,
993+ productseries integer,
994+ date_incomplete timestamp without time zone,
995+ date_left_new timestamp without time zone,
996+ date_triaged timestamp without time zone,
997+ date_fix_committed timestamp without time zone,
998+ date_fix_released timestamp without time zone,
999+ date_left_closed timestamp without time zone,
1000+ heat_rank integer DEFAULT 0 NOT NULL,
1001+ date_milestone_set timestamp without time zone,
1002+ heat integer DEFAULT 0 NOT NULL,
1003+ CONSTRAINT bugtask_assignment_checks CHECK (CASE WHEN (product IS NOT NULL) THEN ((((productseries IS NULL) AND (distribution IS NULL)) AND (distroseries IS NULL)) AND (sourcepackagename IS NULL)) WHEN (productseries IS NOT NULL) THEN (((distribution IS NULL) AND (distroseries IS NULL)) AND (sourcepackagename IS NULL)) WHEN (distribution IS NOT NULL) THEN (distroseries IS NULL) WHEN (distroseries IS NOT NULL) THEN true ELSE false END)
1004+);
1005+
1006+
1007+COMMENT ON TABLE bugtask IS 'Links a given Bug to a particular (sourcepackagename, distro) or product.';
1008+
1009+
1010+COMMENT ON COLUMN bugtask.bug IS 'The bug that is assigned to this (sourcepackagename, distro) or product.';
1011+
1012+
1013+COMMENT ON COLUMN bugtask.product IS 'The product in which this bug shows up.';
1014+
1015+
1016+COMMENT ON COLUMN bugtask.distribution IS 'The distro of the named sourcepackage.';
1017+
1018+
1019+COMMENT ON COLUMN bugtask.sourcepackagename IS 'The name of the sourcepackage in which this bug shows up.';
1020+
1021+
1022+COMMENT ON COLUMN bugtask.binarypackagename IS 'The name of the binary package built from the source package. This column may only contain a value if this bug task is linked to a sourcepackage (not a product)';
1023+
1024+
1025+COMMENT ON COLUMN bugtask.status IS 'The general health of the bug, e.g. Accepted, Rejected, etc.';
1026+
1027+
1028+COMMENT ON COLUMN bugtask.importance IS 'The importance of fixing the bug.';
1029+
1030+
1031+COMMENT ON COLUMN bugtask.assignee IS 'The person who has been assigned to fix this bug in this product or (sourcepackagename, distro)';
1032+
1033+
1034+COMMENT ON COLUMN bugtask.date_assigned IS 'The date on which the bug in this (sourcepackagename, distro) or product was assigned to someone to fix';
1035+
1036+
1037+COMMENT ON COLUMN bugtask.datecreated IS 'A timestamp for the creation of this bug assignment. Note that this is not the date the bug was created (though it might be), it''s the date the bug was assigned to this product, which could have come later.';
1038+
1039+
1040+COMMENT ON COLUMN bugtask.milestone IS 'A way to mark a bug for grouping purposes, e.g. to say it needs to be fixed by version 1.2';
1041+
1042+
1043+COMMENT ON COLUMN bugtask.bugwatch IS 'This column allows us to link a bug
1044+task to a bug watch. In other words, we are connecting the state of the task
1045+to the state of the bug in a different bug tracking system. To the best of
1046+our ability we''ll try and keep the bug task syncronised with the state of
1047+the remote bug watch.';
1048+
1049+
1050+COMMENT ON COLUMN bugtask.targetnamecache IS 'A cached value of the target name of this bugtask, to make it easier to sort and search on the target name.';
1051+
1052+
1053+COMMENT ON COLUMN bugtask.date_confirmed IS 'The date when this bug transitioned from an unconfirmed status to a confirmed one. If the state regresses to a one that logically occurs before Confirmed, e.g., Unconfirmed, this date is cleared.';
1054+
1055+
1056+COMMENT ON COLUMN bugtask.date_inprogress IS 'The date on which this bug transitioned from not being in progress to a state >= In Progress. If the status moves back to a pre-In Progress state, this date is cleared';
1057+
1058+
1059+COMMENT ON COLUMN bugtask.date_closed IS 'The date when this bug transitioned to a resolved state, e.g., Rejected, Fix Released, etc. If the state changes back to a pre-closed state, this date is cleared';
1060+
1061+
1062+COMMENT ON COLUMN bugtask.productseries IS 'The product series to which the bug is targeted';
1063+
1064+
1065+COMMENT ON COLUMN bugtask.date_left_new IS 'The date when this bug first transitioned out of the NEW status.';
1066+
1067+
1068+COMMENT ON COLUMN bugtask.date_triaged IS 'The date when this bug transitioned to a status >= TRIAGED.';
1069+
1070+
1071+COMMENT ON COLUMN bugtask.date_fix_committed IS 'The date when this bug transitioned to a status >= FIXCOMMITTED.';
1072+
1073+
1074+COMMENT ON COLUMN bugtask.date_fix_released IS 'The date when this bug transitioned to a FIXRELEASED status.';
1075+
1076+
1077+COMMENT ON COLUMN bugtask.date_left_closed IS 'The date when this bug last transitioned out of a CLOSED status.';
1078+
1079+
1080+COMMENT ON COLUMN bugtask.heat_rank IS 'The heat bin in which this bugtask appears, as a value from the BugTaskHeatRank enumeration.';
1081+
1082+
1083+COMMENT ON COLUMN bugtask.date_milestone_set IS 'The date when this bug was targed to the milestone that is currently set.';
1084+
1085+
1086+CREATE FUNCTION bugsummary_tasks(bug_row bug) RETURNS SETOF bugtask
1087+ LANGUAGE plpgsql STABLE
1088+ AS $$
1089+DECLARE
1090+ bt bugtask%ROWTYPE;
1091+ r record;
1092+BEGIN
1093+ bt.bug = BUG_ROW.id;
1094+
1095+ -- One row only for each target permutation - need to ignore other fields
1096+ -- like date last modified to deal with conjoined masters and multiple
1097+ -- sourcepackage tasks in a distro.
1098+ FOR r IN
1099+ SELECT
1100+ product, productseries, distribution, distroseries,
1101+ sourcepackagename, status, milestone, importance, bugwatch
1102+ FROM BugTask WHERE bug=BUG_ROW.id
1103+ UNION -- Implicit DISTINCT
1104+ SELECT
1105+ product, productseries, distribution, distroseries,
1106+ NULL, status, milestone, importance, bugwatch
1107+ FROM BugTask WHERE bug=BUG_ROW.id AND sourcepackagename IS NOT NULL
1108+ LOOP
1109+ bt.product = r.product;
1110+ bt.productseries = r.productseries;
1111+ bt.distribution = r.distribution;
1112+ bt.distroseries = r.distroseries;
1113+ bt.sourcepackagename = r.sourcepackagename;
1114+ bt.status = r.status;
1115+ bt.milestone = r.milestone;
1116+ bt.importance = r.importance;
1117+ bt.bugwatch = r.bugwatch;
1118+ RETURN NEXT bt;
1119+ END LOOP;
1120+END;
1121+$$;
1122+
1123+
1124+COMMENT ON FUNCTION bugsummary_tasks(bug_row bug) IS 'Return all tasks for the bug + all sourcepackagename tasks again with the sourcepackagename squashed';
1125+
1126+
1127+CREATE TABLE bugsubscription (
1128+ id integer NOT NULL,
1129+ person integer NOT NULL,
1130+ bug integer NOT NULL,
1131+ date_created timestamp without time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
1132+ subscribed_by integer NOT NULL,
1133+ bug_notification_level integer DEFAULT 40 NOT NULL
1134+);
1135+
1136+
1137+COMMENT ON TABLE bugsubscription IS 'A subscription by a Person to a bug.';
1138+
1139+
1140+COMMENT ON COLUMN bugsubscription.bug_notification_level IS 'The level of notifications which the Person will receive from this subscription.';
1141+
1142+
1143+CREATE FUNCTION bugsummary_viewers(bug_row bug) RETURNS SETOF bugsubscription
1144+ LANGUAGE sql STABLE
1145+ AS $_$
1146+ SELECT *
1147+ FROM BugSubscription
1148+ WHERE
1149+ bugsubscription.bug=$1.id
1150+ AND $1.private IS TRUE;
1151+$_$;
1152+
1153+
1154+COMMENT ON FUNCTION bugsummary_viewers(bug_row bug) IS 'Return (bug, viewer) for all viewers if private, nothing otherwise';
1155+
1156+
1157+CREATE FUNCTION bugtag_maintain_bug_summary() RETURNS trigger
1158+ LANGUAGE plpgsql SECURITY DEFINER
1159+ SET search_path TO public
1160+ AS $$
1161+BEGIN
1162+ IF TG_OP = 'INSERT' THEN
1163+ IF TG_WHEN = 'BEFORE' THEN
1164+ PERFORM unsummarise_bug(bug_row(NEW.bug));
1165+ ELSE
1166+ PERFORM summarise_bug(bug_row(NEW.bug));
1167+ END IF;
1168+ PERFORM bug_summary_flush_temp_journal();
1169+ RETURN NEW;
1170+ ELSIF TG_OP = 'DELETE' THEN
1171+ IF TG_WHEN = 'BEFORE' THEN
1172+ PERFORM unsummarise_bug(bug_row(OLD.bug));
1173+ ELSE
1174+ PERFORM summarise_bug(bug_row(OLD.bug));
1175+ END IF;
1176+ PERFORM bug_summary_flush_temp_journal();
1177+ RETURN OLD;
1178+ ELSE
1179+ IF TG_WHEN = 'BEFORE' THEN
1180+ PERFORM unsummarise_bug(bug_row(OLD.bug));
1181+ IF OLD.bug <> NEW.bug THEN
1182+ PERFORM unsummarise_bug(bug_row(NEW.bug));
1183+ END IF;
1184+ ELSE
1185+ PERFORM summarise_bug(bug_row(OLD.bug));
1186+ IF OLD.bug <> NEW.bug THEN
1187+ PERFORM summarise_bug(bug_row(NEW.bug));
1188+ END IF;
1189+ END IF;
1190+ PERFORM bug_summary_flush_temp_journal();
1191+ RETURN NEW;
1192+ END IF;
1193+END;
1194+$$;
1195+
1196+
1197+COMMENT ON FUNCTION bugtag_maintain_bug_summary() IS 'AFTER trigger on bugtag maintaining the bugs summaries in bugsummary.';
1198+
1199+
1200+CREATE FUNCTION bugtask_maintain_bug_summary() RETURNS trigger
1201+ LANGUAGE plpgsql SECURITY DEFINER
1202+ SET search_path TO public
1203+ AS $$
1204+BEGIN
1205+ -- This trigger only works if we are inserting, updating or deleting
1206+ -- a single row per statement.
1207+
1208+ -- Unlike bug_maintain_bug_summary, this trigger does not have access
1209+ -- to the old bug when invoked as an AFTER trigger. To work around this
1210+ -- we install this trigger as both a BEFORE and an AFTER trigger.
1211+ IF TG_OP = 'INSERT' THEN
1212+ IF TG_WHEN = 'BEFORE' THEN
1213+ PERFORM unsummarise_bug(bug_row(NEW.bug));
1214+ ELSE
1215+ PERFORM summarise_bug(bug_row(NEW.bug));
1216+ END IF;
1217+ PERFORM bug_summary_flush_temp_journal();
1218+ RETURN NEW;
1219+
1220+ ELSIF TG_OP = 'DELETE' THEN
1221+ IF TG_WHEN = 'BEFORE' THEN
1222+ PERFORM unsummarise_bug(bug_row(OLD.bug));
1223+ ELSE
1224+ PERFORM summarise_bug(bug_row(OLD.bug));
1225+ END IF;
1226+ PERFORM bug_summary_flush_temp_journal();
1227+ RETURN OLD;
1228+
1229+ ELSE
1230+ IF (OLD.product IS DISTINCT FROM NEW.product
1231+ OR OLD.productseries IS DISTINCT FROM NEW.productseries
1232+ OR OLD.distribution IS DISTINCT FROM NEW.distribution
1233+ OR OLD.distroseries IS DISTINCT FROM NEW.distroseries
1234+ OR OLD.sourcepackagename IS DISTINCT FROM NEW.sourcepackagename
1235+ OR OLD.status IS DISTINCT FROM NEW.status
1236+ OR OLD.importance IS DISTINCT FROM NEW.importance
1237+ OR OLD.bugwatch IS DISTINCT FROM NEW.bugwatch
1238+ OR OLD.milestone IS DISTINCT FROM NEW.milestone) THEN
1239+
1240+ IF TG_WHEN = 'BEFORE' THEN
1241+ PERFORM unsummarise_bug(bug_row(OLD.bug));
1242+ IF OLD.bug <> NEW.bug THEN
1243+ PERFORM unsummarise_bug(bug_row(NEW.bug));
1244+ END IF;
1245+ ELSE
1246+ PERFORM summarise_bug(bug_row(OLD.bug));
1247+ IF OLD.bug <> NEW.bug THEN
1248+ PERFORM summarise_bug(bug_row(NEW.bug));
1249+ END IF;
1250+ END IF;
1251+ END IF;
1252+ PERFORM bug_summary_flush_temp_journal();
1253+ RETURN NEW;
1254+ END IF;
1255+END;
1256+$$;
1257+
1258+
1259+COMMENT ON FUNCTION bugtask_maintain_bug_summary() IS 'Both BEFORE & AFTER trigger on bugtask maintaining the bugs summaries in bugsummary.';
1260+
1261+
1262+CREATE FUNCTION calculate_bug_heat(bug_id integer) RETURNS integer
1263+ LANGUAGE plpythonu STABLE STRICT
1264+ AS $$
1265+ from datetime import datetime
1266+
1267+ class BugHeatConstants:
1268+ PRIVACY = 150
1269+ SECURITY = 250
1270+ DUPLICATE = 6
1271+ AFFECTED_USER = 4
1272+ SUBSCRIBER = 2
1273+
1274+ def get_max_heat_for_bug(bug_id):
1275+ results = plpy.execute("""
1276+ SELECT MAX(
1277+ GREATEST(Product.max_bug_heat,
1278+ DistributionSourcePackage.max_bug_heat))
1279+ AS max_heat
1280+ FROM BugTask
1281+ LEFT OUTER JOIN ProductSeries ON
1282+ BugTask.productseries = ProductSeries.id
1283+ LEFT OUTER JOIN Product ON (
1284+ BugTask.product = Product.id
1285+ OR ProductSeries.product = Product.id)
1286+ LEFT OUTER JOIN DistroSeries ON
1287+ BugTask.distroseries = DistroSeries.id
1288+ LEFT OUTER JOIN Distribution ON (
1289+ BugTask.distribution = Distribution.id
1290+ OR DistroSeries.distribution = Distribution.id)
1291+ LEFT OUTER JOIN DistributionSourcePackage ON (
1292+ BugTask.sourcepackagename =
1293+ DistributionSourcePackage.sourcepackagename)
1294+ WHERE
1295+ BugTask.bug = %s""" % bug_id)
1296+
1297+ return results[0]['max_heat']
1298+
1299+ # It would be nice to be able to just SELECT * here, but we need the
1300+ # timestamps to be in a format that datetime.fromtimestamp() will
1301+ # understand.
1302+ bug_data = plpy.execute("""
1303+ SELECT
1304+ duplicateof,
1305+ private,
1306+ security_related,
1307+ number_of_duplicates,
1308+ users_affected_count,
1309+ EXTRACT(epoch from datecreated)
1310+ AS timestamp_date_created,
1311+ EXTRACT(epoch from date_last_updated)
1312+ AS timestamp_date_last_updated,
1313+ EXTRACT(epoch from date_last_message)
1314+ AS timestamp_date_last_message
1315+ FROM Bug WHERE id = %s""" % bug_id)
1316+
1317+ if bug_data.nrows() == 0:
1318+ raise Exception("Bug %s doesn't exist." % bug_id)
1319+
1320+ bug = bug_data[0]
1321+ if bug['duplicateof'] is not None:
1322+ return None
1323+
1324+ heat = {}
1325+ heat['dupes'] = (
1326+ BugHeatConstants.DUPLICATE * bug['number_of_duplicates'])
1327+ heat['affected_users'] = (
1328+ BugHeatConstants.AFFECTED_USER *
1329+ bug['users_affected_count'])
1330+
1331+ if bug['private']:
1332+ heat['privacy'] = BugHeatConstants.PRIVACY
1333+ if bug['security_related']:
1334+ heat['security'] = BugHeatConstants.SECURITY
1335+
1336+ # Get the heat from subscribers, both direct and via duplicates.
1337+ subs_from_dupes = plpy.execute("""
1338+ SELECT COUNT(DISTINCT BugSubscription.person) AS sub_count
1339+ FROM BugSubscription, Bug
1340+ WHERE Bug.id = BugSubscription.bug
1341+ AND (Bug.id = %s OR Bug.duplicateof = %s)"""
1342+ % (bug_id, bug_id))
1343+
1344+ heat['subcribers'] = (
1345+ BugHeatConstants.SUBSCRIBER
1346+ * subs_from_dupes[0]['sub_count'])
1347+
1348+ total_heat = sum(heat.values())
1349+
1350+ # Bugs decay over time. Every day the bug isn't touched its heat
1351+ # decreases by 1%.
1352+ date_last_updated = datetime.fromtimestamp(
1353+ bug['timestamp_date_last_updated'])
1354+ days_since_last_update = (datetime.utcnow() - date_last_updated).days
1355+ total_heat = int(total_heat * (0.99 ** days_since_last_update))
1356+
1357+ if days_since_last_update > 0:
1358+ # Bug heat increases by a quarter of the maximum bug heat
1359+ # divided by the number of days since the bug's creation date.
1360+ date_created = datetime.fromtimestamp(
1361+ bug['timestamp_date_created'])
1362+
1363+ if bug['timestamp_date_last_message'] is not None:
1364+ date_last_message = datetime.fromtimestamp(
1365+ bug['timestamp_date_last_message'])
1366+ oldest_date = max(date_last_updated, date_last_message)
1367+ else:
1368+ date_last_message = None
1369+ oldest_date = date_last_updated
1370+
1371+ days_since_last_activity = (datetime.utcnow() - oldest_date).days
1372+ days_since_created = (datetime.utcnow() - date_created).days
1373+ max_heat = get_max_heat_for_bug(bug_id)
1374+ if max_heat is not None and days_since_created > 0:
1375+ total_heat = (
1376+ total_heat + (max_heat * 0.25 / days_since_created))
1377+
1378+ return int(total_heat)
1379+$$;
1380+
1381+
1382+CREATE FUNCTION cursor_fetch(cur refcursor, n integer) RETURNS SETOF record
1383+ LANGUAGE plpgsql
1384+ AS $$
1385+DECLARE
1386+ r record;
1387+ count integer;
1388+BEGIN
1389+ FOR count IN 1..n LOOP
1390+ FETCH FORWARD FROM cur INTO r;
1391+ IF NOT FOUND THEN
1392+ RETURN;
1393+ END IF;
1394+ RETURN NEXT r;
1395+ END LOOP;
1396+END;
1397+$$;
1398+
1399+
1400+COMMENT ON FUNCTION cursor_fetch(cur refcursor, n integer) IS 'Fetch the next n items from a cursor. Work around for not being able to use FETCH inside a SELECT statement.';
1401+
1402+
1403+CREATE FUNCTION debversion(character) RETURNS debversion
1404+ LANGUAGE internal IMMUTABLE STRICT
1405+ AS $$rtrim1$$;
1406+
1407+
1408+CREATE FUNCTION debversion_cmp(version1 debversion, version2 debversion) RETURNS integer
1409+ LANGUAGE c IMMUTABLE STRICT
1410+ AS '$libdir/debversion', 'debversion_cmp';
1411+
1412+
1413+COMMENT ON FUNCTION debversion_cmp(version1 debversion, version2 debversion) IS 'Compare Debian versions';
1414+
1415+
1416+CREATE FUNCTION debversion_eq(version1 debversion, version2 debversion) RETURNS boolean
1417+ LANGUAGE c IMMUTABLE STRICT
1418+ AS '$libdir/debversion', 'debversion_eq';
1419+
1420+
1421+COMMENT ON FUNCTION debversion_eq(version1 debversion, version2 debversion) IS 'debversion equal';
1422+
1423+
1424+CREATE FUNCTION debversion_ge(version1 debversion, version2 debversion) RETURNS boolean
1425+ LANGUAGE c IMMUTABLE STRICT
1426+ AS '$libdir/debversion', 'debversion_ge';
1427+
1428+
1429+COMMENT ON FUNCTION debversion_ge(version1 debversion, version2 debversion) IS 'debversion greater-than-or-equal';
1430+
1431+
1432+CREATE FUNCTION debversion_gt(version1 debversion, version2 debversion) RETURNS boolean
1433+ LANGUAGE c IMMUTABLE STRICT
1434+ AS '$libdir/debversion', 'debversion_gt';
1435+
1436+
1437+COMMENT ON FUNCTION debversion_gt(version1 debversion, version2 debversion) IS 'debversion greater-than';
1438+
1439+
1440+CREATE FUNCTION debversion_hash(debversion) RETURNS integer
1441+ LANGUAGE c IMMUTABLE STRICT
1442+ AS '$libdir/debversion', 'debversion_hash';
1443+
1444+
1445+CREATE FUNCTION debversion_larger(version1 debversion, version2 debversion) RETURNS debversion
1446+ LANGUAGE c IMMUTABLE STRICT
1447+ AS '$libdir/debversion', 'debversion_larger';
1448+
1449+
1450+CREATE FUNCTION debversion_le(version1 debversion, version2 debversion) RETURNS boolean
1451+ LANGUAGE c IMMUTABLE STRICT
1452+ AS '$libdir/debversion', 'debversion_le';
1453+
1454+
1455+COMMENT ON FUNCTION debversion_le(version1 debversion, version2 debversion) IS 'debversion less-than-or-equal';
1456+
1457+
1458+CREATE FUNCTION debversion_lt(version1 debversion, version2 debversion) RETURNS boolean
1459+ LANGUAGE c IMMUTABLE STRICT
1460+ AS '$libdir/debversion', 'debversion_lt';
1461+
1462+
1463+COMMENT ON FUNCTION debversion_lt(version1 debversion, version2 debversion) IS 'debversion less-than';
1464+
1465+
1466+CREATE FUNCTION debversion_ne(version1 debversion, version2 debversion) RETURNS boolean
1467+ LANGUAGE c IMMUTABLE STRICT
1468+ AS '$libdir/debversion', 'debversion_ne';
1469+
1470+
1471+COMMENT ON FUNCTION debversion_ne(version1 debversion, version2 debversion) IS 'debversion not equal';
1472+
1473+
1474+CREATE FUNCTION debversion_smaller(version1 debversion, version2 debversion) RETURNS debversion
1475+ LANGUAGE c IMMUTABLE STRICT
1476+ AS '$libdir/debversion', 'debversion_smaller';
1477+
1478+
1479+CREATE FUNCTION debversion_sort_key(version text) RETURNS text
1480+ LANGUAGE plpythonu IMMUTABLE STRICT
1481+ AS $_$
1482+ # If this method is altered, then any functional indexes using it
1483+ # need to be rebuilt.
1484+ import re
1485+
1486+ VERRE = re.compile("(?:([0-9]+):)?(.+?)(?:-([^-]+))?$")
1487+
1488+ MAP = "0123456789ABCDEFGHIJKLMNOPQRSTUV"
1489+
1490+ epoch, version, release = VERRE.match(args[0]).groups()
1491+ key = []
1492+ for part, part_weight in ((epoch, 3000), (version, 2000), (release, 1000)):
1493+ if not part:
1494+ continue
1495+ i = 0
1496+ l = len(part)
1497+ while i != l:
1498+ c = part[i]
1499+ if c.isdigit():
1500+ key.append(part_weight)
1501+ j = i
1502+ while i != l and part[i].isdigit(): i += 1
1503+ key.append(part_weight+int(part[j:i] or "0"))
1504+ elif c == "~":
1505+ key.append(0)
1506+ i += 1
1507+ elif c.isalpha():
1508+ key.append(part_weight+ord(c))
1509+ i += 1
1510+ else:
1511+ key.append(part_weight+256+ord(c))
1512+ i += 1
1513+ if not key or key[-1] != part_weight:
1514+ key.append(part_weight)
1515+ key.append(part_weight)
1516+ key.append(1)
1517+
1518+ # Encode our key and return it
1519+ #
1520+ result = []
1521+ for value in key:
1522+ if not value:
1523+ result.append("000")
1524+ else:
1525+ element = []
1526+ while value:
1527+ element.insert(0, MAP[value & 0x1F])
1528+ value >>= 5
1529+ element_len = len(element)
1530+ if element_len < 3:
1531+ element.insert(0, "0"*(3-element_len))
1532+ elif element_len == 3:
1533+ pass
1534+ elif element_len < 35:
1535+ element.insert(0, MAP[element_len-4])
1536+ element.insert(0, "X")
1537+ elif element_len < 1027:
1538+ element.insert(0, MAP[(element_len-4) & 0x1F])
1539+ element.insert(0, MAP[(element_len-4) & 0x3E0])
1540+ element.insert(0, "Y")
1541+ else:
1542+ raise ValueError("Number too large")
1543+ result.extend(element)
1544+ return "".join(result)
1545+$_$;
1546+
1547+
1548+COMMENT ON FUNCTION debversion_sort_key(version text) IS 'Return a string suitable for sorting debian version strings on';
1549+
1550+
1551+CREATE FUNCTION ensure_bugsummary_temp_journal() RETURNS void
1552+ LANGUAGE plpgsql
1553+ AS $$
1554+DECLARE
1555+BEGIN
1556+ CREATE TEMPORARY TABLE bugsummary_temp_journal (
1557+ LIKE bugsummary ) ON COMMIT DROP;
1558+ ALTER TABLE bugsummary_temp_journal ALTER COLUMN id DROP NOT NULL;
1559+EXCEPTION
1560+ WHEN duplicate_table THEN
1561+ NULL;
1562+END;
1563+$$;
1564+
1565+
1566+COMMENT ON FUNCTION ensure_bugsummary_temp_journal() IS 'Create a temporary table bugsummary_temp_journal if it does not exist.';
1567+
1568+
1569+CREATE FUNCTION generate_openid_identifier() RETURNS text
1570+ LANGUAGE plpythonu
1571+ AS $$
1572+ from random import choice
1573+
1574+ # Non display confusing characters.
1575+ chars = '34678bcdefhkmnprstwxyzABCDEFGHJKLMNPQRTWXY'
1576+
1577+ # Character length of tokens. Can be increased, decreased or even made
1578+ # random - Launchpad does not care. 7 means it takes 40 bytes to store
1579+ # a null-terminated Launchpad identity URL on the current domain name.
1580+ length=7
1581+
1582+ loop_count = 0
1583+ while loop_count < 20000:
1584+ # Generate a random openid_identifier
1585+ oid = ''.join(choice(chars) for count in range(length))
1586+
1587+ # Check if the oid is already in the db, although this is pretty
1588+ # unlikely
1589+ rv = plpy.execute("""
1590+ SELECT COUNT(*) AS num FROM Account WHERE openid_identifier = '%s'
1591+ """ % oid, 1)
1592+ if rv[0]['num'] == 0:
1593+ return oid
1594+ loop_count += 1
1595+ if loop_count == 1:
1596+ plpy.warning(
1597+ 'Clash generating unique openid_identifier. '
1598+ 'Increase length if you see this warning too much.')
1599+ plpy.error(
1600+ "Unable to generate unique openid_identifier. "
1601+ "Need to increase length of tokens.")
1602+$$;
1603+
1604+
1605+CREATE FUNCTION getlocalnodeid() RETURNS integer
1606+ LANGUAGE plpgsql STABLE SECURITY DEFINER
1607+ SET search_path TO public
1608+ AS $$
1609+ DECLARE
1610+ v_node_id integer;
1611+ BEGIN
1612+ SELECT INTO v_node_id _sl.getlocalnodeid('_sl');
1613+ RETURN v_node_id;
1614+ EXCEPTION
1615+ WHEN invalid_schema_name THEN
1616+ RETURN NULL;
1617+ END;
1618+$$;
1619+
1620+
1621+COMMENT ON FUNCTION getlocalnodeid() IS 'Return the replication node id for this node, or NULL if not a replicated installation.';
1622+
1623+
1624+CREATE FUNCTION is_blacklisted_name(text, integer) RETURNS boolean
1625+ LANGUAGE sql STABLE STRICT SECURITY DEFINER
1626+ SET search_path TO public
1627+ AS $_$
1628+ SELECT COALESCE(name_blacklist_match($1, $2)::boolean, FALSE);
1629+$_$;
1630+
1631+
1632+COMMENT ON FUNCTION is_blacklisted_name(text, integer) IS 'Return TRUE if any regular expressions stored in the NameBlacklist table match the givenname, otherwise return FALSE.';
1633+
1634+
1635+CREATE FUNCTION is_person(text) RETURNS boolean
1636+ LANGUAGE sql STABLE STRICT
1637+ AS $_$
1638+ SELECT count(*)>0 FROM Person WHERE name=$1 AND teamowner IS NULL;
1639+$_$;
1640+
1641+
1642+COMMENT ON FUNCTION is_person(text) IS 'True if the given name identifies a person in the Person table';
1643+
1644+
1645+CREATE FUNCTION is_printable_ascii(text) RETURNS boolean
1646+ LANGUAGE plpythonu IMMUTABLE STRICT
1647+ AS $_$
1648+ import re, string
1649+ try:
1650+ text = args[0].decode("ASCII")
1651+ except UnicodeError:
1652+ return False
1653+ if re.search(r"^[%s]*$" % re.escape(string.printable), text) is None:
1654+ return False
1655+ return True
1656+$_$;
1657+
1658+
1659+COMMENT ON FUNCTION is_printable_ascii(text) IS 'True if the string is pure printable US-ASCII';
1660+
1661+
1662+CREATE FUNCTION is_team(integer) RETURNS boolean
1663+ LANGUAGE sql STABLE STRICT
1664+ AS $_$
1665+ SELECT count(*)>0 FROM Person WHERE id=$1 AND teamowner IS NOT NULL;
1666+$_$;
1667+
1668+
1669+COMMENT ON FUNCTION is_team(integer) IS 'True if the given id identifies a team in the Person table';
1670+
1671+
1672+CREATE FUNCTION is_team(text) RETURNS boolean
1673+ LANGUAGE sql STABLE STRICT
1674+ AS $_$
1675+ SELECT count(*)>0 FROM Person WHERE name=$1 AND teamowner IS NOT NULL;
1676+$_$;
1677+
1678+
1679+COMMENT ON FUNCTION is_team(text) IS 'True if the given name identifies a team in the Person table';
1680+
1681+
1682+CREATE FUNCTION lp_mirror_account_ins() RETURNS trigger
1683+ LANGUAGE plpgsql SECURITY DEFINER
1684+ SET search_path TO public
1685+ AS $$
1686+BEGIN
1687+ INSERT INTO lp_Account (id, openid_identifier)
1688+ VALUES (NEW.id, NEW.openid_identifier);
1689+ RETURN NULL; -- Ignored for AFTER triggers.
1690+END;
1691+$$;
1692+
1693+
1694+CREATE FUNCTION lp_mirror_account_upd() RETURNS trigger
1695+ LANGUAGE plpgsql SECURITY DEFINER
1696+ SET search_path TO public
1697+ AS $$
1698+BEGIN
1699+ IF OLD.id <> NEW.id OR OLD.openid_identifier <> NEW.openid_identifier THEN
1700+ UPDATE lp_Account
1701+ SET id = NEW.id, openid_identifier = NEW.openid_identifier
1702+ WHERE id = OLD.id;
1703+ END IF;
1704+ RETURN NULL; -- Ignored for AFTER triggers.
1705+END;
1706+$$;
1707+
1708+
1709+CREATE FUNCTION lp_mirror_del() RETURNS trigger
1710+ LANGUAGE plpgsql SECURITY DEFINER
1711+ SET search_path TO public
1712+ AS $$
1713+BEGIN
1714+ EXECUTE 'DELETE FROM lp_' || TG_TABLE_NAME || ' WHERE id=' || OLD.id;
1715+ RETURN NULL; -- Ignored for AFTER triggers.
1716+END;
1717+$$;
1718+
1719+
1720+CREATE FUNCTION lp_mirror_openididentifier_del() RETURNS trigger
1721+ LANGUAGE plpgsql SECURITY DEFINER
1722+ SET search_path TO public
1723+ AS $$
1724+DECLARE
1725+ next_identifier text;
1726+BEGIN
1727+ SELECT INTO next_identifier identifier FROM OpenIdIdentifier
1728+ WHERE account = OLD.account AND identifier <> OLD.identifier
1729+ ORDER BY date_created DESC LIMIT 1;
1730+
1731+ IF next_identifier IS NOT NULL THEN
1732+ UPDATE lp_account SET openid_identifier = next_identifier
1733+ WHERE openid_identifier = OLD.identifier;
1734+ ELSE
1735+ DELETE FROM lp_account WHERE openid_identifier = OLD.identifier;
1736+ END IF;
1737+
1738+ DELETE FROM lp_OpenIdIdentifier WHERE identifier = OLD.identifier;
1739+
1740+ RETURN NULL; -- Ignored for AFTER triggers.
1741+END;
1742+$$;
1743+
1744+
1745+CREATE FUNCTION lp_mirror_openididentifier_ins() RETURNS trigger
1746+ LANGUAGE plpgsql SECURITY DEFINER
1747+ SET search_path TO public
1748+ AS $$
1749+BEGIN
1750+ -- Support obsolete lp_Account.openid_identifier as best we can
1751+ -- until ISD migrates to using lp_OpenIdIdentifier.
1752+ UPDATE lp_account SET openid_identifier = NEW.identifier
1753+ WHERE id = NEW.account;
1754+ IF NOT found THEN
1755+ INSERT INTO lp_account (id, openid_identifier)
1756+ VALUES (NEW.account, NEW.identifier);
1757+ END IF;
1758+
1759+ INSERT INTO lp_OpenIdIdentifier (identifier, account, date_created)
1760+ VALUES (NEW.identifier, NEW.account, NEW.date_created);
1761+
1762+ RETURN NULL; -- Ignored for AFTER triggers.
1763+END;
1764+$$;
1765+
1766+
1767+CREATE FUNCTION lp_mirror_openididentifier_upd() RETURNS trigger
1768+ LANGUAGE plpgsql SECURITY DEFINER
1769+ SET search_path TO public
1770+ AS $$
1771+BEGIN
1772+ IF OLD.identifier <> NEW.identifier THEN
1773+ UPDATE lp_Account SET openid_identifier = NEW.identifier
1774+ WHERE openid_identifier = OLD.identifier;
1775+ END IF;
1776+ UPDATE lp_OpenIdIdentifier
1777+ SET
1778+ identifier = NEW.identifier,
1779+ account = NEW.account,
1780+ date_created = NEW.date_created
1781+ WHERE identifier = OLD.identifier;
1782+ RETURN NULL; -- Ignored for AFTER triggers.
1783+END;
1784+$$;
1785+
1786+
1787+CREATE FUNCTION lp_mirror_person_ins() RETURNS trigger
1788+ LANGUAGE plpgsql SECURITY DEFINER
1789+ SET search_path TO public
1790+ AS $$
1791+BEGIN
1792+ INSERT INTO lp_Person (
1793+ id, displayname, teamowner, teamdescription, name, language, fti,
1794+ defaultmembershipperiod, defaultrenewalperiod, subscriptionpolicy,
1795+ merged, datecreated, homepage_content, icon, mugshot,
1796+ hide_email_addresses, creation_rationale, creation_comment,
1797+ registrant, logo, renewal_policy, personal_standing,
1798+ personal_standing_reason, mail_resumption_date,
1799+ mailing_list_auto_subscribe_policy, mailing_list_receive_duplicates,
1800+ visibility, verbose_bugnotifications, account)
1801+ VALUES (
1802+ NEW.id, NEW.displayname, NEW.teamowner, NULL,
1803+ NEW.name, NEW.language, NEW.fti, NEW.defaultmembershipperiod,
1804+ NEW.defaultrenewalperiod, NEW.subscriptionpolicy,
1805+ NEW.merged, NEW.datecreated, NULL, NEW.icon,
1806+ NEW.mugshot, NEW.hide_email_addresses, NEW.creation_rationale,
1807+ NEW.creation_comment, NEW.registrant, NEW.logo, NEW.renewal_policy,
1808+ NEW.personal_standing, NEW.personal_standing_reason,
1809+ NEW.mail_resumption_date, NEW.mailing_list_auto_subscribe_policy,
1810+ NEW.mailing_list_receive_duplicates, NEW.visibility,
1811+ NEW.verbose_bugnotifications, NEW.account);
1812+ RETURN NULL; -- Ignored for AFTER triggers.
1813+END;
1814+$$;
1815+
1816+
1817+CREATE FUNCTION lp_mirror_person_upd() RETURNS trigger
1818+ LANGUAGE plpgsql SECURITY DEFINER
1819+ SET search_path TO public
1820+ AS $$
1821+BEGIN
1822+ UPDATE lp_Person
1823+ SET id = NEW.id,
1824+ displayname = NEW.displayname,
1825+ teamowner = NEW.teamowner,
1826+ teamdescription = NULL,
1827+ name = NEW.name,
1828+ language = NEW.language,
1829+ fti = NEW.fti,
1830+ defaultmembershipperiod = NEW.defaultmembershipperiod,
1831+ defaultrenewalperiod = NEW.defaultrenewalperiod,
1832+ subscriptionpolicy = NEW.subscriptionpolicy,
1833+ merged = NEW.merged,
1834+ datecreated = NEW.datecreated,
1835+ homepage_content = NULL,
1836+ icon = NEW.icon,
1837+ mugshot = NEW.mugshot,
1838+ hide_email_addresses = NEW.hide_email_addresses,
1839+ creation_rationale = NEW.creation_rationale,
1840+ creation_comment = NEW.creation_comment,
1841+ registrant = NEW.registrant,
1842+ logo = NEW.logo,
1843+ renewal_policy = NEW.renewal_policy,
1844+ personal_standing = NEW.personal_standing,
1845+ personal_standing_reason = NEW.personal_standing_reason,
1846+ mail_resumption_date = NEW.mail_resumption_date,
1847+ mailing_list_auto_subscribe_policy
1848+ = NEW.mailing_list_auto_subscribe_policy,
1849+ mailing_list_receive_duplicates = NEW.mailing_list_receive_duplicates,
1850+ visibility = NEW.visibility,
1851+ verbose_bugnotifications = NEW.verbose_bugnotifications,
1852+ account = NEW.account
1853+ WHERE id = OLD.id;
1854+ RETURN NULL; -- Ignored for AFTER triggers.
1855+END;
1856+$$;
1857+
1858+
1859+CREATE FUNCTION lp_mirror_personlocation_ins() RETURNS trigger
1860+ LANGUAGE plpgsql SECURITY DEFINER
1861+ SET search_path TO public
1862+ AS $$
1863+BEGIN
1864+ INSERT INTO lp_PersonLocation SELECT NEW.*;
1865+ RETURN NULL; -- Ignored for AFTER triggers.
1866+END;
1867+$$;
1868+
1869+
1870+CREATE FUNCTION lp_mirror_personlocation_upd() RETURNS trigger
1871+ LANGUAGE plpgsql SECURITY DEFINER
1872+ SET search_path TO public
1873+ AS $$
1874+BEGIN
1875+ UPDATE lp_PersonLocation
1876+ SET id = NEW.id,
1877+ date_created = NEW.date_created,
1878+ person = NEW.person,
1879+ latitude = NEW.latitude,
1880+ longitude = NEW.longitude,
1881+ time_zone = NEW.time_zone,
1882+ last_modified_by = NEW.last_modified_by,
1883+ date_last_modified = NEW.date_last_modified,
1884+ visible = NEW.visible,
1885+ locked = NEW.locked
1886+ WHERE id = OLD.id;
1887+ RETURN NULL; -- Ignored for AFTER triggers.
1888+END;
1889+$$;
1890+
1891+
1892+CREATE FUNCTION lp_mirror_teamparticipation_ins() RETURNS trigger
1893+ LANGUAGE plpgsql SECURITY DEFINER
1894+ SET search_path TO public
1895+ AS $$
1896+BEGIN
1897+ INSERT INTO lp_TeamParticipation SELECT NEW.*;
1898+ RETURN NULL; -- Ignored for AFTER triggers.
1899+END;
1900+$$;
1901+
1902+
1903+CREATE FUNCTION lp_mirror_teamparticipation_upd() RETURNS trigger
1904+ LANGUAGE plpgsql SECURITY DEFINER
1905+ SET search_path TO public
1906+ AS $$
1907+BEGIN
1908+ UPDATE lp_TeamParticipation
1909+ SET id = NEW.id,
1910+ team = NEW.team,
1911+ person = NEW.person
1912+ WHERE id = OLD.id;
1913+ RETURN NULL; -- Ignored for AFTER triggers.
1914+END;
1915+$$;
1916+
1917+
1918+CREATE FUNCTION maintain_transitively_private() RETURNS trigger
1919+ LANGUAGE plpgsql
1920+ AS $$
1921+BEGIN
1922+ IF TG_OP = 'UPDATE' THEN
1923+ IF (NEW.stacked_on IS NOT DISTINCT FROM OLD.stacked_on
1924+ AND NEW.private IS NOT DISTINCT FROM OLD.private) THEN
1925+ RETURN NULL;
1926+ END IF;
1927+ END IF;
1928+ PERFORM update_transitively_private(NEW.id);
1929+ RETURN NULL;
1930+END;
1931+$$;
1932+
1933+
1934+COMMENT ON FUNCTION maintain_transitively_private() IS 'Trigger maintaining the Branch transitively_private column';
1935+
1936+
1937+CREATE FUNCTION message_copy_owner_to_bugmessage() RETURNS trigger
1938+ LANGUAGE plpgsql SECURITY DEFINER
1939+ SET search_path TO public
1940+ AS $$
1941+BEGIN
1942+ IF NEW.owner != OLD.owner THEN
1943+ UPDATE BugMessage
1944+ SET owner = NEW.owner
1945+ WHERE
1946+ BugMessage.message = NEW.id;
1947+ END IF;
1948+ RETURN NULL; -- Ignored - this is an AFTER trigger
1949+END;
1950+$$;
1951+
1952+
1953+COMMENT ON FUNCTION message_copy_owner_to_bugmessage() IS 'Copies the message owner into bugmessage when message changes.';
1954+
1955+
1956+CREATE FUNCTION message_copy_owner_to_questionmessage() RETURNS trigger
1957+ LANGUAGE plpgsql SECURITY DEFINER
1958+ SET search_path TO public
1959+ AS $$
1960+BEGIN
1961+ IF NEW.owner != OLD.owner THEN
1962+ UPDATE QuestionMessage
1963+ SET owner = NEW.owner
1964+ WHERE
1965+ QuestionMessage.message = NEW.id;
1966+ END IF;
1967+ RETURN NULL; -- Ignored - this is an AFTER trigger
1968+END;
1969+$$;
1970+
1971+
1972+COMMENT ON FUNCTION message_copy_owner_to_questionmessage() IS 'Copies the message owner into questionmessage when message changes.';
1973+
1974+
1975+CREATE FUNCTION milestone_sort_key(dateexpected timestamp without time zone, name text) RETURNS text
1976+ LANGUAGE plpythonu IMMUTABLE
1977+ AS $$
1978+ # If this method is altered, then any functional indexes using it
1979+ # need to be rebuilt.
1980+ import re
1981+ import datetime
1982+
1983+ date_expected, name = args
1984+
1985+ def substitute_filled_numbers(match):
1986+ return match.group(0).zfill(5)
1987+
1988+ name = re.sub(u'\d+', substitute_filled_numbers, name)
1989+ if date_expected is None:
1990+ # NULL dates are considered to be in the future.
1991+ date_expected = datetime.datetime(datetime.MAXYEAR, 1, 1)
1992+ return '%s %s' % (date_expected, name)
1993+$$;
1994+
1995+
1996+COMMENT ON FUNCTION milestone_sort_key(dateexpected timestamp without time zone, name text) IS 'Sort by the Milestone dateexpected and name. If the dateexpected is NULL, then it is converted to a date far in the future, so it will be sorted as a milestone in the future.';
1997+
1998+
1999+CREATE FUNCTION mv_branch_distribution_update() RETURNS trigger
2000+ LANGUAGE plpgsql
2001+ AS $$
2002+BEGIN
2003+ IF OLD.id != NEW.id THEN
2004+ RAISE EXCEPTION 'Cannot change Distribution.id';
2005+ END IF;
2006+ IF OLD.name != NEW.name THEN
2007+ UPDATE Branch SET unique_name = NULL
2008+ FROM DistroSeries
2009+ WHERE Branch.distroseries = Distroseries.id
2010+ AND Distroseries.distribution = NEW.id;
2011+ END IF;
2012+ RETURN NULL;
2013+END;
2014+$$;
2015+
2016+
2017+COMMENT ON FUNCTION mv_branch_distribution_update() IS 'Maintain Branch name cache when Distribution is modified.';
2018+
2019+
2020+CREATE FUNCTION mv_branch_distroseries_update() RETURNS trigger
2021+ LANGUAGE plpgsql
2022+ AS $$
2023+BEGIN
2024+ IF OLD.id != NEW.id THEN
2025+ RAISE EXCEPTION 'Cannot change Distroseries.id';
2026+ END IF;
2027+ IF OLD.name != NEW.name THEN
2028+ UPDATE Branch SET unique_name = NULL
2029+ WHERE Branch.distroseries = NEW.id;
2030+ END IF;
2031+ RETURN NULL;
2032+END;
2033+$$;
2034+
2035+
2036+COMMENT ON FUNCTION mv_branch_distroseries_update() IS 'Maintain Branch name cache when Distroseries is modified.';
2037+
2038+
2039+CREATE FUNCTION mv_branch_person_update() RETURNS trigger
2040+ LANGUAGE plpgsql
2041+ AS $$
2042+DECLARE
2043+ v_branch RECORD;
2044+BEGIN
2045+ IF OLD.id != NEW.id THEN
2046+ RAISE EXCEPTION 'Cannot change Person.id';
2047+ END IF;
2048+ IF OLD.name != NEW.name THEN
2049+ UPDATE Branch SET owner_name = NEW.name WHERE owner = NEW.id;
2050+ END IF;
2051+ RETURN NULL;
2052+END;
2053+$$;
2054+
2055+
2056+COMMENT ON FUNCTION mv_branch_person_update() IS 'Maintain Branch name cache when Person is modified.';
2057+
2058+
2059+CREATE FUNCTION mv_branch_product_update() RETURNS trigger
2060+ LANGUAGE plpgsql
2061+ AS $$
2062+DECLARE
2063+ v_branch RECORD;
2064+BEGIN
2065+ IF OLD.id != NEW.id THEN
2066+ RAISE EXCEPTION 'Cannot change Product.id';
2067+ END IF;
2068+ IF OLD.name != NEW.name THEN
2069+ UPDATE Branch SET target_suffix = NEW.name WHERE product=NEW.id;
2070+ END IF;
2071+ RETURN NULL;
2072+END;
2073+$$;
2074+
2075+
2076+COMMENT ON FUNCTION mv_branch_product_update() IS 'Maintain Branch name cache when Product is modified.';
2077+
2078+
2079+CREATE FUNCTION mv_pillarname_distribution() RETURNS trigger
2080+ LANGUAGE plpgsql SECURITY DEFINER
2081+ SET search_path TO public
2082+ AS $$
2083+BEGIN
2084+ IF TG_OP = 'INSERT' THEN
2085+ INSERT INTO PillarName (name, distribution)
2086+ VALUES (NEW.name, NEW.id);
2087+ ELSIF NEW.name != OLD.name THEN
2088+ UPDATE PillarName SET name=NEW.name WHERE distribution=NEW.id;
2089+ END IF;
2090+ RETURN NULL; -- Ignored - this is an AFTER trigger
2091+END;
2092+$$;
2093+
2094+
2095+COMMENT ON FUNCTION mv_pillarname_distribution() IS 'Trigger maintaining the PillarName table';
2096+
2097+
2098+CREATE FUNCTION mv_pillarname_product() RETURNS trigger
2099+ LANGUAGE plpgsql SECURITY DEFINER
2100+ SET search_path TO public
2101+ AS $$
2102+BEGIN
2103+ IF TG_OP = 'INSERT' THEN
2104+ INSERT INTO PillarName (name, product, active)
2105+ VALUES (NEW.name, NEW.id, NEW.active);
2106+ ELSIF NEW.name != OLD.name OR NEW.active != OLD.active THEN
2107+ UPDATE PillarName SET name=NEW.name, active=NEW.active
2108+ WHERE product=NEW.id;
2109+ END IF;
2110+ RETURN NULL; -- Ignored - this is an AFTER trigger
2111+END;
2112+$$;
2113+
2114+
2115+COMMENT ON FUNCTION mv_pillarname_product() IS 'Trigger maintaining the PillarName table';
2116+
2117+
2118+CREATE FUNCTION mv_pillarname_project() RETURNS trigger
2119+ LANGUAGE plpgsql SECURITY DEFINER
2120+ SET search_path TO public
2121+ AS $$
2122+BEGIN
2123+ IF TG_OP = 'INSERT' THEN
2124+ INSERT INTO PillarName (name, project, active)
2125+ VALUES (NEW.name, NEW.id, NEW.active);
2126+ ELSIF NEW.name != OLD.name or NEW.active != OLD.active THEN
2127+ UPDATE PillarName SET name=NEW.name, active=NEW.active
2128+ WHERE project=NEW.id;
2129+ END IF;
2130+ RETURN NULL; -- Ignored - this is an AFTER trigger
2131+END;
2132+$$;
2133+
2134+
2135+COMMENT ON FUNCTION mv_pillarname_project() IS 'Trigger maintaining the PillarName table';
2136+
2137+
2138+CREATE FUNCTION mv_pofiletranslator_pomsgset() RETURNS trigger
2139+ LANGUAGE plpgsql
2140+ AS $$
2141+BEGIN
2142+ IF TG_OP = 'DELETE' THEN
2143+ RAISE EXCEPTION
2144+ 'Deletions from POMsgSet not supported by the POFileTranslator materialized view';
2145+ ELSIF TG_OP = 'UPDATE' THEN
2146+ IF OLD.pofile != NEW.pofile THEN
2147+ RAISE EXCEPTION
2148+ 'Changing POMsgSet.pofile not supported by the POFileTranslator materialized view';
2149+ END IF;
2150+ END IF;
2151+ RETURN NEW;
2152+END;
2153+$$;
2154+
2155+
2156+COMMENT ON FUNCTION mv_pofiletranslator_pomsgset() IS 'Trigger enforing no POMsgSet deletions or POMsgSet.pofile changes';
2157+
2158+
2159+CREATE FUNCTION mv_pofiletranslator_posubmission() RETURNS trigger
2160+ LANGUAGE plpgsql SECURITY DEFINER
2161+ AS $$
2162+DECLARE
2163+ v_pofile INTEGER;
2164+ v_trash_old BOOLEAN;
2165+BEGIN
2166+ -- If we are deleting a row, we need to remove the existing
2167+ -- POFileTranslator row and reinsert the historical data if it exists.
2168+ -- We also treat UPDATEs that change the key (person, pofile) the same
2169+ -- as deletes. UPDATEs that don't change these columns are treated like
2170+ -- INSERTs below.
2171+ IF TG_OP = 'INSERT' THEN
2172+ v_trash_old := FALSE;
2173+ ELSIF TG_OP = 'DELETE' THEN
2174+ v_trash_old := TRUE;
2175+ ELSE -- UPDATE
2176+ v_trash_old = (
2177+ OLD.person != NEW.person OR OLD.pomsgset != NEW.pomsgset
2178+ );
2179+ END IF;
2180+
2181+ IF v_trash_old THEN
2182+
2183+ -- Delete the old record.
2184+ DELETE FROM POFileTranslator USING POMsgSet
2185+ WHERE POFileTranslator.pofile = POMsgSet.pofile
2186+ AND POFileTranslator.person = OLD.person
2187+ AND POMsgSet.id = OLD.pomsgset;
2188+
2189+ -- Insert a past record if there is one.
2190+ INSERT INTO POFileTranslator (
2191+ person, pofile, latest_posubmission, date_last_touched
2192+ )
2193+ SELECT DISTINCT ON (POSubmission.person, POMsgSet.pofile)
2194+ POSubmission.person, POMsgSet.pofile,
2195+ POSubmission.id, POSubmission.datecreated
2196+ FROM POSubmission, POMsgSet
2197+ WHERE POSubmission.pomsgset = POMsgSet.id
2198+ AND POSubmission.pomsgset = OLD.pomsgset
2199+ AND POSubmission.person = OLD.person
2200+ ORDER BY
2201+ POSubmission.person, POMsgSet.pofile,
2202+ POSubmission.datecreated DESC, POSubmission.id DESC;
2203+
2204+ -- No NEW with DELETE, so we can short circuit and leave.
2205+ IF TG_OP = 'DELETE' THEN
2206+ RETURN NULL; -- Ignored because this is an AFTER trigger
2207+ END IF;
2208+ END IF;
2209+
2210+ -- Get our new pofile id
2211+ SELECT INTO v_pofile POMsgSet.pofile FROM POMsgSet
2212+ WHERE POMsgSet.id = NEW.pomsgset;
2213+
2214+ -- Standard 'upsert' loop to avoid race conditions.
2215+ LOOP
2216+ UPDATE POFileTranslator
2217+ SET
2218+ date_last_touched = CURRENT_TIMESTAMP AT TIME ZONE 'UTC',
2219+ latest_posubmission = NEW.id
2220+ WHERE
2221+ person = NEW.person
2222+ AND pofile = v_pofile;
2223+ IF found THEN
2224+ RETURN NULL; -- Return value ignored as this is an AFTER trigger
2225+ END IF;
2226+
2227+ BEGIN
2228+ INSERT INTO POFileTranslator (person, pofile, latest_posubmission)
2229+ VALUES (NEW.person, v_pofile, NEW.id);
2230+ RETURN NULL; -- Return value ignored as this is an AFTER trigger
2231+ EXCEPTION WHEN unique_violation THEN
2232+ -- do nothing
2233+ END;
2234+ END LOOP;
2235+END;
2236+$$;
2237+
2238+
2239+COMMENT ON FUNCTION mv_pofiletranslator_posubmission() IS 'Trigger maintaining the POFileTranslator table';
2240+
2241+
2242+CREATE FUNCTION mv_pofiletranslator_translationmessage() RETURNS trigger
2243+ LANGUAGE plpgsql SECURITY DEFINER
2244+ SET search_path TO public
2245+ AS $$
2246+DECLARE
2247+ v_trash_old BOOLEAN;
2248+BEGIN
2249+ -- If we are deleting a row, we need to remove the existing
2250+ -- POFileTranslator row and reinsert the historical data if it exists.
2251+ -- We also treat UPDATEs that change the key (submitter) the same
2252+ -- as deletes. UPDATEs that don't change these columns are treated like
2253+ -- INSERTs below.
2254+ IF TG_OP = 'INSERT' THEN
2255+ v_trash_old := FALSE;
2256+ ELSIF TG_OP = 'DELETE' THEN
2257+ v_trash_old := TRUE;
2258+ ELSE -- UPDATE
2259+ v_trash_old = (
2260+ OLD.submitter != NEW.submitter
2261+ );
2262+ END IF;
2263+
2264+ IF v_trash_old THEN
2265+ -- Was this somebody's most-recently-changed message?
2266+ -- If so, delete the entry for that change.
2267+ DELETE FROM POFileTranslator
2268+ WHERE latest_message = OLD.id;
2269+ IF FOUND THEN
2270+ -- We deleted the entry for somebody's latest contribution.
2271+ -- Find that person's latest remaining contribution and
2272+ -- create a new record for that.
2273+ INSERT INTO POFileTranslator (
2274+ person, pofile, latest_message, date_last_touched
2275+ )
2276+ SELECT DISTINCT ON (person, pofile.id)
2277+ new_latest_message.submitter AS person,
2278+ pofile.id,
2279+ new_latest_message.id,
2280+ greatest(new_latest_message.date_created,
2281+ new_latest_message.date_reviewed)
2282+ FROM POFile
2283+ JOIN TranslationTemplateItem AS old_template_item
2284+ ON OLD.potmsgset = old_template_item.potmsgset AND
2285+ old_template_item.potemplate = pofile.potemplate AND
2286+ pofile.language = OLD.language
2287+ JOIN TranslationTemplateItem AS new_template_item
2288+ ON (old_template_item.potemplate =
2289+ new_template_item.potemplate)
2290+ JOIN TranslationMessage AS new_latest_message
2291+ ON new_latest_message.potmsgset =
2292+ new_template_item.potmsgset AND
2293+ new_latest_message.language = OLD.language
2294+ LEFT OUTER JOIN POfileTranslator AS ExistingEntry
2295+ ON ExistingEntry.person = OLD.submitter AND
2296+ ExistingEntry.pofile = POFile.id
2297+ WHERE
2298+ new_latest_message.submitter = OLD.submitter AND
2299+ ExistingEntry IS NULL
2300+ ORDER BY new_latest_message.submitter, pofile.id,
2301+ new_latest_message.date_created DESC,
2302+ new_latest_message.id DESC;
2303+ END IF;
2304+
2305+ -- No NEW with DELETE, so we can short circuit and leave.
2306+ IF TG_OP = 'DELETE' THEN
2307+ RETURN NULL; -- Ignored because this is an AFTER trigger
2308+ END IF;
2309+ END IF;
2310+
2311+ -- Standard 'upsert' loop to avoid race conditions.
2312+ LOOP
2313+ UPDATE POFileTranslator
2314+ SET
2315+ date_last_touched = CURRENT_TIMESTAMP AT TIME ZONE 'UTC',
2316+ latest_message = NEW.id
2317+ FROM POFile, TranslationTemplateItem
2318+ WHERE person = NEW.submitter AND
2319+ TranslationTemplateItem.potmsgset=NEW.potmsgset AND
2320+ TranslationTemplateItem.potemplate=pofile.potemplate AND
2321+ pofile.language=NEW.language AND
2322+ POFileTranslator.pofile = pofile.id;
2323+ IF found THEN
2324+ RETURN NULL; -- Return value ignored as this is an AFTER trigger
2325+ END IF;
2326+
2327+ BEGIN
2328+ INSERT INTO POFileTranslator (person, pofile, latest_message)
2329+ SELECT DISTINCT ON (NEW.submitter, pofile.id)
2330+ NEW.submitter, pofile.id, NEW.id
2331+ FROM TranslationTemplateItem
2332+ JOIN POFile
2333+ ON pofile.language = NEW.language AND
2334+ pofile.potemplate = translationtemplateitem.potemplate
2335+ WHERE
2336+ TranslationTemplateItem.potmsgset = NEW.potmsgset;
2337+ RETURN NULL; -- Return value ignored as this is an AFTER trigger
2338+ EXCEPTION WHEN unique_violation THEN
2339+ -- do nothing
2340+ END;
2341+ END LOOP;
2342+END;
2343+$$;
2344+
2345+
2346+COMMENT ON FUNCTION mv_pofiletranslator_translationmessage() IS 'Trigger maintaining the POFileTranslator table';
2347+
2348+
2349+CREATE FUNCTION mv_validpersonorteamcache_emailaddress() RETURNS trigger
2350+ LANGUAGE plpythonu SECURITY DEFINER
2351+ AS $_$
2352+ # This trigger function keeps the ValidPersonOrTeamCache materialized
2353+ # view in sync when updates are made to the EmailAddress table.
2354+ # Note that if the corresponding person is a team, changes to this table
2355+ # have no effect.
2356+ PREF = 4 # Constant indicating preferred email address
2357+
2358+ if not SD.has_key("delete_plan"):
2359+ param_types = ["int4"]
2360+
2361+ SD["is_team"] = plpy.prepare("""
2362+ SELECT teamowner IS NOT NULL AS is_team FROM Person WHERE id = $1
2363+ """, param_types)
2364+
2365+ SD["delete_plan"] = plpy.prepare("""
2366+ DELETE FROM ValidPersonOrTeamCache WHERE id = $1
2367+ """, param_types)
2368+
2369+ SD["insert_plan"] = plpy.prepare("""
2370+ INSERT INTO ValidPersonOrTeamCache (id) VALUES ($1)
2371+ """, param_types)
2372+
2373+ SD["maybe_insert_plan"] = plpy.prepare("""
2374+ INSERT INTO ValidPersonOrTeamCache (id)
2375+ SELECT Person.id
2376+ FROM Person
2377+ JOIN EmailAddress ON Person.id = EmailAddress.person
2378+ LEFT OUTER JOIN ValidPersonOrTeamCache
2379+ ON Person.id = ValidPersonOrTeamCache.id
2380+ WHERE Person.id = $1
2381+ AND ValidPersonOrTeamCache.id IS NULL
2382+ AND status = %(PREF)d
2383+ AND merged IS NULL
2384+ -- AND password IS NOT NULL
2385+ """ % vars(), param_types)
2386+
2387+ def is_team(person_id):
2388+ """Return true if person_id corresponds to a team"""
2389+ if person_id is None:
2390+ return False
2391+ return plpy.execute(SD["is_team"], [person_id], 1)[0]["is_team"]
2392+
2393+ class NoneDict:
2394+ def __getitem__(self, key):
2395+ return None
2396+
2397+ old = TD["old"] or NoneDict()
2398+ new = TD["new"] or NoneDict()
2399+
2400+ #plpy.info("old.id == %s" % old["id"])
2401+ #plpy.info("old.person == %s" % old["person"])
2402+ #plpy.info("old.status == %s" % old["status"])
2403+ #plpy.info("new.id == %s" % new["id"])
2404+ #plpy.info("new.person == %s" % new["person"])
2405+ #plpy.info("new.status == %s" % new["status"])
2406+
2407+ # Short circuit if neither person nor status has changed
2408+ if old["person"] == new["person"] and old["status"] == new["status"]:
2409+ return
2410+
2411+ # Short circuit if we are not mucking around with preferred email
2412+ # addresses
2413+ if old["status"] != PREF and new["status"] != PREF:
2414+ return
2415+
2416+ # Note that we have a constraint ensuring that there is only one
2417+ # status == PREF email address per person at any point in time.
2418+ # This simplifies our logic, as we know that if old.status == PREF,
2419+ # old.person does not have any other preferred email addresses.
2420+ # Also if new.status == PREF, we know new.person previously did not
2421+ # have a preferred email address.
2422+
2423+ if old["person"] != new["person"]:
2424+ if old["status"] == PREF and not is_team(old["person"]):
2425+ # old.person is no longer valid, unless they are a team
2426+ plpy.execute(SD["delete_plan"], [old["person"]])
2427+ if new["status"] == PREF and not is_team(new["person"]):
2428+ # new["person"] is now valid, or unchanged if they are a team
2429+ plpy.execute(SD["insert_plan"], [new["person"]])
2430+
2431+ elif old["status"] == PREF and not is_team(old["person"]):
2432+ # No longer valid, or unchanged if they are a team
2433+ plpy.execute(SD["delete_plan"], [old["person"]])
2434+
2435+ elif new["status"] == PREF and not is_team(new["person"]):
2436+ # May now be valid, or unchanged if they are a team.
2437+ plpy.execute(SD["maybe_insert_plan"], [new["person"]])
2438+$_$;
2439+
2440+
2441+COMMENT ON FUNCTION mv_validpersonorteamcache_emailaddress() IS 'A trigger for maintaining the ValidPersonOrTeamCache eager materialized view when changes are made to the EmailAddress table';
2442+
2443+
2444+CREATE FUNCTION mv_validpersonorteamcache_person() RETURNS trigger
2445+ LANGUAGE plpythonu SECURITY DEFINER
2446+ AS $_$
2447+ # This trigger function could be simplified by simply issuing
2448+ # one DELETE followed by one INSERT statement. However, we want to minimize
2449+ # expensive writes so we use this more complex logic.
2450+ PREF = 4 # Constant indicating preferred email address
2451+
2452+ if not SD.has_key("delete_plan"):
2453+ param_types = ["int4"]
2454+
2455+ SD["delete_plan"] = plpy.prepare("""
2456+ DELETE FROM ValidPersonOrTeamCache WHERE id = $1
2457+ """, param_types)
2458+
2459+ SD["maybe_insert_plan"] = plpy.prepare("""
2460+ INSERT INTO ValidPersonOrTeamCache (id)
2461+ SELECT Person.id
2462+ FROM Person
2463+ LEFT OUTER JOIN EmailAddress
2464+ ON Person.id = EmailAddress.person AND status = %(PREF)d
2465+ LEFT OUTER JOIN ValidPersonOrTeamCache
2466+ ON Person.id = ValidPersonOrTeamCache.id
2467+ WHERE Person.id = $1
2468+ AND ValidPersonOrTeamCache.id IS NULL
2469+ AND merged IS NULL
2470+ AND (teamowner IS NOT NULL OR EmailAddress.id IS NOT NULL)
2471+ """ % vars(), param_types)
2472+
2473+ new = TD["new"]
2474+ old = TD["old"]
2475+
2476+ # We should always have new, as this is not a DELETE trigger
2477+ assert new is not None, 'New is None'
2478+
2479+ person_id = new["id"]
2480+ query_params = [person_id] # All the same
2481+
2482+ # Short circuit if this is a new person (not team), as it cannot
2483+ # be valid until a status == 4 EmailAddress entry has been created
2484+ # (unless it is a team, in which case it is valid on creation)
2485+ if old is None:
2486+ if new["teamowner"] is not None:
2487+ plpy.execute(SD["maybe_insert_plan"], query_params)
2488+ return
2489+
2490+ # Short circuit if there are no relevant changes
2491+ if (new["teamowner"] == old["teamowner"]
2492+ and new["merged"] == old["merged"]):
2493+ return
2494+
2495+ # This function is only dealing with updates to the Person table.
2496+ # This means we do not have to worry about EmailAddress changes here
2497+
2498+ if (new["merged"] is not None or new["teamowner"] is None):
2499+ plpy.execute(SD["delete_plan"], query_params)
2500+ else:
2501+ plpy.execute(SD["maybe_insert_plan"], query_params)
2502+$_$;
2503+
2504+
2505+COMMENT ON FUNCTION mv_validpersonorteamcache_person() IS 'A trigger for maintaining the ValidPersonOrTeamCache eager materialized view when changes are made to the Person table';
2506+
2507+
2508+CREATE FUNCTION name_blacklist_match(text, integer) RETURNS integer
2509+ LANGUAGE plpythonu STABLE STRICT SECURITY DEFINER
2510+ SET search_path TO public
2511+ AS $_$
2512+ import re
2513+ name = args[0].decode("UTF-8")
2514+ user_id = args[1]
2515+
2516+ # Initialize shared storage, shared between invocations.
2517+ if not SD.has_key("regexp_select_plan"):
2518+
2519+ # All the blacklist regexps except the ones we are an admin
2520+ # for. These we do not check since they are not blacklisted to us.
2521+ SD["regexp_select_plan"] = plpy.prepare("""
2522+ SELECT id, regexp FROM NameBlacklist
2523+ WHERE admin IS NULL OR admin NOT IN (
2524+ SELECT team FROM TeamParticipation
2525+ WHERE person = $1)
2526+ ORDER BY id
2527+ """, ["integer"])
2528+
2529+ # Storage for compiled regexps
2530+ SD["compiled"] = {}
2531+
2532+ # admins is a celebrity and its id is immutable.
2533+ admins_id = plpy.execute(
2534+ "SELECT id FROM Person WHERE name='admins'")[0]["id"]
2535+
2536+ SD["admin_select_plan"] = plpy.prepare("""
2537+ SELECT TRUE FROM TeamParticipation
2538+ WHERE
2539+ TeamParticipation.team = %d
2540+ AND TeamParticipation.person = $1
2541+ LIMIT 1
2542+ """ % admins_id, ["integer"])
2543+
2544+ # All the blacklist regexps except those that have an admin because
2545+ # members of ~admin can use any name that any other admin can use.
2546+ SD["admin_regexp_select_plan"] = plpy.prepare("""
2547+ SELECT id, regexp FROM NameBlacklist
2548+ WHERE admin IS NULL
2549+ ORDER BY id
2550+ """, ["integer"])
2551+
2552+
2553+ compiled = SD["compiled"]
2554+
2555+ # Names are never blacklisted for Lauchpad admins.
2556+ if user_id is not None and plpy.execute(
2557+ SD["admin_select_plan"], [user_id]).nrows() > 0:
2558+ blacklist_plan = "admin_regexp_select_plan"
2559+ else:
2560+ blacklist_plan = "regexp_select_plan"
2561+
2562+ for row in plpy.execute(SD[blacklist_plan], [user_id]):
2563+ regexp_id = row["id"]
2564+ regexp_txt = row["regexp"]
2565+ if (compiled.get(regexp_id) is None
2566+ or compiled[regexp_id][0] != regexp_txt):
2567+ regexp = re.compile(
2568+ regexp_txt, re.IGNORECASE | re.UNICODE | re.VERBOSE
2569+ )
2570+ compiled[regexp_id] = (regexp_txt, regexp)
2571+ else:
2572+ regexp = compiled[regexp_id][1]
2573+ if regexp.search(name) is not None:
2574+ return regexp_id
2575+ return None
2576+$_$;
2577+
2578+
2579+COMMENT ON FUNCTION name_blacklist_match(text, integer) IS 'Return the id of the row in the NameBlacklist table that matches the given name, or NULL if no regexps in the NameBlacklist table match.';
2580+
2581+
2582+CREATE FUNCTION null_count(p_values anyarray) RETURNS integer
2583+ LANGUAGE plpgsql IMMUTABLE STRICT
2584+ AS $$
2585+DECLARE
2586+ v_index integer;
2587+ v_null_count integer := 0;
2588+BEGIN
2589+ FOR v_index IN array_lower(p_values,1)..array_upper(p_values,1) LOOP
2590+ IF p_values[v_index] IS NULL THEN
2591+ v_null_count := v_null_count + 1;
2592+ END IF;
2593+ END LOOP;
2594+ RETURN v_null_count;
2595+END;
2596+$$;
2597+
2598+
2599+COMMENT ON FUNCTION null_count(p_values anyarray) IS 'Return the number of NULLs in the first row of the given array.';
2600+
2601+
2602+CREATE FUNCTION packageset_deleted_trig() RETURNS trigger
2603+ LANGUAGE plpgsql
2604+ AS $$
2605+BEGIN
2606+ DELETE FROM flatpackagesetinclusion
2607+ WHERE parent = OLD.id AND child = OLD.id;
2608+
2609+ -- A package set was deleted; it may have participated in package set
2610+ -- inclusion relations in a sub/superset role; delete all inclusion
2611+ -- relationships in which it participated.
2612+ DELETE FROM packagesetinclusion
2613+ WHERE parent = OLD.id OR child = OLD.id;
2614+ RETURN OLD;
2615+END;
2616+$$;
2617+
2618+
2619+COMMENT ON FUNCTION packageset_deleted_trig() IS 'Remove any DAG edges leading to/from the deleted package set.';
2620+
2621+
2622+CREATE FUNCTION packageset_inserted_trig() RETURNS trigger
2623+ LANGUAGE plpgsql
2624+ AS $$
2625+BEGIN
2626+ -- A new package set was inserted; make it a descendent of itself in
2627+ -- the flattened package set inclusion table in order to facilitate
2628+ -- querying.
2629+ INSERT INTO flatpackagesetinclusion(parent, child)
2630+ VALUES (NEW.id, NEW.id);
2631+ RETURN NULL;
2632+END;
2633+$$;
2634+
2635+
2636+COMMENT ON FUNCTION packageset_inserted_trig() IS 'Insert self-referencing DAG edge when a new package set is inserted.';
2637+
2638+
2639+CREATE FUNCTION packagesetinclusion_deleted_trig() RETURNS trigger
2640+ LANGUAGE plpgsql
2641+ AS $$
2642+BEGIN
2643+ -- A package set inclusion relationship was deleted i.e. a set M
2644+ -- ceases to include another set N as a subset.
2645+ -- For an explanation of the queries below please see page 5 of
2646+ -- "Maintaining Transitive Closure of Graphs in SQL"
2647+ -- http://www.comp.nus.edu.sg/~wongls/psZ/dlsw-ijit97-16.ps
2648+ CREATE TEMP TABLE tmp_fpsi_suspect(
2649+ parent integer NOT NULL,
2650+ child integer NOT NULL);
2651+ CREATE TEMP TABLE tmp_fpsi_trusted(
2652+ parent integer NOT NULL,
2653+ child integer NOT NULL);
2654+ CREATE TEMP TABLE tmp_fpsi_good(
2655+ parent integer NOT NULL,
2656+ child integer NOT NULL);
2657+
2658+ INSERT INTO tmp_fpsi_suspect (
2659+ SELECT X.parent, Y.child
2660+ FROM flatpackagesetinclusion X, flatpackagesetinclusion Y
2661+ WHERE X.child = OLD.parent AND Y.parent = OLD.child
2662+ UNION
2663+ SELECT X.parent, OLD.child FROM flatpackagesetinclusion X
2664+ WHERE X.child = OLD.parent
2665+ UNION
2666+ SELECT OLD.parent, X.child FROM flatpackagesetinclusion X
2667+ WHERE X.parent = OLD.child
2668+ UNION
2669+ SELECT OLD.parent, OLD.child
2670+ );
2671+
2672+ INSERT INTO tmp_fpsi_trusted (
2673+ SELECT parent, child FROM flatpackagesetinclusion
2674+ EXCEPT
2675+ SELECT parent, child FROM tmp_fpsi_suspect
2676+ UNION
2677+ SELECT parent, child FROM packagesetinclusion psi
2678+ WHERE psi.parent != OLD.parent AND psi.child != OLD.child
2679+ );
2680+
2681+ INSERT INTO tmp_fpsi_good (
2682+ SELECT parent, child FROM tmp_fpsi_trusted
2683+ UNION
2684+ SELECT T1.parent, T2.child
2685+ FROM tmp_fpsi_trusted T1, tmp_fpsi_trusted T2
2686+ WHERE T1.child = T2.parent
2687+ UNION
2688+ SELECT T1.parent, T3.child
2689+ FROM tmp_fpsi_trusted T1, tmp_fpsi_trusted T2, tmp_fpsi_trusted T3
2690+ WHERE T1.child = T2.parent AND T2.child = T3.parent
2691+ );
2692+
2693+ DELETE FROM flatpackagesetinclusion fpsi
2694+ WHERE NOT EXISTS (
2695+ SELECT * FROM tmp_fpsi_good T
2696+ WHERE T.parent = fpsi.parent AND T.child = fpsi.child);
2697+
2698+ DROP TABLE tmp_fpsi_good;
2699+ DROP TABLE tmp_fpsi_trusted;
2700+ DROP TABLE tmp_fpsi_suspect;
2701+
2702+ RETURN OLD;
2703+END;
2704+$$;
2705+
2706+
2707+COMMENT ON FUNCTION packagesetinclusion_deleted_trig() IS 'Maintain the transitive closure in the DAG when an edge leading to/from a package set is deleted.';
2708+
2709+
2710+CREATE FUNCTION packagesetinclusion_inserted_trig() RETURNS trigger
2711+ LANGUAGE plpgsql
2712+ AS $$
2713+BEGIN
2714+ DECLARE
2715+ parent_name text;
2716+ child_name text;
2717+ parent_distroseries text;
2718+ child_distroseries text;
2719+ BEGIN
2720+ -- Make sure that the package sets being associated here belong
2721+ -- to the same distro series.
2722+ IF (SELECT parent.distroseries != child.distroseries
2723+ FROM packageset parent, packageset child
2724+ WHERE parent.id = NEW.parent AND child.id = NEW.child)
2725+ THEN
2726+ SELECT name INTO parent_name FROM packageset WHERE id = NEW.parent;
2727+ SELECT name INTO child_name FROM packageset WHERE id = NEW.child;
2728+ SELECT ds.name INTO parent_distroseries FROM packageset ps, distroseries ds WHERE ps.id = NEW.parent AND ps.distroseries = ds.id;
2729+ SELECT ds.name INTO child_distroseries FROM packageset ps, distroseries ds WHERE ps.id = NEW.child AND ps.distroseries = ds.id;
2730+ RAISE EXCEPTION 'Package sets % and % belong to different distro series (to % and % respectively) and thus cannot be associated.', child_name, parent_name, child_distroseries, parent_distroseries;
2731+ END IF;
2732+
2733+ IF EXISTS(
2734+ SELECT * FROM flatpackagesetinclusion
2735+ WHERE parent = NEW.child AND child = NEW.parent LIMIT 1)
2736+ THEN
2737+ SELECT name INTO parent_name FROM packageset WHERE id = NEW.parent;
2738+ SELECT name INTO child_name FROM packageset WHERE id = NEW.child;
2739+ RAISE EXCEPTION 'Package set % already includes %. Adding (% -> %) would introduce a cycle in the package set graph (DAG).', child_name, parent_name, parent_name, child_name;
2740+ END IF;
2741+ END;
2742+ -- A new package set inclusion relationship was inserted i.e. a set M
2743+ -- now includes another set N as a subset.
2744+ -- For an explanation of the queries below please see page 4 of
2745+ -- "Maintaining Transitive Closure of Graphs in SQL"
2746+ -- http://www.comp.nus.edu.sg/~wongls/psZ/dlsw-ijit97-16.ps
2747+ CREATE TEMP TABLE tmp_fpsi_new(
2748+ parent integer NOT NULL,
2749+ child integer NOT NULL);
2750+
2751+ INSERT INTO tmp_fpsi_new (
2752+ SELECT
2753+ X.parent AS parent, NEW.child AS child
2754+ FROM flatpackagesetinclusion X WHERE X.child = NEW.parent
2755+ UNION
2756+ SELECT
2757+ NEW.parent AS parent, X.child AS child
2758+ FROM flatpackagesetinclusion X WHERE X.parent = NEW.child
2759+ UNION
2760+ SELECT
2761+ X.parent AS parent, Y.child AS child
2762+ FROM flatpackagesetinclusion X, flatpackagesetinclusion Y
2763+ WHERE X.child = NEW.parent AND Y.parent = NEW.child
2764+ );
2765+ INSERT INTO tmp_fpsi_new(parent, child) VALUES(NEW.parent, NEW.child);
2766+
2767+ INSERT INTO flatpackagesetinclusion(parent, child) (
2768+ SELECT
2769+ parent, child FROM tmp_fpsi_new
2770+ EXCEPT
2771+ SELECT F.parent, F.child FROM flatpackagesetinclusion F
2772+ );
2773+
2774+ DROP TABLE tmp_fpsi_new;
2775+
2776+ RETURN NULL;
2777+END;
2778+$$;
2779+
2780+
2781+COMMENT ON FUNCTION packagesetinclusion_inserted_trig() IS 'Maintain the transitive closure in the DAG for a newly inserted edge leading to/from a package set.';
2782+
2783+
2784+CREATE FUNCTION person_sort_key(displayname text, name text) RETURNS text
2785+ LANGUAGE plpythonu IMMUTABLE STRICT
2786+ AS $$
2787+ # NB: If this implementation is changed, the person_sort_idx needs to be
2788+ # rebuilt along with any other indexes using it.
2789+ import re
2790+
2791+ try:
2792+ strip_re = SD["strip_re"]
2793+ except KeyError:
2794+ strip_re = re.compile("(?:[^\w\s]|[\d_])", re.U)
2795+ SD["strip_re"] = strip_re
2796+
2797+ displayname, name = args
2798+
2799+ # Strip noise out of displayname. We do not have to bother with
2800+ # name, as we know it is just plain ascii.
2801+ displayname = strip_re.sub('', displayname.decode('UTF-8').lower())
2802+ return ("%s, %s" % (displayname.strip(), name)).encode('UTF-8')
2803+$$;
2804+
2805+
2806+COMMENT ON FUNCTION person_sort_key(displayname text, name text) IS 'Return a string suitable for sorting people on, generated by stripping noise out of displayname and concatenating name';
2807+
2808
2809 CREATE FUNCTION pgstattuple(text) RETURNS pgstattuple_type
2810- AS '$libdir/pgstattuple', 'pgstattuple'
2811- LANGUAGE c STRICT;
2812+ LANGUAGE c STRICT
2813+ AS '$libdir/pgstattuple', 'pgstattuple';
2814+
2815
2816 CREATE FUNCTION pgstattuple(oid) RETURNS pgstattuple_type
2817- AS '$libdir/pgstattuple', 'pgstattuplebyid'
2818- LANGUAGE c STRICT;
2819+ LANGUAGE c STRICT
2820+ AS '$libdir/pgstattuple', 'pgstattuplebyid';
2821+
2822
2823 CREATE FUNCTION plpgsql_call_handler() RETURNS language_handler
2824- AS '$libdir/plpgsql', 'plpgsql_call_handler'
2825- LANGUAGE c;
2826+ LANGUAGE c
2827+ AS '$libdir/plpgsql', 'plpgsql_call_handler';
2828+
2829
2830 CREATE FUNCTION plpython_call_handler() RETURNS language_handler
2831- AS '$libdir/plpython', 'plpython_call_handler'
2832- LANGUAGE c;
2833+ LANGUAGE c
2834+ AS '$libdir/plpython', 'plpython_call_handler';
2835+
2836+
2837+CREATE FUNCTION questionmessage_copy_owner_from_message() RETURNS trigger
2838+ LANGUAGE plpgsql SECURITY DEFINER
2839+ SET search_path TO public
2840+ AS $$
2841+BEGIN
2842+ IF TG_OP = 'INSERT' THEN
2843+ IF NEW.owner is NULL THEN
2844+ UPDATE QuestionMessage
2845+ SET owner = Message.owner FROM
2846+ Message WHERE
2847+ Message.id = NEW.message AND
2848+ QuestionMessage.id = NEW.id;
2849+ END IF;
2850+ ELSIF NEW.message != OLD.message THEN
2851+ UPDATE QuestionMessage
2852+ SET owner = Message.owner FROM
2853+ Message WHERE
2854+ Message.id = NEW.message AND
2855+ QuestionMessage.id = NEW.id;
2856+ END IF;
2857+ RETURN NULL; -- Ignored - this is an AFTER trigger
2858+END;
2859+$$;
2860+
2861+
2862+COMMENT ON FUNCTION questionmessage_copy_owner_from_message() IS 'Copies the message owner into QuestionMessage when QuestionMessage changes.';
2863+
2864+
2865+CREATE FUNCTION replication_lag() RETURNS interval
2866+ LANGUAGE plpgsql STABLE SECURITY DEFINER
2867+ SET search_path TO public
2868+ AS $$
2869+ DECLARE
2870+ v_lag interval;
2871+ BEGIN
2872+ SELECT INTO v_lag max(st_lag_time) FROM _sl.sl_status;
2873+ RETURN v_lag;
2874+ -- Slony-I not installed here - non-replicated setup.
2875+ EXCEPTION
2876+ WHEN invalid_schema_name THEN
2877+ RETURN NULL;
2878+ WHEN undefined_table THEN
2879+ RETURN NULL;
2880+ END;
2881+$$;
2882+
2883+
2884+COMMENT ON FUNCTION replication_lag() IS 'Returns the worst lag time in our cluster, or NULL if not a replicated installation. Only returns meaningful results on the lpmain replication set master.';
2885+
2886+
2887+CREATE FUNCTION replication_lag(node_id integer) RETURNS interval
2888+ LANGUAGE plpgsql STABLE SECURITY DEFINER
2889+ SET search_path TO public
2890+ AS $$
2891+ DECLARE
2892+ v_lag interval;
2893+ BEGIN
2894+ SELECT INTO v_lag st_lag_time FROM _sl.sl_status
2895+ WHERE st_origin = _sl.getlocalnodeid('_sl')
2896+ AND st_received = node_id;
2897+ RETURN v_lag;
2898+ -- Slony-I not installed here - non-replicated setup.
2899+ EXCEPTION
2900+ WHEN invalid_schema_name THEN
2901+ RETURN NULL;
2902+ WHEN undefined_table THEN
2903+ RETURN NULL;
2904+ END;
2905+$$;
2906+
2907+
2908+COMMENT ON FUNCTION replication_lag(node_id integer) IS 'Returns the lag time of the lpmain replication set to the given node, or NULL if not a replicated installation. The node id parameter can be obtained by calling getlocalnodeid() on the relevant database. This function only returns meaningful results on the lpmain replication set master.';
2909+
2910+
2911+CREATE FUNCTION sane_version(text) RETURNS boolean
2912+ LANGUAGE plpythonu IMMUTABLE STRICT
2913+ AS $_$
2914+ import re
2915+ if re.search("""^(?ix)
2916+ [0-9a-z]
2917+ ( [0-9a-z] | [0-9a-z.-]*[0-9a-z] )*
2918+ $""", args[0]):
2919+ return 1
2920+ return 0
2921+$_$;
2922+
2923+
2924+COMMENT ON FUNCTION sane_version(text) IS 'A sane version number for use by ProductRelease and DistroRelease. We may make it less strict if required, but it would be nice if we can enforce simple version strings because we use them in URLs';
2925+
2926+
2927+CREATE FUNCTION set_bug_date_last_message() RETURNS trigger
2928+ LANGUAGE plpgsql SECURITY DEFINER
2929+ SET search_path TO public
2930+ AS $$
2931+BEGIN
2932+ IF TG_OP = 'INSERT' THEN
2933+ UPDATE Bug
2934+ SET date_last_message = CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
2935+ WHERE Bug.id = NEW.bug;
2936+ ELSE
2937+ UPDATE Bug
2938+ SET date_last_message = max_datecreated
2939+ FROM (
2940+ SELECT BugMessage.bug, max(Message.datecreated) AS max_datecreated
2941+ FROM BugMessage, Message
2942+ WHERE BugMessage.id <> OLD.id
2943+ AND BugMessage.bug = OLD.bug
2944+ AND BugMessage.message = Message.id
2945+ GROUP BY BugMessage.bug
2946+ ) AS MessageSummary
2947+ WHERE Bug.id = MessageSummary.bug;
2948+ END IF;
2949+ RETURN NULL; -- Ignored - this is an AFTER trigger
2950+END;
2951+$$;
2952+
2953+
2954+COMMENT ON FUNCTION set_bug_date_last_message() IS 'AFTER INSERT trigger on BugMessage maintaining the Bug.date_last_message column';
2955+
2956+
2957+CREATE FUNCTION set_bug_message_count() RETURNS trigger
2958+ LANGUAGE plpgsql
2959+ AS $$
2960+BEGIN
2961+ IF TG_OP = 'UPDATE' THEN
2962+ IF NEW.bug = OLD.bug THEN
2963+ RETURN NULL; -- Ignored - this is an AFTER trigger.
2964+ END IF;
2965+ END IF;
2966+
2967+ IF TG_OP <> 'DELETE' THEN
2968+ UPDATE Bug SET message_count = message_count + 1
2969+ WHERE Bug.id = NEW.bug;
2970+ END IF;
2971+
2972+ IF TG_OP <> 'INSERT' THEN
2973+ UPDATE Bug SET message_count = message_count - 1
2974+ WHERE Bug.id = OLD.bug;
2975+ END IF;
2976+
2977+ RETURN NULL; -- Ignored - this is an AFTER trigger.
2978+END;
2979+$$;
2980+
2981+
2982+COMMENT ON FUNCTION set_bug_message_count() IS 'AFTER UPDATE trigger on BugAffectsPerson maintaining the Bug.users_affected_count column';
2983+
2984+
2985+CREATE FUNCTION set_bug_number_of_duplicates() RETURNS trigger
2986+ LANGUAGE plpgsql
2987+ AS $$
2988+BEGIN
2989+ -- Short circuit on an update that doesn't change duplicateof
2990+ IF TG_OP = 'UPDATE' THEN
2991+ IF NEW.duplicateof = OLD.duplicateof THEN
2992+ RETURN NULL; -- Ignored - this is an AFTER trigger
2993+ END IF;
2994+ END IF;
2995+
2996+ -- For update or delete, possibly decrement a bug's dupe count
2997+ IF TG_OP <> 'INSERT' THEN
2998+ IF OLD.duplicateof IS NOT NULL THEN
2999+ UPDATE Bug SET number_of_duplicates = number_of_duplicates - 1
3000+ WHERE Bug.id = OLD.duplicateof;
3001+ END IF;
3002+ END IF;
3003+
3004+ -- For update or insert, possibly increment a bug's dupe cout
3005+ IF TG_OP <> 'DELETE' THEN
3006+ IF NEW.duplicateof IS NOT NULL THEN
3007+ UPDATE Bug SET number_of_duplicates = number_of_duplicates + 1
3008+ WHERE Bug.id = NEW.duplicateof;
3009+ END IF;
3010+ END IF;
3011+
3012+ RETURN NULL; -- Ignored - this is an AFTER trigger
3013+END;
3014+$$;
3015+
3016+
3017+COMMENT ON FUNCTION set_bug_number_of_duplicates() IS 'AFTER UPDATE trigger on Bug maintaining the Bug.number_of_duplicates column';
3018+
3019+
3020+CREATE FUNCTION set_bug_users_affected_count() RETURNS trigger
3021+ LANGUAGE plpgsql
3022+ AS $$
3023+BEGIN
3024+ IF TG_OP = 'INSERT' THEN
3025+ IF NEW.affected = TRUE THEN
3026+ UPDATE Bug
3027+ SET users_affected_count = users_affected_count + 1
3028+ WHERE Bug.id = NEW.bug;
3029+ ELSE
3030+ UPDATE Bug
3031+ SET users_unaffected_count = users_unaffected_count + 1
3032+ WHERE Bug.id = NEW.bug;
3033+ END IF;
3034+ END IF;
3035+
3036+ IF TG_OP = 'DELETE' THEN
3037+ IF OLD.affected = TRUE THEN
3038+ UPDATE Bug
3039+ SET users_affected_count = users_affected_count - 1
3040+ WHERE Bug.id = OLD.bug;
3041+ ELSE
3042+ UPDATE Bug
3043+ SET users_unaffected_count = users_unaffected_count - 1
3044+ WHERE Bug.id = OLD.bug;
3045+ END IF;
3046+ END IF;
3047+
3048+ IF TG_OP = 'UPDATE' THEN
3049+ IF OLD.affected <> NEW.affected THEN
3050+ IF NEW.affected THEN
3051+ UPDATE Bug
3052+ SET users_affected_count = users_affected_count + 1,
3053+ users_unaffected_count = users_unaffected_count - 1
3054+ WHERE Bug.id = OLD.bug;
3055+ ELSE
3056+ UPDATE Bug
3057+ SET users_affected_count = users_affected_count - 1,
3058+ users_unaffected_count = users_unaffected_count + 1
3059+ WHERE Bug.id = OLD.bug;
3060+ END IF;
3061+ END IF;
3062+ END IF;
3063+
3064+ RETURN NULL;
3065+END;
3066+$$;
3067+
3068+
3069+CREATE FUNCTION set_bugtask_date_milestone_set() RETURNS trigger
3070+ LANGUAGE plpgsql
3071+ AS $$
3072+BEGIN
3073+ IF TG_OP = 'INSERT' THEN
3074+ -- If the inserted row as a milestone set, set date_milestone_set.
3075+ IF NEW.milestone IS NOT NULL THEN
3076+ UPDATE BugTask
3077+ SET date_milestone_set = CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
3078+ WHERE BugTask.id = NEW.id;
3079+ END IF;
3080+ END IF;
3081+
3082+ IF TG_OP = 'UPDATE' THEN
3083+ IF OLD.milestone IS NULL THEN
3084+ -- If there was no milestone set, check if the new row has a
3085+ -- milestone set and set date_milestone_set.
3086+ IF NEW.milestone IS NOT NULL THEN
3087+ UPDATE BugTask
3088+ SET date_milestone_set = CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
3089+ WHERE BugTask.id = NEW.id;
3090+ END IF;
3091+ ELSE
3092+ IF NEW.milestone IS NULL THEN
3093+ -- If the milestone was unset, clear date_milestone_set.
3094+ UPDATE BugTask
3095+ SET date_milestone_set = NULL
3096+ WHERE BugTask.id = NEW.id;
3097+ ELSE
3098+ -- Update date_milestone_set if the bug task was
3099+ -- targeted to another milestone.
3100+ IF NEW.milestone != OLD.milestone THEN
3101+ UPDATE BugTask
3102+ SET date_milestone_set = CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
3103+ WHERE BugTask.id = NEW.id;
3104+ END IF;
3105+
3106+ END IF;
3107+ END IF;
3108+ END IF;
3109+
3110+ RETURN NULL; -- Ignored - this is an AFTER trigger.
3111+END;
3112+$$;
3113+
3114+
3115+COMMENT ON FUNCTION set_bugtask_date_milestone_set() IS 'Update BugTask.date_milestone_set when BugTask.milestone is changed.';
3116+
3117+
3118+CREATE FUNCTION set_date_status_set() RETURNS trigger
3119+ LANGUAGE plpgsql
3120+ AS $$
3121+BEGIN
3122+ IF OLD.status <> NEW.status THEN
3123+ NEW.date_status_set = CURRENT_TIMESTAMP AT TIME ZONE 'UTC';
3124+ END IF;
3125+ RETURN NEW;
3126+END;
3127+$$;
3128+
3129+
3130+COMMENT ON FUNCTION set_date_status_set() IS 'BEFORE UPDATE trigger on Account that maintains the Account.date_status_set column.';
3131+
3132+
3133+CREATE FUNCTION set_openid_identifier() RETURNS trigger
3134+ LANGUAGE plpythonu
3135+ AS $$
3136+ # If someone is trying to explicitly set the openid_identifier, let them.
3137+ # This also causes openid_identifiers to be left alone if this is an
3138+ # UPDATE trigger.
3139+ if TD['new']['openid_identifier'] is not None:
3140+ return None
3141+
3142+ from random import choice
3143+
3144+ # Non display confusing characters
3145+ chars = '34678bcdefhkmnprstwxyzABCDEFGHJKLMNPQRTWXY'
3146+
3147+ # character length of tokens. Can be increased, decreased or even made
3148+ # random - Launchpad does not care. 7 means it takes 40 bytes to store
3149+ # a null-terminated Launchpad identity URL on the current domain name.
3150+ length=7
3151+
3152+ loop_count = 0
3153+ while loop_count < 20000:
3154+ # Generate a random openid_identifier
3155+ oid = ''.join(choice(chars) for count in range(length))
3156+
3157+ # Check if the oid is already in the db, although this is pretty
3158+ # unlikely
3159+ rv = plpy.execute("""
3160+ SELECT COUNT(*) AS num FROM Person WHERE openid_identifier = '%s'
3161+ """ % oid, 1)
3162+ if rv[0]['num'] == 0:
3163+ TD['new']['openid_identifier'] = oid
3164+ return "MODIFY"
3165+ loop_count += 1
3166+ if loop_count == 1:
3167+ plpy.warning(
3168+ 'Clash generating unique openid_identifier. '
3169+ 'Increase length if you see this warning too much.')
3170+ plpy.error(
3171+ "Unable to generate unique openid_identifier. "
3172+ "Need to increase length of tokens.")
3173+$$;
3174+
3175+
3176+CREATE FUNCTION set_shipit_normalized_address() RETURNS trigger
3177+ LANGUAGE plpgsql
3178+ AS $$
3179+ BEGIN
3180+ NEW.normalized_address =
3181+ lower(
3182+ -- Strip off everything that's not alphanumeric
3183+ -- characters.
3184+ regexp_replace(
3185+ coalesce(NEW.addressline1, '') || ' ' ||
3186+ coalesce(NEW.addressline2, '') || ' ' ||
3187+ coalesce(NEW.city, ''),
3188+ '[^a-zA-Z0-9]+', '', 'g'));
3189+ RETURN NEW;
3190+ END;
3191+$$;
3192+
3193+
3194+COMMENT ON FUNCTION set_shipit_normalized_address() IS 'Store a normalized concatenation of the request''s address into the normalized_address column.';
3195+
3196+
3197+CREATE FUNCTION sha1(text) RETURNS character
3198+ LANGUAGE plpythonu IMMUTABLE STRICT
3199+ AS $$
3200+ import hashlib
3201+ return hashlib.sha1(args[0]).hexdigest()
3202+$$;
3203+
3204+
3205+COMMENT ON FUNCTION sha1(text) IS 'Return the SHA1 one way cryptographic hash as a string of 40 hex digits';
3206+
3207+
3208+CREATE FUNCTION summarise_bug(bug_row bug) RETURNS void
3209+ LANGUAGE plpgsql
3210+ AS $$
3211+DECLARE
3212+ d bugsummary%ROWTYPE;
3213+BEGIN
3214+ PERFORM ensure_bugsummary_temp_journal();
3215+ FOR d IN SELECT * FROM bugsummary_locations(BUG_ROW) LOOP
3216+ d.count = 1;
3217+ PERFORM bug_summary_temp_journal_ins(d);
3218+ END LOOP;
3219+END;
3220+$$;
3221+
3222+
3223+COMMENT ON FUNCTION summarise_bug(bug_row bug) IS 'AFTER summarise a bug row into bugsummary.';
3224+
3225+
3226+CREATE FUNCTION ulower(text) RETURNS text
3227+ LANGUAGE plpythonu IMMUTABLE STRICT
3228+ AS $$
3229+ return args[0].decode('utf8').lower().encode('utf8')
3230+$$;
3231+
3232+
3233+COMMENT ON FUNCTION ulower(text) IS 'Return the lower case version of a UTF-8 encoded string.';
3234+
3235+
3236+CREATE FUNCTION unsummarise_bug(bug_row bug) RETURNS void
3237+ LANGUAGE plpgsql
3238+ AS $$
3239+DECLARE
3240+ d bugsummary%ROWTYPE;
3241+BEGIN
3242+ PERFORM ensure_bugsummary_temp_journal();
3243+ FOR d IN SELECT * FROM bugsummary_locations(BUG_ROW) LOOP
3244+ d.count = -1;
3245+ PERFORM bug_summary_temp_journal_ins(d);
3246+ END LOOP;
3247+END;
3248+$$;
3249+
3250+
3251+COMMENT ON FUNCTION unsummarise_bug(bug_row bug) IS 'AFTER unsummarise a bug row from bugsummary.';
3252+
3253+
3254+CREATE FUNCTION update_branch_name_cache() RETURNS trigger
3255+ LANGUAGE plpgsql
3256+ AS $$
3257+DECLARE
3258+ needs_update boolean := FALSE;
3259+BEGIN
3260+ IF TG_OP = 'INSERT' THEN
3261+ needs_update := TRUE;
3262+ ELSIF (NEW.owner_name IS NULL
3263+ OR NEW.unique_name IS NULL
3264+ OR OLD.owner_name <> NEW.owner_name
3265+ OR OLD.unique_name <> NEW.unique_name
3266+ OR (NEW.target_suffix IS NULL <> OLD.target_suffix IS NULL)
3267+ OR COALESCE(OLD.target_suffix, '') <> COALESCE(NEW.target_suffix, '')
3268+ OR OLD.name <> NEW.name
3269+ OR OLD.owner <> NEW.owner
3270+ OR COALESCE(OLD.product, -1) <> COALESCE(NEW.product, -1)
3271+ OR COALESCE(OLD.distroseries, -1) <> COALESCE(NEW.distroseries, -1)
3272+ OR COALESCE(OLD.sourcepackagename, -1)
3273+ <> COALESCE(NEW.sourcepackagename, -1)) THEN
3274+ needs_update := TRUE;
3275+ END IF;
3276+
3277+ IF needs_update THEN
3278+ SELECT
3279+ Person.name AS owner_name,
3280+ COALESCE(Product.name, SPN.name) AS target_suffix,
3281+ '~' || Person.name || '/' || COALESCE(
3282+ Product.name,
3283+ Distribution.name || '/' || Distroseries.name
3284+ || '/' || SPN.name,
3285+ '+junk') || '/' || NEW.name AS unique_name
3286+ INTO NEW.owner_name, NEW.target_suffix, NEW.unique_name
3287+ FROM Person
3288+ LEFT OUTER JOIN DistroSeries ON NEW.distroseries = DistroSeries.id
3289+ LEFT OUTER JOIN Product ON NEW.product = Product.id
3290+ LEFT OUTER JOIN Distribution
3291+ ON Distroseries.distribution = Distribution.id
3292+ LEFT OUTER JOIN SourcepackageName AS SPN
3293+ ON SPN.id = NEW.sourcepackagename
3294+ WHERE Person.id = NEW.owner;
3295+ END IF;
3296+
3297+ RETURN NEW;
3298+END;
3299+$$;
3300+
3301+
3302+COMMENT ON FUNCTION update_branch_name_cache() IS 'Maintain the cached name columns in Branch.';
3303+
3304+
3305+CREATE FUNCTION update_database_disk_utilization() RETURNS void
3306+ LANGUAGE sql SECURITY DEFINER
3307+ SET search_path TO public
3308+ AS $$
3309+ INSERT INTO DatabaseDiskUtilization
3310+ SELECT
3311+ CURRENT_TIMESTAMP AT TIME ZONE 'UTC',
3312+ namespace, name,
3313+ sub_namespace, sub_name,
3314+ kind,
3315+ (namespace || '.' || name || COALESCE(
3316+ '/' || sub_namespace || '.' || sub_name, '')) AS sort,
3317+ (stat).table_len,
3318+ (stat).tuple_count,
3319+ (stat).tuple_len,
3320+ (stat).tuple_percent,
3321+ (stat).dead_tuple_count,
3322+ (stat).dead_tuple_len,
3323+ (stat).dead_tuple_percent,
3324+ (stat).free_space,
3325+ (stat).free_percent
3326+ FROM (
3327+ SELECT
3328+ pg_namespace.nspname AS namespace,
3329+ pg_class.relname AS name,
3330+ NULL AS sub_namespace,
3331+ NULL AS sub_name,
3332+ pg_class.relkind AS kind,
3333+ pgstattuple(pg_class.oid) AS stat
3334+ FROM pg_class, pg_namespace
3335+ WHERE
3336+ pg_class.relnamespace = pg_namespace.oid
3337+ AND pg_class.relkind = 'r'
3338+ AND pg_table_is_visible(pg_class.oid)
3339+
3340+ UNION ALL
3341+
3342+ SELECT
3343+ pg_namespace_table.nspname AS namespace,
3344+ pg_class_table.relname AS name,
3345+ pg_namespace_index.nspname AS sub_namespace,
3346+ pg_class_index.relname AS sub_name,
3347+ pg_class_index.relkind AS kind,
3348+ pgstattuple(pg_class_index.oid) AS stat
3349+ FROM
3350+ pg_namespace AS pg_namespace_table,
3351+ pg_namespace AS pg_namespace_index,
3352+ pg_class AS pg_class_table,
3353+ pg_class AS pg_class_index,
3354+ pg_index
3355+ WHERE
3356+ pg_class_index.relkind = 'i'
3357+ AND pg_table_is_visible(pg_class_table.oid)
3358+ AND pg_class_index.relnamespace = pg_namespace_index.oid
3359+ AND pg_class_table.relnamespace = pg_namespace_table.oid
3360+ AND pg_index.indexrelid = pg_class_index.oid
3361+ AND pg_index.indrelid = pg_class_table.oid
3362+
3363+ UNION ALL
3364+
3365+ -- TOAST tables
3366+ SELECT
3367+ pg_namespace_table.nspname AS namespace,
3368+ pg_class_table.relname AS name,
3369+ pg_namespace_toast.nspname AS sub_namespace,
3370+ pg_class_toast.relname AS sub_name,
3371+ pg_class_toast.relkind AS kind,
3372+ pgstattuple(pg_class_toast.oid) AS stat
3373+ FROM
3374+ pg_namespace AS pg_namespace_table,
3375+ pg_namespace AS pg_namespace_toast,
3376+ pg_class AS pg_class_table,
3377+ pg_class AS pg_class_toast
3378+ WHERE
3379+ pg_class_toast.relnamespace = pg_namespace_toast.oid
3380+ AND pg_table_is_visible(pg_class_table.oid)
3381+ AND pg_class_table.relnamespace = pg_namespace_table.oid
3382+ AND pg_class_toast.oid = pg_class_table.reltoastrelid
3383+
3384+ UNION ALL
3385+
3386+ -- TOAST indexes
3387+ SELECT
3388+ pg_namespace_table.nspname AS namespace,
3389+ pg_class_table.relname AS name,
3390+ pg_namespace_index.nspname AS sub_namespace,
3391+ pg_class_index.relname AS sub_name,
3392+ pg_class_index.relkind AS kind,
3393+ pgstattuple(pg_class_index.oid) AS stat
3394+ FROM
3395+ pg_namespace AS pg_namespace_table,
3396+ pg_namespace AS pg_namespace_index,
3397+ pg_class AS pg_class_table,
3398+ pg_class AS pg_class_index,
3399+ pg_class AS pg_class_toast
3400+ WHERE
3401+ pg_class_table.relnamespace = pg_namespace_table.oid
3402+ AND pg_table_is_visible(pg_class_table.oid)
3403+ AND pg_class_index.relnamespace = pg_namespace_index.oid
3404+ AND pg_class_table.reltoastrelid = pg_class_toast.oid
3405+ AND pg_class_index.oid = pg_class_toast.reltoastidxid
3406+ ) AS whatever;
3407+$$;
3408+
3409+
3410+CREATE FUNCTION update_database_stats() RETURNS void
3411+ LANGUAGE plpythonu SECURITY DEFINER
3412+ SET search_path TO public
3413+ AS $_$
3414+ import re
3415+ import subprocess
3416+
3417+ # Prune DatabaseTableStats and insert current data.
3418+ # First, detect if the statistics have been reset.
3419+ stats_reset = plpy.execute("""
3420+ SELECT *
3421+ FROM
3422+ pg_catalog.pg_stat_user_tables AS NowStat,
3423+ DatabaseTableStats AS LastStat
3424+ WHERE
3425+ LastStat.date_created = (
3426+ SELECT max(date_created) FROM DatabaseTableStats)
3427+ AND NowStat.schemaname = LastStat.schemaname
3428+ AND NowStat.relname = LastStat.relname
3429+ AND (
3430+ NowStat.seq_scan < LastStat.seq_scan
3431+ OR NowStat.idx_scan < LastStat.idx_scan
3432+ OR NowStat.n_tup_ins < LastStat.n_tup_ins
3433+ OR NowStat.n_tup_upd < LastStat.n_tup_upd
3434+ OR NowStat.n_tup_del < LastStat.n_tup_del
3435+ OR NowStat.n_tup_hot_upd < LastStat.n_tup_hot_upd)
3436+ LIMIT 1
3437+ """, 1).nrows() > 0
3438+ if stats_reset:
3439+ # The database stats have been reset. We cannot calculate
3440+ # deltas because we do not know when this happened. So we trash
3441+ # our records as they are now useless to us. We could be more
3442+ # sophisticated about this, but this should only happen
3443+ # when an admin explicitly resets the statistics or if the
3444+ # database is rebuilt.
3445+ plpy.notice("Stats wraparound. Purging DatabaseTableStats")
3446+ plpy.execute("DELETE FROM DatabaseTableStats")
3447+ else:
3448+ plpy.execute("""
3449+ DELETE FROM DatabaseTableStats
3450+ WHERE date_created < (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
3451+ - CAST('21 days' AS interval));
3452+ """)
3453+ # Insert current data.
3454+ plpy.execute("""
3455+ INSERT INTO DatabaseTableStats
3456+ SELECT
3457+ CURRENT_TIMESTAMP AT TIME ZONE 'UTC',
3458+ schemaname, relname, seq_scan, seq_tup_read,
3459+ coalesce(idx_scan, 0), coalesce(idx_tup_fetch, 0),
3460+ n_tup_ins, n_tup_upd, n_tup_del,
3461+ n_tup_hot_upd, n_live_tup, n_dead_tup, last_vacuum,
3462+ last_autovacuum, last_analyze, last_autoanalyze
3463+ FROM pg_catalog.pg_stat_user_tables;
3464+ """)
3465+
3466+ # Prune DatabaseCpuStats. Calculate CPU utilization information
3467+ # and insert current data.
3468+ plpy.execute("""
3469+ DELETE FROM DatabaseCpuStats
3470+ WHERE date_created < (CURRENT_TIMESTAMP AT TIME ZONE 'UTC'
3471+ - CAST('21 days' AS interval));
3472+ """)
3473+ dbname = plpy.execute(
3474+ "SELECT current_database() AS dbname", 1)[0]['dbname']
3475+ ps = subprocess.Popen(
3476+ ["ps", "-C", "postgres", "--no-headers", "-o", "cp,args"],
3477+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
3478+ stderr=subprocess.STDOUT)
3479+ stdout, stderr = ps.communicate()
3480+ cpus = {}
3481+ # We make the username match non-greedy so the trailing \d eats
3482+ # trailing digits from the database username. This collapses
3483+ # lpnet1, lpnet2 etc. into just lpnet.
3484+ ps_re = re.compile(
3485+ r"(?m)^\s*(\d+)\spostgres:\s(\w+?)\d*\s%s\s" % dbname)
3486+ for ps_match in ps_re.finditer(stdout):
3487+ cpu, username = ps_match.groups()
3488+ cpus[username] = int(cpu) + cpus.setdefault(username, 0)
3489+ cpu_ins = plpy.prepare(
3490+ "INSERT INTO DatabaseCpuStats (username, cpu) VALUES ($1, $2)",
3491+ ["text", "integer"])
3492+ for cpu_tuple in cpus.items():
3493+ plpy.execute(cpu_ins, cpu_tuple)
3494+$_$;
3495+
3496+
3497+COMMENT ON FUNCTION update_database_stats() IS 'Copies rows from pg_stat_user_tables into DatabaseTableStats. We use a stored procedure because it is problematic for us to grant permissions on objects in the pg_catalog schema.';
3498+
3499+
3500+CREATE FUNCTION update_replication_lag_cache() RETURNS boolean
3501+ LANGUAGE plpgsql SECURITY DEFINER
3502+ SET search_path TO public
3503+ AS $$
3504+ BEGIN
3505+ DELETE FROM DatabaseReplicationLag;
3506+ INSERT INTO DatabaseReplicationLag (node, lag)
3507+ SELECT st_received, st_lag_time FROM _sl.sl_status
3508+ WHERE st_origin = _sl.getlocalnodeid('_sl');
3509+ RETURN TRUE;
3510+ -- Slony-I not installed here - non-replicated setup.
3511+ EXCEPTION
3512+ WHEN invalid_schema_name THEN
3513+ RETURN FALSE;
3514+ WHEN undefined_table THEN
3515+ RETURN FALSE;
3516+ END;
3517+$$;
3518+
3519+
3520+COMMENT ON FUNCTION update_replication_lag_cache() IS 'Updates the DatabaseReplicationLag materialized view.';
3521+
3522+
3523+CREATE FUNCTION update_transitively_private(start_branch integer, _root_branch integer DEFAULT NULL::integer, _root_transitively_private boolean DEFAULT NULL::boolean) RETURNS void
3524+ LANGUAGE plpgsql SECURITY DEFINER
3525+ SET search_path TO public
3526+ AS $$
3527+DECLARE
3528+ root_transitively_private boolean := _root_transitively_private;
3529+ root_branch int := _root_branch;
3530+BEGIN
3531+ IF root_transitively_private IS NULL THEN
3532+ -- We can't just trust the transitively_private flag of the
3533+ -- branch we are stacked on, as if we are updating multiple
3534+ -- records they will be updated in an indeterminate order.
3535+ -- We need a recursive query.
3536+ UPDATE Branch SET transitively_private = (
3537+ WITH RECURSIVE stacked_branches AS (
3538+ SELECT
3539+ top_branch.id, top_branch.stacked_on, top_branch.private
3540+ FROM Branch AS top_branch
3541+ WHERE top_branch.id = start_branch
3542+ UNION ALL
3543+ SELECT
3544+ sub_branch.id, sub_branch.stacked_on, sub_branch.private
3545+ FROM stacked_branches, Branch AS sub_branch
3546+ WHERE
3547+ stacked_branches.stacked_on = sub_branch.id
3548+ AND stacked_branches.stacked_on != start_branch
3549+ -- Shortcircuit. No need to recurse if already private.
3550+ AND stacked_branches.private IS FALSE
3551+ )
3552+ SELECT COUNT(*) > 0
3553+ FROM stacked_branches
3554+ WHERE private IS TRUE)
3555+ WHERE Branch.id = start_branch
3556+ RETURNING transitively_private INTO root_transitively_private;
3557+ root_branch := start_branch;
3558+ ELSE
3559+ -- Now we have calculated the correct transitively_private flag
3560+ -- we can trust it.
3561+ UPDATE Branch SET
3562+ transitively_private = GREATEST(private, root_transitively_private)
3563+ WHERE id = root_branch;
3564+ END IF;
3565+
3566+ -- Recurse to branches stacked on this one.
3567+ PERFORM update_transitively_private(
3568+ start_branch, id, GREATEST(private, root_transitively_private))
3569+ FROM Branch WHERE stacked_on = root_branch AND id != start_branch;
3570+END;
3571+$$;
3572+
3573+
3574+COMMENT ON FUNCTION update_transitively_private(start_branch integer, _root_branch integer, _root_transitively_private boolean) IS 'A branch is transitively private if it is private or is stacked on any transitively private branches.';
3575+
3576+
3577+CREATE FUNCTION valid_absolute_url(text) RETURNS boolean
3578+ LANGUAGE plpythonu IMMUTABLE STRICT
3579+ AS $$
3580+ from urlparse import urlparse, uses_netloc
3581+ # Extend list of schemes that specify netloc. We can drop sftp
3582+ # with Python 2.5 in the DB.
3583+ if 'git' not in uses_netloc:
3584+ uses_netloc.insert(0, 'sftp')
3585+ uses_netloc.insert(0, 'bzr')
3586+ uses_netloc.insert(0, 'bzr+ssh')
3587+ uses_netloc.insert(0, 'ssh') # Mercurial
3588+ uses_netloc.insert(0, 'git')
3589+ (scheme, netloc, path, params, query, fragment) = urlparse(args[0])
3590+ return bool(scheme and netloc)
3591+$$;
3592+
3593+
3594+COMMENT ON FUNCTION valid_absolute_url(text) IS 'Ensure the given test is a valid absolute URL, containing both protocol and network location';
3595+
3596+
3597+CREATE FUNCTION valid_branch_name(text) RETURNS boolean
3598+ LANGUAGE plpythonu IMMUTABLE STRICT
3599+ AS $$
3600+ import re
3601+ name = args[0]
3602+ pat = r"^(?i)[a-z0-9][a-z0-9+\.\-@_]*\Z"
3603+ if re.match(pat, name):
3604+ return 1
3605+ return 0
3606+$$;
3607+
3608+
3609+COMMENT ON FUNCTION valid_branch_name(text) IS 'validate a branch name.
3610+
3611+ As per valid_name, except we allow uppercase and @';
3612+
3613+
3614+CREATE FUNCTION valid_cve(text) RETURNS boolean
3615+ LANGUAGE plpythonu IMMUTABLE STRICT
3616+ AS $_$
3617+ import re
3618+ name = args[0]
3619+ pat = r"^(19|20)\d{2}-\d{4}$"
3620+ if re.match(pat, name):
3621+ return 1
3622+ return 0
3623+$_$;
3624+
3625+
3626+COMMENT ON FUNCTION valid_cve(text) IS 'validate a common vulnerability number as defined on www.cve.mitre.org, minus the CAN- or CVE- prefix.';
3627+
3628+
3629+CREATE FUNCTION valid_debian_version(text) RETURNS boolean
3630+ LANGUAGE plpythonu IMMUTABLE STRICT
3631+ AS $_$
3632+ import re
3633+ m = re.search("""^(?ix)
3634+ ([0-9]+:)?
3635+ ([0-9a-z][a-z0-9+:.~-]*?)
3636+ (-[a-z0-9+.~]+)?
3637+ $""", args[0])
3638+ if m is None:
3639+ return 0
3640+ epoch, version, revision = m.groups()
3641+ if not epoch:
3642+ # Can''t contain : if no epoch
3643+ if ":" in version:
3644+ return 0
3645+ if not revision:
3646+ # Can''t contain - if no revision
3647+ if "-" in version:
3648+ return 0
3649+ return 1
3650+$_$;
3651+
3652+
3653+COMMENT ON FUNCTION valid_debian_version(text) IS 'validate a version number as per Debian Policy';
3654+
3655+
3656+CREATE FUNCTION valid_fingerprint(text) RETURNS boolean
3657+ LANGUAGE plpythonu IMMUTABLE STRICT
3658+ AS $$
3659+ import re
3660+ if re.match(r"[\dA-F]{40}", args[0]) is not None:
3661+ return 1
3662+ else:
3663+ return 0
3664+$$;
3665+
3666+
3667+COMMENT ON FUNCTION valid_fingerprint(text) IS 'Returns true if passed a valid GPG fingerprint. Valid GPG fingerprints are a 40 character long hexadecimal number in uppercase.';
3668+
3669+
3670+CREATE FUNCTION valid_keyid(text) RETURNS boolean
3671+ LANGUAGE plpythonu IMMUTABLE STRICT
3672+ AS $$
3673+ import re
3674+ if re.match(r"[\dA-F]{8}", args[0]) is not None:
3675+ return 1
3676+ else:
3677+ return 0
3678+$$;
3679+
3680+
3681+COMMENT ON FUNCTION valid_keyid(text) IS 'Returns true if passed a valid GPG keyid. Valid GPG keyids are an 8 character long hexadecimal number in uppercase (in reality, they are 16 characters long but we are using the ''common'' definition.';
3682+
3683+
3684+CREATE FUNCTION valid_regexp(text) RETURNS boolean
3685+ LANGUAGE plpythonu IMMUTABLE STRICT
3686+ AS $$
3687+ import re
3688+ try:
3689+ re.compile(args[0])
3690+ except:
3691+ return False
3692+ else:
3693+ return True
3694+$$;
3695+
3696+
3697+COMMENT ON FUNCTION valid_regexp(text) IS 'Returns true if the input can be compiled as a regular expression.';
3698+
3699+
3700+CREATE FUNCTION version_sort_key(version text) RETURNS text
3701+ LANGUAGE plpythonu IMMUTABLE STRICT
3702+ AS $$
3703+ # If this method is altered, then any functional indexes using it
3704+ # need to be rebuilt.
3705+ import re
3706+
3707+ [version] = args
3708+
3709+ def substitute_filled_numbers(match):
3710+ # Prepend "~" so that version numbers will show up first
3711+ # when sorted descending, i.e. [3, 2c, 2b, 1, c, b, a] instead
3712+ # of [c, b, a, 3, 2c, 2b, 1]. "~" has the highest ASCII value
3713+ # of visible ASCII characters.
3714+ return '~' + match.group(0).zfill(5)
3715+
3716+ return re.sub(u'\d+', substitute_filled_numbers, version)
3717+$$;
3718+
3719+
3720+COMMENT ON FUNCTION version_sort_key(version text) IS 'Sort a field as version numbers that do not necessarily conform to debian package versions (For example, when "2-2" should be considered greater than "1:1"). debversion_sort_key() should be used for debian versions. Numbers will be sorted after letters unlike typical ASCII, so that a descending sort will put the latest version number that starts with a number instead of a letter will be at the top. E.g. ascending is [a, z, 1, 9] and descending is [9, 1, z, a].';
3721+
3722+
3723+CREATE FUNCTION you_are_your_own_member() RETURNS trigger
3724+ LANGUAGE plpgsql
3725+ AS $$
3726+ BEGIN
3727+ INSERT INTO TeamParticipation (person, team)
3728+ VALUES (NEW.id, NEW.id);
3729+ RETURN NULL;
3730+ END;
3731+$$;
3732+
3733+
3734+COMMENT ON FUNCTION you_are_your_own_member() IS 'Trigger function to ensure that every row added to the Person table gets a corresponding row in the TeamParticipation table, as per the TeamParticipationUsage page on the Launchpad wiki';
3735+
3736+
3737+SET search_path = ts2, pg_catalog;
3738+
3739+CREATE FUNCTION _ftq(text) RETURNS text
3740+ LANGUAGE plpythonu IMMUTABLE STRICT
3741+ AS $_$
3742+ import re
3743+
3744+ # I think this method would be more robust if we used a real
3745+ # tokenizer and parser to generate the query string, but we need
3746+ # something suitable for use as a stored procedure which currently
3747+ # means no external dependancies.
3748+
3749+ # Convert to Unicode
3750+ query = args[0].decode('utf8')
3751+ ## plpy.debug('1 query is %s' % repr(query))
3752+
3753+ # Normalize whitespace
3754+ query = re.sub("(?u)\s+"," ", query)
3755+
3756+ # Convert AND, OR, NOT and - to tsearch2 punctuation
3757+ query = re.sub(r"(?u)(?:^|\s)-([\w\(])", r" !\1", query)
3758+ query = re.sub(r"(?u)\bAND\b", "&", query)
3759+ query = re.sub(r"(?u)\bOR\b", "|", query)
3760+ query = re.sub(r"(?u)\bNOT\b", " !", query)
3761+ ## plpy.debug('2 query is %s' % repr(query))
3762+
3763+ # Deal with unwanted punctuation. We convert strings of punctuation
3764+ # inside words to a '-' character for the hypenation handling below
3765+ # to deal with further. Outside of words we replace with whitespace.
3766+ # We don't mess with -&|!()' as they are handled later.
3767+ #punctuation = re.escape(r'`~@#$%^*+=[]{}:;"<>,.?\/')
3768+ punctuation = r"[^\w\s\-\&\|\!\(\)']"
3769+ query = re.sub(r"(?u)(\w)%s+(\w)" % (punctuation,), r"\1-\2", query)
3770+ query = re.sub(r"(?u)%s+" % (punctuation,), " ", query)
3771+ ## plpy.debug('3 query is %s' % repr(query))
3772+
3773+ # Strip ! characters inside and at the end of a word
3774+ query = re.sub(r"(?u)(?<=\w)[\!]+", " ", query)
3775+
3776+ # Now that we have handle case sensitive booleans, convert to lowercase
3777+ query = query.lower()
3778+
3779+ # Convert foo-bar to ((foo&bar)|foobar) and foo-bar-baz to
3780+ # ((foo&bar&baz)|foobarbaz)
3781+ def hyphen_repl(match):
3782+ bits = match.group(0).split("-")
3783+ return "((%s)|%s)" % ("&".join(bits), "".join(bits))
3784+ query = re.sub(r"(?u)\b\w+-[\w\-]+\b", hyphen_repl, query)
3785+ ## plpy.debug('4 query is %s' % repr(query))
3786+
3787+ # Any remaining - characters are spurious
3788+ query = query.replace('-','')
3789+
3790+ # Remove unpartnered bracket on the left and right
3791+ query = re.sub(r"(?ux) ^ ( [^(]* ) \)", r"(\1)", query)
3792+ query = re.sub(r"(?ux) \( ( [^)]* ) $", r"(\1)", query)
3793+
3794+ # Remove spurious brackets
3795+ query = re.sub(r"(?u)\(([^\&\|]*?)\)", r" \1 ", query)
3796+ ## plpy.debug('5 query is %s' % repr(query))
3797+
3798+ # Insert & between tokens without an existing boolean operator
3799+ # ( not proceeded by (|&!
3800+ query = re.sub(r"(?u)(?<![\(\|\&\!])\s*\(", "&(", query)
3801+ ## plpy.debug('6 query is %s' % repr(query))
3802+ # ) not followed by )|&
3803+ query = re.sub(r"(?u)\)(?!\s*(\)|\||\&|\s*$))", ")&", query)
3804+ ## plpy.debug('6.1 query is %s' % repr(query))
3805+ # Whitespace not proceded by (|&! not followed by &|
3806+ query = re.sub(r"(?u)(?<![\(\|\&\!\s])\s+(?![\&\|\s])", "&", query)
3807+ ## plpy.debug('7 query is %s' % repr(query))
3808+
3809+ # Detect and repair syntax errors - we are lenient because
3810+ # this input is generally from users.
3811+
3812+ # Fix unbalanced brackets
3813+ openings = query.count("(")
3814+ closings = query.count(")")
3815+ if openings > closings:
3816+ query = query + " ) "*(openings-closings)
3817+ elif closings > openings:
3818+ query = " ( "*(closings-openings) + query
3819+ ## plpy.debug('8 query is %s' % repr(query))
3820+
3821+ # Strip ' character that do not have letters on both sides
3822+ query = re.sub(r"(?u)((?<!\w)'|'(?!\w))", "", query)
3823+
3824+ # Brackets containing nothing but whitespace and booleans, recursive
3825+ last = ""
3826+ while last != query:
3827+ last = query
3828+ query = re.sub(r"(?u)\([\s\&\|\!]*\)", "", query)
3829+ ## plpy.debug('9 query is %s' % repr(query))
3830+
3831+ # An & or | following a (
3832+ query = re.sub(r"(?u)(?<=\()[\&\|\s]+", "", query)
3833+ ## plpy.debug('10 query is %s' % repr(query))
3834+
3835+ # An &, | or ! immediatly before a )
3836+ query = re.sub(r"(?u)[\&\|\!\s]*[\&\|\!]+\s*(?=\))", "", query)
3837+ ## plpy.debug('11 query is %s' % repr(query))
3838+
3839+ # An &,| or ! followed by another boolean.
3840+ query = re.sub(r"(?ux) \s* ( [\&\|\!] ) [\s\&\|]+", r"\1", query)
3841+ ## plpy.debug('12 query is %s' % repr(query))
3842+
3843+ # Leading & or |
3844+ query = re.sub(r"(?u)^[\s\&\|]+", "", query)
3845+ ## plpy.debug('13 query is %s' % repr(query))
3846+
3847+ # Trailing &, | or !
3848+ query = re.sub(r"(?u)[\&\|\!\s]+$", "", query)
3849+ ## plpy.debug('14 query is %s' % repr(query))
3850+
3851+ # If we have nothing but whitespace and tsearch2 operators,
3852+ # return NULL.
3853+ if re.search(r"(?u)^[\&\|\!\s\(\)]*$", query) is not None:
3854+ return None
3855+
3856+ # Convert back to UTF-8
3857+ query = query.encode('utf8')
3858+ ## plpy.debug('15 query is %s' % repr(query))
3859+
3860+ return query or None
3861+ $_$;
3862+
3863+
3864+CREATE FUNCTION _get_parser_from_curcfg() RETURNS text
3865+ LANGUAGE sql IMMUTABLE STRICT
3866+ AS $$select prsname::text from pg_catalog.pg_ts_parser p join pg_ts_config c on cfgparser = p.oid where c.oid = show_curcfg();$$;
3867+
3868+
3869+CREATE FUNCTION concat(pg_catalog.tsvector, pg_catalog.tsvector) RETURNS pg_catalog.tsvector
3870+ LANGUAGE internal IMMUTABLE STRICT
3871+ AS $$tsvector_concat$$;
3872+
3873+
3874+CREATE FUNCTION dex_init(internal) RETURNS internal
3875+ LANGUAGE c
3876+ AS '$libdir/tsearch2', 'tsa_dex_init';
3877+
3878+
3879+CREATE FUNCTION dex_lexize(internal, internal, integer) RETURNS internal
3880+ LANGUAGE c STRICT
3881+ AS '$libdir/tsearch2', 'tsa_dex_lexize';
3882+
3883+
3884+CREATE FUNCTION ftiupdate() RETURNS trigger
3885+ LANGUAGE plpythonu
3886+ AS $_$
3887+ new = TD["new"]
3888+ args = TD["args"][:]
3889+
3890+ # Short circuit if none of the relevant columns have been
3891+ # modified and fti is not being set to NULL (setting the fti
3892+ # column to NULL is thus how we can force a rebuild of the fti
3893+ # column).
3894+ if TD["event"] == "UPDATE" and new["fti"] != None:
3895+ old = TD["old"]
3896+ relevant_modification = False
3897+ for column_name in args[::2]:
3898+ if new[column_name] != old[column_name]:
3899+ relevant_modification = True
3900+ break
3901+ if not relevant_modification:
3902+ return "OK"
3903+
3904+ # Generate an SQL statement that turns the requested
3905+ # column values into a weighted tsvector
3906+ sql = []
3907+ for i in range(0, len(args), 2):
3908+ sql.append(
3909+ "ts2.setweight(ts2.to_tsvector('default', coalesce("
3910+ "substring(ltrim($%d) from 1 for 2500),'')),"
3911+ "CAST($%d AS \"char\"))" % (i + 1, i + 2))
3912+ args[i] = new[args[i]]
3913+
3914+ sql = "SELECT %s AS fti" % "||".join(sql)
3915+
3916+ # Execute and store in the fti column
3917+ plan = plpy.prepare(sql, ["text", "char"] * (len(args)/2))
3918+ new["fti"] = plpy.execute(plan, args, 1)[0]["fti"]
3919+
3920+ # Tell PostgreSQL we have modified the data
3921+ return "MODIFY"
3922+$_$;
3923+
3924+
3925+COMMENT ON FUNCTION ftiupdate() IS 'Trigger function that keeps the fti tsvector column up to date.';
3926+
3927+
3928+CREATE FUNCTION ftq(text) RETURNS pg_catalog.tsquery
3929+ LANGUAGE plpythonu IMMUTABLE STRICT
3930+ AS $_$
3931+ import re
3932+
3933+ # I think this method would be more robust if we used a real
3934+ # tokenizer and parser to generate the query string, but we need
3935+ # something suitable for use as a stored procedure which currently
3936+ # means no external dependancies.
3937+
3938+ # Convert to Unicode
3939+ query = args[0].decode('utf8')
3940+ ## plpy.debug('1 query is %s' % repr(query))
3941+
3942+ # Normalize whitespace
3943+ query = re.sub("(?u)\s+"," ", query)
3944+
3945+ # Convert AND, OR, NOT and - to tsearch2 punctuation
3946+ query = re.sub(r"(?u)(?:^|\s)-([\w\(])", r" !\1", query)
3947+ query = re.sub(r"(?u)\bAND\b", "&", query)
3948+ query = re.sub(r"(?u)\bOR\b", "|", query)
3949+ query = re.sub(r"(?u)\bNOT\b", " !", query)
3950+ ## plpy.debug('2 query is %s' % repr(query))
3951+
3952+ # Deal with unwanted punctuation. We convert strings of punctuation
3953+ # inside words to a '-' character for the hypenation handling below
3954+ # to deal with further. Outside of words we replace with whitespace.
3955+ # We don't mess with -&|!()' as they are handled later.
3956+ #punctuation = re.escape(r'`~@#$%^*+=[]{}:;"<>,.?\/')
3957+ punctuation = r"[^\w\s\-\&\|\!\(\)']"
3958+ query = re.sub(r"(?u)(\w)%s+(\w)" % (punctuation,), r"\1-\2", query)
3959+ query = re.sub(r"(?u)%s+" % (punctuation,), " ", query)
3960+ ## plpy.debug('3 query is %s' % repr(query))
3961+
3962+ # Strip ! characters inside and at the end of a word
3963+ query = re.sub(r"(?u)(?<=\w)[\!]+", " ", query)
3964+
3965+ # Now that we have handle case sensitive booleans, convert to lowercase
3966+ query = query.lower()
3967+
3968+ # Convert foo-bar to ((foo&bar)|foobar) and foo-bar-baz to
3969+ # ((foo&bar&baz)|foobarbaz)
3970+ def hyphen_repl(match):
3971+ bits = match.group(0).split("-")
3972+ return "((%s)|%s)" % ("&".join(bits), "".join(bits))
3973+ query = re.sub(r"(?u)\b\w+-[\w\-]+\b", hyphen_repl, query)
3974+ ## plpy.debug('4 query is %s' % repr(query))
3975+
3976+ # Any remaining - characters are spurious
3977+ query = query.replace('-','')
3978+
3979+ # Remove unpartnered bracket on the left and right
3980+ query = re.sub(r"(?ux) ^ ( [^(]* ) \)", r"(\1)", query)
3981+ query = re.sub(r"(?ux) \( ( [^)]* ) $", r"(\1)", query)
3982+
3983+ # Remove spurious brackets
3984+ query = re.sub(r"(?u)\(([^\&\|]*?)\)", r" \1 ", query)
3985+ ## plpy.debug('5 query is %s' % repr(query))
3986+
3987+ # Insert & between tokens without an existing boolean operator
3988+ # ( not proceeded by (|&!
3989+ query = re.sub(r"(?u)(?<![\(\|\&\!])\s*\(", "&(", query)
3990+ ## plpy.debug('6 query is %s' % repr(query))
3991+ # ) not followed by )|&
3992+ query = re.sub(r"(?u)\)(?!\s*(\)|\||\&|\s*$))", ")&", query)
3993+ ## plpy.debug('6.1 query is %s' % repr(query))
3994+ # Whitespace not proceded by (|&! not followed by &|
3995+ query = re.sub(r"(?u)(?<![\(\|\&\!\s])\s+(?![\&\|\s])", "&", query)
3996+ ## plpy.debug('7 query is %s' % repr(query))
3997+
3998+ # Detect and repair syntax errors - we are lenient because
3999+ # this input is generally from users.
4000+
4001+ # Fix unbalanced brackets
4002+ openings = query.count("(")
4003+ closings = query.count(")")
4004+ if openings > closings:
4005+ query = query + " ) "*(openings-closings)
4006+ elif closings > openings:
4007+ query = " ( "*(closings-openings) + query
4008+ ## plpy.debug('8 query is %s' % repr(query))
4009+
4010+ # Strip ' character that do not have letters on both sides
4011+ query = re.sub(r"(?u)((?<!\w)'|'(?!\w))", "", query)
4012+
4013+ # Brackets containing nothing but whitespace and booleans, recursive
4014+ last = ""
4015+ while last != query:
4016+ last = query
4017+ query = re.sub(r"(?u)\([\s\&\|\!]*\)", "", query)
4018+ ## plpy.debug('9 query is %s' % repr(query))
4019+
4020+ # An & or | following a (
4021+ query = re.sub(r"(?u)(?<=\()[\&\|\s]+", "", query)
4022+ ## plpy.debug('10 query is %s' % repr(query))
4023+
4024+ # An &, | or ! immediatly before a )
4025+ query = re.sub(r"(?u)[\&\|\!\s]*[\&\|\!]+\s*(?=\))", "", query)
4026+ ## plpy.debug('11 query is %s' % repr(query))
4027+
4028+ # An &,| or ! followed by another boolean.
4029+ query = re.sub(r"(?ux) \s* ( [\&\|\!] ) [\s\&\|]+", r"\1", query)
4030+ ## plpy.debug('12 query is %s' % repr(query))
4031+
4032+ # Leading & or |
4033+ query = re.sub(r"(?u)^[\s\&\|]+", "", query)
4034+ ## plpy.debug('13 query is %s' % repr(query))
4035+
4036+ # Trailing &, | or !
4037+ query = re.sub(r"(?u)[\&\|\!\s]+$", "", query)
4038+ ## plpy.debug('14 query is %s' % repr(query))
4039+
4040+ # If we have nothing but whitespace and tsearch2 operators,
4041+ # return NULL.
4042+ if re.search(r"(?u)^[\&\|\!\s\(\)]*$", query) is not None:
4043+ return None
4044+
4045+ # Convert back to UTF-8
4046+ query = query.encode('utf8')
4047+ ## plpy.debug('15 query is %s' % repr(query))
4048+
4049+ p = plpy.prepare("SELECT to_tsquery('default', $1) AS x", ["text"])
4050+ query = plpy.execute(p, [query], 1)[0]["x"]
4051+ return query or None
4052+ $_$;
4053+
4054+
4055+COMMENT ON FUNCTION ftq(text) IS 'Convert a string to an unparsed tsearch2 query';
4056+
4057+
4058+CREATE FUNCTION get_covers(pg_catalog.tsvector, pg_catalog.tsquery) RETURNS text
4059+ LANGUAGE c STRICT
4060+ AS '$libdir/tsearch2', 'tsa_get_covers';
4061+
4062+
4063+CREATE FUNCTION headline(oid, text, pg_catalog.tsquery, text) RETURNS text
4064+ LANGUAGE internal IMMUTABLE STRICT
4065+ AS $$ts_headline_byid_opt$$;
4066+
4067+
4068+CREATE FUNCTION headline(oid, text, pg_catalog.tsquery) RETURNS text
4069+ LANGUAGE internal IMMUTABLE STRICT
4070+ AS $$ts_headline_byid$$;
4071+
4072+
4073+CREATE FUNCTION headline(text, text, pg_catalog.tsquery, text) RETURNS text
4074+ LANGUAGE c IMMUTABLE STRICT
4075+ AS '$libdir/tsearch2', 'tsa_headline_byname';
4076+
4077+
4078+CREATE FUNCTION headline(text, text, pg_catalog.tsquery) RETURNS text
4079+ LANGUAGE c IMMUTABLE STRICT
4080+ AS '$libdir/tsearch2', 'tsa_headline_byname';
4081+
4082+
4083+CREATE FUNCTION headline(text, pg_catalog.tsquery, text) RETURNS text
4084+ LANGUAGE internal IMMUTABLE STRICT
4085+ AS $$ts_headline_opt$$;
4086+
4087+
4088+CREATE FUNCTION headline(text, pg_catalog.tsquery) RETURNS text
4089+ LANGUAGE internal IMMUTABLE STRICT
4090+ AS $$ts_headline$$;
4091+
4092+
4093+CREATE FUNCTION length(pg_catalog.tsvector) RETURNS integer
4094+ LANGUAGE internal IMMUTABLE STRICT
4095+ AS $$tsvector_length$$;
4096+
4097+
4098+CREATE FUNCTION lexize(oid, text) RETURNS text[]
4099+ LANGUAGE internal STRICT
4100+ AS $$ts_lexize$$;
4101+
4102+
4103+CREATE FUNCTION lexize(text, text) RETURNS text[]
4104+ LANGUAGE c STRICT
4105+ AS '$libdir/tsearch2', 'tsa_lexize_byname';
4106+
4107+
4108+CREATE FUNCTION lexize(text) RETURNS text[]
4109+ LANGUAGE c STRICT
4110+ AS '$libdir/tsearch2', 'tsa_lexize_bycurrent';
4111+
4112+
4113+CREATE FUNCTION numnode(pg_catalog.tsquery) RETURNS integer
4114+ LANGUAGE internal IMMUTABLE STRICT
4115+ AS $$tsquery_numnode$$;
4116+
4117+
4118+CREATE FUNCTION parse(oid, text) RETURNS SETOF tokenout
4119+ LANGUAGE internal STRICT
4120+ AS $$ts_parse_byid$$;
4121+
4122+
4123+CREATE FUNCTION parse(text, text) RETURNS SETOF tokenout
4124+ LANGUAGE internal STRICT
4125+ AS $$ts_parse_byname$$;
4126+
4127+
4128+CREATE FUNCTION parse(text) RETURNS SETOF tokenout
4129+ LANGUAGE c STRICT
4130+ AS '$libdir/tsearch2', 'tsa_parse_current';
4131+
4132+
4133+CREATE FUNCTION plainto_tsquery(oid, text) RETURNS pg_catalog.tsquery
4134+ LANGUAGE internal IMMUTABLE STRICT
4135+ AS $$plainto_tsquery_byid$$;
4136+
4137+
4138+CREATE FUNCTION plainto_tsquery(text, text) RETURNS pg_catalog.tsquery
4139+ LANGUAGE c IMMUTABLE STRICT
4140+ AS '$libdir/tsearch2', 'tsa_plainto_tsquery_name';
4141+
4142+
4143+CREATE FUNCTION plainto_tsquery(text) RETURNS pg_catalog.tsquery
4144+ LANGUAGE internal IMMUTABLE STRICT
4145+ AS $$plainto_tsquery$$;
4146+
4147+
4148+CREATE FUNCTION prsd_end(internal) RETURNS void
4149+ LANGUAGE c
4150+ AS '$libdir/tsearch2', 'tsa_prsd_end';
4151+
4152+
4153+CREATE FUNCTION prsd_getlexeme(internal, internal, internal) RETURNS integer
4154+ LANGUAGE c
4155+ AS '$libdir/tsearch2', 'tsa_prsd_getlexeme';
4156+
4157+
4158+CREATE FUNCTION prsd_headline(internal, internal, internal) RETURNS internal
4159+ LANGUAGE c
4160+ AS '$libdir/tsearch2', 'tsa_prsd_headline';
4161+
4162+
4163+CREATE FUNCTION prsd_lextype(internal) RETURNS internal
4164+ LANGUAGE c
4165+ AS '$libdir/tsearch2', 'tsa_prsd_lextype';
4166+
4167+
4168+CREATE FUNCTION prsd_start(internal, integer) RETURNS internal
4169+ LANGUAGE c
4170+ AS '$libdir/tsearch2', 'tsa_prsd_start';
4171+
4172+
4173+CREATE FUNCTION querytree(pg_catalog.tsquery) RETURNS text
4174+ LANGUAGE internal STRICT
4175+ AS $$tsquerytree$$;
4176+
4177+
4178+CREATE FUNCTION rank(real[], pg_catalog.tsvector, pg_catalog.tsquery) RETURNS real
4179+ LANGUAGE internal IMMUTABLE STRICT
4180+ AS $$ts_rank_wtt$$;
4181+
4182+
4183+CREATE FUNCTION rank(real[], pg_catalog.tsvector, pg_catalog.tsquery, integer) RETURNS real
4184+ LANGUAGE internal IMMUTABLE STRICT
4185+ AS $$ts_rank_wttf$$;
4186+
4187+
4188+CREATE FUNCTION rank(pg_catalog.tsvector, pg_catalog.tsquery) RETURNS real
4189+ LANGUAGE internal IMMUTABLE STRICT
4190+ AS $$ts_rank_tt$$;
4191+
4192+
4193+CREATE FUNCTION rank(pg_catalog.tsvector, pg_catalog.tsquery, integer) RETURNS real
4194+ LANGUAGE internal IMMUTABLE STRICT
4195+ AS $$ts_rank_ttf$$;
4196+
4197+
4198+CREATE FUNCTION rank_cd(real[], pg_catalog.tsvector, pg_catalog.tsquery) RETURNS real
4199+ LANGUAGE internal IMMUTABLE STRICT
4200+ AS $$ts_rankcd_wtt$$;
4201+
4202+
4203+CREATE FUNCTION rank_cd(real[], pg_catalog.tsvector, pg_catalog.tsquery, integer) RETURNS real
4204+ LANGUAGE internal IMMUTABLE STRICT
4205+ AS $$ts_rankcd_wttf$$;
4206+
4207+
4208+CREATE FUNCTION rank_cd(pg_catalog.tsvector, pg_catalog.tsquery) RETURNS real
4209+ LANGUAGE internal IMMUTABLE STRICT
4210+ AS $$ts_rankcd_tt$$;
4211+
4212+
4213+CREATE FUNCTION rank_cd(pg_catalog.tsvector, pg_catalog.tsquery, integer) RETURNS real
4214+ LANGUAGE internal IMMUTABLE STRICT
4215+ AS $$ts_rankcd_ttf$$;
4216+
4217+
4218+CREATE FUNCTION reset_tsearch() RETURNS void
4219+ LANGUAGE c STRICT
4220+ AS '$libdir/tsearch2', 'tsa_reset_tsearch';
4221+
4222+
4223+CREATE FUNCTION rewrite(pg_catalog.tsquery, text) RETURNS pg_catalog.tsquery
4224+ LANGUAGE internal IMMUTABLE STRICT
4225+ AS $$tsquery_rewrite_query$$;
4226+
4227+
4228+CREATE FUNCTION rewrite(pg_catalog.tsquery, pg_catalog.tsquery, pg_catalog.tsquery) RETURNS pg_catalog.tsquery
4229+ LANGUAGE internal IMMUTABLE STRICT
4230+ AS $$tsquery_rewrite$$;
4231+
4232+
4233+CREATE FUNCTION rewrite_accum(pg_catalog.tsquery, pg_catalog.tsquery[]) RETURNS pg_catalog.tsquery
4234+ LANGUAGE c
4235+ AS '$libdir/tsearch2', 'tsa_rewrite_accum';
4236+
4237+
4238+CREATE FUNCTION rewrite_finish(pg_catalog.tsquery) RETURNS pg_catalog.tsquery
4239+ LANGUAGE c
4240+ AS '$libdir/tsearch2', 'tsa_rewrite_finish';
4241+
4242+
4243+CREATE FUNCTION set_curcfg(integer) RETURNS void
4244+ LANGUAGE c STRICT
4245+ AS '$libdir/tsearch2', 'tsa_set_curcfg';
4246+
4247+
4248+CREATE FUNCTION set_curcfg(text) RETURNS void
4249+ LANGUAGE c STRICT
4250+ AS '$libdir/tsearch2', 'tsa_set_curcfg_byname';
4251+
4252+
4253+CREATE FUNCTION set_curdict(integer) RETURNS void
4254+ LANGUAGE c STRICT
4255+ AS '$libdir/tsearch2', 'tsa_set_curdict';
4256+
4257+
4258+CREATE FUNCTION set_curdict(text) RETURNS void
4259+ LANGUAGE c STRICT
4260+ AS '$libdir/tsearch2', 'tsa_set_curdict_byname';
4261+
4262+
4263+CREATE FUNCTION set_curprs(integer) RETURNS void
4264+ LANGUAGE c STRICT
4265+ AS '$libdir/tsearch2', 'tsa_set_curprs';
4266+
4267+
4268+CREATE FUNCTION set_curprs(text) RETURNS void
4269+ LANGUAGE c STRICT
4270+ AS '$libdir/tsearch2', 'tsa_set_curprs_byname';
4271+
4272+
4273+CREATE FUNCTION setweight(pg_catalog.tsvector, "char") RETURNS pg_catalog.tsvector
4274+ LANGUAGE internal IMMUTABLE STRICT
4275+ AS $$tsvector_setweight$$;
4276+
4277+
4278+CREATE FUNCTION show_curcfg() RETURNS oid
4279+ LANGUAGE internal STABLE STRICT
4280+ AS $$get_current_ts_config$$;
4281+
4282+
4283+CREATE FUNCTION snb_en_init(internal) RETURNS internal
4284+ LANGUAGE c
4285+ AS '$libdir/tsearch2', 'tsa_snb_en_init';
4286+
4287+
4288+CREATE FUNCTION snb_lexize(internal, internal, integer) RETURNS internal
4289+ LANGUAGE c STRICT
4290+ AS '$libdir/tsearch2', 'tsa_snb_lexize';
4291+
4292+
4293+CREATE FUNCTION snb_ru_init(internal) RETURNS internal
4294+ LANGUAGE c
4295+ AS '$libdir/tsearch2', 'tsa_snb_ru_init';
4296+
4297+
4298+CREATE FUNCTION snb_ru_init_koi8(internal) RETURNS internal
4299+ LANGUAGE c
4300+ AS '$libdir/tsearch2', 'tsa_snb_ru_init_koi8';
4301+
4302+
4303+CREATE FUNCTION snb_ru_init_utf8(internal) RETURNS internal
4304+ LANGUAGE c
4305+ AS '$libdir/tsearch2', 'tsa_snb_ru_init_utf8';
4306+
4307+
4308+CREATE FUNCTION spell_init(internal) RETURNS internal
4309+ LANGUAGE c
4310+ AS '$libdir/tsearch2', 'tsa_spell_init';
4311+
4312+
4313+CREATE FUNCTION spell_lexize(internal, internal, integer) RETURNS internal
4314+ LANGUAGE c STRICT
4315+ AS '$libdir/tsearch2', 'tsa_spell_lexize';
4316+
4317+
4318+CREATE FUNCTION stat(text) RETURNS SETOF statinfo
4319+ LANGUAGE internal STRICT
4320+ AS $$ts_stat1$$;
4321+
4322+
4323+CREATE FUNCTION stat(text, text) RETURNS SETOF statinfo
4324+ LANGUAGE internal STRICT
4325+ AS $$ts_stat2$$;
4326+
4327+
4328+CREATE FUNCTION strip(pg_catalog.tsvector) RETURNS pg_catalog.tsvector
4329+ LANGUAGE internal IMMUTABLE STRICT
4330+ AS $$tsvector_strip$$;
4331+
4332+
4333+CREATE FUNCTION syn_init(internal) RETURNS internal
4334+ LANGUAGE c
4335+ AS '$libdir/tsearch2', 'tsa_syn_init';
4336+
4337+
4338+CREATE FUNCTION syn_lexize(internal, internal, integer) RETURNS internal
4339+ LANGUAGE c STRICT
4340+ AS '$libdir/tsearch2', 'tsa_syn_lexize';
4341+
4342+
4343+CREATE FUNCTION thesaurus_init(internal) RETURNS internal
4344+ LANGUAGE c
4345+ AS '$libdir/tsearch2', 'tsa_thesaurus_init';
4346+
4347+
4348+CREATE FUNCTION thesaurus_lexize(internal, internal, integer, internal) RETURNS internal
4349+ LANGUAGE c STRICT
4350+ AS '$libdir/tsearch2', 'tsa_thesaurus_lexize';
4351+
4352+
4353+CREATE FUNCTION to_tsquery(oid, text) RETURNS pg_catalog.tsquery
4354+ LANGUAGE internal IMMUTABLE STRICT
4355+ AS $$to_tsquery_byid$$;
4356+
4357+
4358+CREATE FUNCTION to_tsquery(text, text) RETURNS pg_catalog.tsquery
4359+ LANGUAGE c IMMUTABLE STRICT
4360+ AS '$libdir/tsearch2', 'tsa_to_tsquery_name';
4361+
4362+
4363+CREATE FUNCTION to_tsquery(text) RETURNS pg_catalog.tsquery
4364+ LANGUAGE internal IMMUTABLE STRICT
4365+ AS $$to_tsquery$$;
4366+
4367+
4368+CREATE FUNCTION to_tsvector(oid, text) RETURNS pg_catalog.tsvector
4369+ LANGUAGE internal IMMUTABLE STRICT
4370+ AS $$to_tsvector_byid$$;
4371+
4372+
4373+CREATE FUNCTION to_tsvector(text, text) RETURNS pg_catalog.tsvector
4374+ LANGUAGE c IMMUTABLE STRICT
4375+ AS '$libdir/tsearch2', 'tsa_to_tsvector_name';
4376+
4377+
4378+CREATE FUNCTION to_tsvector(text) RETURNS pg_catalog.tsvector
4379+ LANGUAGE internal IMMUTABLE STRICT
4380+ AS $$to_tsvector$$;
4381+
4382+
4383+CREATE FUNCTION token_type(integer) RETURNS SETOF tokentype
4384+ LANGUAGE internal STRICT ROWS 16
4385+ AS $$ts_token_type_byid$$;
4386+
4387+
4388+CREATE FUNCTION token_type(text) RETURNS SETOF tokentype
4389+ LANGUAGE internal STRICT ROWS 16
4390+ AS $$ts_token_type_byname$$;
4391+
4392+
4393+CREATE FUNCTION token_type() RETURNS SETOF tokentype
4394+ LANGUAGE c STRICT ROWS 16
4395+ AS '$libdir/tsearch2', 'tsa_token_type_current';
4396+
4397+
4398+CREATE FUNCTION ts_debug(text) RETURNS SETOF tsdebug
4399+ LANGUAGE sql STRICT
4400+ AS $_$
4401+select
4402+ (select c.cfgname::text from pg_catalog.pg_ts_config as c
4403+ where c.oid = show_curcfg()),
4404+ t.alias as tok_type,
4405+ t.descr as description,
4406+ p.token,
4407+ ARRAY ( SELECT m.mapdict::pg_catalog.regdictionary::pg_catalog.text
4408+ FROM pg_catalog.pg_ts_config_map AS m
4409+ WHERE m.mapcfg = show_curcfg() AND m.maptokentype = p.tokid
4410+ ORDER BY m.mapseqno )
4411+ AS dict_name,
4412+ strip(to_tsvector(p.token)) as tsvector
4413+from
4414+ parse( _get_parser_from_curcfg(), $1 ) as p,
4415+ token_type() as t
4416+where
4417+ t.tokid = p.tokid
4418+$_$;
4419+
4420+
4421+CREATE FUNCTION tsearch2() RETURNS trigger
4422+ LANGUAGE c
4423+ AS '$libdir/tsearch2', 'tsa_tsearch2';
4424+
4425+
4426+CREATE FUNCTION tsq_mcontained(pg_catalog.tsquery, pg_catalog.tsquery) RETURNS boolean
4427+ LANGUAGE internal IMMUTABLE STRICT
4428+ AS $$tsq_mcontained$$;
4429+
4430+
4431+CREATE FUNCTION tsq_mcontains(pg_catalog.tsquery, pg_catalog.tsquery) RETURNS boolean
4432+ LANGUAGE internal IMMUTABLE STRICT
4433+ AS $$tsq_mcontains$$;
4434+
4435+
4436+CREATE FUNCTION tsquery_and(pg_catalog.tsquery, pg_catalog.tsquery) RETURNS pg_catalog.tsquery
4437+ LANGUAGE internal IMMUTABLE STRICT
4438+ AS $$tsquery_and$$;
4439+
4440+
4441+CREATE FUNCTION tsquery_not(pg_catalog.tsquery) RETURNS pg_catalog.tsquery
4442+ LANGUAGE internal IMMUTABLE STRICT
4443+ AS $$tsquery_not$$;
4444+
4445+
4446+CREATE FUNCTION tsquery_or(pg_catalog.tsquery, pg_catalog.tsquery) RETURNS pg_catalog.tsquery
4447+ LANGUAGE internal IMMUTABLE STRICT
4448+ AS $$tsquery_or$$;
4449+
4450+
4451+SET search_path = public, pg_catalog;
4452+
4453+CREATE OPERATOR > (
4454+ PROCEDURE = debversion_gt,
4455+ LEFTARG = debversion,
4456+ RIGHTARG = debversion,
4457+ COMMUTATOR = <,
4458+ NEGATOR = >=
4459+);
4460+
4461+
4462+COMMENT ON OPERATOR > (debversion, debversion) IS 'debversion greater-than';
4463+
4464+
4465+CREATE AGGREGATE max(debversion) (
4466+ SFUNC = debversion_larger,
4467+ STYPE = debversion,
4468+ SORTOP = >
4469+);
4470+
4471+
4472+CREATE OPERATOR < (
4473+ PROCEDURE = debversion_lt,
4474+ LEFTARG = debversion,
4475+ RIGHTARG = debversion,
4476+ COMMUTATOR = >,
4477+ NEGATOR = >=
4478+);
4479+
4480+
4481+COMMENT ON OPERATOR < (debversion, debversion) IS 'debversion less-than';
4482+
4483+
4484+CREATE AGGREGATE min(debversion) (
4485+ SFUNC = debversion_smaller,
4486+ STYPE = debversion,
4487+ SORTOP = <
4488+);
4489+
4490+
4491+SET search_path = ts2, pg_catalog;
4492+
4493+CREATE AGGREGATE rewrite(pg_catalog.tsquery[]) (
4494+ SFUNC = rewrite_accum,
4495+ STYPE = pg_catalog.tsquery,
4496+ FINALFUNC = rewrite_finish
4497+);
4498+
4499+
4500+SET search_path = public, pg_catalog;
4501+
4502+CREATE OPERATOR <= (
4503+ PROCEDURE = debversion_le,
4504+ LEFTARG = debversion,
4505+ RIGHTARG = debversion,
4506+ COMMUTATOR = >=,
4507+ NEGATOR = >
4508+);
4509+
4510+
4511+COMMENT ON OPERATOR <= (debversion, debversion) IS 'debversion less-than-or-equal';
4512+
4513+
4514+CREATE OPERATOR <> (
4515+ PROCEDURE = debversion_ne,
4516+ LEFTARG = debversion,
4517+ RIGHTARG = debversion,
4518+ COMMUTATOR = <>,
4519+ NEGATOR = =
4520+);
4521+
4522+
4523+COMMENT ON OPERATOR <> (debversion, debversion) IS 'debversion not equal';
4524+
4525+
4526+CREATE OPERATOR = (
4527+ PROCEDURE = debversion_eq,
4528+ LEFTARG = debversion,
4529+ RIGHTARG = debversion,
4530+ COMMUTATOR = =,
4531+ NEGATOR = <>
4532+);
4533+
4534+
4535+COMMENT ON OPERATOR = (debversion, debversion) IS 'debversion equal';
4536+
4537+
4538+CREATE OPERATOR >= (
4539+ PROCEDURE = debversion_ge,
4540+ LEFTARG = debversion,
4541+ RIGHTARG = debversion,
4542+ COMMUTATOR = <=,
4543+ NEGATOR = <
4544+);
4545+
4546+
4547+COMMENT ON OPERATOR >= (debversion, debversion) IS 'debversion greater-than-or-equal';
4548+
4549+
4550+CREATE OPERATOR FAMILY debversion_ops USING btree;
4551+
4552+
4553+CREATE OPERATOR CLASS debversion_ops
4554+ DEFAULT FOR TYPE debversion USING btree AS
4555+ OPERATOR 1 <(debversion,debversion) ,
4556+ OPERATOR 2 <=(debversion,debversion) ,
4557+ OPERATOR 3 =(debversion,debversion) ,
4558+ OPERATOR 4 >=(debversion,debversion) ,
4559+ OPERATOR 5 >(debversion,debversion) ,
4560+ FUNCTION 1 debversion_cmp(debversion,debversion);
4561+
4562+
4563+CREATE OPERATOR FAMILY debversion_ops USING hash;
4564+
4565+
4566+CREATE OPERATOR CLASS debversion_ops
4567+ DEFAULT FOR TYPE debversion USING hash AS
4568+ OPERATOR 1 =(debversion,debversion) ,
4569+ FUNCTION 1 debversion_hash(debversion);
4570+
4571+
4572+SET search_path = ts2, pg_catalog;
4573+
4574+CREATE OPERATOR FAMILY tsquery_ops USING btree;
4575+
4576+
4577+CREATE OPERATOR CLASS tsquery_ops
4578+ FOR TYPE pg_catalog.tsquery USING btree AS
4579+ OPERATOR 1 <(pg_catalog.tsquery,pg_catalog.tsquery) ,
4580+ OPERATOR 2 <=(pg_catalog.tsquery,pg_catalog.tsquery) ,
4581+ OPERATOR 3 =(pg_catalog.tsquery,pg_catalog.tsquery) ,
4582+ OPERATOR 4 >=(pg_catalog.tsquery,pg_catalog.tsquery) ,
4583+ OPERATOR 5 >(pg_catalog.tsquery,pg_catalog.tsquery) ,
4584+ FUNCTION 1 tsquery_cmp(pg_catalog.tsquery,pg_catalog.tsquery);
4585+
4586+
4587+CREATE OPERATOR FAMILY tsvector_ops USING btree;
4588+
4589+
4590+CREATE OPERATOR CLASS tsvector_ops
4591+ FOR TYPE pg_catalog.tsvector USING btree AS
4592+ OPERATOR 1 <(pg_catalog.tsvector,pg_catalog.tsvector) ,
4593+ OPERATOR 2 <=(pg_catalog.tsvector,pg_catalog.tsvector) ,
4594+ OPERATOR 3 =(pg_catalog.tsvector,pg_catalog.tsvector) ,
4595+ OPERATOR 4 >=(pg_catalog.tsvector,pg_catalog.tsvector) ,
4596+ OPERATOR 5 >(pg_catalog.tsvector,pg_catalog.tsvector) ,
4597+ FUNCTION 1 tsvector_cmp(pg_catalog.tsvector,pg_catalog.tsvector);
4598+
4599+
4600+SET search_path = pg_catalog;
4601+
4602+CREATE CAST (character AS public.debversion) WITH FUNCTION public.debversion(character);
4603+
4604+
4605+CREATE CAST (public.debversion AS character) WITHOUT FUNCTION AS ASSIGNMENT;
4606+
4607+
4608+CREATE CAST (public.debversion AS text) WITHOUT FUNCTION AS IMPLICIT;
4609+
4610+
4611+CREATE CAST (public.debversion AS character varying) WITHOUT FUNCTION AS IMPLICIT;
4612+
4613+
4614+CREATE CAST (text AS public.debversion) WITHOUT FUNCTION AS ASSIGNMENT;
4615+
4616+
4617+CREATE CAST (character varying AS public.debversion) WITHOUT FUNCTION AS ASSIGNMENT;
4618+
4619+
4620+SET search_path = ts2, pg_catalog;
4621+
4622+CREATE TEXT SEARCH CONFIGURATION "default" (
4623+ PARSER = pg_catalog."default" );
4624+
4625+ALTER TEXT SEARCH CONFIGURATION "default"
4626+ ADD MAPPING FOR asciiword WITH english_stem;
4627+
4628+ALTER TEXT SEARCH CONFIGURATION "default"
4629+ ADD MAPPING FOR word WITH english_stem;
4630+
4631+ALTER TEXT SEARCH CONFIGURATION "default"
4632+ ADD MAPPING FOR numword WITH simple;
4633+
4634+ALTER TEXT SEARCH CONFIGURATION "default"
4635+ ADD MAPPING FOR email WITH simple;
4636+
4637+ALTER TEXT SEARCH CONFIGURATION "default"
4638+ ADD MAPPING FOR url WITH simple;
4639+
4640+ALTER TEXT SEARCH CONFIGURATION "default"
4641+ ADD MAPPING FOR host WITH simple;
4642+
4643+ALTER TEXT SEARCH CONFIGURATION "default"
4644+ ADD MAPPING FOR sfloat WITH simple;
4645+
4646+ALTER TEXT SEARCH CONFIGURATION "default"
4647+ ADD MAPPING FOR version WITH simple;
4648+
4649+ALTER TEXT SEARCH CONFIGURATION "default"
4650+ ADD MAPPING FOR hword_numpart WITH simple;
4651+
4652+ALTER TEXT SEARCH CONFIGURATION "default"
4653+ ADD MAPPING FOR hword_part WITH english_stem;
4654+
4655+ALTER TEXT SEARCH CONFIGURATION "default"
4656+ ADD MAPPING FOR hword_asciipart WITH english_stem;
4657+
4658+ALTER TEXT SEARCH CONFIGURATION "default"
4659+ ADD MAPPING FOR numhword WITH simple;
4660+
4661+ALTER TEXT SEARCH CONFIGURATION "default"
4662+ ADD MAPPING FOR asciihword WITH english_stem;
4663+
4664+ALTER TEXT SEARCH CONFIGURATION "default"
4665+ ADD MAPPING FOR hword WITH english_stem;
4666+
4667+ALTER TEXT SEARCH CONFIGURATION "default"
4668+ ADD MAPPING FOR url_path WITH simple;
4669+
4670+ALTER TEXT SEARCH CONFIGURATION "default"
4671+ ADD MAPPING FOR file WITH simple;
4672+
4673+ALTER TEXT SEARCH CONFIGURATION "default"
4674+ ADD MAPPING FOR "float" WITH simple;
4675+
4676+ALTER TEXT SEARCH CONFIGURATION "default"
4677+ ADD MAPPING FOR "int" WITH simple;
4678+
4679+ALTER TEXT SEARCH CONFIGURATION "default"
4680+ ADD MAPPING FOR uint WITH simple;
4681+
4682+
4683+SET search_path = public, pg_catalog;
4684+
4685+CREATE TABLE accesspolicy (
4686+ id integer NOT NULL,
4687+ product integer,
4688+ distribution integer,
4689+ type integer NOT NULL,
4690+ CONSTRAINT has_target CHECK (((product IS NULL) <> (distribution IS NULL)))
4691+);
4692+
4693+
4694+CREATE SEQUENCE accesspolicy_id_seq
4695+ START WITH 1
4696+ INCREMENT BY 1
4697+ NO MAXVALUE
4698+ NO MINVALUE
4699+ CACHE 1;
4700+
4701+
4702+ALTER SEQUENCE accesspolicy_id_seq OWNED BY accesspolicy.id;
4703+
4704+
4705+CREATE TABLE accesspolicyartifact (
4706+ id integer NOT NULL,
4707+ bug integer,
4708+ branch integer,
4709+ policy integer,
4710+ CONSTRAINT has_artifact CHECK (((bug IS NULL) <> (branch IS NULL)))
4711+);
4712+
4713+
4714+CREATE SEQUENCE accesspolicyartifact_id_seq
4715+ START WITH 1
4716+ INCREMENT BY 1
4717+ NO MAXVALUE
4718+ NO MINVALUE
4719+ CACHE 1;
4720+
4721+
4722+ALTER SEQUENCE accesspolicyartifact_id_seq OWNED BY accesspolicyartifact.id;
4723+
4724+
4725+CREATE TABLE accesspolicygrant (
4726+ id integer NOT NULL,
4727+ grantee integer NOT NULL,
4728+ grantor integer NOT NULL,
4729+ date_created timestamp without time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
4730+ policy integer,
4731+ artifact integer,
4732+ CONSTRAINT has_target CHECK (((policy IS NULL) <> (artifact IS NULL)))
4733+);
4734+
4735+
4736+CREATE SEQUENCE accesspolicygrant_id_seq
4737+ START WITH 1
4738+ INCREMENT BY 1
4739+ NO MAXVALUE
4740+ NO MINVALUE
4741+ CACHE 1;
4742+
4743+
4744+ALTER SEQUENCE accesspolicygrant_id_seq OWNED BY accesspolicygrant.id;
4745+
4746
4747 CREATE TABLE account (
4748 id integer NOT NULL,
4749@@ -63,36 +4563,64 @@
4750 status integer NOT NULL,
4751 date_status_set timestamp without time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
4752 displayname text NOT NULL,
4753- openid_identifier text DEFAULT generate_openid_identifier() NOT NULL,
4754- status_comment text,
4755- old_openid_identifier text
4756+ status_comment text
4757 );
4758
4759+
4760+COMMENT ON TABLE account IS 'An account that may be used for authenticating to Canonical or other systems.';
4761+
4762+
4763+COMMENT ON COLUMN account.status IS 'The status of the account.';
4764+
4765+
4766+COMMENT ON COLUMN account.date_status_set IS 'When the status was last changed.';
4767+
4768+
4769+COMMENT ON COLUMN account.displayname IS 'Name to display when rendering information about this account.';
4770+
4771+
4772+COMMENT ON COLUMN account.status_comment IS 'The comment on the status of the account.';
4773+
4774+
4775 CREATE SEQUENCE account_id_seq
4776+ START WITH 1
4777 INCREMENT BY 1
4778 NO MAXVALUE
4779 NO MINVALUE
4780 CACHE 1;
4781
4782+
4783 ALTER SEQUENCE account_id_seq OWNED BY account.id;
4784
4785+
4786 CREATE TABLE accountpassword (
4787 id integer NOT NULL,
4788 account integer NOT NULL,
4789 password text NOT NULL
4790 );
4791
4792+
4793+COMMENT ON TABLE accountpassword IS 'A password used to authenticate an Account.';
4794+
4795+
4796+COMMENT ON COLUMN accountpassword.password IS 'SSHA digest encrypted password.';
4797+
4798+
4799 CREATE SEQUENCE accountpassword_id_seq
4800+ START WITH 1
4801 INCREMENT BY 1
4802 NO MAXVALUE
4803 NO MINVALUE
4804 CACHE 1;
4805
4806+
4807 ALTER SEQUENCE accountpassword_id_seq OWNED BY accountpassword.id;
4808
4809+
4810 CREATE VIEW alllocks AS
4811 SELECT a.procpid, a.usename, (now() - a.query_start) AS age, c.relname, l.mode, l.granted, a.current_query FROM ((pg_locks l JOIN pg_class c ON ((l.relation = c.oid))) LEFT JOIN pg_stat_activity a ON ((a.procpid = l.pid)));
4812
4813+
4814 CREATE TABLE announcement (
4815 id integer NOT NULL,
4816 date_created timestamp without time zone DEFAULT timezone('UTC'::text, now()) NOT NULL,
4817@@ -110,14 +4638,30 @@
4818 CONSTRAINT valid_url CHECK (valid_absolute_url(url))
4819 );
4820
4821+
4822+COMMENT ON TABLE announcement IS 'A project announcement. This is a single item of news or information that the project is communicating. Announcements can be attached to a Project, a Product or a Distribution.';
4823+
4824+
4825+COMMENT ON COLUMN announcement.date_announced IS 'The date at which an announcement will become public, if it is active. If this is not set then the announcement will not become public until someone consciously publishes it (which sets this date).';
4826+
4827+
4828+COMMENT ON COLUMN announcement.url IS 'A web location for the announcement itself.';
4829+
4830+
4831+COMMENT ON COLUMN announcement.active IS 'Whether or not the announcement is public. This is TRUE by default, but can be set to FALSE if the project "retracts" the announcement.';
4832+
4833+
4834 CREATE SEQUENCE announcement_id_seq
4835+ START WITH 1
4836 INCREMENT BY 1
4837 NO MAXVALUE
4838 NO MINVALUE
4839 CACHE 1;
4840
4841+
4842 ALTER SEQUENCE announcement_id_seq OWNED BY announcement.id;
4843
4844+
4845 CREATE TABLE answercontact (
4846 id integer NOT NULL,
4847 product integer,
4848@@ -128,14 +4672,36 @@
4849 CONSTRAINT valid_target CHECK ((((product IS NULL) <> (distribution IS NULL)) AND ((product IS NULL) OR (sourcepackagename IS NULL))))
4850 );
4851
4852+
4853+COMMENT ON TABLE answercontact IS 'Defines the answer contact for a given question target. The answer contact will be automatically notified about changes to any questions filed on the question target.';
4854+
4855+
4856+COMMENT ON COLUMN answercontact.product IS 'The product that the answer contact supports.';
4857+
4858+
4859+COMMENT ON COLUMN answercontact.distribution IS 'The distribution that the answer contact supports.';
4860+
4861+
4862+COMMENT ON COLUMN answercontact.sourcepackagename IS 'The sourcepackagename that the answer contact supports.';
4863+
4864+
4865+COMMENT ON COLUMN answercontact.person IS 'The person or team associated with the question target.';
4866+
4867+
4868+COMMENT ON COLUMN answercontact.date_created IS 'The date the answer contact was submitted.';
4869+
4870+
4871 CREATE SEQUENCE answercontact_id_seq
4872+ START WITH 1
4873 INCREMENT BY 1
4874 NO MAXVALUE
4875 NO MINVALUE
4876 CACHE 1;
4877
4878+
4879 ALTER SEQUENCE answercontact_id_seq OWNED BY answercontact.id;
4880
4881+
4882 CREATE TABLE apportjob (
4883 id integer NOT NULL,
4884 job integer NOT NULL,
4885@@ -144,14 +4710,30 @@
4886 json_data text
4887 );
4888
4889+
4890+COMMENT ON TABLE apportjob IS 'Contains references to jobs to be run against Apport BLOBs.';
4891+
4892+
4893+COMMENT ON COLUMN apportjob.blob IS 'The TemporaryBlobStorage entry on which the job is to be run.';
4894+
4895+
4896+COMMENT ON COLUMN apportjob.job_type IS 'The type of job (enumeration value). Allows us to query the database for a given subset of ApportJobs.';
4897+
4898+
4899+COMMENT ON COLUMN apportjob.json_data IS 'A JSON struct containing data for the job.';
4900+
4901+
4902 CREATE SEQUENCE apportjob_id_seq
4903+ START WITH 1
4904 INCREMENT BY 1
4905 NO MAXVALUE
4906 NO MINVALUE
4907 CACHE 1;
4908
4909+
4910 ALTER SEQUENCE apportjob_id_seq OWNED BY apportjob.id;
4911
4912+
4913 CREATE TABLE archive (
4914 id integer NOT NULL,
4915 owner integer NOT NULL,
4916@@ -189,28 +4771,132 @@
4917 CONSTRAINT valid_name CHECK (valid_name(name))
4918 );
4919
4920+
4921+COMMENT ON TABLE archive IS 'A package archive. Commonly either a distribution''s main_archive or a ppa''s archive.';
4922+
4923+
4924+COMMENT ON COLUMN archive.owner IS 'Identifies the PPA owner when it has one.';
4925+
4926+
4927+COMMENT ON COLUMN archive.description IS 'Allow users to describe their PPAs content.';
4928+
4929+
4930+COMMENT ON COLUMN archive.enabled IS 'Whether or not the PPA is enabled for accepting uploads.';
4931+
4932+
4933+COMMENT ON COLUMN archive.authorized_size IS 'Size, in MiB, allowed for this PPA.';
4934+
4935+
4936+COMMENT ON COLUMN archive.distribution IS 'The distribution that uses this archive.';
4937+
4938+
4939+COMMENT ON COLUMN archive.purpose IS 'The purpose of this archive, e.g. COMMERCIAL. See the ArchivePurpose DBSchema item.';
4940+
4941+
4942+COMMENT ON COLUMN archive.private IS 'Whether or not the archive is private. This affects the global visibility of the archive.';
4943+
4944+
4945+COMMENT ON COLUMN archive.sources_cached IS 'Number of sources already cached for this archive.';
4946+
4947+
4948+COMMENT ON COLUMN archive.binaries_cached IS 'Number of binaries already cached for this archive.';
4949+
4950+
4951+COMMENT ON COLUMN archive.package_description_cache IS 'Text blob containing all source and binary names and descriptions concatenated. Used to to build the tsearch indexes on this table.';
4952+
4953+
4954+COMMENT ON COLUMN archive.require_virtualized IS 'Whether this archive has binaries that should be built on a virtual machine, e.g. PPAs';
4955+
4956+
4957+COMMENT ON COLUMN archive.name IS 'The name of the archive.';
4958+
4959+
4960+COMMENT ON COLUMN archive.publish IS 'Whether this archive should be published.';
4961+
4962+
4963+COMMENT ON COLUMN archive.date_updated IS 'When were the rebuild statistics last updated?';
4964+
4965+
4966+COMMENT ON COLUMN archive.total_count IS 'How many source packages are in the rebuild archive altogether?';
4967+
4968+
4969+COMMENT ON COLUMN archive.pending_count IS 'How many packages still need building?';
4970+
4971+
4972+COMMENT ON COLUMN archive.succeeded_count IS 'How many source packages were built sucessfully?';
4973+
4974+
4975+COMMENT ON COLUMN archive.failed_count IS 'How many packages failed to build?';
4976+
4977+
4978+COMMENT ON COLUMN archive.building_count IS 'How many packages are building at present?';
4979+
4980+
4981+COMMENT ON COLUMN archive.signing_key IS 'The GpgKey used for signing this archive.';
4982+
4983+
4984+COMMENT ON COLUMN archive.removed_binary_retention_days IS 'The number of days before superseded or deleted binary files are expired in the librarian, or zero for never.';
4985+
4986+
4987+COMMENT ON COLUMN archive.num_old_versions_published IS 'The number of versions of a package to keep published before older versions are superseded.';
4988+
4989+
4990+COMMENT ON COLUMN archive.displayname IS 'User defined displayname for this archive.';
4991+
4992+
4993+COMMENT ON COLUMN archive.relative_build_score IS 'A delta to the build score that is applied to all builds in this archive.';
4994+
4995+
4996+COMMENT ON COLUMN archive.external_dependencies IS 'Newline-separated list of repositories to be used to retrieve any external build dependencies when building packages in this archive, in the format: deb http[s]://[user:pass@]<host>[/path] %(series)s[-pocket] [components] The series variable is replaced with the series name of the context build. This column is specifically and only intended for OEM migration to Launchpad and should be re-examined in October 2010 to see if it is still relevant.';
4997+
4998+
4999+COMMENT ON COLUMN archive.status IS 'The status of this archive, e.g. ACTIVE. See the ArchiveState DBSchema item.';
5000+
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to status/vote changes: