liquid_feedback_core

view core.sql @ 68:28c1af992cd3

Added tag v1.2.5 for changeset 72e5356b5454
author jbe
date Mon Aug 16 14:45:28 2010 +0200 (2010-08-16)
parents 72e5356b5454
children c65002c1a509
line source
2 CREATE LANGUAGE plpgsql; -- Triggers are implemented in PL/pgSQL
4 -- NOTE: In PostgreSQL every UNIQUE constraint implies creation of an index
6 BEGIN;
8 CREATE VIEW "liquid_feedback_version" AS
9 SELECT * FROM (VALUES ('1.2.5', 1, 2, 5))
10 AS "subquery"("string", "major", "minor", "revision");
14 ----------------------
15 -- Full text search --
16 ----------------------
19 CREATE FUNCTION "text_search_query"("query_text_p" TEXT)
20 RETURNS TSQUERY
21 LANGUAGE 'plpgsql' IMMUTABLE AS $$
22 BEGIN
23 RETURN plainto_tsquery('pg_catalog.simple', "query_text_p");
24 END;
25 $$;
27 COMMENT ON FUNCTION "text_search_query"(TEXT) IS 'Usage: WHERE "text_search_data" @@ "text_search_query"(''<user query>'')';
30 CREATE FUNCTION "highlight"
31 ( "body_p" TEXT,
32 "query_text_p" TEXT )
33 RETURNS TEXT
34 LANGUAGE 'plpgsql' IMMUTABLE AS $$
35 BEGIN
36 RETURN ts_headline(
37 'pg_catalog.simple',
38 replace(replace("body_p", e'\\', e'\\\\'), '*', e'\\*'),
39 "text_search_query"("query_text_p"),
40 'StartSel=* StopSel=* HighlightAll=TRUE' );
41 END;
42 $$;
44 COMMENT ON FUNCTION "highlight"
45 ( "body_p" TEXT,
46 "query_text_p" TEXT )
47 IS 'For a given a user query this function encapsulates all matches with asterisks. Asterisks and backslashes being already present are preceeded with one extra backslash.';
51 -------------------------
52 -- Tables and indicies --
53 -------------------------
56 CREATE TABLE "member" (
57 "id" SERIAL4 PRIMARY KEY,
58 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
59 "last_login" TIMESTAMPTZ,
60 "login" TEXT UNIQUE,
61 "password" TEXT,
62 "active" BOOLEAN NOT NULL DEFAULT TRUE,
63 "admin" BOOLEAN NOT NULL DEFAULT FALSE,
64 "notify_email" TEXT,
65 "notify_email_unconfirmed" TEXT,
66 "notify_email_secret" TEXT UNIQUE,
67 "notify_email_secret_expiry" TIMESTAMPTZ,
68 "notify_email_lock_expiry" TIMESTAMPTZ,
69 "password_reset_secret" TEXT UNIQUE,
70 "password_reset_secret_expiry" TIMESTAMPTZ,
71 "name" TEXT NOT NULL UNIQUE,
72 "identification" TEXT UNIQUE,
73 "organizational_unit" TEXT,
74 "internal_posts" TEXT,
75 "realname" TEXT,
76 "birthday" DATE,
77 "address" TEXT,
78 "email" TEXT,
79 "xmpp_address" TEXT,
80 "website" TEXT,
81 "phone" TEXT,
82 "mobile_phone" TEXT,
83 "profession" TEXT,
84 "external_memberships" TEXT,
85 "external_posts" TEXT,
86 "statement" TEXT,
87 "text_search_data" TSVECTOR );
88 CREATE INDEX "member_active_idx" ON "member" ("active");
89 CREATE INDEX "member_text_search_data_idx" ON "member" USING gin ("text_search_data");
90 CREATE TRIGGER "update_text_search_data"
91 BEFORE INSERT OR UPDATE ON "member"
92 FOR EACH ROW EXECUTE PROCEDURE
93 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
94 "name", "identification", "organizational_unit", "internal_posts",
95 "realname", "external_memberships", "external_posts", "statement" );
97 COMMENT ON TABLE "member" IS 'Users of the system, e.g. members of an organization';
99 COMMENT ON COLUMN "member"."login" IS 'Login name';
100 COMMENT ON COLUMN "member"."password" IS 'Password (preferably as crypto-hash, depending on the frontend or access layer)';
101 COMMENT ON COLUMN "member"."active" IS 'Inactive members can not login and their supports/votes are not counted by the system.';
102 COMMENT ON COLUMN "member"."admin" IS 'TRUE for admins, which can administrate other users and setup policies and areas';
103 COMMENT ON COLUMN "member"."notify_email" IS 'Email address where notifications of the system are sent to';
104 COMMENT ON COLUMN "member"."notify_email_unconfirmed" IS 'Unconfirmed email address provided by the member to be copied into "notify_email" field after verification';
105 COMMENT ON COLUMN "member"."notify_email_secret" IS 'Secret sent to the address in "notify_email_unconformed"';
106 COMMENT ON COLUMN "member"."notify_email_secret_expiry" IS 'Expiry date/time for "notify_email_secret"';
107 COMMENT ON COLUMN "member"."notify_email_lock_expiry" IS 'Date/time until no further email confirmation mails may be sent (abuse protection)';
108 COMMENT ON COLUMN "member"."name" IS 'Distinct name of the member';
109 COMMENT ON COLUMN "member"."identification" IS 'Optional identification number or code of the member';
110 COMMENT ON COLUMN "member"."organizational_unit" IS 'Branch or division of the organization the member belongs to';
111 COMMENT ON COLUMN "member"."internal_posts" IS 'Posts (offices) of the member inside the organization';
112 COMMENT ON COLUMN "member"."realname" IS 'Real name of the member, may be identical with "name"';
113 COMMENT ON COLUMN "member"."email" IS 'Published email address of the member; not used for system notifications';
114 COMMENT ON COLUMN "member"."external_memberships" IS 'Other organizations the member is involved in';
115 COMMENT ON COLUMN "member"."external_posts" IS 'Posts (offices) outside the organization';
116 COMMENT ON COLUMN "member"."statement" IS 'Freely chosen text of the member for his homepage within the system';
119 CREATE TABLE "member_history" (
120 "id" SERIAL8 PRIMARY KEY,
121 "member_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
122 "until" TIMESTAMPTZ NOT NULL DEFAULT now(),
123 "active" BOOLEAN NOT NULL,
124 "name" TEXT NOT NULL );
125 CREATE INDEX "member_history_member_id_idx" ON "member_history" ("member_id");
127 COMMENT ON TABLE "member_history" IS 'Filled by trigger; keeps information about old names and active flag of members';
129 COMMENT ON COLUMN "member_history"."id" IS 'Primary key, which can be used to sort entries correctly (and time warp resistant)';
130 COMMENT ON COLUMN "member_history"."until" IS 'Timestamp until the data was valid';
133 CREATE TABLE "invite_code" (
134 "code" TEXT PRIMARY KEY,
135 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
136 "used" TIMESTAMPTZ,
137 "member_id" INT4 UNIQUE REFERENCES "member" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
138 "comment" TEXT,
139 CONSTRAINT "only_used_codes_may_refer_to_member" CHECK ("used" NOTNULL OR "member_id" ISNULL) );
141 COMMENT ON TABLE "invite_code" IS 'Invite codes can be used once to create a new member account.';
143 COMMENT ON COLUMN "invite_code"."code" IS 'Secret code';
144 COMMENT ON COLUMN "invite_code"."created" IS 'Time of creation of the secret code';
145 COMMENT ON COLUMN "invite_code"."used" IS 'NULL, if not used yet, otherwise tells when this code was used to create a member account';
146 COMMENT ON COLUMN "invite_code"."member_id" IS 'References the member whose account was created with this code';
147 COMMENT ON COLUMN "invite_code"."comment" IS 'Comment on the code, which is to be used for administrative reasons only';
150 CREATE TABLE "setting" (
151 PRIMARY KEY ("member_id", "key"),
152 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
153 "key" TEXT NOT NULL,
154 "value" TEXT NOT NULL );
155 CREATE INDEX "setting_key_idx" ON "setting" ("key");
157 COMMENT ON TABLE "setting" IS 'Place to store a frontend specific setting for members as a string';
159 COMMENT ON COLUMN "setting"."key" IS 'Name of the setting, preceded by a frontend specific prefix';
162 CREATE TABLE "setting_map" (
163 PRIMARY KEY ("member_id", "key", "subkey"),
164 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
165 "key" TEXT NOT NULL,
166 "subkey" TEXT NOT NULL,
167 "value" TEXT NOT NULL );
168 CREATE INDEX "setting_map_key_idx" ON "setting_map" ("key");
170 COMMENT ON TABLE "setting_map" IS 'Place to store a frontend specific setting for members as a map of key value pairs';
172 COMMENT ON COLUMN "setting_map"."key" IS 'Name of the setting, preceded by a frontend specific prefix';
173 COMMENT ON COLUMN "setting_map"."subkey" IS 'Key of a map entry';
174 COMMENT ON COLUMN "setting_map"."value" IS 'Value of a map entry';
177 CREATE TABLE "member_relation_setting" (
178 PRIMARY KEY ("member_id", "key", "other_member_id"),
179 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
180 "key" TEXT NOT NULL,
181 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
182 "value" TEXT NOT NULL );
184 COMMENT ON TABLE "member_relation_setting" IS 'Place to store a frontend specific setting related to relations between members as a string';
187 CREATE TYPE "member_image_type" AS ENUM ('photo', 'avatar');
189 COMMENT ON TYPE "member_image_type" IS 'Types of images for a member';
192 CREATE TABLE "member_image" (
193 PRIMARY KEY ("member_id", "image_type", "scaled"),
194 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
195 "image_type" "member_image_type",
196 "scaled" BOOLEAN,
197 "content_type" TEXT,
198 "data" BYTEA NOT NULL );
200 COMMENT ON TABLE "member_image" IS 'Images of members';
202 COMMENT ON COLUMN "member_image"."scaled" IS 'FALSE for original image, TRUE for scaled version of the image';
205 CREATE TABLE "member_count" (
206 "calculated" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
207 "total_count" INT4 NOT NULL );
209 COMMENT ON TABLE "member_count" IS 'Contains one row which contains the total count of active(!) members and a timestamp indicating when the total member count and area member counts were calculated';
211 COMMENT ON COLUMN "member_count"."calculated" IS 'timestamp indicating when the total member count and area member counts were calculated';
212 COMMENT ON COLUMN "member_count"."total_count" IS 'Total count of active(!) members';
215 CREATE TABLE "contact" (
216 PRIMARY KEY ("member_id", "other_member_id"),
217 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
218 "other_member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
219 "public" BOOLEAN NOT NULL DEFAULT FALSE,
220 CONSTRAINT "cant_save_yourself_as_contact"
221 CHECK ("member_id" != "other_member_id") );
223 COMMENT ON TABLE "contact" IS 'Contact lists';
225 COMMENT ON COLUMN "contact"."member_id" IS 'Member having the contact list';
226 COMMENT ON COLUMN "contact"."other_member_id" IS 'Member referenced in the contact list';
227 COMMENT ON COLUMN "contact"."public" IS 'TRUE = display contact publically';
230 CREATE TABLE "session" (
231 "ident" TEXT PRIMARY KEY,
232 "additional_secret" TEXT,
233 "expiry" TIMESTAMPTZ NOT NULL DEFAULT now() + '24 hours',
234 "member_id" INT8 REFERENCES "member" ("id") ON DELETE SET NULL,
235 "lang" TEXT );
236 CREATE INDEX "session_expiry_idx" ON "session" ("expiry");
238 COMMENT ON TABLE "session" IS 'Sessions, i.e. for a web-frontend';
240 COMMENT ON COLUMN "session"."ident" IS 'Secret session identifier (i.e. random string)';
241 COMMENT ON COLUMN "session"."additional_secret" IS 'Additional field to store a secret, which can be used against CSRF attacks';
242 COMMENT ON COLUMN "session"."member_id" IS 'Reference to member, who is logged in';
243 COMMENT ON COLUMN "session"."lang" IS 'Language code of the selected language';
246 CREATE TABLE "policy" (
247 "id" SERIAL4 PRIMARY KEY,
248 "index" INT4 NOT NULL,
249 "active" BOOLEAN NOT NULL DEFAULT TRUE,
250 "name" TEXT NOT NULL UNIQUE,
251 "description" TEXT NOT NULL DEFAULT '',
252 "admission_time" INTERVAL NOT NULL,
253 "discussion_time" INTERVAL NOT NULL,
254 "verification_time" INTERVAL NOT NULL,
255 "voting_time" INTERVAL NOT NULL,
256 "issue_quorum_num" INT4 NOT NULL,
257 "issue_quorum_den" INT4 NOT NULL,
258 "initiative_quorum_num" INT4 NOT NULL,
259 "initiative_quorum_den" INT4 NOT NULL,
260 "majority_num" INT4 NOT NULL DEFAULT 1,
261 "majority_den" INT4 NOT NULL DEFAULT 2,
262 "majority_strict" BOOLEAN NOT NULL DEFAULT TRUE );
263 CREATE INDEX "policy_active_idx" ON "policy" ("active");
265 COMMENT ON TABLE "policy" IS 'Policies for a particular proceeding type (timelimits, quorum)';
267 COMMENT ON COLUMN "policy"."index" IS 'Determines the order in listings';
268 COMMENT ON COLUMN "policy"."active" IS 'TRUE = policy can be used for new issues';
269 COMMENT ON COLUMN "policy"."admission_time" IS 'Maximum time an issue stays open without being "accepted"';
270 COMMENT ON COLUMN "policy"."discussion_time" IS 'Regular time until an issue is "half_frozen" after being "accepted"';
271 COMMENT ON COLUMN "policy"."verification_time" IS 'Regular time until an issue is "fully_frozen" after being "half_frozen"';
272 COMMENT ON COLUMN "policy"."voting_time" IS 'Time after an issue is "fully_frozen" but not "closed"';
273 COMMENT ON COLUMN "policy"."issue_quorum_num" IS 'Numerator of potential supporter quorum to be reached by one initiative of an issue to be "accepted"';
274 COMMENT ON COLUMN "policy"."issue_quorum_den" IS 'Denominator of potential supporter quorum to be reached by one initiative of an issue to be "accepted"';
275 COMMENT ON COLUMN "policy"."initiative_quorum_num" IS 'Numerator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
276 COMMENT ON COLUMN "policy"."initiative_quorum_den" IS 'Denominator of satisfied supporter quorum to be reached by an initiative to be "admitted" for voting';
277 COMMENT ON COLUMN "policy"."majority_num" IS 'Numerator of fraction of majority to be reached during voting by an initiative to be aggreed upon';
278 COMMENT ON COLUMN "policy"."majority_den" IS 'Denominator of fraction of majority to be reached during voting by an initiative to be aggreed upon';
279 COMMENT ON COLUMN "policy"."majority_strict" IS 'If TRUE, then the majority must be strictly greater than "majority_num"/"majority_den", otherwise it may also be equal.';
282 CREATE TABLE "area" (
283 "id" SERIAL4 PRIMARY KEY,
284 "active" BOOLEAN NOT NULL DEFAULT TRUE,
285 "name" TEXT NOT NULL,
286 "description" TEXT NOT NULL DEFAULT '',
287 "direct_member_count" INT4,
288 "member_weight" INT4,
289 "autoreject_weight" INT4,
290 "text_search_data" TSVECTOR );
291 CREATE INDEX "area_active_idx" ON "area" ("active");
292 CREATE INDEX "area_text_search_data_idx" ON "area" USING gin ("text_search_data");
293 CREATE TRIGGER "update_text_search_data"
294 BEFORE INSERT OR UPDATE ON "area"
295 FOR EACH ROW EXECUTE PROCEDURE
296 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
297 "name", "description" );
299 COMMENT ON TABLE "area" IS 'Subject areas';
301 COMMENT ON COLUMN "area"."active" IS 'TRUE means new issues can be created in this area';
302 COMMENT ON COLUMN "area"."direct_member_count" IS 'Number of active members of that area (ignoring their weight), as calculated from view "area_member_count"';
303 COMMENT ON COLUMN "area"."member_weight" IS 'Same as "direct_member_count" but respecting delegations';
304 COMMENT ON COLUMN "area"."autoreject_weight" IS 'Sum of weight of members using the autoreject feature';
307 CREATE TABLE "area_setting" (
308 PRIMARY KEY ("member_id", "key", "area_id"),
309 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
310 "key" TEXT NOT NULL,
311 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
312 "value" TEXT NOT NULL );
314 COMMENT ON TABLE "area_setting" IS 'Place for frontend to store area specific settings of members as strings';
317 CREATE TABLE "allowed_policy" (
318 PRIMARY KEY ("area_id", "policy_id"),
319 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
320 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
321 "default_policy" BOOLEAN NOT NULL DEFAULT FALSE );
322 CREATE UNIQUE INDEX "allowed_policy_one_default_per_area_idx" ON "allowed_policy" ("area_id") WHERE "default_policy";
324 COMMENT ON TABLE "allowed_policy" IS 'Selects which policies can be used in each area';
326 COMMENT ON COLUMN "allowed_policy"."default_policy" IS 'One policy per area can be set as default.';
329 CREATE TYPE "snapshot_event" AS ENUM ('periodic', 'end_of_admission', 'half_freeze', 'full_freeze');
331 COMMENT ON TYPE "snapshot_event" IS 'Reason for snapshots: ''periodic'' = due to periodic recalculation, ''end_of_admission'' = saved state at end of admission period, ''half_freeze'' = saved state at end of discussion period, ''full_freeze'' = saved state at end of verification period';
334 CREATE TABLE "issue" (
335 "id" SERIAL4 PRIMARY KEY,
336 "area_id" INT4 NOT NULL REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
337 "policy_id" INT4 NOT NULL REFERENCES "policy" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
338 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
339 "accepted" TIMESTAMPTZ,
340 "half_frozen" TIMESTAMPTZ,
341 "fully_frozen" TIMESTAMPTZ,
342 "closed" TIMESTAMPTZ,
343 "ranks_available" BOOLEAN NOT NULL DEFAULT FALSE,
344 "cleaned" TIMESTAMPTZ,
345 "admission_time" INTERVAL NOT NULL,
346 "discussion_time" INTERVAL NOT NULL,
347 "verification_time" INTERVAL NOT NULL,
348 "voting_time" INTERVAL NOT NULL,
349 "snapshot" TIMESTAMPTZ,
350 "latest_snapshot_event" "snapshot_event",
351 "population" INT4,
352 "vote_now" INT4,
353 "vote_later" INT4,
354 "voter_count" INT4,
355 CONSTRAINT "valid_state" CHECK (
356 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
357 ("accepted" ISNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
358 ("accepted" NOTNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
359 ("accepted" NOTNULL AND "half_frozen" ISNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
360 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
361 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" ISNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
362 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" ISNULL AND "ranks_available" = FALSE) OR
363 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = FALSE) OR
364 ("accepted" NOTNULL AND "half_frozen" NOTNULL AND "fully_frozen" NOTNULL AND "closed" NOTNULL AND "ranks_available" = TRUE) ),
365 CONSTRAINT "state_change_order" CHECK (
366 "created" <= "accepted" AND
367 "accepted" <= "half_frozen" AND
368 "half_frozen" <= "fully_frozen" AND
369 "fully_frozen" <= "closed" ),
370 CONSTRAINT "only_closed_issues_may_be_cleaned" CHECK (
371 "cleaned" ISNULL OR "closed" NOTNULL ),
372 CONSTRAINT "last_snapshot_on_full_freeze"
373 CHECK ("snapshot" = "fully_frozen"), -- NOTE: snapshot can be set, while frozen is NULL yet
374 CONSTRAINT "freeze_requires_snapshot"
375 CHECK ("fully_frozen" ISNULL OR "snapshot" NOTNULL),
376 CONSTRAINT "set_both_or_none_of_snapshot_and_latest_snapshot_event"
377 CHECK ("snapshot" NOTNULL = "latest_snapshot_event" NOTNULL) );
378 CREATE INDEX "issue_area_id_idx" ON "issue" ("area_id");
379 CREATE INDEX "issue_policy_id_idx" ON "issue" ("policy_id");
380 CREATE INDEX "issue_created_idx" ON "issue" ("created");
381 CREATE INDEX "issue_accepted_idx" ON "issue" ("accepted");
382 CREATE INDEX "issue_half_frozen_idx" ON "issue" ("half_frozen");
383 CREATE INDEX "issue_fully_frozen_idx" ON "issue" ("fully_frozen");
384 CREATE INDEX "issue_closed_idx" ON "issue" ("closed");
385 CREATE INDEX "issue_created_idx_open" ON "issue" ("created") WHERE "closed" ISNULL;
386 CREATE INDEX "issue_closed_idx_canceled" ON "issue" ("closed") WHERE "fully_frozen" ISNULL;
388 COMMENT ON TABLE "issue" IS 'Groups of initiatives';
390 COMMENT ON COLUMN "issue"."accepted" IS 'Point in time, when one initiative of issue reached the "issue_quorum"';
391 COMMENT ON COLUMN "issue"."half_frozen" IS 'Point in time, when "discussion_time" has elapsed, or members voted for voting; Frontends must ensure that for half_frozen issues a) initiatives are not revoked, b) no new drafts are created, c) no initiators are added or removed.';
392 COMMENT ON COLUMN "issue"."fully_frozen" IS 'Point in time, when "verification_time" has elapsed; Frontends must ensure that for fully_frozen issues additionally to the restrictions for half_frozen issues a) initiatives are not created, b) no interest is created or removed, c) no supporters are added or removed, d) no opinions are created, changed or deleted.';
393 COMMENT ON COLUMN "issue"."closed" IS 'Point in time, when "admission_time" or "voting_time" have elapsed, and issue is no longer active; Frontends must ensure that for closed issues additionally to the restrictions for half_frozen and fully_frozen issues a) no voter is added or removed to/from the direct_voter table, b) no votes are added, modified or removed.';
394 COMMENT ON COLUMN "issue"."ranks_available" IS 'TRUE = ranks have been calculated';
395 COMMENT ON COLUMN "issue"."cleaned" IS 'Point in time, when discussion data and votes had been deleted';
396 COMMENT ON COLUMN "issue"."admission_time" IS 'Copied from "policy" table at creation of issue';
397 COMMENT ON COLUMN "issue"."discussion_time" IS 'Copied from "policy" table at creation of issue';
398 COMMENT ON COLUMN "issue"."verification_time" IS 'Copied from "policy" table at creation of issue';
399 COMMENT ON COLUMN "issue"."voting_time" IS 'Copied from "policy" table at creation of issue';
400 COMMENT ON COLUMN "issue"."snapshot" IS 'Point in time, when snapshot tables have been updated and "population", "vote_now", "vote_later" and *_count values were precalculated';
401 COMMENT ON COLUMN "issue"."latest_snapshot_event" IS 'Event type of latest snapshot for issue; Can be used to select the latest snapshot data in the snapshot tables';
402 COMMENT ON COLUMN "issue"."population" IS 'Sum of "weight" column in table "direct_population_snapshot"';
403 COMMENT ON COLUMN "issue"."vote_now" IS 'Number of votes in favor of voting now, as calculated from table "direct_interest_snapshot"';
404 COMMENT ON COLUMN "issue"."vote_later" IS 'Number of votes against voting now, as calculated from table "direct_interest_snapshot"';
405 COMMENT ON COLUMN "issue"."voter_count" IS 'Total number of direct and delegating voters; This value is related to the final voting, while "population" is related to snapshots before the final voting';
408 CREATE TABLE "issue_setting" (
409 PRIMARY KEY ("member_id", "key", "issue_id"),
410 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
411 "key" TEXT NOT NULL,
412 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
413 "value" TEXT NOT NULL );
415 COMMENT ON TABLE "issue_setting" IS 'Place for frontend to store issue specific settings of members as strings';
418 CREATE TABLE "initiative" (
419 UNIQUE ("issue_id", "id"), -- index needed for foreign-key on table "vote"
420 "issue_id" INT4 NOT NULL REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
421 "id" SERIAL4 PRIMARY KEY,
422 "name" TEXT NOT NULL,
423 "discussion_url" TEXT,
424 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
425 "revoked" TIMESTAMPTZ,
426 "suggested_initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
427 "admitted" BOOLEAN,
428 "supporter_count" INT4,
429 "informed_supporter_count" INT4,
430 "satisfied_supporter_count" INT4,
431 "satisfied_informed_supporter_count" INT4,
432 "positive_votes" INT4,
433 "negative_votes" INT4,
434 "agreed" BOOLEAN,
435 "rank" INT4,
436 "text_search_data" TSVECTOR,
437 CONSTRAINT "non_revoked_initiatives_cant_suggest_other"
438 CHECK ("revoked" NOTNULL OR "suggested_initiative_id" ISNULL),
439 CONSTRAINT "revoked_initiatives_cant_be_admitted"
440 CHECK ("revoked" ISNULL OR "admitted" ISNULL),
441 CONSTRAINT "non_admitted_initiatives_cant_contain_voting_results"
442 CHECK (("admitted" NOTNULL AND "admitted" = TRUE) OR ("positive_votes" ISNULL AND "negative_votes" ISNULL AND "agreed" ISNULL)),
443 CONSTRAINT "all_or_none_of_positive_votes_negative_votes_and_agreed_must_be_null"
444 CHECK ("positive_votes" NOTNULL = "negative_votes" NOTNULL AND "positive_votes" NOTNULL = "agreed" NOTNULL),
445 CONSTRAINT "non_agreed_initiatives_cant_get_a_rank"
446 CHECK (("agreed" NOTNULL AND "agreed" = TRUE) OR "rank" ISNULL) );
447 CREATE INDEX "initiative_created_idx" ON "initiative" ("created");
448 CREATE INDEX "initiative_revoked_idx" ON "initiative" ("revoked");
449 CREATE INDEX "initiative_text_search_data_idx" ON "initiative" USING gin ("text_search_data");
450 CREATE TRIGGER "update_text_search_data"
451 BEFORE INSERT OR UPDATE ON "initiative"
452 FOR EACH ROW EXECUTE PROCEDURE
453 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
454 "name", "discussion_url");
456 COMMENT ON TABLE "initiative" IS 'Group of members publishing drafts for resolutions to be passed; Frontends must ensure that initiatives of half_frozen issues are not revoked, and that initiatives of fully_frozen or closed issues are neither revoked nor created.';
458 COMMENT ON COLUMN "initiative"."discussion_url" IS 'URL pointing to a discussion platform for this initiative';
459 COMMENT ON COLUMN "initiative"."revoked" IS 'Point in time, when one initiator decided to revoke the initiative';
460 COMMENT ON COLUMN "initiative"."admitted" IS 'TRUE, if initiative reaches the "initiative_quorum" when freezing the issue';
461 COMMENT ON COLUMN "initiative"."supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
462 COMMENT ON COLUMN "initiative"."informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
463 COMMENT ON COLUMN "initiative"."satisfied_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
464 COMMENT ON COLUMN "initiative"."satisfied_informed_supporter_count" IS 'Calculated from table "direct_supporter_snapshot"';
465 COMMENT ON COLUMN "initiative"."positive_votes" IS 'Calculated from table "direct_voter"';
466 COMMENT ON COLUMN "initiative"."negative_votes" IS 'Calculated from table "direct_voter"';
467 COMMENT ON COLUMN "initiative"."agreed" IS 'TRUE, if "positive_votes"/("positive_votes"+"negative_votes") is strictly greater or greater-equal than "majority_num"/"majority_den"';
468 COMMENT ON COLUMN "initiative"."rank" IS 'Rank of approved initiatives (winner is 1), calculated from table "direct_voter"';
471 CREATE TABLE "battle" (
472 PRIMARY KEY ("issue_id", "winning_initiative_id", "losing_initiative_id"),
473 "issue_id" INT4,
474 "winning_initiative_id" INT4,
475 FOREIGN KEY ("issue_id", "winning_initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
476 "losing_initiative_id" INT4,
477 FOREIGN KEY ("issue_id", "losing_initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
478 "count" INT4 NOT NULL);
480 COMMENT ON TABLE "battle" IS 'Number of members preferring one initiative to another; Filled by "battle_view" when closing an issue';
483 CREATE TABLE "initiative_setting" (
484 PRIMARY KEY ("member_id", "key", "initiative_id"),
485 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
486 "key" TEXT NOT NULL,
487 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
488 "value" TEXT NOT NULL );
490 COMMENT ON TABLE "initiative_setting" IS 'Place for frontend to store initiative specific settings of members as strings';
493 CREATE TABLE "draft" (
494 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "supporter"
495 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
496 "id" SERIAL8 PRIMARY KEY,
497 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
498 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
499 "formatting_engine" TEXT,
500 "content" TEXT NOT NULL,
501 "text_search_data" TSVECTOR );
502 CREATE INDEX "draft_created_idx" ON "draft" ("created");
503 CREATE INDEX "draft_author_id_created_idx" ON "draft" ("author_id", "created");
504 CREATE INDEX "draft_text_search_data_idx" ON "draft" USING gin ("text_search_data");
505 CREATE TRIGGER "update_text_search_data"
506 BEFORE INSERT OR UPDATE ON "draft"
507 FOR EACH ROW EXECUTE PROCEDURE
508 tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "content");
510 COMMENT ON TABLE "draft" IS 'Drafts of initiatives to solve issues; Frontends must ensure that new drafts for initiatives of half_frozen, fully_frozen or closed issues can''t be created.';
512 COMMENT ON COLUMN "draft"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used';
513 COMMENT ON COLUMN "draft"."content" IS 'Text of the draft in a format depending on the field "formatting_engine"';
516 CREATE TABLE "rendered_draft" (
517 PRIMARY KEY ("draft_id", "format"),
518 "draft_id" INT8 REFERENCES "draft" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
519 "format" TEXT,
520 "content" TEXT NOT NULL );
522 COMMENT ON TABLE "rendered_draft" IS 'This table may be used by frontends to cache "rendered" drafts (e.g. HTML output generated from wiki text)';
525 CREATE TABLE "suggestion" (
526 UNIQUE ("initiative_id", "id"), -- index needed for foreign-key on table "opinion"
527 "initiative_id" INT4 NOT NULL REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
528 "id" SERIAL8 PRIMARY KEY,
529 "created" TIMESTAMPTZ NOT NULL DEFAULT now(),
530 "author_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
531 "name" TEXT NOT NULL,
532 "description" TEXT NOT NULL DEFAULT '',
533 "text_search_data" TSVECTOR,
534 "minus2_unfulfilled_count" INT4,
535 "minus2_fulfilled_count" INT4,
536 "minus1_unfulfilled_count" INT4,
537 "minus1_fulfilled_count" INT4,
538 "plus1_unfulfilled_count" INT4,
539 "plus1_fulfilled_count" INT4,
540 "plus2_unfulfilled_count" INT4,
541 "plus2_fulfilled_count" INT4 );
542 CREATE INDEX "suggestion_created_idx" ON "suggestion" ("created");
543 CREATE INDEX "suggestion_author_id_created_idx" ON "suggestion" ("author_id", "created");
544 CREATE INDEX "suggestion_text_search_data_idx" ON "suggestion" USING gin ("text_search_data");
545 CREATE TRIGGER "update_text_search_data"
546 BEFORE INSERT OR UPDATE ON "suggestion"
547 FOR EACH ROW EXECUTE PROCEDURE
548 tsvector_update_trigger('text_search_data', 'pg_catalog.simple',
549 "name", "description");
551 COMMENT ON TABLE "suggestion" IS 'Suggestions to initiators, to change the current draft; must not be deleted explicitly, as they vanish automatically if the last opinion is deleted';
553 COMMENT ON COLUMN "suggestion"."minus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
554 COMMENT ON COLUMN "suggestion"."minus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
555 COMMENT ON COLUMN "suggestion"."minus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
556 COMMENT ON COLUMN "suggestion"."minus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
557 COMMENT ON COLUMN "suggestion"."plus1_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
558 COMMENT ON COLUMN "suggestion"."plus1_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
559 COMMENT ON COLUMN "suggestion"."plus2_unfulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
560 COMMENT ON COLUMN "suggestion"."plus2_fulfilled_count" IS 'Calculated from table "direct_supporter_snapshot", not requiring informed supporters';
563 CREATE TABLE "suggestion_setting" (
564 PRIMARY KEY ("member_id", "key", "suggestion_id"),
565 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
566 "key" TEXT NOT NULL,
567 "suggestion_id" INT8 REFERENCES "suggestion" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
568 "value" TEXT NOT NULL );
570 COMMENT ON TABLE "suggestion_setting" IS 'Place for frontend to store suggestion specific settings of members as strings';
573 CREATE TABLE "membership" (
574 PRIMARY KEY ("area_id", "member_id"),
575 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
576 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
577 "autoreject" BOOLEAN NOT NULL DEFAULT FALSE );
578 CREATE INDEX "membership_member_id_idx" ON "membership" ("member_id");
580 COMMENT ON TABLE "membership" IS 'Interest of members in topic areas';
582 COMMENT ON COLUMN "membership"."autoreject" IS 'TRUE = member votes against all initiatives in case of not explicitly taking part in the voting procedure; If there exists an "interest" entry, the interest entry has precedence';
585 CREATE TABLE "interest" (
586 PRIMARY KEY ("issue_id", "member_id"),
587 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
588 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
589 "autoreject" BOOLEAN NOT NULL,
590 "voting_requested" BOOLEAN );
591 CREATE INDEX "interest_member_id_idx" ON "interest" ("member_id");
593 COMMENT ON TABLE "interest" IS 'Interest of members in a particular issue; Frontends must ensure that interest for fully_frozen or closed issues is not added or removed.';
595 COMMENT ON COLUMN "interest"."autoreject" IS 'TRUE = member votes against all initiatives in case of not explicitly taking part in the voting procedure';
596 COMMENT ON COLUMN "interest"."voting_requested" IS 'TRUE = member wants to vote now, FALSE = member wants to vote later, NULL = policy rules should apply';
599 CREATE TABLE "initiator" (
600 PRIMARY KEY ("initiative_id", "member_id"),
601 "initiative_id" INT4 REFERENCES "initiative" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
602 "member_id" INT4 REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
603 "accepted" BOOLEAN );
604 CREATE INDEX "initiator_member_id_idx" ON "initiator" ("member_id");
606 COMMENT ON TABLE "initiator" IS 'Members who are allowed to post new drafts; Frontends must ensure that initiators are not added or removed from half_frozen, fully_frozen or closed initiatives.';
608 COMMENT ON COLUMN "initiator"."accepted" IS 'If "accepted" is NULL, then the member was invited to be a co-initiator, but has not answered yet. If it is TRUE, the member has accepted the invitation, if it is FALSE, the member has rejected the invitation.';
611 CREATE TABLE "supporter" (
612 "issue_id" INT4 NOT NULL,
613 PRIMARY KEY ("initiative_id", "member_id"),
614 "initiative_id" INT4,
615 "member_id" INT4,
616 "draft_id" INT8 NOT NULL,
617 FOREIGN KEY ("issue_id", "member_id") REFERENCES "interest" ("issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE,
618 FOREIGN KEY ("initiative_id", "draft_id") REFERENCES "draft" ("initiative_id", "id") ON DELETE CASCADE ON UPDATE CASCADE );
619 CREATE INDEX "supporter_member_id_idx" ON "supporter" ("member_id");
621 COMMENT ON TABLE "supporter" IS 'Members who support an initiative (conditionally); Frontends must ensure that supporters are not added or removed from fully_frozen or closed initiatives.';
623 COMMENT ON COLUMN "supporter"."draft_id" IS 'Latest seen draft, defaults to current draft of the initiative (implemented by trigger "default_for_draft_id")';
626 CREATE TABLE "opinion" (
627 "initiative_id" INT4 NOT NULL,
628 PRIMARY KEY ("suggestion_id", "member_id"),
629 "suggestion_id" INT8,
630 "member_id" INT4,
631 "degree" INT2 NOT NULL CHECK ("degree" >= -2 AND "degree" <= 2 AND "degree" != 0),
632 "fulfilled" BOOLEAN NOT NULL DEFAULT FALSE,
633 FOREIGN KEY ("initiative_id", "suggestion_id") REFERENCES "suggestion" ("initiative_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
634 FOREIGN KEY ("initiative_id", "member_id") REFERENCES "supporter" ("initiative_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
635 CREATE INDEX "opinion_member_id_initiative_id_idx" ON "opinion" ("member_id", "initiative_id");
637 COMMENT ON TABLE "opinion" IS 'Opinion on suggestions (criticism related to initiatives); Frontends must ensure that opinions are not created modified or deleted when related to fully_frozen or closed issues.';
639 COMMENT ON COLUMN "opinion"."degree" IS '2 = fulfillment required for support; 1 = fulfillment desired; -1 = fulfillment unwanted; -2 = fulfillment cancels support';
642 CREATE TYPE "delegation_scope" AS ENUM ('global', 'area', 'issue');
644 COMMENT ON TYPE "delegation_scope" IS 'Scope for delegations: ''global'', ''area'', or ''issue'' (order is relevant)';
647 CREATE TABLE "delegation" (
648 "id" SERIAL8 PRIMARY KEY,
649 "truster_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
650 "trustee_id" INT4 NOT NULL REFERENCES "member" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
651 "scope" "delegation_scope" NOT NULL,
652 "area_id" INT4 REFERENCES "area" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
653 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
654 CONSTRAINT "cant_delegate_to_yourself" CHECK ("truster_id" != "trustee_id"),
655 CONSTRAINT "area_id_and_issue_id_set_according_to_scope" CHECK (
656 ("scope" = 'global' AND "area_id" ISNULL AND "issue_id" ISNULL ) OR
657 ("scope" = 'area' AND "area_id" NOTNULL AND "issue_id" ISNULL ) OR
658 ("scope" = 'issue' AND "area_id" ISNULL AND "issue_id" NOTNULL) ),
659 UNIQUE ("area_id", "truster_id", "trustee_id"),
660 UNIQUE ("issue_id", "truster_id", "trustee_id") );
661 CREATE UNIQUE INDEX "delegation_global_truster_id_trustee_id_unique_idx"
662 ON "delegation" ("truster_id", "trustee_id") WHERE "scope" = 'global';
663 CREATE INDEX "delegation_truster_id_idx" ON "delegation" ("truster_id");
664 CREATE INDEX "delegation_trustee_id_idx" ON "delegation" ("trustee_id");
666 COMMENT ON TABLE "delegation" IS 'Delegation of vote-weight to other members';
668 COMMENT ON COLUMN "delegation"."area_id" IS 'Reference to area, if delegation is area-wide, otherwise NULL';
669 COMMENT ON COLUMN "delegation"."issue_id" IS 'Reference to issue, if delegation is issue-wide, otherwise NULL';
672 CREATE TABLE "direct_population_snapshot" (
673 PRIMARY KEY ("issue_id", "event", "member_id"),
674 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
675 "event" "snapshot_event",
676 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
677 "weight" INT4 );
678 CREATE INDEX "direct_population_snapshot_member_id_idx" ON "direct_population_snapshot" ("member_id");
680 COMMENT ON TABLE "direct_population_snapshot" IS 'Snapshot of active members having either a "membership" in the "area" or an "interest" in the "issue"';
682 COMMENT ON COLUMN "direct_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
683 COMMENT ON COLUMN "direct_population_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_population_snapshot"';
686 CREATE TABLE "delegating_population_snapshot" (
687 PRIMARY KEY ("issue_id", "event", "member_id"),
688 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
689 "event" "snapshot_event",
690 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
691 "weight" INT4,
692 "scope" "delegation_scope" NOT NULL,
693 "delegate_member_ids" INT4[] NOT NULL );
694 CREATE INDEX "delegating_population_snapshot_member_id_idx" ON "delegating_population_snapshot" ("member_id");
696 COMMENT ON TABLE "direct_population_snapshot" IS 'Delegations increasing the weight of entries in the "direct_population_snapshot" table';
698 COMMENT ON COLUMN "delegating_population_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
699 COMMENT ON COLUMN "delegating_population_snapshot"."member_id" IS 'Delegating member';
700 COMMENT ON COLUMN "delegating_population_snapshot"."weight" IS 'Intermediate weight';
701 COMMENT ON COLUMN "delegating_population_snapshot"."delegate_member_ids" IS 'Chain of members who act as delegates; last entry referes to "member_id" column of table "direct_population_snapshot"';
704 CREATE TABLE "direct_interest_snapshot" (
705 PRIMARY KEY ("issue_id", "event", "member_id"),
706 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
707 "event" "snapshot_event",
708 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
709 "weight" INT4,
710 "voting_requested" BOOLEAN );
711 CREATE INDEX "direct_interest_snapshot_member_id_idx" ON "direct_interest_snapshot" ("member_id");
713 COMMENT ON TABLE "direct_interest_snapshot" IS 'Snapshot of active members having an "interest" in the "issue"';
715 COMMENT ON COLUMN "direct_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
716 COMMENT ON COLUMN "direct_interest_snapshot"."weight" IS 'Weight of member (1 or higher) according to "delegating_interest_snapshot"';
717 COMMENT ON COLUMN "direct_interest_snapshot"."voting_requested" IS 'Copied from column "voting_requested" of table "interest"';
720 CREATE TABLE "delegating_interest_snapshot" (
721 PRIMARY KEY ("issue_id", "event", "member_id"),
722 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
723 "event" "snapshot_event",
724 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
725 "weight" INT4,
726 "scope" "delegation_scope" NOT NULL,
727 "delegate_member_ids" INT4[] NOT NULL );
728 CREATE INDEX "delegating_interest_snapshot_member_id_idx" ON "delegating_interest_snapshot" ("member_id");
730 COMMENT ON TABLE "delegating_interest_snapshot" IS 'Delegations increasing the weight of entries in the "direct_interest_snapshot" table';
732 COMMENT ON COLUMN "delegating_interest_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
733 COMMENT ON COLUMN "delegating_interest_snapshot"."member_id" IS 'Delegating member';
734 COMMENT ON COLUMN "delegating_interest_snapshot"."weight" IS 'Intermediate weight';
735 COMMENT ON COLUMN "delegating_interest_snapshot"."delegate_member_ids" IS 'Chain of members who act as delegates; last entry referes to "member_id" column of table "direct_interest_snapshot"';
738 CREATE TABLE "direct_supporter_snapshot" (
739 "issue_id" INT4 NOT NULL,
740 PRIMARY KEY ("initiative_id", "event", "member_id"),
741 "initiative_id" INT4,
742 "event" "snapshot_event",
743 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
744 "informed" BOOLEAN NOT NULL,
745 "satisfied" BOOLEAN NOT NULL,
746 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
747 FOREIGN KEY ("issue_id", "event", "member_id") REFERENCES "direct_interest_snapshot" ("issue_id", "event", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
748 CREATE INDEX "direct_supporter_snapshot_member_id_idx" ON "direct_supporter_snapshot" ("member_id");
750 COMMENT ON TABLE "direct_supporter_snapshot" IS 'Snapshot of supporters of initiatives (weight is stored in "direct_interest_snapshot")';
752 COMMENT ON COLUMN "direct_supporter_snapshot"."event" IS 'Reason for snapshot, see "snapshot_event" type for details';
753 COMMENT ON COLUMN "direct_supporter_snapshot"."informed" IS 'Supporter has seen the latest draft of the initiative';
754 COMMENT ON COLUMN "direct_supporter_snapshot"."satisfied" IS 'Supporter has no "critical_opinion"s';
757 CREATE TABLE "direct_voter" (
758 PRIMARY KEY ("issue_id", "member_id"),
759 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
760 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
761 "weight" INT4,
762 "autoreject" BOOLEAN NOT NULL DEFAULT FALSE );
763 CREATE INDEX "direct_voter_member_id_idx" ON "direct_voter" ("member_id");
765 COMMENT ON TABLE "direct_voter" IS 'Members having directly voted for/against initiatives of an issue; Frontends must ensure that no voters are added or removed to/from this table when the issue has been closed.';
767 COMMENT ON COLUMN "direct_voter"."weight" IS 'Weight of member (1 or higher) according to "delegating_voter" table';
768 COMMENT ON COLUMN "direct_voter"."autoreject" IS 'Votes were inserted due to "autoreject" feature';
771 CREATE TABLE "delegating_voter" (
772 PRIMARY KEY ("issue_id", "member_id"),
773 "issue_id" INT4 REFERENCES "issue" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
774 "member_id" INT4 REFERENCES "member" ("id") ON DELETE RESTRICT ON UPDATE RESTRICT,
775 "weight" INT4,
776 "scope" "delegation_scope" NOT NULL,
777 "delegate_member_ids" INT4[] NOT NULL );
778 CREATE INDEX "delegating_voter_member_id_idx" ON "delegating_voter" ("member_id");
780 COMMENT ON TABLE "delegating_voter" IS 'Delegations increasing the weight of entries in the "direct_voter" table';
782 COMMENT ON COLUMN "delegating_voter"."member_id" IS 'Delegating member';
783 COMMENT ON COLUMN "delegating_voter"."weight" IS 'Intermediate weight';
784 COMMENT ON COLUMN "delegating_voter"."delegate_member_ids" IS 'Chain of members who act as delegates; last entry referes to "member_id" column of table "direct_voter"';
787 CREATE TABLE "vote" (
788 "issue_id" INT4 NOT NULL,
789 PRIMARY KEY ("initiative_id", "member_id"),
790 "initiative_id" INT4,
791 "member_id" INT4,
792 "grade" INT4,
793 FOREIGN KEY ("issue_id", "initiative_id") REFERENCES "initiative" ("issue_id", "id") ON DELETE CASCADE ON UPDATE CASCADE,
794 FOREIGN KEY ("issue_id", "member_id") REFERENCES "direct_voter" ("issue_id", "member_id") ON DELETE CASCADE ON UPDATE CASCADE );
795 CREATE INDEX "vote_member_id_idx" ON "vote" ("member_id");
797 COMMENT ON TABLE "vote" IS 'Manual and delegated votes without abstentions; Frontends must ensure that no votes are added modified or removed when the issue has been closed.';
799 COMMENT ON COLUMN "vote"."grade" IS 'Values smaller than zero mean reject, values greater than zero mean acceptance, zero or missing row means abstention. Preferences are expressed by different positive or negative numbers.';
802 CREATE TABLE "contingent" (
803 "time_frame" INTERVAL PRIMARY KEY,
804 "text_entry_limit" INT4,
805 "initiative_limit" INT4 );
807 COMMENT ON TABLE "contingent" IS 'Amount of text entries or initiatives a user may create within a given time frame. Only one row needs to be fulfilled for a member to be allowed to post. This table must not be empty.';
809 COMMENT ON COLUMN "contingent"."text_entry_limit" IS 'Number of new drafts or suggestions to be submitted by each member within the given time frame';
810 COMMENT ON COLUMN "contingent"."initiative_limit" IS 'Number of new initiatives to be opened by each member within a given time frame';
814 --------------------------------
815 -- Writing of history entries --
816 --------------------------------
818 CREATE FUNCTION "write_member_history_trigger"()
819 RETURNS TRIGGER
820 LANGUAGE 'plpgsql' VOLATILE AS $$
821 BEGIN
822 IF
823 NEW."active" != OLD."active" OR
824 NEW."name" != OLD."name"
825 THEN
826 INSERT INTO "member_history"
827 ("member_id", "active", "name")
828 VALUES (NEW."id", OLD."active", OLD."name");
829 END IF;
830 RETURN NULL;
831 END;
832 $$;
834 CREATE TRIGGER "write_member_history"
835 AFTER UPDATE ON "member" FOR EACH ROW EXECUTE PROCEDURE
836 "write_member_history_trigger"();
838 COMMENT ON FUNCTION "write_member_history_trigger"() IS 'Implementation of trigger "write_member_history" on table "member"';
839 COMMENT ON TRIGGER "write_member_history" ON "member" IS 'When changing certain fields of a member, create a history entry in "member_history" table';
843 ----------------------------
844 -- Additional constraints --
845 ----------------------------
848 CREATE FUNCTION "issue_requires_first_initiative_trigger"()
849 RETURNS TRIGGER
850 LANGUAGE 'plpgsql' VOLATILE AS $$
851 BEGIN
852 IF NOT EXISTS (
853 SELECT NULL FROM "initiative" WHERE "issue_id" = NEW."id"
854 ) THEN
855 --RAISE 'Cannot create issue without an initial initiative.' USING
856 -- ERRCODE = 'integrity_constraint_violation',
857 -- HINT = 'Create issue, initiative, and draft within the same transaction.';
858 RAISE EXCEPTION 'Cannot create issue without an initial initiative.';
859 END IF;
860 RETURN NULL;
861 END;
862 $$;
864 CREATE CONSTRAINT TRIGGER "issue_requires_first_initiative"
865 AFTER INSERT OR UPDATE ON "issue" DEFERRABLE INITIALLY DEFERRED
866 FOR EACH ROW EXECUTE PROCEDURE
867 "issue_requires_first_initiative_trigger"();
869 COMMENT ON FUNCTION "issue_requires_first_initiative_trigger"() IS 'Implementation of trigger "issue_requires_first_initiative" on table "issue"';
870 COMMENT ON TRIGGER "issue_requires_first_initiative" ON "issue" IS 'Ensure that new issues have at least one initiative';
873 CREATE FUNCTION "last_initiative_deletes_issue_trigger"()
874 RETURNS TRIGGER
875 LANGUAGE 'plpgsql' VOLATILE AS $$
876 DECLARE
877 "reference_lost" BOOLEAN;
878 BEGIN
879 IF TG_OP = 'DELETE' THEN
880 "reference_lost" := TRUE;
881 ELSE
882 "reference_lost" := NEW."issue_id" != OLD."issue_id";
883 END IF;
884 IF
885 "reference_lost" AND NOT EXISTS (
886 SELECT NULL FROM "initiative" WHERE "issue_id" = OLD."issue_id"
887 )
888 THEN
889 DELETE FROM "issue" WHERE "id" = OLD."issue_id";
890 END IF;
891 RETURN NULL;
892 END;
893 $$;
895 CREATE CONSTRAINT TRIGGER "last_initiative_deletes_issue"
896 AFTER UPDATE OR DELETE ON "initiative" DEFERRABLE INITIALLY DEFERRED
897 FOR EACH ROW EXECUTE PROCEDURE
898 "last_initiative_deletes_issue_trigger"();
900 COMMENT ON FUNCTION "last_initiative_deletes_issue_trigger"() IS 'Implementation of trigger "last_initiative_deletes_issue" on table "initiative"';
901 COMMENT ON TRIGGER "last_initiative_deletes_issue" ON "initiative" IS 'Removing the last initiative of an issue deletes the issue';
904 CREATE FUNCTION "initiative_requires_first_draft_trigger"()
905 RETURNS TRIGGER
906 LANGUAGE 'plpgsql' VOLATILE AS $$
907 BEGIN
908 IF NOT EXISTS (
909 SELECT NULL FROM "draft" WHERE "initiative_id" = NEW."id"
910 ) THEN
911 --RAISE 'Cannot create initiative without an initial draft.' USING
912 -- ERRCODE = 'integrity_constraint_violation',
913 -- HINT = 'Create issue, initiative and draft within the same transaction.';
914 RAISE EXCEPTION 'Cannot create initiative without an initial draft.';
915 END IF;
916 RETURN NULL;
917 END;
918 $$;
920 CREATE CONSTRAINT TRIGGER "initiative_requires_first_draft"
921 AFTER INSERT OR UPDATE ON "initiative" DEFERRABLE INITIALLY DEFERRED
922 FOR EACH ROW EXECUTE PROCEDURE
923 "initiative_requires_first_draft_trigger"();
925 COMMENT ON FUNCTION "initiative_requires_first_draft_trigger"() IS 'Implementation of trigger "initiative_requires_first_draft" on table "initiative"';
926 COMMENT ON TRIGGER "initiative_requires_first_draft" ON "initiative" IS 'Ensure that new initiatives have at least one draft';
929 CREATE FUNCTION "last_draft_deletes_initiative_trigger"()
930 RETURNS TRIGGER
931 LANGUAGE 'plpgsql' VOLATILE AS $$
932 DECLARE
933 "reference_lost" BOOLEAN;
934 BEGIN
935 IF TG_OP = 'DELETE' THEN
936 "reference_lost" := TRUE;
937 ELSE
938 "reference_lost" := NEW."initiative_id" != OLD."initiative_id";
939 END IF;
940 IF
941 "reference_lost" AND NOT EXISTS (
942 SELECT NULL FROM "draft" WHERE "initiative_id" = OLD."initiative_id"
943 )
944 THEN
945 DELETE FROM "initiative" WHERE "id" = OLD."initiative_id";
946 END IF;
947 RETURN NULL;
948 END;
949 $$;
951 CREATE CONSTRAINT TRIGGER "last_draft_deletes_initiative"
952 AFTER UPDATE OR DELETE ON "draft" DEFERRABLE INITIALLY DEFERRED
953 FOR EACH ROW EXECUTE PROCEDURE
954 "last_draft_deletes_initiative_trigger"();
956 COMMENT ON FUNCTION "last_draft_deletes_initiative_trigger"() IS 'Implementation of trigger "last_draft_deletes_initiative" on table "draft"';
957 COMMENT ON TRIGGER "last_draft_deletes_initiative" ON "draft" IS 'Removing the last draft of an initiative deletes the initiative';
960 CREATE FUNCTION "suggestion_requires_first_opinion_trigger"()
961 RETURNS TRIGGER
962 LANGUAGE 'plpgsql' VOLATILE AS $$
963 BEGIN
964 IF NOT EXISTS (
965 SELECT NULL FROM "opinion" WHERE "suggestion_id" = NEW."id"
966 ) THEN
967 RAISE EXCEPTION 'Cannot create a suggestion without an opinion.';
968 END IF;
969 RETURN NULL;
970 END;
971 $$;
973 CREATE CONSTRAINT TRIGGER "suggestion_requires_first_opinion"
974 AFTER INSERT OR UPDATE ON "suggestion" DEFERRABLE INITIALLY DEFERRED
975 FOR EACH ROW EXECUTE PROCEDURE
976 "suggestion_requires_first_opinion_trigger"();
978 COMMENT ON FUNCTION "suggestion_requires_first_opinion_trigger"() IS 'Implementation of trigger "suggestion_requires_first_opinion" on table "suggestion"';
979 COMMENT ON TRIGGER "suggestion_requires_first_opinion" ON "suggestion" IS 'Ensure that new suggestions have at least one opinion';
982 CREATE FUNCTION "last_opinion_deletes_suggestion_trigger"()
983 RETURNS TRIGGER
984 LANGUAGE 'plpgsql' VOLATILE AS $$
985 DECLARE
986 "reference_lost" BOOLEAN;
987 BEGIN
988 IF TG_OP = 'DELETE' THEN
989 "reference_lost" := TRUE;
990 ELSE
991 "reference_lost" := NEW."suggestion_id" != OLD."suggestion_id";
992 END IF;
993 IF
994 "reference_lost" AND NOT EXISTS (
995 SELECT NULL FROM "opinion" WHERE "suggestion_id" = OLD."suggestion_id"
996 )
997 THEN
998 DELETE FROM "suggestion" WHERE "id" = OLD."suggestion_id";
999 END IF;
1000 RETURN NULL;
1001 END;
1002 $$;
1004 CREATE CONSTRAINT TRIGGER "last_opinion_deletes_suggestion"
1005 AFTER UPDATE OR DELETE ON "opinion" DEFERRABLE INITIALLY DEFERRED
1006 FOR EACH ROW EXECUTE PROCEDURE
1007 "last_opinion_deletes_suggestion_trigger"();
1009 COMMENT ON FUNCTION "last_opinion_deletes_suggestion_trigger"() IS 'Implementation of trigger "last_opinion_deletes_suggestion" on table "opinion"';
1010 COMMENT ON TRIGGER "last_opinion_deletes_suggestion" ON "opinion" IS 'Removing the last opinion of a suggestion deletes the suggestion';
1014 ---------------------------------------------------------------
1015 -- Ensure that votes are not modified when issues are frozen --
1016 ---------------------------------------------------------------
1018 -- NOTE: Frontends should ensure this anyway, but in case of programming
1019 -- errors the following triggers ensure data integrity.
1022 CREATE FUNCTION "forbid_changes_on_closed_issue_trigger"()
1023 RETURNS TRIGGER
1024 LANGUAGE 'plpgsql' VOLATILE AS $$
1025 DECLARE
1026 "issue_id_v" "issue"."id"%TYPE;
1027 "issue_row" "issue"%ROWTYPE;
1028 BEGIN
1029 IF TG_OP = 'DELETE' THEN
1030 "issue_id_v" := OLD."issue_id";
1031 ELSE
1032 "issue_id_v" := NEW."issue_id";
1033 END IF;
1034 SELECT INTO "issue_row" * FROM "issue"
1035 WHERE "id" = "issue_id_v" FOR SHARE;
1036 IF "issue_row"."closed" NOTNULL THEN
1037 RAISE EXCEPTION 'Tried to modify data belonging to a closed issue.';
1038 END IF;
1039 RETURN NULL;
1040 END;
1041 $$;
1043 CREATE TRIGGER "forbid_changes_on_closed_issue"
1044 AFTER INSERT OR UPDATE OR DELETE ON "direct_voter"
1045 FOR EACH ROW EXECUTE PROCEDURE
1046 "forbid_changes_on_closed_issue_trigger"();
1048 CREATE TRIGGER "forbid_changes_on_closed_issue"
1049 AFTER INSERT OR UPDATE OR DELETE ON "delegating_voter"
1050 FOR EACH ROW EXECUTE PROCEDURE
1051 "forbid_changes_on_closed_issue_trigger"();
1053 CREATE TRIGGER "forbid_changes_on_closed_issue"
1054 AFTER INSERT OR UPDATE OR DELETE ON "vote"
1055 FOR EACH ROW EXECUTE PROCEDURE
1056 "forbid_changes_on_closed_issue_trigger"();
1058 COMMENT ON FUNCTION "forbid_changes_on_closed_issue_trigger"() IS 'Implementation of triggers "forbid_changes_on_closed_issue" on tables "direct_voter", "delegating_voter" and "vote"';
1059 COMMENT ON TRIGGER "forbid_changes_on_closed_issue" ON "direct_voter" IS 'Ensures that frontends can''t tamper with votings of closed issues, in case of programming errors';
1060 COMMENT ON TRIGGER "forbid_changes_on_closed_issue" ON "delegating_voter" IS 'Ensures that frontends can''t tamper with votings of closed issues, in case of programming errors';
1061 COMMENT ON TRIGGER "forbid_changes_on_closed_issue" ON "vote" IS 'Ensures that frontends can''t tamper with votings of closed issues, in case of programming errors';
1065 --------------------------------------------------------------------
1066 -- Auto-retrieval of fields only needed for referential integrity --
1067 --------------------------------------------------------------------
1070 CREATE FUNCTION "autofill_issue_id_trigger"()
1071 RETURNS TRIGGER
1072 LANGUAGE 'plpgsql' VOLATILE AS $$
1073 BEGIN
1074 IF NEW."issue_id" ISNULL THEN
1075 SELECT "issue_id" INTO NEW."issue_id"
1076 FROM "initiative" WHERE "id" = NEW."initiative_id";
1077 END IF;
1078 RETURN NEW;
1079 END;
1080 $$;
1082 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "supporter"
1083 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
1085 CREATE TRIGGER "autofill_issue_id" BEFORE INSERT ON "vote"
1086 FOR EACH ROW EXECUTE PROCEDURE "autofill_issue_id_trigger"();
1088 COMMENT ON FUNCTION "autofill_issue_id_trigger"() IS 'Implementation of triggers "autofill_issue_id" on tables "supporter" and "vote"';
1089 COMMENT ON TRIGGER "autofill_issue_id" ON "supporter" IS 'Set "issue_id" field automatically, if NULL';
1090 COMMENT ON TRIGGER "autofill_issue_id" ON "vote" IS 'Set "issue_id" field automatically, if NULL';
1093 CREATE FUNCTION "autofill_initiative_id_trigger"()
1094 RETURNS TRIGGER
1095 LANGUAGE 'plpgsql' VOLATILE AS $$
1096 BEGIN
1097 IF NEW."initiative_id" ISNULL THEN
1098 SELECT "initiative_id" INTO NEW."initiative_id"
1099 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
1100 END IF;
1101 RETURN NEW;
1102 END;
1103 $$;
1105 CREATE TRIGGER "autofill_initiative_id" BEFORE INSERT ON "opinion"
1106 FOR EACH ROW EXECUTE PROCEDURE "autofill_initiative_id_trigger"();
1108 COMMENT ON FUNCTION "autofill_initiative_id_trigger"() IS 'Implementation of trigger "autofill_initiative_id" on table "opinion"';
1109 COMMENT ON TRIGGER "autofill_initiative_id" ON "opinion" IS 'Set "initiative_id" field automatically, if NULL';
1113 -----------------------------------------------------
1114 -- Automatic calculation of certain default values --
1115 -----------------------------------------------------
1118 CREATE FUNCTION "copy_timings_trigger"()
1119 RETURNS TRIGGER
1120 LANGUAGE 'plpgsql' VOLATILE AS $$
1121 DECLARE
1122 "policy_row" "policy"%ROWTYPE;
1123 BEGIN
1124 SELECT * INTO "policy_row" FROM "policy"
1125 WHERE "id" = NEW."policy_id";
1126 IF NEW."admission_time" ISNULL THEN
1127 NEW."admission_time" := "policy_row"."admission_time";
1128 END IF;
1129 IF NEW."discussion_time" ISNULL THEN
1130 NEW."discussion_time" := "policy_row"."discussion_time";
1131 END IF;
1132 IF NEW."verification_time" ISNULL THEN
1133 NEW."verification_time" := "policy_row"."verification_time";
1134 END IF;
1135 IF NEW."voting_time" ISNULL THEN
1136 NEW."voting_time" := "policy_row"."voting_time";
1137 END IF;
1138 RETURN NEW;
1139 END;
1140 $$;
1142 CREATE TRIGGER "copy_timings" BEFORE INSERT OR UPDATE ON "issue"
1143 FOR EACH ROW EXECUTE PROCEDURE "copy_timings_trigger"();
1145 COMMENT ON FUNCTION "copy_timings_trigger"() IS 'Implementation of trigger "copy_timings" on table "issue"';
1146 COMMENT ON TRIGGER "copy_timings" ON "issue" IS 'If timing fields are NULL, copy values from policy.';
1149 CREATE FUNCTION "copy_autoreject_trigger"()
1150 RETURNS TRIGGER
1151 LANGUAGE 'plpgsql' VOLATILE AS $$
1152 BEGIN
1153 IF NEW."autoreject" ISNULL THEN
1154 SELECT "membership"."autoreject" INTO NEW."autoreject"
1155 FROM "issue" JOIN "membership"
1156 ON "issue"."area_id" = "membership"."area_id"
1157 WHERE "issue"."id" = NEW."issue_id"
1158 AND "membership"."member_id" = NEW."member_id";
1159 END IF;
1160 IF NEW."autoreject" ISNULL THEN
1161 NEW."autoreject" := FALSE;
1162 END IF;
1163 RETURN NEW;
1164 END;
1165 $$;
1167 CREATE TRIGGER "copy_autoreject" BEFORE INSERT OR UPDATE ON "interest"
1168 FOR EACH ROW EXECUTE PROCEDURE "copy_autoreject_trigger"();
1170 COMMENT ON FUNCTION "copy_autoreject_trigger"() IS 'Implementation of trigger "copy_autoreject" on table "interest"';
1171 COMMENT ON TRIGGER "copy_autoreject" ON "interest" IS 'If "autoreject" is NULL, then copy it from the area setting, or set to FALSE, if no membership existent';
1174 CREATE FUNCTION "supporter_default_for_draft_id_trigger"()
1175 RETURNS TRIGGER
1176 LANGUAGE 'plpgsql' VOLATILE AS $$
1177 BEGIN
1178 IF NEW."draft_id" ISNULL THEN
1179 SELECT "id" INTO NEW."draft_id" FROM "current_draft"
1180 WHERE "initiative_id" = NEW."initiative_id";
1181 END IF;
1182 RETURN NEW;
1183 END;
1184 $$;
1186 CREATE TRIGGER "default_for_draft_id" BEFORE INSERT OR UPDATE ON "supporter"
1187 FOR EACH ROW EXECUTE PROCEDURE "supporter_default_for_draft_id_trigger"();
1189 COMMENT ON FUNCTION "supporter_default_for_draft_id_trigger"() IS 'Implementation of trigger "default_for_draft" on table "supporter"';
1190 COMMENT ON TRIGGER "default_for_draft_id" ON "supporter" IS 'If "draft_id" is NULL, then use the current draft of the initiative as default';
1194 ----------------------------------------
1195 -- Automatic creation of dependencies --
1196 ----------------------------------------
1199 CREATE FUNCTION "autocreate_interest_trigger"()
1200 RETURNS TRIGGER
1201 LANGUAGE 'plpgsql' VOLATILE AS $$
1202 BEGIN
1203 IF NOT EXISTS (
1204 SELECT NULL FROM "initiative" JOIN "interest"
1205 ON "initiative"."issue_id" = "interest"."issue_id"
1206 WHERE "initiative"."id" = NEW."initiative_id"
1207 AND "interest"."member_id" = NEW."member_id"
1208 ) THEN
1209 BEGIN
1210 INSERT INTO "interest" ("issue_id", "member_id")
1211 SELECT "issue_id", NEW."member_id"
1212 FROM "initiative" WHERE "id" = NEW."initiative_id";
1213 EXCEPTION WHEN unique_violation THEN END;
1214 END IF;
1215 RETURN NEW;
1216 END;
1217 $$;
1219 CREATE TRIGGER "autocreate_interest" BEFORE INSERT ON "supporter"
1220 FOR EACH ROW EXECUTE PROCEDURE "autocreate_interest_trigger"();
1222 COMMENT ON FUNCTION "autocreate_interest_trigger"() IS 'Implementation of trigger "autocreate_interest" on table "supporter"';
1223 COMMENT ON TRIGGER "autocreate_interest" ON "supporter" IS 'Supporting an initiative implies interest in the issue, thus automatically creates an entry in the "interest" table';
1226 CREATE FUNCTION "autocreate_supporter_trigger"()
1227 RETURNS TRIGGER
1228 LANGUAGE 'plpgsql' VOLATILE AS $$
1229 BEGIN
1230 IF NOT EXISTS (
1231 SELECT NULL FROM "suggestion" JOIN "supporter"
1232 ON "suggestion"."initiative_id" = "supporter"."initiative_id"
1233 WHERE "suggestion"."id" = NEW."suggestion_id"
1234 AND "supporter"."member_id" = NEW."member_id"
1235 ) THEN
1236 BEGIN
1237 INSERT INTO "supporter" ("initiative_id", "member_id")
1238 SELECT "initiative_id", NEW."member_id"
1239 FROM "suggestion" WHERE "id" = NEW."suggestion_id";
1240 EXCEPTION WHEN unique_violation THEN END;
1241 END IF;
1242 RETURN NEW;
1243 END;
1244 $$;
1246 CREATE TRIGGER "autocreate_supporter" BEFORE INSERT ON "opinion"
1247 FOR EACH ROW EXECUTE PROCEDURE "autocreate_supporter_trigger"();
1249 COMMENT ON FUNCTION "autocreate_supporter_trigger"() IS 'Implementation of trigger "autocreate_supporter" on table "opinion"';
1250 COMMENT ON TRIGGER "autocreate_supporter" ON "opinion" IS 'Opinions can only be added for supported initiatives. This trigger automatrically creates an entry in the "supporter" table, if not existent yet.';
1254 ------------------------------------------
1255 -- Views and helper functions for views --
1256 ------------------------------------------
1259 CREATE VIEW "global_delegation" AS
1260 SELECT
1261 "delegation"."id",
1262 "delegation"."truster_id",
1263 "delegation"."trustee_id"
1264 FROM "delegation" JOIN "member"
1265 ON "delegation"."trustee_id" = "member"."id"
1266 WHERE "delegation"."scope" = 'global' AND "member"."active";
1268 COMMENT ON VIEW "global_delegation" IS 'Global delegations to active members';
1271 CREATE VIEW "area_delegation" AS
1272 SELECT "subquery".* FROM (
1273 SELECT DISTINCT ON ("area"."id", "delegation"."truster_id")
1274 "area"."id" AS "area_id",
1275 "delegation"."id",
1276 "delegation"."truster_id",
1277 "delegation"."trustee_id",
1278 "delegation"."scope"
1279 FROM "area" JOIN "delegation"
1280 ON "delegation"."scope" = 'global'
1281 OR "delegation"."area_id" = "area"."id"
1282 ORDER BY
1283 "area"."id",
1284 "delegation"."truster_id",
1285 "delegation"."scope" DESC
1286 ) AS "subquery"
1287 JOIN "member" ON "subquery"."trustee_id" = "member"."id"
1288 WHERE "member"."active";
1290 COMMENT ON VIEW "area_delegation" IS 'Active delegations for areas';
1293 CREATE VIEW "issue_delegation" AS
1294 SELECT "subquery".* FROM (
1295 SELECT DISTINCT ON ("issue"."id", "delegation"."truster_id")
1296 "issue"."id" AS "issue_id",
1297 "delegation"."id",
1298 "delegation"."truster_id",
1299 "delegation"."trustee_id",
1300 "delegation"."scope"
1301 FROM "issue" JOIN "delegation"
1302 ON "delegation"."scope" = 'global'
1303 OR "delegation"."area_id" = "issue"."area_id"
1304 OR "delegation"."issue_id" = "issue"."id"
1305 ORDER BY
1306 "issue"."id",
1307 "delegation"."truster_id",
1308 "delegation"."scope" DESC
1309 ) AS "subquery"
1310 JOIN "member" ON "subquery"."trustee_id" = "member"."id"
1311 WHERE "member"."active";
1313 COMMENT ON VIEW "issue_delegation" IS 'Active delegations for issues';
1316 CREATE FUNCTION "membership_weight_with_skipping"
1317 ( "area_id_p" "area"."id"%TYPE,
1318 "member_id_p" "member"."id"%TYPE,
1319 "skip_member_ids_p" INT4[] ) -- "member"."id"%TYPE[]
1320 RETURNS INT4
1321 LANGUAGE 'plpgsql' STABLE AS $$
1322 DECLARE
1323 "sum_v" INT4;
1324 "delegation_row" "area_delegation"%ROWTYPE;
1325 BEGIN
1326 "sum_v" := 1;
1327 FOR "delegation_row" IN
1328 SELECT "area_delegation".*
1329 FROM "area_delegation" LEFT JOIN "membership"
1330 ON "membership"."area_id" = "area_id_p"
1331 AND "membership"."member_id" = "area_delegation"."truster_id"
1332 WHERE "area_delegation"."area_id" = "area_id_p"
1333 AND "area_delegation"."trustee_id" = "member_id_p"
1334 AND "membership"."member_id" ISNULL
1335 LOOP
1336 IF NOT
1337 "skip_member_ids_p" @> ARRAY["delegation_row"."truster_id"]
1338 THEN
1339 "sum_v" := "sum_v" + "membership_weight_with_skipping"(
1340 "area_id_p",
1341 "delegation_row"."truster_id",
1342 "skip_member_ids_p" || "delegation_row"."truster_id"
1343 );
1344 END IF;
1345 END LOOP;
1346 RETURN "sum_v";
1347 END;
1348 $$;
1350 COMMENT ON FUNCTION "membership_weight_with_skipping"
1351 ( "area"."id"%TYPE,
1352 "member"."id"%TYPE,
1353 INT4[] )
1354 IS 'Helper function for "membership_weight" function';
1357 CREATE FUNCTION "membership_weight"
1358 ( "area_id_p" "area"."id"%TYPE,
1359 "member_id_p" "member"."id"%TYPE ) -- "member"."id"%TYPE[]
1360 RETURNS INT4
1361 LANGUAGE 'plpgsql' STABLE AS $$
1362 BEGIN
1363 RETURN "membership_weight_with_skipping"(
1364 "area_id_p",
1365 "member_id_p",
1366 ARRAY["member_id_p"]
1367 );
1368 END;
1369 $$;
1371 COMMENT ON FUNCTION "membership_weight"
1372 ( "area"."id"%TYPE,
1373 "member"."id"%TYPE )
1374 IS 'Calculates the potential voting weight of a member in a given area';
1377 CREATE VIEW "member_count_view" AS
1378 SELECT count(1) AS "total_count" FROM "member" WHERE "active";
1380 COMMENT ON VIEW "member_count_view" IS 'View used to update "member_count" table';
1383 CREATE VIEW "area_member_count" AS
1384 SELECT
1385 "area"."id" AS "area_id",
1386 count("member"."id") AS "direct_member_count",
1387 coalesce(
1388 sum(
1389 CASE WHEN "member"."id" NOTNULL THEN
1390 "membership_weight"("area"."id", "member"."id")
1391 ELSE 0 END
1393 ) AS "member_weight",
1394 coalesce(
1395 sum(
1396 CASE WHEN "member"."id" NOTNULL AND "membership"."autoreject" THEN
1397 "membership_weight"("area"."id", "member"."id")
1398 ELSE 0 END
1400 ) AS "autoreject_weight"
1401 FROM "area"
1402 LEFT JOIN "membership"
1403 ON "area"."id" = "membership"."area_id"
1404 LEFT JOIN "member"
1405 ON "membership"."member_id" = "member"."id"
1406 AND "member"."active"
1407 GROUP BY "area"."id";
1409 COMMENT ON VIEW "area_member_count" IS 'View used to update "member_count" column of table "area"';
1412 CREATE VIEW "opening_draft" AS
1413 SELECT "draft".* FROM (
1414 SELECT
1415 "initiative"."id" AS "initiative_id",
1416 min("draft"."id") AS "draft_id"
1417 FROM "initiative" JOIN "draft"
1418 ON "initiative"."id" = "draft"."initiative_id"
1419 GROUP BY "initiative"."id"
1420 ) AS "subquery"
1421 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
1423 COMMENT ON VIEW "opening_draft" IS 'First drafts of all initiatives';
1426 CREATE VIEW "current_draft" AS
1427 SELECT "draft".* FROM (
1428 SELECT
1429 "initiative"."id" AS "initiative_id",
1430 max("draft"."id") AS "draft_id"
1431 FROM "initiative" JOIN "draft"
1432 ON "initiative"."id" = "draft"."initiative_id"
1433 GROUP BY "initiative"."id"
1434 ) AS "subquery"
1435 JOIN "draft" ON "subquery"."draft_id" = "draft"."id";
1437 COMMENT ON VIEW "current_draft" IS 'All latest drafts for each initiative';
1440 CREATE VIEW "critical_opinion" AS
1441 SELECT * FROM "opinion"
1442 WHERE ("degree" = 2 AND "fulfilled" = FALSE)
1443 OR ("degree" = -2 AND "fulfilled" = TRUE);
1445 COMMENT ON VIEW "critical_opinion" IS 'Opinions currently causing dissatisfaction';
1448 CREATE VIEW "battle_view" AS
1449 SELECT
1450 "issue"."id" AS "issue_id",
1451 "winning_initiative"."id" AS "winning_initiative_id",
1452 "losing_initiative"."id" AS "losing_initiative_id",
1453 sum(
1454 CASE WHEN
1455 coalesce("better_vote"."grade", 0) >
1456 coalesce("worse_vote"."grade", 0)
1457 THEN "direct_voter"."weight" ELSE 0 END
1458 ) AS "count"
1459 FROM "issue"
1460 LEFT JOIN "direct_voter"
1461 ON "issue"."id" = "direct_voter"."issue_id"
1462 JOIN "initiative" AS "winning_initiative"
1463 ON "issue"."id" = "winning_initiative"."issue_id"
1464 AND "winning_initiative"."agreed"
1465 JOIN "initiative" AS "losing_initiative"
1466 ON "issue"."id" = "losing_initiative"."issue_id"
1467 AND "losing_initiative"."agreed"
1468 LEFT JOIN "vote" AS "better_vote"
1469 ON "direct_voter"."member_id" = "better_vote"."member_id"
1470 AND "winning_initiative"."id" = "better_vote"."initiative_id"
1471 LEFT JOIN "vote" AS "worse_vote"
1472 ON "direct_voter"."member_id" = "worse_vote"."member_id"
1473 AND "losing_initiative"."id" = "worse_vote"."initiative_id"
1474 WHERE "issue"."closed" NOTNULL
1475 AND "issue"."cleaned" ISNULL
1476 AND "winning_initiative"."id" != "losing_initiative"."id"
1477 GROUP BY
1478 "issue"."id",
1479 "winning_initiative"."id",
1480 "losing_initiative"."id";
1482 COMMENT ON VIEW "battle_view" IS 'Number of members preferring one initiative to another; Used to fill "battle" table';
1485 CREATE VIEW "expired_session" AS
1486 SELECT * FROM "session" WHERE now() > "expiry";
1488 CREATE RULE "delete" AS ON DELETE TO "expired_session" DO INSTEAD
1489 DELETE FROM "session" WHERE "ident" = OLD."ident";
1491 COMMENT ON VIEW "expired_session" IS 'View containing all expired sessions where DELETE is possible';
1492 COMMENT ON RULE "delete" ON "expired_session" IS 'Rule allowing DELETE on rows in "expired_session" view, i.e. DELETE FROM "expired_session"';
1495 CREATE VIEW "open_issue" AS
1496 SELECT * FROM "issue" WHERE "closed" ISNULL;
1498 COMMENT ON VIEW "open_issue" IS 'All open issues';
1501 CREATE VIEW "issue_with_ranks_missing" AS
1502 SELECT * FROM "issue"
1503 WHERE "fully_frozen" NOTNULL
1504 AND "closed" NOTNULL
1505 AND "ranks_available" = FALSE;
1507 COMMENT ON VIEW "issue_with_ranks_missing" IS 'Issues where voting was finished, but no ranks have been calculated yet';
1510 CREATE VIEW "member_contingent" AS
1511 SELECT
1512 "member"."id" AS "member_id",
1513 "contingent"."time_frame",
1514 CASE WHEN "contingent"."text_entry_limit" NOTNULL THEN
1516 SELECT count(1) FROM "draft"
1517 WHERE "draft"."author_id" = "member"."id"
1518 AND "draft"."created" > now() - "contingent"."time_frame"
1519 ) + (
1520 SELECT count(1) FROM "suggestion"
1521 WHERE "suggestion"."author_id" = "member"."id"
1522 AND "suggestion"."created" > now() - "contingent"."time_frame"
1524 ELSE NULL END AS "text_entry_count",
1525 "contingent"."text_entry_limit",
1526 CASE WHEN "contingent"."initiative_limit" NOTNULL THEN (
1527 SELECT count(1) FROM "opening_draft"
1528 WHERE "opening_draft"."author_id" = "member"."id"
1529 AND "opening_draft"."created" > now() - "contingent"."time_frame"
1530 ) ELSE NULL END AS "initiative_count",
1531 "contingent"."initiative_limit"
1532 FROM "member" CROSS JOIN "contingent";
1534 COMMENT ON VIEW "member_contingent" IS 'Actual counts of text entries and initiatives are calculated per member for each limit in the "contingent" table.';
1536 COMMENT ON COLUMN "member_contingent"."text_entry_count" IS 'Only calculated when "text_entry_limit" is not null in the same row';
1537 COMMENT ON COLUMN "member_contingent"."initiative_count" IS 'Only calculated when "initiative_limit" is not null in the same row';
1540 CREATE VIEW "member_contingent_left" AS
1541 SELECT
1542 "member_id",
1543 max("text_entry_limit" - "text_entry_count") AS "text_entries_left",
1544 max("initiative_limit" - "initiative_count") AS "initiatives_left"
1545 FROM "member_contingent" GROUP BY "member_id";
1547 COMMENT ON VIEW "member_contingent_left" IS 'Amount of text entries or initiatives which can be posted now instantly by a member. This view should be used by a frontend to determine, if the contingent for posting is exhausted.';
1550 CREATE TYPE "timeline_event" AS ENUM (
1551 'issue_created',
1552 'issue_canceled',
1553 'issue_accepted',
1554 'issue_half_frozen',
1555 'issue_finished_without_voting',
1556 'issue_voting_started',
1557 'issue_finished_after_voting',
1558 'initiative_created',
1559 'initiative_revoked',
1560 'draft_created',
1561 'suggestion_created');
1563 COMMENT ON TYPE "timeline_event" IS 'Types of event in timeline tables';
1566 CREATE VIEW "timeline_issue" AS
1567 SELECT
1568 "created" AS "occurrence",
1569 'issue_created'::"timeline_event" AS "event",
1570 "id" AS "issue_id"
1571 FROM "issue"
1572 UNION ALL
1573 SELECT
1574 "closed" AS "occurrence",
1575 'issue_canceled'::"timeline_event" AS "event",
1576 "id" AS "issue_id"
1577 FROM "issue" WHERE "closed" NOTNULL AND "fully_frozen" ISNULL
1578 UNION ALL
1579 SELECT
1580 "accepted" AS "occurrence",
1581 'issue_accepted'::"timeline_event" AS "event",
1582 "id" AS "issue_id"
1583 FROM "issue" WHERE "accepted" NOTNULL
1584 UNION ALL
1585 SELECT
1586 "half_frozen" AS "occurrence",
1587 'issue_half_frozen'::"timeline_event" AS "event",
1588 "id" AS "issue_id"
1589 FROM "issue" WHERE "half_frozen" NOTNULL
1590 UNION ALL
1591 SELECT
1592 "fully_frozen" AS "occurrence",
1593 'issue_voting_started'::"timeline_event" AS "event",
1594 "id" AS "issue_id"
1595 FROM "issue"
1596 WHERE "fully_frozen" NOTNULL
1597 AND ("closed" ISNULL OR "closed" != "fully_frozen")
1598 UNION ALL
1599 SELECT
1600 "closed" AS "occurrence",
1601 CASE WHEN "fully_frozen" = "closed" THEN
1602 'issue_finished_without_voting'::"timeline_event"
1603 ELSE
1604 'issue_finished_after_voting'::"timeline_event"
1605 END AS "event",
1606 "id" AS "issue_id"
1607 FROM "issue" WHERE "closed" NOTNULL AND "fully_frozen" NOTNULL;
1609 COMMENT ON VIEW "timeline_issue" IS 'Helper view for "timeline" view';
1612 CREATE VIEW "timeline_initiative" AS
1613 SELECT
1614 "created" AS "occurrence",
1615 'initiative_created'::"timeline_event" AS "event",
1616 "id" AS "initiative_id"
1617 FROM "initiative"
1618 UNION ALL
1619 SELECT
1620 "revoked" AS "occurrence",
1621 'initiative_revoked'::"timeline_event" AS "event",
1622 "id" AS "initiative_id"
1623 FROM "initiative" WHERE "revoked" NOTNULL;
1625 COMMENT ON VIEW "timeline_initiative" IS 'Helper view for "timeline" view';
1628 CREATE VIEW "timeline_draft" AS
1629 SELECT
1630 "created" AS "occurrence",
1631 'draft_created'::"timeline_event" AS "event",
1632 "id" AS "draft_id"
1633 FROM "draft";
1635 COMMENT ON VIEW "timeline_draft" IS 'Helper view for "timeline" view';
1638 CREATE VIEW "timeline_suggestion" AS
1639 SELECT
1640 "created" AS "occurrence",
1641 'suggestion_created'::"timeline_event" AS "event",
1642 "id" AS "suggestion_id"
1643 FROM "suggestion";
1645 COMMENT ON VIEW "timeline_suggestion" IS 'Helper view for "timeline" view';
1648 CREATE VIEW "timeline" AS
1649 SELECT
1650 "occurrence",
1651 "event",
1652 "issue_id",
1653 NULL AS "initiative_id",
1654 NULL::INT8 AS "draft_id", -- TODO: Why do we need a type-cast here? Is this due to 32 bit architecture?
1655 NULL::INT8 AS "suggestion_id"
1656 FROM "timeline_issue"
1657 UNION ALL
1658 SELECT
1659 "occurrence",
1660 "event",
1661 NULL AS "issue_id",
1662 "initiative_id",
1663 NULL AS "draft_id",
1664 NULL AS "suggestion_id"
1665 FROM "timeline_initiative"
1666 UNION ALL
1667 SELECT
1668 "occurrence",
1669 "event",
1670 NULL AS "issue_id",
1671 NULL AS "initiative_id",
1672 "draft_id",
1673 NULL AS "suggestion_id"
1674 FROM "timeline_draft"
1675 UNION ALL
1676 SELECT
1677 "occurrence",
1678 "event",
1679 NULL AS "issue_id",
1680 NULL AS "initiative_id",
1681 NULL AS "draft_id",
1682 "suggestion_id"
1683 FROM "timeline_suggestion";
1685 COMMENT ON VIEW "timeline" IS 'Aggregation of different events in the system';
1689 --------------------------------------------------
1690 -- Set returning function for delegation chains --
1691 --------------------------------------------------
1694 CREATE TYPE "delegation_chain_loop_tag" AS ENUM
1695 ('first', 'intermediate', 'last', 'repetition');
1697 COMMENT ON TYPE "delegation_chain_loop_tag" IS 'Type for loop tags in "delegation_chain_row" type';
1700 CREATE TYPE "delegation_chain_row" AS (
1701 "index" INT4,
1702 "member_id" INT4,
1703 "member_active" BOOLEAN,
1704 "participation" BOOLEAN,
1705 "overridden" BOOLEAN,
1706 "scope_in" "delegation_scope",
1707 "scope_out" "delegation_scope",
1708 "loop" "delegation_chain_loop_tag" );
1710 COMMENT ON TYPE "delegation_chain_row" IS 'Type of rows returned by "delegation_chain"(...) functions';
1712 COMMENT ON COLUMN "delegation_chain_row"."index" IS 'Index starting with 0 and counting up';
1713 COMMENT ON COLUMN "delegation_chain_row"."participation" IS 'In case of delegation chains for issues: interest, for areas: membership, for global delegation chains: always null';
1714 COMMENT ON COLUMN "delegation_chain_row"."overridden" IS 'True, if an entry with lower index has "participation" set to true';
1715 COMMENT ON COLUMN "delegation_chain_row"."scope_in" IS 'Scope of used incoming delegation';
1716 COMMENT ON COLUMN "delegation_chain_row"."scope_out" IS 'Scope of used outgoing delegation';
1717 COMMENT ON COLUMN "delegation_chain_row"."loop" IS 'Not null, if member is part of a loop, see "delegation_chain_loop_tag" type';
1720 CREATE FUNCTION "delegation_chain"
1721 ( "member_id_p" "member"."id"%TYPE,
1722 "area_id_p" "area"."id"%TYPE,
1723 "issue_id_p" "issue"."id"%TYPE,
1724 "simulate_trustee_id_p" "member"."id"%TYPE )
1725 RETURNS SETOF "delegation_chain_row"
1726 LANGUAGE 'plpgsql' STABLE AS $$
1727 DECLARE
1728 "issue_row" "issue"%ROWTYPE;
1729 "visited_member_ids" INT4[]; -- "member"."id"%TYPE[]
1730 "loop_member_id_v" "member"."id"%TYPE;
1731 "output_row" "delegation_chain_row";
1732 "output_rows" "delegation_chain_row"[];
1733 "delegation_row" "delegation"%ROWTYPE;
1734 "row_count" INT4;
1735 "i" INT4;
1736 "loop_v" BOOLEAN;
1737 BEGIN
1738 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
1739 "visited_member_ids" := '{}';
1740 "loop_member_id_v" := NULL;
1741 "output_rows" := '{}';
1742 "output_row"."index" := 0;
1743 "output_row"."member_id" := "member_id_p";
1744 "output_row"."member_active" := TRUE;
1745 "output_row"."participation" := FALSE;
1746 "output_row"."overridden" := FALSE;
1747 "output_row"."scope_out" := NULL;
1748 LOOP
1749 IF "visited_member_ids" @> ARRAY["output_row"."member_id"] THEN
1750 "loop_member_id_v" := "output_row"."member_id";
1751 ELSE
1752 "visited_member_ids" :=
1753 "visited_member_ids" || "output_row"."member_id";
1754 END IF;
1755 IF "output_row"."participation" THEN
1756 "output_row"."overridden" := TRUE;
1757 END IF;
1758 "output_row"."scope_in" := "output_row"."scope_out";
1759 IF EXISTS (
1760 SELECT NULL FROM "member"
1761 WHERE "id" = "output_row"."member_id" AND "active"
1762 ) THEN
1763 IF "area_id_p" ISNULL AND "issue_id_p" ISNULL THEN
1764 SELECT * INTO "delegation_row" FROM "delegation"
1765 WHERE "truster_id" = "output_row"."member_id"
1766 AND "scope" = 'global';
1767 ELSIF "area_id_p" NOTNULL AND "issue_id_p" ISNULL THEN
1768 "output_row"."participation" := EXISTS (
1769 SELECT NULL FROM "membership"
1770 WHERE "area_id" = "area_id_p"
1771 AND "member_id" = "output_row"."member_id"
1772 );
1773 SELECT * INTO "delegation_row" FROM "delegation"
1774 WHERE "truster_id" = "output_row"."member_id"
1775 AND ("scope" = 'global' OR "area_id" = "area_id_p")
1776 ORDER BY "scope" DESC;
1777 ELSIF "area_id_p" ISNULL AND "issue_id_p" NOTNULL THEN
1778 "output_row"."participation" := EXISTS (
1779 SELECT NULL FROM "interest"
1780 WHERE "issue_id" = "issue_id_p"
1781 AND "member_id" = "output_row"."member_id"
1782 );
1783 SELECT * INTO "delegation_row" FROM "delegation"
1784 WHERE "truster_id" = "output_row"."member_id"
1785 AND ("scope" = 'global' OR
1786 "area_id" = "issue_row"."area_id" OR
1787 "issue_id" = "issue_id_p"
1789 ORDER BY "scope" DESC;
1790 ELSE
1791 RAISE EXCEPTION 'Either area_id or issue_id or both must be NULL.';
1792 END IF;
1793 ELSE
1794 "output_row"."member_active" := FALSE;
1795 "output_row"."participation" := FALSE;
1796 "output_row"."scope_out" := NULL;
1797 "delegation_row" := ROW(NULL);
1798 END IF;
1799 IF
1800 "output_row"."member_id" = "member_id_p" AND
1801 "simulate_trustee_id_p" NOTNULL
1802 THEN
1803 "output_row"."scope_out" := CASE
1804 WHEN "area_id_p" ISNULL AND "issue_id_p" ISNULL THEN 'global'
1805 WHEN "area_id_p" NOTNULL AND "issue_id_p" ISNULL THEN 'area'
1806 WHEN "area_id_p" ISNULL AND "issue_id_p" NOTNULL THEN 'issue'
1807 END;
1808 "output_rows" := "output_rows" || "output_row";
1809 "output_row"."member_id" := "simulate_trustee_id_p";
1810 ELSIF "delegation_row"."trustee_id" NOTNULL THEN
1811 "output_row"."scope_out" := "delegation_row"."scope";
1812 "output_rows" := "output_rows" || "output_row";
1813 "output_row"."member_id" := "delegation_row"."trustee_id";
1814 ELSE
1815 "output_row"."scope_out" := NULL;
1816 "output_rows" := "output_rows" || "output_row";
1817 EXIT;
1818 END IF;
1819 EXIT WHEN "loop_member_id_v" NOTNULL;
1820 "output_row"."index" := "output_row"."index" + 1;
1821 END LOOP;
1822 "row_count" := array_upper("output_rows", 1);
1823 "i" := 1;
1824 "loop_v" := FALSE;
1825 LOOP
1826 "output_row" := "output_rows"["i"];
1827 EXIT WHEN "output_row"."member_id" ISNULL;
1828 IF "loop_v" THEN
1829 IF "i" + 1 = "row_count" THEN
1830 "output_row"."loop" := 'last';
1831 ELSIF "i" = "row_count" THEN
1832 "output_row"."loop" := 'repetition';
1833 ELSE
1834 "output_row"."loop" := 'intermediate';
1835 END IF;
1836 ELSIF "output_row"."member_id" = "loop_member_id_v" THEN
1837 "output_row"."loop" := 'first';
1838 "loop_v" := TRUE;
1839 END IF;
1840 IF "area_id_p" ISNULL AND "issue_id_p" ISNULL THEN
1841 "output_row"."participation" := NULL;
1842 END IF;
1843 RETURN NEXT "output_row";
1844 "i" := "i" + 1;
1845 END LOOP;
1846 RETURN;
1847 END;
1848 $$;
1850 COMMENT ON FUNCTION "delegation_chain"
1851 ( "member"."id"%TYPE,
1852 "area"."id"%TYPE,
1853 "issue"."id"%TYPE,
1854 "member"."id"%TYPE )
1855 IS 'Helper function for frontends to display delegation chains; Not part of internal voting logic';
1857 CREATE FUNCTION "delegation_chain"
1858 ( "member_id_p" "member"."id"%TYPE,
1859 "area_id_p" "area"."id"%TYPE,
1860 "issue_id_p" "issue"."id"%TYPE )
1861 RETURNS SETOF "delegation_chain_row"
1862 LANGUAGE 'plpgsql' STABLE AS $$
1863 DECLARE
1864 "result_row" "delegation_chain_row";
1865 BEGIN
1866 FOR "result_row" IN
1867 SELECT * FROM "delegation_chain"(
1868 "member_id_p", "area_id_p", "issue_id_p", NULL
1870 LOOP
1871 RETURN NEXT "result_row";
1872 END LOOP;
1873 RETURN;
1874 END;
1875 $$;
1877 COMMENT ON FUNCTION "delegation_chain"
1878 ( "member"."id"%TYPE,
1879 "area"."id"%TYPE,
1880 "issue"."id"%TYPE )
1881 IS 'Shortcut for "delegation_chain"(...) function where 4th parameter is null';
1885 ------------------------------
1886 -- Comparison by vote count --
1887 ------------------------------
1889 CREATE FUNCTION "vote_ratio"
1890 ( "positive_votes_p" "initiative"."positive_votes"%TYPE,
1891 "negative_votes_p" "initiative"."negative_votes"%TYPE )
1892 RETURNS FLOAT8
1893 LANGUAGE 'plpgsql' STABLE AS $$
1894 BEGIN
1895 IF "positive_votes_p" > 0 AND "negative_votes_p" > 0 THEN
1896 RETURN
1897 "positive_votes_p"::FLOAT8 /
1898 ("positive_votes_p" + "negative_votes_p")::FLOAT8;
1899 ELSIF "positive_votes_p" > 0 THEN
1900 RETURN "positive_votes_p";
1901 ELSIF "negative_votes_p" > 0 THEN
1902 RETURN 1 - "negative_votes_p";
1903 ELSE
1904 RETURN 0.5;
1905 END IF;
1906 END;
1907 $$;
1909 COMMENT ON FUNCTION "vote_ratio"
1910 ( "initiative"."positive_votes"%TYPE,
1911 "initiative"."negative_votes"%TYPE )
1912 IS 'Returns a number, which can be used for comparison of initiatives based on count of approvals and disapprovals. Greater numbers indicate a better result. This function is NOT injective.';
1916 ------------------------------------------------
1917 -- Locking for snapshots and voting procedure --
1918 ------------------------------------------------
1921 CREATE FUNCTION "share_row_lock_issue_trigger"()
1922 RETURNS TRIGGER
1923 LANGUAGE 'plpgsql' VOLATILE AS $$
1924 BEGIN
1925 IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
1926 PERFORM NULL FROM "issue" WHERE "id" = OLD."issue_id" FOR SHARE;
1927 END IF;
1928 IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
1929 PERFORM NULL FROM "issue" WHERE "id" = NEW."issue_id" FOR SHARE;
1930 RETURN NEW;
1931 ELSE
1932 RETURN OLD;
1933 END IF;
1934 END;
1935 $$;
1937 COMMENT ON FUNCTION "share_row_lock_issue_trigger"() IS 'Implementation of triggers "share_row_lock_issue" on multiple tables';
1940 CREATE FUNCTION "share_row_lock_issue_via_initiative_trigger"()
1941 RETURNS TRIGGER
1942 LANGUAGE 'plpgsql' VOLATILE AS $$
1943 BEGIN
1944 IF TG_OP = 'UPDATE' OR TG_OP = 'DELETE' THEN
1945 PERFORM NULL FROM "issue"
1946 JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
1947 WHERE "initiative"."id" = OLD."initiative_id"
1948 FOR SHARE OF "issue";
1949 END IF;
1950 IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
1951 PERFORM NULL FROM "issue"
1952 JOIN "initiative" ON "issue"."id" = "initiative"."issue_id"
1953 WHERE "initiative"."id" = NEW."initiative_id"
1954 FOR SHARE OF "issue";
1955 RETURN NEW;
1956 ELSE
1957 RETURN OLD;
1958 END IF;
1959 END;
1960 $$;
1962 COMMENT ON FUNCTION "share_row_lock_issue_trigger"() IS 'Implementation of trigger "share_row_lock_issue_via_initiative" on table "opinion"';
1965 CREATE TRIGGER "share_row_lock_issue"
1966 BEFORE INSERT OR UPDATE OR DELETE ON "initiative"
1967 FOR EACH ROW EXECUTE PROCEDURE
1968 "share_row_lock_issue_trigger"();
1970 CREATE TRIGGER "share_row_lock_issue"
1971 BEFORE INSERT OR UPDATE OR DELETE ON "interest"
1972 FOR EACH ROW EXECUTE PROCEDURE
1973 "share_row_lock_issue_trigger"();
1975 CREATE TRIGGER "share_row_lock_issue"
1976 BEFORE INSERT OR UPDATE OR DELETE ON "supporter"
1977 FOR EACH ROW EXECUTE PROCEDURE
1978 "share_row_lock_issue_trigger"();
1980 CREATE TRIGGER "share_row_lock_issue_via_initiative"
1981 BEFORE INSERT OR UPDATE OR DELETE ON "opinion"
1982 FOR EACH ROW EXECUTE PROCEDURE
1983 "share_row_lock_issue_via_initiative_trigger"();
1985 CREATE TRIGGER "share_row_lock_issue"
1986 BEFORE INSERT OR UPDATE OR DELETE ON "direct_voter"
1987 FOR EACH ROW EXECUTE PROCEDURE
1988 "share_row_lock_issue_trigger"();
1990 CREATE TRIGGER "share_row_lock_issue"
1991 BEFORE INSERT OR UPDATE OR DELETE ON "delegating_voter"
1992 FOR EACH ROW EXECUTE PROCEDURE
1993 "share_row_lock_issue_trigger"();
1995 CREATE TRIGGER "share_row_lock_issue"
1996 BEFORE INSERT OR UPDATE OR DELETE ON "vote"
1997 FOR EACH ROW EXECUTE PROCEDURE
1998 "share_row_lock_issue_trigger"();
2000 COMMENT ON TRIGGER "share_row_lock_issue" ON "initiative" IS 'See "lock_issue" function';
2001 COMMENT ON TRIGGER "share_row_lock_issue" ON "interest" IS 'See "lock_issue" function';
2002 COMMENT ON TRIGGER "share_row_lock_issue" ON "supporter" IS 'See "lock_issue" function';
2003 COMMENT ON TRIGGER "share_row_lock_issue_via_initiative" ON "opinion" IS 'See "lock_issue" function';
2004 COMMENT ON TRIGGER "share_row_lock_issue" ON "direct_voter" IS 'See "lock_issue" function';
2005 COMMENT ON TRIGGER "share_row_lock_issue" ON "delegating_voter" IS 'See "lock_issue" function';
2006 COMMENT ON TRIGGER "share_row_lock_issue" ON "vote" IS 'See "lock_issue" function';
2009 CREATE FUNCTION "lock_issue"
2010 ( "issue_id_p" "issue"."id"%TYPE )
2011 RETURNS VOID
2012 LANGUAGE 'plpgsql' VOLATILE AS $$
2013 BEGIN
2014 LOCK TABLE "member" IN SHARE MODE;
2015 LOCK TABLE "membership" IN SHARE MODE;
2016 LOCK TABLE "policy" IN SHARE MODE;
2017 PERFORM NULL FROM "issue" WHERE "id" = "issue_id_p" FOR UPDATE;
2018 -- NOTE: The row-level exclusive lock in combination with the
2019 -- share_row_lock_issue(_via_initiative)_trigger functions (which
2020 -- acquire a row-level share lock on the issue) ensure that no data
2021 -- is changed, which could affect calculation of snapshots or
2022 -- counting of votes. Table "delegation" must be table-level-locked,
2023 -- as it also contains issue- and global-scope delegations.
2024 LOCK TABLE "delegation" IN SHARE MODE;
2025 LOCK TABLE "direct_population_snapshot" IN EXCLUSIVE MODE;
2026 LOCK TABLE "delegating_population_snapshot" IN EXCLUSIVE MODE;
2027 LOCK TABLE "direct_interest_snapshot" IN EXCLUSIVE MODE;
2028 LOCK TABLE "delegating_interest_snapshot" IN EXCLUSIVE MODE;
2029 LOCK TABLE "direct_supporter_snapshot" IN EXCLUSIVE MODE;
2030 RETURN;
2031 END;
2032 $$;
2034 COMMENT ON FUNCTION "lock_issue"
2035 ( "issue"."id"%TYPE )
2036 IS 'Locks the issue and all other data which is used for calculating snapshots or counting votes.';
2040 -------------------------------
2041 -- Materialize member counts --
2042 -------------------------------
2044 CREATE FUNCTION "calculate_member_counts"()
2045 RETURNS VOID
2046 LANGUAGE 'plpgsql' VOLATILE AS $$
2047 BEGIN
2048 LOCK TABLE "member" IN SHARE MODE;
2049 LOCK TABLE "member_count" IN EXCLUSIVE MODE;
2050 LOCK TABLE "area" IN EXCLUSIVE MODE;
2051 LOCK TABLE "membership" IN SHARE MODE;
2052 DELETE FROM "member_count";
2053 INSERT INTO "member_count" ("total_count")
2054 SELECT "total_count" FROM "member_count_view";
2055 UPDATE "area" SET
2056 "direct_member_count" = "view"."direct_member_count",
2057 "member_weight" = "view"."member_weight",
2058 "autoreject_weight" = "view"."autoreject_weight"
2059 FROM "area_member_count" AS "view"
2060 WHERE "view"."area_id" = "area"."id";
2061 RETURN;
2062 END;
2063 $$;
2065 COMMENT ON FUNCTION "calculate_member_counts"() IS 'Updates "member_count" table and "member_count" column of table "area" by materializing data from views "member_count_view" and "area_member_count"';
2069 ------------------------------
2070 -- Calculation of snapshots --
2071 ------------------------------
2073 CREATE FUNCTION "weight_of_added_delegations_for_population_snapshot"
2074 ( "issue_id_p" "issue"."id"%TYPE,
2075 "member_id_p" "member"."id"%TYPE,
2076 "delegate_member_ids_p" "delegating_population_snapshot"."delegate_member_ids"%TYPE )
2077 RETURNS "direct_population_snapshot"."weight"%TYPE
2078 LANGUAGE 'plpgsql' VOLATILE AS $$
2079 DECLARE
2080 "issue_delegation_row" "issue_delegation"%ROWTYPE;
2081 "delegate_member_ids_v" "delegating_population_snapshot"."delegate_member_ids"%TYPE;
2082 "weight_v" INT4;
2083 "sub_weight_v" INT4;
2084 BEGIN
2085 "weight_v" := 0;
2086 FOR "issue_delegation_row" IN
2087 SELECT * FROM "issue_delegation"
2088 WHERE "trustee_id" = "member_id_p"
2089 AND "issue_id" = "issue_id_p"
2090 LOOP
2091 IF NOT EXISTS (
2092 SELECT NULL FROM "direct_population_snapshot"
2093 WHERE "issue_id" = "issue_id_p"
2094 AND "event" = 'periodic'
2095 AND "member_id" = "issue_delegation_row"."truster_id"
2096 ) AND NOT EXISTS (
2097 SELECT NULL FROM "delegating_population_snapshot"
2098 WHERE "issue_id" = "issue_id_p"
2099 AND "event" = 'periodic'
2100 AND "member_id" = "issue_delegation_row"."truster_id"
2101 ) THEN
2102 "delegate_member_ids_v" :=
2103 "member_id_p" || "delegate_member_ids_p";
2104 INSERT INTO "delegating_population_snapshot" (
2105 "issue_id",
2106 "event",
2107 "member_id",
2108 "scope",
2109 "delegate_member_ids"
2110 ) VALUES (
2111 "issue_id_p",
2112 'periodic',
2113 "issue_delegation_row"."truster_id",
2114 "issue_delegation_row"."scope",
2115 "delegate_member_ids_v"
2116 );
2117 "sub_weight_v" := 1 +
2118 "weight_of_added_delegations_for_population_snapshot"(
2119 "issue_id_p",
2120 "issue_delegation_row"."truster_id",
2121 "delegate_member_ids_v"
2122 );
2123 UPDATE "delegating_population_snapshot"
2124 SET "weight" = "sub_weight_v"
2125 WHERE "issue_id" = "issue_id_p"
2126 AND "event" = 'periodic'
2127 AND "member_id" = "issue_delegation_row"."truster_id";
2128 "weight_v" := "weight_v" + "sub_weight_v";
2129 END IF;
2130 END LOOP;
2131 RETURN "weight_v";
2132 END;
2133 $$;
2135 COMMENT ON FUNCTION "weight_of_added_delegations_for_population_snapshot"
2136 ( "issue"."id"%TYPE,
2137 "member"."id"%TYPE,
2138 "delegating_population_snapshot"."delegate_member_ids"%TYPE )
2139 IS 'Helper function for "create_population_snapshot" function';
2142 CREATE FUNCTION "create_population_snapshot"
2143 ( "issue_id_p" "issue"."id"%TYPE )
2144 RETURNS VOID
2145 LANGUAGE 'plpgsql' VOLATILE AS $$
2146 DECLARE
2147 "member_id_v" "member"."id"%TYPE;
2148 BEGIN
2149 DELETE FROM "direct_population_snapshot"
2150 WHERE "issue_id" = "issue_id_p"
2151 AND "event" = 'periodic';
2152 DELETE FROM "delegating_population_snapshot"
2153 WHERE "issue_id" = "issue_id_p"
2154 AND "event" = 'periodic';
2155 INSERT INTO "direct_population_snapshot"
2156 ("issue_id", "event", "member_id")
2157 SELECT
2158 "issue_id_p" AS "issue_id",
2159 'periodic'::"snapshot_event" AS "event",
2160 "member"."id" AS "member_id"
2161 FROM "issue"
2162 JOIN "area" ON "issue"."area_id" = "area"."id"
2163 JOIN "membership" ON "area"."id" = "membership"."area_id"
2164 JOIN "member" ON "membership"."member_id" = "member"."id"
2165 WHERE "issue"."id" = "issue_id_p"
2166 AND "member"."active"
2167 UNION
2168 SELECT
2169 "issue_id_p" AS "issue_id",
2170 'periodic'::"snapshot_event" AS "event",
2171 "member"."id" AS "member_id"
2172 FROM "interest" JOIN "member"
2173 ON "interest"."member_id" = "member"."id"
2174 WHERE "interest"."issue_id" = "issue_id_p"
2175 AND "member"."active";
2176 FOR "member_id_v" IN
2177 SELECT "member_id" FROM "direct_population_snapshot"
2178 WHERE "issue_id" = "issue_id_p"
2179 AND "event" = 'periodic'
2180 LOOP
2181 UPDATE "direct_population_snapshot" SET
2182 "weight" = 1 +
2183 "weight_of_added_delegations_for_population_snapshot"(
2184 "issue_id_p",
2185 "member_id_v",
2186 '{}'
2188 WHERE "issue_id" = "issue_id_p"
2189 AND "event" = 'periodic'
2190 AND "member_id" = "member_id_v";
2191 END LOOP;
2192 RETURN;
2193 END;
2194 $$;
2196 COMMENT ON FUNCTION "create_population_snapshot"
2197 ( "issue"."id"%TYPE )
2198 IS 'This function creates a new ''periodic'' population snapshot for the given issue. It does neither lock any tables, nor updates precalculated values in other tables.';
2201 CREATE FUNCTION "weight_of_added_delegations_for_interest_snapshot"
2202 ( "issue_id_p" "issue"."id"%TYPE,
2203 "member_id_p" "member"."id"%TYPE,
2204 "delegate_member_ids_p" "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
2205 RETURNS "direct_interest_snapshot"."weight"%TYPE
2206 LANGUAGE 'plpgsql' VOLATILE AS $$
2207 DECLARE
2208 "issue_delegation_row" "issue_delegation"%ROWTYPE;
2209 "delegate_member_ids_v" "delegating_interest_snapshot"."delegate_member_ids"%TYPE;
2210 "weight_v" INT4;
2211 "sub_weight_v" INT4;
2212 BEGIN
2213 "weight_v" := 0;
2214 FOR "issue_delegation_row" IN
2215 SELECT * FROM "issue_delegation"
2216 WHERE "trustee_id" = "member_id_p"
2217 AND "issue_id" = "issue_id_p"
2218 LOOP
2219 IF NOT EXISTS (
2220 SELECT NULL FROM "direct_interest_snapshot"
2221 WHERE "issue_id" = "issue_id_p"
2222 AND "event" = 'periodic'
2223 AND "member_id" = "issue_delegation_row"."truster_id"
2224 ) AND NOT EXISTS (
2225 SELECT NULL FROM "delegating_interest_snapshot"
2226 WHERE "issue_id" = "issue_id_p"
2227 AND "event" = 'periodic'
2228 AND "member_id" = "issue_delegation_row"."truster_id"
2229 ) THEN
2230 "delegate_member_ids_v" :=
2231 "member_id_p" || "delegate_member_ids_p";
2232 INSERT INTO "delegating_interest_snapshot" (
2233 "issue_id",
2234 "event",
2235 "member_id",
2236 "scope",
2237 "delegate_member_ids"
2238 ) VALUES (
2239 "issue_id_p",
2240 'periodic',
2241 "issue_delegation_row"."truster_id",
2242 "issue_delegation_row"."scope",
2243 "delegate_member_ids_v"
2244 );
2245 "sub_weight_v" := 1 +
2246 "weight_of_added_delegations_for_interest_snapshot"(
2247 "issue_id_p",
2248 "issue_delegation_row"."truster_id",
2249 "delegate_member_ids_v"
2250 );
2251 UPDATE "delegating_interest_snapshot"
2252 SET "weight" = "sub_weight_v"
2253 WHERE "issue_id" = "issue_id_p"
2254 AND "event" = 'periodic'
2255 AND "member_id" = "issue_delegation_row"."truster_id";
2256 "weight_v" := "weight_v" + "sub_weight_v";
2257 END IF;
2258 END LOOP;
2259 RETURN "weight_v";
2260 END;
2261 $$;
2263 COMMENT ON FUNCTION "weight_of_added_delegations_for_interest_snapshot"
2264 ( "issue"."id"%TYPE,
2265 "member"."id"%TYPE,
2266 "delegating_interest_snapshot"."delegate_member_ids"%TYPE )
2267 IS 'Helper function for "create_interest_snapshot" function';
2270 CREATE FUNCTION "create_interest_snapshot"
2271 ( "issue_id_p" "issue"."id"%TYPE )
2272 RETURNS VOID
2273 LANGUAGE 'plpgsql' VOLATILE AS $$
2274 DECLARE
2275 "member_id_v" "member"."id"%TYPE;
2276 BEGIN
2277 DELETE FROM "direct_interest_snapshot"
2278 WHERE "issue_id" = "issue_id_p"
2279 AND "event" = 'periodic';
2280 DELETE FROM "delegating_interest_snapshot"
2281 WHERE "issue_id" = "issue_id_p"
2282 AND "event" = 'periodic';
2283 DELETE FROM "direct_supporter_snapshot"
2284 WHERE "issue_id" = "issue_id_p"
2285 AND "event" = 'periodic';
2286 INSERT INTO "direct_interest_snapshot"
2287 ("issue_id", "event", "member_id", "voting_requested")
2288 SELECT
2289 "issue_id_p" AS "issue_id",
2290 'periodic' AS "event",
2291 "member"."id" AS "member_id",
2292 "interest"."voting_requested"
2293 FROM "interest" JOIN "member"
2294 ON "interest"."member_id" = "member"."id"
2295 WHERE "interest"."issue_id" = "issue_id_p"
2296 AND "member"."active";
2297 FOR "member_id_v" IN
2298 SELECT "member_id" FROM "direct_interest_snapshot"
2299 WHERE "issue_id" = "issue_id_p"
2300 AND "event" = 'periodic'
2301 LOOP
2302 UPDATE "direct_interest_snapshot" SET
2303 "weight" = 1 +
2304 "weight_of_added_delegations_for_interest_snapshot"(
2305 "issue_id_p",
2306 "member_id_v",
2307 '{}'
2309 WHERE "issue_id" = "issue_id_p"
2310 AND "event" = 'periodic'
2311 AND "member_id" = "member_id_v";
2312 END LOOP;
2313 INSERT INTO "direct_supporter_snapshot"
2314 ( "issue_id", "initiative_id", "event", "member_id",
2315 "informed", "satisfied" )
2316 SELECT
2317 "issue_id_p" AS "issue_id",
2318 "initiative"."id" AS "initiative_id",
2319 'periodic' AS "event",
2320 "member"."id" AS "member_id",
2321 "supporter"."draft_id" = "current_draft"."id" AS "informed",
2322 NOT EXISTS (
2323 SELECT NULL FROM "critical_opinion"
2324 WHERE "initiative_id" = "initiative"."id"
2325 AND "member_id" = "member"."id"
2326 ) AS "satisfied"
2327 FROM "supporter"
2328 JOIN "member"
2329 ON "supporter"."member_id" = "member"."id"
2330 JOIN "initiative"
2331 ON "supporter"."initiative_id" = "initiative"."id"
2332 JOIN "current_draft"
2333 ON "initiative"."id" = "current_draft"."initiative_id"
2334 JOIN "direct_interest_snapshot"
2335 ON "member"."id" = "direct_interest_snapshot"."member_id"
2336 AND "initiative"."issue_id" = "direct_interest_snapshot"."issue_id"
2337 AND "event" = 'periodic'
2338 WHERE "member"."active"
2339 AND "initiative"."issue_id" = "issue_id_p";
2340 RETURN;
2341 END;
2342 $$;
2344 COMMENT ON FUNCTION "create_interest_snapshot"
2345 ( "issue"."id"%TYPE )
2346 IS 'This function creates a new ''periodic'' interest/supporter snapshot for the given issue. It does neither lock any tables, nor updates precalculated values in other tables.';
2349 CREATE FUNCTION "create_snapshot"
2350 ( "issue_id_p" "issue"."id"%TYPE )
2351 RETURNS VOID
2352 LANGUAGE 'plpgsql' VOLATILE AS $$
2353 DECLARE
2354 "initiative_id_v" "initiative"."id"%TYPE;
2355 "suggestion_id_v" "suggestion"."id"%TYPE;
2356 BEGIN
2357 PERFORM "lock_issue"("issue_id_p");
2358 PERFORM "create_population_snapshot"("issue_id_p");
2359 PERFORM "create_interest_snapshot"("issue_id_p");
2360 UPDATE "issue" SET
2361 "snapshot" = now(),
2362 "latest_snapshot_event" = 'periodic',
2363 "population" = (
2364 SELECT coalesce(sum("weight"), 0)
2365 FROM "direct_population_snapshot"
2366 WHERE "issue_id" = "issue_id_p"
2367 AND "event" = 'periodic'
2368 ),
2369 "vote_now" = (
2370 SELECT coalesce(sum("weight"), 0)
2371 FROM "direct_interest_snapshot"
2372 WHERE "issue_id" = "issue_id_p"
2373 AND "event" = 'periodic'
2374 AND "voting_requested" = TRUE
2375 ),
2376 "vote_later" = (
2377 SELECT coalesce(sum("weight"), 0)
2378 FROM "direct_interest_snapshot"
2379 WHERE "issue_id" = "issue_id_p"
2380 AND "event" = 'periodic'
2381 AND "voting_requested" = FALSE
2383 WHERE "id" = "issue_id_p";
2384 FOR "initiative_id_v" IN
2385 SELECT "id" FROM "initiative" WHERE "issue_id" = "issue_id_p"
2386 LOOP
2387 UPDATE "initiative" SET
2388 "supporter_count" = (
2389 SELECT coalesce(sum("di"."weight"), 0)
2390 FROM "direct_interest_snapshot" AS "di"
2391 JOIN "direct_supporter_snapshot" AS "ds"
2392 ON "di"."member_id" = "ds"."member_id"
2393 WHERE "di"."issue_id" = "issue_id_p"
2394 AND "di"."event" = 'periodic'
2395 AND "ds"."initiative_id" = "initiative_id_v"
2396 AND "ds"."event" = 'periodic'
2397 ),
2398 "informed_supporter_count" = (
2399 SELECT coalesce(sum("di"."weight"), 0)
2400 FROM "direct_interest_snapshot" AS "di"
2401 JOIN "direct_supporter_snapshot" AS "ds"
2402 ON "di"."member_id" = "ds"."member_id"
2403 WHERE "di"."issue_id" = "issue_id_p"
2404 AND "di"."event" = 'periodic'
2405 AND "ds"."initiative_id" = "initiative_id_v"
2406 AND "ds"."event" = 'periodic'
2407 AND "ds"."informed"
2408 ),
2409 "satisfied_supporter_count" = (
2410 SELECT coalesce(sum("di"."weight"), 0)
2411 FROM "direct_interest_snapshot" AS "di"
2412 JOIN "direct_supporter_snapshot" AS "ds"
2413 ON "di"."member_id" = "ds"."member_id"
2414 WHERE "di"."issue_id" = "issue_id_p"
2415 AND "di"."event" = 'periodic'
2416 AND "ds"."initiative_id" = "initiative_id_v"
2417 AND "ds"."event" = 'periodic'
2418 AND "ds"."satisfied"
2419 ),
2420 "satisfied_informed_supporter_count" = (
2421 SELECT coalesce(sum("di"."weight"), 0)
2422 FROM "direct_interest_snapshot" AS "di"
2423 JOIN "direct_supporter_snapshot" AS "ds"
2424 ON "di"."member_id" = "ds"."member_id"
2425 WHERE "di"."issue_id" = "issue_id_p"
2426 AND "di"."event" = 'periodic'
2427 AND "ds"."initiative_id" = "initiative_id_v"
2428 AND "ds"."event" = 'periodic'
2429 AND "ds"."informed"
2430 AND "ds"."satisfied"
2432 WHERE "id" = "initiative_id_v";
2433 FOR "suggestion_id_v" IN
2434 SELECT "id" FROM "suggestion"
2435 WHERE "initiative_id" = "initiative_id_v"
2436 LOOP
2437 UPDATE "suggestion" SET
2438 "minus2_unfulfilled_count" = (
2439 SELECT coalesce(sum("snapshot"."weight"), 0)
2440 FROM "issue" CROSS JOIN "opinion"
2441 JOIN "direct_interest_snapshot" AS "snapshot"
2442 ON "snapshot"."issue_id" = "issue"."id"
2443 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2444 AND "snapshot"."member_id" = "opinion"."member_id"
2445 WHERE "issue"."id" = "issue_id_p"
2446 AND "opinion"."suggestion_id" = "suggestion_id_v"
2447 AND "opinion"."degree" = -2
2448 AND "opinion"."fulfilled" = FALSE
2449 ),
2450 "minus2_fulfilled_count" = (
2451 SELECT coalesce(sum("snapshot"."weight"), 0)
2452 FROM "issue" CROSS JOIN "opinion"
2453 JOIN "direct_interest_snapshot" AS "snapshot"
2454 ON "snapshot"."issue_id" = "issue"."id"
2455 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2456 AND "snapshot"."member_id" = "opinion"."member_id"
2457 WHERE "issue"."id" = "issue_id_p"
2458 AND "opinion"."suggestion_id" = "suggestion_id_v"
2459 AND "opinion"."degree" = -2
2460 AND "opinion"."fulfilled" = TRUE
2461 ),
2462 "minus1_unfulfilled_count" = (
2463 SELECT coalesce(sum("snapshot"."weight"), 0)
2464 FROM "issue" CROSS JOIN "opinion"
2465 JOIN "direct_interest_snapshot" AS "snapshot"
2466 ON "snapshot"."issue_id" = "issue"."id"
2467 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2468 AND "snapshot"."member_id" = "opinion"."member_id"
2469 WHERE "issue"."id" = "issue_id_p"
2470 AND "opinion"."suggestion_id" = "suggestion_id_v"
2471 AND "opinion"."degree" = -1
2472 AND "opinion"."fulfilled" = FALSE
2473 ),
2474 "minus1_fulfilled_count" = (
2475 SELECT coalesce(sum("snapshot"."weight"), 0)
2476 FROM "issue" CROSS JOIN "opinion"
2477 JOIN "direct_interest_snapshot" AS "snapshot"
2478 ON "snapshot"."issue_id" = "issue"."id"
2479 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2480 AND "snapshot"."member_id" = "opinion"."member_id"
2481 WHERE "issue"."id" = "issue_id_p"
2482 AND "opinion"."suggestion_id" = "suggestion_id_v"
2483 AND "opinion"."degree" = -1
2484 AND "opinion"."fulfilled" = TRUE
2485 ),
2486 "plus1_unfulfilled_count" = (
2487 SELECT coalesce(sum("snapshot"."weight"), 0)
2488 FROM "issue" CROSS JOIN "opinion"
2489 JOIN "direct_interest_snapshot" AS "snapshot"
2490 ON "snapshot"."issue_id" = "issue"."id"
2491 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2492 AND "snapshot"."member_id" = "opinion"."member_id"
2493 WHERE "issue"."id" = "issue_id_p"
2494 AND "opinion"."suggestion_id" = "suggestion_id_v"
2495 AND "opinion"."degree" = 1
2496 AND "opinion"."fulfilled" = FALSE
2497 ),
2498 "plus1_fulfilled_count" = (
2499 SELECT coalesce(sum("snapshot"."weight"), 0)
2500 FROM "issue" CROSS JOIN "opinion"
2501 JOIN "direct_interest_snapshot" AS "snapshot"
2502 ON "snapshot"."issue_id" = "issue"."id"
2503 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2504 AND "snapshot"."member_id" = "opinion"."member_id"
2505 WHERE "issue"."id" = "issue_id_p"
2506 AND "opinion"."suggestion_id" = "suggestion_id_v"
2507 AND "opinion"."degree" = 1
2508 AND "opinion"."fulfilled" = TRUE
2509 ),
2510 "plus2_unfulfilled_count" = (
2511 SELECT coalesce(sum("snapshot"."weight"), 0)
2512 FROM "issue" CROSS JOIN "opinion"
2513 JOIN "direct_interest_snapshot" AS "snapshot"
2514 ON "snapshot"."issue_id" = "issue"."id"
2515 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2516 AND "snapshot"."member_id" = "opinion"."member_id"
2517 WHERE "issue"."id" = "issue_id_p"
2518 AND "opinion"."suggestion_id" = "suggestion_id_v"
2519 AND "opinion"."degree" = 2
2520 AND "opinion"."fulfilled" = FALSE
2521 ),
2522 "plus2_fulfilled_count" = (
2523 SELECT coalesce(sum("snapshot"."weight"), 0)
2524 FROM "issue" CROSS JOIN "opinion"
2525 JOIN "direct_interest_snapshot" AS "snapshot"
2526 ON "snapshot"."issue_id" = "issue"."id"
2527 AND "snapshot"."event" = "issue"."latest_snapshot_event"
2528 AND "snapshot"."member_id" = "opinion"."member_id"
2529 WHERE "issue"."id" = "issue_id_p"
2530 AND "opinion"."suggestion_id" = "suggestion_id_v"
2531 AND "opinion"."degree" = 2
2532 AND "opinion"."fulfilled" = TRUE
2534 WHERE "suggestion"."id" = "suggestion_id_v";
2535 END LOOP;
2536 END LOOP;
2537 RETURN;
2538 END;
2539 $$;
2541 COMMENT ON FUNCTION "create_snapshot"
2542 ( "issue"."id"%TYPE )
2543 IS 'This function creates a complete new ''periodic'' snapshot of population, interest and support for the given issue. All involved tables are locked, and after completion precalculated values in the source tables are updated.';
2546 CREATE FUNCTION "set_snapshot_event"
2547 ( "issue_id_p" "issue"."id"%TYPE,
2548 "event_p" "snapshot_event" )
2549 RETURNS VOID
2550 LANGUAGE 'plpgsql' VOLATILE AS $$
2551 DECLARE
2552 "event_v" "issue"."latest_snapshot_event"%TYPE;
2553 BEGIN
2554 SELECT "latest_snapshot_event" INTO "event_v" FROM "issue"
2555 WHERE "id" = "issue_id_p" FOR UPDATE;
2556 UPDATE "issue" SET "latest_snapshot_event" = "event_p"
2557 WHERE "id" = "issue_id_p";
2558 UPDATE "direct_population_snapshot" SET "event" = "event_p"
2559 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
2560 UPDATE "delegating_population_snapshot" SET "event" = "event_p"
2561 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
2562 UPDATE "direct_interest_snapshot" SET "event" = "event_p"
2563 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
2564 UPDATE "delegating_interest_snapshot" SET "event" = "event_p"
2565 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
2566 UPDATE "direct_supporter_snapshot" SET "event" = "event_p"
2567 WHERE "issue_id" = "issue_id_p" AND "event" = "event_v";
2568 RETURN;
2569 END;
2570 $$;
2572 COMMENT ON FUNCTION "set_snapshot_event"
2573 ( "issue"."id"%TYPE,
2574 "snapshot_event" )
2575 IS 'Change "event" attribute of the previous ''periodic'' snapshot';
2579 ---------------------
2580 -- Freezing issues --
2581 ---------------------
2583 CREATE FUNCTION "freeze_after_snapshot"
2584 ( "issue_id_p" "issue"."id"%TYPE )
2585 RETURNS VOID
2586 LANGUAGE 'plpgsql' VOLATILE AS $$
2587 DECLARE
2588 "issue_row" "issue"%ROWTYPE;
2589 "policy_row" "policy"%ROWTYPE;
2590 "initiative_row" "initiative"%ROWTYPE;
2591 BEGIN
2592 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
2593 SELECT * INTO "policy_row"
2594 FROM "policy" WHERE "id" = "issue_row"."policy_id";
2595 PERFORM "set_snapshot_event"("issue_id_p", 'full_freeze');
2596 UPDATE "issue" SET
2597 "accepted" = coalesce("accepted", now()),
2598 "half_frozen" = coalesce("half_frozen", now()),
2599 "fully_frozen" = now()
2600 WHERE "id" = "issue_id_p";
2601 FOR "initiative_row" IN
2602 SELECT * FROM "initiative"
2603 WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
2604 LOOP
2605 IF
2606 "initiative_row"."satisfied_supporter_count" > 0 AND
2607 "initiative_row"."satisfied_supporter_count" *
2608 "policy_row"."initiative_quorum_den" >=
2609 "issue_row"."population" * "policy_row"."initiative_quorum_num"
2610 THEN
2611 UPDATE "initiative" SET "admitted" = TRUE
2612 WHERE "id" = "initiative_row"."id";
2613 ELSE
2614 UPDATE "initiative" SET "admitted" = FALSE
2615 WHERE "id" = "initiative_row"."id";
2616 END IF;
2617 END LOOP;
2618 IF NOT EXISTS (
2619 SELECT NULL FROM "initiative"
2620 WHERE "issue_id" = "issue_id_p" AND "admitted" = TRUE
2621 ) THEN
2622 PERFORM "close_voting"("issue_id_p");
2623 END IF;
2624 RETURN;
2625 END;
2626 $$;
2628 COMMENT ON FUNCTION "freeze_after_snapshot"
2629 ( "issue"."id"%TYPE )
2630 IS 'This function freezes an issue (fully) and starts voting, but must only be called when "create_snapshot" was called in the same transaction.';
2633 CREATE FUNCTION "manual_freeze"("issue_id_p" "issue"."id"%TYPE)
2634 RETURNS VOID
2635 LANGUAGE 'plpgsql' VOLATILE AS $$
2636 DECLARE
2637 "issue_row" "issue"%ROWTYPE;
2638 BEGIN
2639 PERFORM "create_snapshot"("issue_id_p");
2640 PERFORM "freeze_after_snapshot"("issue_id_p");
2641 RETURN;
2642 END;
2643 $$;
2645 COMMENT ON FUNCTION "manual_freeze"
2646 ( "issue"."id"%TYPE )
2647 IS 'Freeze an issue manually (fully) and start voting';
2651 -----------------------
2652 -- Counting of votes --
2653 -----------------------
2656 CREATE FUNCTION "weight_of_added_vote_delegations"
2657 ( "issue_id_p" "issue"."id"%TYPE,
2658 "member_id_p" "member"."id"%TYPE,
2659 "delegate_member_ids_p" "delegating_voter"."delegate_member_ids"%TYPE )
2660 RETURNS "direct_voter"."weight"%TYPE
2661 LANGUAGE 'plpgsql' VOLATILE AS $$
2662 DECLARE
2663 "issue_delegation_row" "issue_delegation"%ROWTYPE;
2664 "delegate_member_ids_v" "delegating_voter"."delegate_member_ids"%TYPE;
2665 "weight_v" INT4;
2666 "sub_weight_v" INT4;
2667 BEGIN
2668 "weight_v" := 0;
2669 FOR "issue_delegation_row" IN
2670 SELECT * FROM "issue_delegation"
2671 WHERE "trustee_id" = "member_id_p"
2672 AND "issue_id" = "issue_id_p"
2673 LOOP
2674 IF NOT EXISTS (
2675 SELECT NULL FROM "direct_voter"
2676 WHERE "member_id" = "issue_delegation_row"."truster_id"
2677 AND "issue_id" = "issue_id_p"
2678 ) AND NOT EXISTS (
2679 SELECT NULL FROM "delegating_voter"
2680 WHERE "member_id" = "issue_delegation_row"."truster_id"
2681 AND "issue_id" = "issue_id_p"
2682 ) THEN
2683 "delegate_member_ids_v" :=
2684 "member_id_p" || "delegate_member_ids_p";
2685 INSERT INTO "delegating_voter" (
2686 "issue_id",
2687 "member_id",
2688 "scope",
2689 "delegate_member_ids"
2690 ) VALUES (
2691 "issue_id_p",
2692 "issue_delegation_row"."truster_id",
2693 "issue_delegation_row"."scope",
2694 "delegate_member_ids_v"
2695 );
2696 "sub_weight_v" := 1 +
2697 "weight_of_added_vote_delegations"(
2698 "issue_id_p",
2699 "issue_delegation_row"."truster_id",
2700 "delegate_member_ids_v"
2701 );
2702 UPDATE "delegating_voter"
2703 SET "weight" = "sub_weight_v"
2704 WHERE "issue_id" = "issue_id_p"
2705 AND "member_id" = "issue_delegation_row"."truster_id";
2706 "weight_v" := "weight_v" + "sub_weight_v";
2707 END IF;
2708 END LOOP;
2709 RETURN "weight_v";
2710 END;
2711 $$;
2713 COMMENT ON FUNCTION "weight_of_added_vote_delegations"
2714 ( "issue"."id"%TYPE,
2715 "member"."id"%TYPE,
2716 "delegating_voter"."delegate_member_ids"%TYPE )
2717 IS 'Helper function for "add_vote_delegations" function';
2720 CREATE FUNCTION "add_vote_delegations"
2721 ( "issue_id_p" "issue"."id"%TYPE )
2722 RETURNS VOID
2723 LANGUAGE 'plpgsql' VOLATILE AS $$
2724 DECLARE
2725 "member_id_v" "member"."id"%TYPE;
2726 BEGIN
2727 FOR "member_id_v" IN
2728 SELECT "member_id" FROM "direct_voter"
2729 WHERE "issue_id" = "issue_id_p"
2730 LOOP
2731 UPDATE "direct_voter" SET
2732 "weight" = "weight" + "weight_of_added_vote_delegations"(
2733 "issue_id_p",
2734 "member_id_v",
2735 '{}'
2737 WHERE "member_id" = "member_id_v"
2738 AND "issue_id" = "issue_id_p";
2739 END LOOP;
2740 RETURN;
2741 END;
2742 $$;
2744 COMMENT ON FUNCTION "add_vote_delegations"
2745 ( "issue_id_p" "issue"."id"%TYPE )
2746 IS 'Helper function for "close_voting" function';
2749 CREATE FUNCTION "close_voting"("issue_id_p" "issue"."id"%TYPE)
2750 RETURNS VOID
2751 LANGUAGE 'plpgsql' VOLATILE AS $$
2752 DECLARE
2753 "issue_row" "issue"%ROWTYPE;
2754 "member_id_v" "member"."id"%TYPE;
2755 BEGIN
2756 PERFORM "lock_issue"("issue_id_p");
2757 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
2758 DELETE FROM "delegating_voter"
2759 WHERE "issue_id" = "issue_id_p";
2760 DELETE FROM "direct_voter"
2761 WHERE "issue_id" = "issue_id_p"
2762 AND "autoreject" = TRUE;
2763 DELETE FROM "direct_voter" USING "member"
2764 WHERE "direct_voter"."member_id" = "member"."id"
2765 AND "direct_voter"."issue_id" = "issue_id_p"
2766 AND "member"."active" = FALSE;
2767 UPDATE "direct_voter" SET "weight" = 1
2768 WHERE "issue_id" = "issue_id_p";
2769 PERFORM "add_vote_delegations"("issue_id_p");
2770 FOR "member_id_v" IN
2771 SELECT "interest"."member_id"
2772 FROM "interest"
2773 LEFT JOIN "direct_voter"
2774 ON "interest"."member_id" = "direct_voter"."member_id"
2775 AND "interest"."issue_id" = "direct_voter"."issue_id"
2776 LEFT JOIN "delegating_voter"
2777 ON "interest"."member_id" = "delegating_voter"."member_id"
2778 AND "interest"."issue_id" = "delegating_voter"."issue_id"
2779 WHERE "interest"."issue_id" = "issue_id_p"
2780 AND "interest"."autoreject" = TRUE
2781 AND "direct_voter"."member_id" ISNULL
2782 AND "delegating_voter"."member_id" ISNULL
2783 UNION SELECT "membership"."member_id"
2784 FROM "membership"
2785 LEFT JOIN "interest"
2786 ON "membership"."member_id" = "interest"."member_id"
2787 AND "interest"."issue_id" = "issue_id_p"
2788 LEFT JOIN "direct_voter"
2789 ON "membership"."member_id" = "direct_voter"."member_id"
2790 AND "direct_voter"."issue_id" = "issue_id_p"
2791 LEFT JOIN "delegating_voter"
2792 ON "membership"."member_id" = "delegating_voter"."member_id"
2793 AND "delegating_voter"."issue_id" = "issue_id_p"
2794 WHERE "membership"."area_id" = "issue_row"."area_id"
2795 AND "membership"."autoreject" = TRUE
2796 AND "interest"."autoreject" ISNULL
2797 AND "direct_voter"."member_id" ISNULL
2798 AND "delegating_voter"."member_id" ISNULL
2799 LOOP
2800 INSERT INTO "direct_voter"
2801 ("member_id", "issue_id", "weight", "autoreject") VALUES
2802 ("member_id_v", "issue_id_p", 1, TRUE);
2803 INSERT INTO "vote" (
2804 "member_id",
2805 "issue_id",
2806 "initiative_id",
2807 "grade"
2808 ) SELECT
2809 "member_id_v" AS "member_id",
2810 "issue_id_p" AS "issue_id",
2811 "id" AS "initiative_id",
2812 -1 AS "grade"
2813 FROM "initiative" WHERE "issue_id" = "issue_id_p";
2814 END LOOP;
2815 PERFORM "add_vote_delegations"("issue_id_p");
2816 UPDATE "issue" SET
2817 "closed" = now(),
2818 "voter_count" = (
2819 SELECT coalesce(sum("weight"), 0)
2820 FROM "direct_voter" WHERE "issue_id" = "issue_id_p"
2822 WHERE "id" = "issue_id_p";
2823 UPDATE "initiative" SET
2824 "positive_votes" = "vote_counts"."positive_votes",
2825 "negative_votes" = "vote_counts"."negative_votes",
2826 "agreed" = CASE WHEN "majority_strict" THEN
2827 "vote_counts"."positive_votes" * "majority_den" >
2828 "majority_num" *
2829 ("vote_counts"."positive_votes"+"vote_counts"."negative_votes")
2830 ELSE
2831 "vote_counts"."positive_votes" * "majority_den" >=
2832 "majority_num" *
2833 ("vote_counts"."positive_votes"+"vote_counts"."negative_votes")
2834 END
2835 FROM
2836 ( SELECT
2837 "initiative"."id" AS "initiative_id",
2838 coalesce(
2839 sum(
2840 CASE WHEN "grade" > 0 THEN "direct_voter"."weight" ELSE 0 END
2841 ),
2843 ) AS "positive_votes",
2844 coalesce(
2845 sum(
2846 CASE WHEN "grade" < 0 THEN "direct_voter"."weight" ELSE 0 END
2847 ),
2849 ) AS "negative_votes"
2850 FROM "initiative"
2851 JOIN "issue" ON "initiative"."issue_id" = "issue"."id"
2852 JOIN "policy" ON "issue"."policy_id" = "policy"."id"
2853 LEFT JOIN "direct_voter"
2854 ON "direct_voter"."issue_id" = "initiative"."issue_id"
2855 LEFT JOIN "vote"
2856 ON "vote"."initiative_id" = "initiative"."id"
2857 AND "vote"."member_id" = "direct_voter"."member_id"
2858 WHERE "initiative"."issue_id" = "issue_id_p"
2859 AND "initiative"."admitted" -- NOTE: NULL case is handled too
2860 GROUP BY "initiative"."id"
2861 ) AS "vote_counts",
2862 "issue",
2863 "policy"
2864 WHERE "vote_counts"."initiative_id" = "initiative"."id"
2865 AND "issue"."id" = "initiative"."issue_id"
2866 AND "policy"."id" = "issue"."policy_id";
2867 -- NOTE: "closed" column of issue must be set at this point
2868 DELETE FROM "battle" WHERE "issue_id" = "issue_id_p";
2869 INSERT INTO "battle" (
2870 "issue_id",
2871 "winning_initiative_id", "losing_initiative_id",
2872 "count"
2873 ) SELECT
2874 "issue_id",
2875 "winning_initiative_id", "losing_initiative_id",
2876 "count"
2877 FROM "battle_view" WHERE "issue_id" = "issue_id_p";
2878 END;
2879 $$;
2881 COMMENT ON FUNCTION "close_voting"
2882 ( "issue"."id"%TYPE )
2883 IS 'Closes the voting on an issue, and calculates positive and negative votes for each initiative; The ranking is not calculated yet, to keep the (locking) transaction short.';
2886 CREATE FUNCTION "defeat_strength"
2887 ( "positive_votes_p" INT4, "negative_votes_p" INT4 )
2888 RETURNS INT8
2889 LANGUAGE 'plpgsql' IMMUTABLE AS $$
2890 BEGIN
2891 IF "positive_votes_p" > "negative_votes_p" THEN
2892 RETURN ("positive_votes_p"::INT8 << 31) - "negative_votes_p"::INT8;
2893 ELSIF "positive_votes_p" = "negative_votes_p" THEN
2894 RETURN 0;
2895 ELSE
2896 RETURN -1;
2897 END IF;
2898 END;
2899 $$;
2901 COMMENT ON FUNCTION "defeat_strength"(INT4, INT4) IS 'Calculates defeat strength (INT8!) of a pairwise defeat primarily by the absolute number of votes for the winner and secondarily by the absolute number of votes for the loser';
2904 CREATE FUNCTION "array_init_string"("dim_p" INTEGER)
2905 RETURNS TEXT
2906 LANGUAGE 'plpgsql' IMMUTABLE AS $$
2907 DECLARE
2908 "i" INTEGER;
2909 "ary_text_v" TEXT;
2910 BEGIN
2911 IF "dim_p" >= 1 THEN
2912 "ary_text_v" := '{NULL';
2913 "i" := "dim_p";
2914 LOOP
2915 "i" := "i" - 1;
2916 EXIT WHEN "i" = 0;
2917 "ary_text_v" := "ary_text_v" || ',NULL';
2918 END LOOP;
2919 "ary_text_v" := "ary_text_v" || '}';
2920 RETURN "ary_text_v";
2921 ELSE
2922 RAISE EXCEPTION 'Dimension needs to be at least 1.';
2923 END IF;
2924 END;
2925 $$;
2927 COMMENT ON FUNCTION "array_init_string"(INTEGER) IS 'Needed for PostgreSQL < 8.4, due to missing "array_fill" function';
2930 CREATE FUNCTION "square_matrix_init_string"("dim_p" INTEGER)
2931 RETURNS TEXT
2932 LANGUAGE 'plpgsql' IMMUTABLE AS $$
2933 DECLARE
2934 "i" INTEGER;
2935 "row_text_v" TEXT;
2936 "ary_text_v" TEXT;
2937 BEGIN
2938 IF "dim_p" >= 1 THEN
2939 "row_text_v" := '{NULL';
2940 "i" := "dim_p";
2941 LOOP
2942 "i" := "i" - 1;
2943 EXIT WHEN "i" = 0;
2944 "row_text_v" := "row_text_v" || ',NULL';
2945 END LOOP;
2946 "row_text_v" := "row_text_v" || '}';
2947 "ary_text_v" := '{' || "row_text_v";
2948 "i" := "dim_p";
2949 LOOP
2950 "i" := "i" - 1;
2951 EXIT WHEN "i" = 0;
2952 "ary_text_v" := "ary_text_v" || ',' || "row_text_v";
2953 END LOOP;
2954 "ary_text_v" := "ary_text_v" || '}';
2955 RETURN "ary_text_v";
2956 ELSE
2957 RAISE EXCEPTION 'Dimension needs to be at least 1.';
2958 END IF;
2959 END;
2960 $$;
2962 COMMENT ON FUNCTION "square_matrix_init_string"(INTEGER) IS 'Needed for PostgreSQL < 8.4, due to missing "array_fill" function';
2965 CREATE FUNCTION "calculate_ranks"("issue_id_p" "issue"."id"%TYPE)
2966 RETURNS VOID
2967 LANGUAGE 'plpgsql' VOLATILE AS $$
2968 DECLARE
2969 "dimension_v" INTEGER;
2970 "vote_matrix" INT4[][]; -- absolute votes
2971 "matrix" INT8[][]; -- defeat strength / best paths
2972 "i" INTEGER;
2973 "j" INTEGER;
2974 "k" INTEGER;
2975 "battle_row" "battle"%ROWTYPE;
2976 "rank_ary" INT4[];
2977 "rank_v" INT4;
2978 "done_v" INTEGER;
2979 "winners_ary" INTEGER[];
2980 "initiative_id_v" "initiative"."id"%TYPE;
2981 BEGIN
2982 PERFORM NULL FROM "issue" WHERE "id" = "issue_id_p" FOR UPDATE;
2983 SELECT count(1) INTO "dimension_v" FROM "initiative"
2984 WHERE "issue_id" = "issue_id_p" AND "agreed";
2985 IF "dimension_v" = 1 THEN
2986 UPDATE "initiative" SET "rank" = 1
2987 WHERE "issue_id" = "issue_id_p" AND "agreed";
2988 ELSIF "dimension_v" > 1 THEN
2989 -- Create "vote_matrix" with absolute number of votes in pairwise
2990 -- comparison:
2991 "vote_matrix" := "square_matrix_init_string"("dimension_v"); -- TODO: replace by "array_fill" function (PostgreSQL 8.4)
2992 "i" := 1;
2993 "j" := 2;
2994 FOR "battle_row" IN
2995 SELECT * FROM "battle" WHERE "issue_id" = "issue_id_p"
2996 ORDER BY "winning_initiative_id", "losing_initiative_id"
2997 LOOP
2998 "vote_matrix"["i"]["j"] := "battle_row"."count";
2999 IF "j" = "dimension_v" THEN
3000 "i" := "i" + 1;
3001 "j" := 1;
3002 ELSE
3003 "j" := "j" + 1;
3004 IF "j" = "i" THEN
3005 "j" := "j" + 1;
3006 END IF;
3007 END IF;
3008 END LOOP;
3009 IF "i" != "dimension_v" OR "j" != "dimension_v" + 1 THEN
3010 RAISE EXCEPTION 'Wrong battle count (should not happen)';
3011 END IF;
3012 -- Store defeat strengths in "matrix" using "defeat_strength"
3013 -- function:
3014 "matrix" := "square_matrix_init_string"("dimension_v"); -- TODO: replace by "array_fill" function (PostgreSQL 8.4)
3015 "i" := 1;
3016 LOOP
3017 "j" := 1;
3018 LOOP
3019 IF "i" != "j" THEN
3020 "matrix"["i"]["j"] := "defeat_strength"(
3021 "vote_matrix"["i"]["j"],
3022 "vote_matrix"["j"]["i"]
3023 );
3024 END IF;
3025 EXIT WHEN "j" = "dimension_v";
3026 "j" := "j" + 1;
3027 END LOOP;
3028 EXIT WHEN "i" = "dimension_v";
3029 "i" := "i" + 1;
3030 END LOOP;
3031 -- Find best paths:
3032 "i" := 1;
3033 LOOP
3034 "j" := 1;
3035 LOOP
3036 IF "i" != "j" THEN
3037 "k" := 1;
3038 LOOP
3039 IF "i" != "k" AND "j" != "k" THEN
3040 IF "matrix"["j"]["i"] < "matrix"["i"]["k"] THEN
3041 IF "matrix"["j"]["i"] > "matrix"["j"]["k"] THEN
3042 "matrix"["j"]["k"] := "matrix"["j"]["i"];
3043 END IF;
3044 ELSE
3045 IF "matrix"["i"]["k"] > "matrix"["j"]["k"] THEN
3046 "matrix"["j"]["k"] := "matrix"["i"]["k"];
3047 END IF;
3048 END IF;
3049 END IF;
3050 EXIT WHEN "k" = "dimension_v";
3051 "k" := "k" + 1;
3052 END LOOP;
3053 END IF;
3054 EXIT WHEN "j" = "dimension_v";
3055 "j" := "j" + 1;
3056 END LOOP;
3057 EXIT WHEN "i" = "dimension_v";
3058 "i" := "i" + 1;
3059 END LOOP;
3060 -- Determine order of winners:
3061 "rank_ary" := "array_init_string"("dimension_v"); -- TODO: replace by "array_fill" function (PostgreSQL 8.4)
3062 "rank_v" := 1;
3063 "done_v" := 0;
3064 LOOP
3065 "winners_ary" := '{}';
3066 "i" := 1;
3067 LOOP
3068 IF "rank_ary"["i"] ISNULL THEN
3069 "j" := 1;
3070 LOOP
3071 IF
3072 "i" != "j" AND
3073 "rank_ary"["j"] ISNULL AND
3074 "matrix"["j"]["i"] > "matrix"["i"]["j"]
3075 THEN
3076 -- someone else is better
3077 EXIT;
3078 END IF;
3079 IF "j" = "dimension_v" THEN
3080 -- noone is better
3081 "winners_ary" := "winners_ary" || "i";
3082 EXIT;
3083 END IF;
3084 "j" := "j" + 1;
3085 END LOOP;
3086 END IF;
3087 EXIT WHEN "i" = "dimension_v";
3088 "i" := "i" + 1;
3089 END LOOP;
3090 "i" := 1;
3091 LOOP
3092 "rank_ary"["winners_ary"["i"]] := "rank_v";
3093 "done_v" := "done_v" + 1;
3094 EXIT WHEN "i" = array_upper("winners_ary", 1);
3095 "i" := "i" + 1;
3096 END LOOP;
3097 EXIT WHEN "done_v" = "dimension_v";
3098 "rank_v" := "rank_v" + 1;
3099 END LOOP;
3100 -- write preliminary ranks:
3101 "i" := 1;
3102 FOR "initiative_id_v" IN
3103 SELECT "id" FROM "initiative"
3104 WHERE "issue_id" = "issue_id_p" AND "agreed"
3105 ORDER BY "id"
3106 LOOP
3107 UPDATE "initiative" SET "rank" = "rank_ary"["i"]
3108 WHERE "id" = "initiative_id_v";
3109 "i" := "i" + 1;
3110 END LOOP;
3111 IF "i" != "dimension_v" + 1 THEN
3112 RAISE EXCEPTION 'Wrong winner count (should not happen)';
3113 END IF;
3114 -- straighten ranks (start counting with 1, no equal ranks):
3115 "rank_v" := 1;
3116 FOR "initiative_id_v" IN
3117 SELECT "id" FROM "initiative"
3118 WHERE "issue_id" = "issue_id_p" AND "rank" NOTNULL
3119 ORDER BY
3120 "rank",
3121 "vote_ratio"("positive_votes", "negative_votes") DESC,
3122 "id"
3123 LOOP
3124 UPDATE "initiative" SET "rank" = "rank_v"
3125 WHERE "id" = "initiative_id_v";
3126 "rank_v" := "rank_v" + 1;
3127 END LOOP;
3128 END IF;
3129 -- mark issue as finished
3130 UPDATE "issue" SET "ranks_available" = TRUE
3131 WHERE "id" = "issue_id_p";
3132 RETURN;
3133 END;
3134 $$;
3136 COMMENT ON FUNCTION "calculate_ranks"
3137 ( "issue"."id"%TYPE )
3138 IS 'Determine ranking (Votes have to be counted first)';
3142 -----------------------------
3143 -- Automatic state changes --
3144 -----------------------------
3147 CREATE FUNCTION "check_issue"
3148 ( "issue_id_p" "issue"."id"%TYPE )
3149 RETURNS VOID
3150 LANGUAGE 'plpgsql' VOLATILE AS $$
3151 DECLARE
3152 "issue_row" "issue"%ROWTYPE;
3153 "policy_row" "policy"%ROWTYPE;
3154 "voting_requested_v" BOOLEAN;
3155 BEGIN
3156 PERFORM "lock_issue"("issue_id_p");
3157 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3158 -- only process open issues:
3159 IF "issue_row"."closed" ISNULL THEN
3160 SELECT * INTO "policy_row" FROM "policy"
3161 WHERE "id" = "issue_row"."policy_id";
3162 -- create a snapshot, unless issue is already fully frozen:
3163 IF "issue_row"."fully_frozen" ISNULL THEN
3164 PERFORM "create_snapshot"("issue_id_p");
3165 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3166 END IF;
3167 -- eventually close or accept issues, which have not been accepted:
3168 IF "issue_row"."accepted" ISNULL THEN
3169 IF EXISTS (
3170 SELECT NULL FROM "initiative"
3171 WHERE "issue_id" = "issue_id_p"
3172 AND "supporter_count" > 0
3173 AND "supporter_count" * "policy_row"."issue_quorum_den"
3174 >= "issue_row"."population" * "policy_row"."issue_quorum_num"
3175 ) THEN
3176 -- accept issues, if supporter count is high enough
3177 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
3178 "issue_row"."accepted" = now(); -- NOTE: "issue_row" used later
3179 UPDATE "issue" SET "accepted" = "issue_row"."accepted"
3180 WHERE "id" = "issue_row"."id";
3181 ELSIF
3182 now() >= "issue_row"."created" + "issue_row"."admission_time"
3183 THEN
3184 -- close issues, if admission time has expired
3185 PERFORM "set_snapshot_event"("issue_id_p", 'end_of_admission');
3186 UPDATE "issue" SET "closed" = now()
3187 WHERE "id" = "issue_row"."id";
3188 END IF;
3189 END IF;
3190 -- eventually half freeze issues:
3191 IF
3192 -- NOTE: issue can't be closed at this point, if it has been accepted
3193 "issue_row"."accepted" NOTNULL AND
3194 "issue_row"."half_frozen" ISNULL
3195 THEN
3196 SELECT
3197 CASE
3198 WHEN "vote_now" * 2 > "issue_row"."population" THEN
3199 TRUE
3200 WHEN "vote_later" * 2 > "issue_row"."population" THEN
3201 FALSE
3202 ELSE NULL
3203 END
3204 INTO "voting_requested_v"
3205 FROM "issue" WHERE "id" = "issue_id_p";
3206 IF
3207 "voting_requested_v" OR (
3208 "voting_requested_v" ISNULL AND
3209 now() >= "issue_row"."accepted" + "issue_row"."discussion_time"
3211 THEN
3212 PERFORM "set_snapshot_event"("issue_id_p", 'half_freeze');
3213 "issue_row"."half_frozen" = now(); -- NOTE: "issue_row" used later
3214 UPDATE "issue" SET "half_frozen" = "issue_row"."half_frozen"
3215 WHERE "id" = "issue_row"."id";
3216 END IF;
3217 END IF;
3218 -- close issues after some time, if all initiatives have been revoked:
3219 IF
3220 "issue_row"."closed" ISNULL AND
3221 NOT EXISTS (
3222 -- all initiatives are revoked
3223 SELECT NULL FROM "initiative"
3224 WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
3225 ) AND (
3226 NOT EXISTS (
3227 -- and no initiatives have been revoked lately
3228 SELECT NULL FROM "initiative"
3229 WHERE "issue_id" = "issue_id_p"
3230 AND now() < "revoked" + "issue_row"."verification_time"
3231 ) OR (
3232 -- or verification time has elapsed
3233 "issue_row"."half_frozen" NOTNULL AND
3234 "issue_row"."fully_frozen" ISNULL AND
3235 now() >= "issue_row"."half_frozen" + "issue_row"."verification_time"
3238 THEN
3239 "issue_row"."closed" = now(); -- NOTE: "issue_row" used later
3240 UPDATE "issue" SET "closed" = "issue_row"."closed"
3241 WHERE "id" = "issue_row"."id";
3242 END IF;
3243 -- fully freeze issue after verification time:
3244 IF
3245 "issue_row"."half_frozen" NOTNULL AND
3246 "issue_row"."fully_frozen" ISNULL AND
3247 "issue_row"."closed" ISNULL AND
3248 now() >= "issue_row"."half_frozen" + "issue_row"."verification_time"
3249 THEN
3250 PERFORM "freeze_after_snapshot"("issue_id_p");
3251 -- NOTE: "issue" might change, thus "issue_row" has to be updated below
3252 END IF;
3253 SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
3254 -- close issue by calling close_voting(...) after voting time:
3255 IF
3256 "issue_row"."closed" ISNULL AND
3257 "issue_row"."fully_frozen" NOTNULL AND
3258 now() >= "issue_row"."fully_frozen" + "issue_row"."voting_time"
3259 THEN
3260 PERFORM "close_voting"("issue_id_p");
3261 END IF;
3262 END IF;
3263 RETURN;
3264 END;
3265 $$;
3267 COMMENT ON FUNCTION "check_issue"
3268 ( "issue"."id"%TYPE )
3269 IS 'Precalculate supporter counts etc. for a given issue, and check, if status change is required; At end of voting the ranking is not calculated by this function, but must be calculated in a seperate transaction using the "calculate_ranks" function.';
3272 CREATE FUNCTION "check_everything"()
3273 RETURNS VOID
3274 LANGUAGE 'plpgsql' VOLATILE AS $$
3275 DECLARE
3276 "issue_id_v" "issue"."id"%TYPE;
3277 BEGIN
3278 DELETE FROM "expired_session";
3279 PERFORM "calculate_member_counts"();
3280 FOR "issue_id_v" IN SELECT "id" FROM "open_issue" LOOP
3281 PERFORM "check_issue"("issue_id_v");
3282 END LOOP;
3283 FOR "issue_id_v" IN SELECT "id" FROM "issue_with_ranks_missing" LOOP
3284 PERFORM "calculate_ranks"("issue_id_v");
3285 END LOOP;
3286 RETURN;
3287 END;
3288 $$;
3290 COMMENT ON FUNCTION "check_everything"() IS 'Perform "check_issue" for every open issue, and if possible, automatically calculate ranks. Use this function only for development and debugging purposes, as long transactions with exclusive locking may result.';
3294 ----------------------
3295 -- Deletion of data --
3296 ----------------------
3299 CREATE FUNCTION "clean_issue"("issue_id_p" "issue"."id"%TYPE)
3300 RETURNS VOID
3301 LANGUAGE 'plpgsql' VOLATILE AS $$
3302 DECLARE
3303 "issue_row" "issue"%ROWTYPE;
3304 BEGIN
3305 SELECT * INTO "issue_row"
3306 FROM "issue" WHERE "id" = "issue_id_p"
3307 FOR UPDATE;
3308 IF "issue_row"."cleaned" ISNULL THEN
3309 UPDATE "issue" SET
3310 "closed" = NULL,
3311 "ranks_available" = FALSE
3312 WHERE "id" = "issue_id_p";
3313 DELETE FROM "delegating_voter"
3314 WHERE "issue_id" = "issue_id_p";
3315 DELETE FROM "direct_voter"
3316 WHERE "issue_id" = "issue_id_p";
3317 DELETE FROM "delegating_interest_snapshot"
3318 WHERE "issue_id" = "issue_id_p";
3319 DELETE FROM "direct_interest_snapshot"
3320 WHERE "issue_id" = "issue_id_p";
3321 DELETE FROM "delegating_population_snapshot"
3322 WHERE "issue_id" = "issue_id_p";
3323 DELETE FROM "direct_population_snapshot"
3324 WHERE "issue_id" = "issue_id_p";
3325 DELETE FROM "delegation"
3326 WHERE "issue_id" = "issue_id_p";
3327 DELETE FROM "supporter"
3328 WHERE "issue_id" = "issue_id_p";
3329 UPDATE "issue" SET
3330 "closed" = "issue_row"."closed",
3331 "ranks_available" = "issue_row"."ranks_available",
3332 "cleaned" = now()
3333 WHERE "id" = "issue_id_p";
3334 END IF;
3335 RETURN;
3336 END;
3337 $$;
3339 COMMENT ON FUNCTION "clean_issue"("issue"."id"%TYPE) IS 'Delete discussion data and votes belonging to an issue';
3342 CREATE FUNCTION "delete_member"("member_id_p" "member"."id"%TYPE)
3343 RETURNS VOID
3344 LANGUAGE 'plpgsql' VOLATILE AS $$
3345 BEGIN
3346 UPDATE "member" SET
3347 "last_login" = NULL,
3348 "login" = NULL,
3349 "password" = NULL,
3350 "active" = FALSE,
3351 "notify_email" = NULL,
3352 "notify_email_unconfirmed" = NULL,
3353 "notify_email_secret" = NULL,
3354 "notify_email_secret_expiry" = NULL,
3355 "notify_email_lock_expiry" = NULL,
3356 "password_reset_secret" = NULL,
3357 "password_reset_secret_expiry" = NULL,
3358 "organizational_unit" = NULL,
3359 "internal_posts" = NULL,
3360 "realname" = NULL,
3361 "birthday" = NULL,
3362 "address" = NULL,
3363 "email" = NULL,
3364 "xmpp_address" = NULL,
3365 "website" = NULL,
3366 "phone" = NULL,
3367 "mobile_phone" = NULL,
3368 "profession" = NULL,
3369 "external_memberships" = NULL,
3370 "external_posts" = NULL,
3371 "statement" = NULL
3372 WHERE "id" = "member_id_p";
3373 -- "text_search_data" is updated by triggers
3374 DELETE FROM "setting" WHERE "member_id" = "member_id_p";
3375 DELETE FROM "setting_map" WHERE "member_id" = "member_id_p";
3376 DELETE FROM "member_relation_setting" WHERE "member_id" = "member_id_p";
3377 DELETE FROM "member_image" WHERE "member_id" = "member_id_p";
3378 DELETE FROM "contact" WHERE "member_id" = "member_id_p";
3379 DELETE FROM "area_setting" WHERE "member_id" = "member_id_p";
3380 DELETE FROM "issue_setting" WHERE "member_id" = "member_id_p";
3381 DELETE FROM "initiative_setting" WHERE "member_id" = "member_id_p";
3382 DELETE FROM "suggestion_setting" WHERE "member_id" = "member_id_p";
3383 DELETE FROM "membership" WHERE "member_id" = "member_id_p";
3384 DELETE FROM "delegation" WHERE "truster_id" = "member_id_p";
3385 DELETE FROM "direct_voter" USING "issue"
3386 WHERE "direct_voter"."issue_id" = "issue"."id"
3387 AND "issue"."closed" ISNULL
3388 AND "member_id" = "member_id_p";
3389 RETURN;
3390 END;
3391 $$;
3393 COMMENT ON FUNCTION "delete_member"("member_id_p" "member"."id"%TYPE) IS 'Deactivate member and clear certain settings and data of this member (data protection)';
3396 CREATE FUNCTION "delete_private_data"()
3397 RETURNS VOID
3398 LANGUAGE 'plpgsql' VOLATILE AS $$
3399 BEGIN
3400 UPDATE "member" SET
3401 "last_login" = NULL,
3402 "login" = NULL,
3403 "password" = NULL,
3404 "notify_email" = NULL,
3405 "notify_email_unconfirmed" = NULL,
3406 "notify_email_secret" = NULL,
3407 "notify_email_secret_expiry" = NULL,
3408 "notify_email_lock_expiry" = NULL,
3409 "password_reset_secret" = NULL,
3410 "password_reset_secret_expiry" = NULL,
3411 "organizational_unit" = NULL,
3412 "internal_posts" = NULL,
3413 "realname" = NULL,
3414 "birthday" = NULL,
3415 "address" = NULL,
3416 "email" = NULL,
3417 "xmpp_address" = NULL,
3418 "website" = NULL,
3419 "phone" = NULL,
3420 "mobile_phone" = NULL,
3421 "profession" = NULL,
3422 "external_memberships" = NULL,
3423 "external_posts" = NULL,
3424 "statement" = NULL;
3425 -- "text_search_data" is updated by triggers
3426 DELETE FROM "invite_code";
3427 DELETE FROM "setting";
3428 DELETE FROM "setting_map";
3429 DELETE FROM "member_relation_setting";
3430 DELETE FROM "member_image";
3431 DELETE FROM "contact";
3432 DELETE FROM "session";
3433 DELETE FROM "area_setting";
3434 DELETE FROM "issue_setting";
3435 DELETE FROM "initiative_setting";
3436 DELETE FROM "suggestion_setting";
3437 DELETE FROM "direct_voter" USING "issue"
3438 WHERE "direct_voter"."issue_id" = "issue"."id"
3439 AND "issue"."closed" ISNULL;
3440 RETURN;
3441 END;
3442 $$;
3444 COMMENT ON FUNCTION "delete_private_data"() IS 'DO NOT USE on productive database, but only on a copy! This function deletes all data which should not be publicly available, and can be used to create a database dump for publication.';
3448 COMMIT;

Impressum / About Us