liquid_feedback_core

diff update/core-update.v2.0.12-v2.1.0.sql @ 307:847d59f94ceb

Merged fix from v2.0.12: Removed unwanted (and broken) tie-breaking by approval rate
author jbe
date Fri Oct 05 13:45:37 2012 +0200 (2012-10-05)
parents update/core-update.v2.0.11-v2.1.0.sql@7cad34b945ac
children 5c98265b39a0
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/update/core-update.v2.0.12-v2.1.0.sql	Fri Oct 05 13:45:37 2012 +0200
     1.3 @@ -0,0 +1,458 @@
     1.4 +BEGIN;
     1.5 +
     1.6 +
     1.7 +-- update version number
     1.8 +
     1.9 +CREATE OR REPLACE VIEW "liquid_feedback_version" AS
    1.10 +  SELECT * FROM (VALUES ('2.1.0', 2, 1, 0))
    1.11 +  AS "subquery"("string", "major", "minor", "revision");
    1.12 +
    1.13 +
    1.14 +-- old API tables are now deprecated
    1.15 +
    1.16 +COMMENT ON TYPE "application_access_level" IS 'DEPRECATED, WILL BE REMOVED! Access privileges for applications using the API';
    1.17 +COMMENT ON TABLE "member_application" IS 'DEPRECATED, WILL BE REMOVED! Registered application being allowed to use the API';
    1.18 +
    1.19 +
    1.20 +-- new polling mode and changed privileges
    1.21 +
    1.22 +ALTER TABLE "policy" ADD COLUMN "polling" BOOLEAN NOT NULL DEFAULT FALSE;
    1.23 +ALTER TABLE "policy" ALTER COLUMN "admission_time"    DROP NOT NULL;
    1.24 +ALTER TABLE "policy" ALTER COLUMN "discussion_time"   DROP NOT NULL;
    1.25 +ALTER TABLE "policy" ALTER COLUMN "verification_time" DROP NOT NULL;
    1.26 +ALTER TABLE "policy" ALTER COLUMN "voting_time"       DROP NOT NULL;
    1.27 +ALTER TABLE "policy" ALTER COLUMN "issue_quorum_num"  DROP NOT NULL;
    1.28 +ALTER TABLE "policy" ALTER COLUMN "issue_quorum_den"  DROP NOT NULL;
    1.29 +ALTER TABLE "policy" ADD CONSTRAINT "timing" CHECK (
    1.30 +          ( "polling" = FALSE AND
    1.31 +            "admission_time" NOTNULL AND "discussion_time" NOTNULL AND
    1.32 +            "verification_time" NOTNULL AND "voting_time" NOTNULL ) OR
    1.33 +          ( "polling" = TRUE AND
    1.34 +            "admission_time" ISNULL AND "discussion_time" NOTNULL AND
    1.35 +            "verification_time" NOTNULL AND "voting_time" NOTNULL ) OR
    1.36 +          ( "polling" = TRUE AND
    1.37 +            "admission_time" ISNULL AND "discussion_time" ISNULL AND
    1.38 +            "verification_time" ISNULL AND "voting_time" ISNULL ) );
    1.39 +ALTER TABLE "policy" ADD CONSTRAINT "issue_quorum_if_and_only_if_not_polling" CHECK (
    1.40 +          "polling" = "issue_quorum_num" ISNULL AND
    1.41 +          "polling" = "issue_quorum_den" ISNULL );
    1.42 +COMMENT ON COLUMN "policy"."polling" IS 'TRUE = special policy for non-user-generated issues without issue quorum, where certain initiatives (those having the "polling" flag set) do not need to pass the initiative quorum; "admission_time" MUST be set to NULL, the other timings may be set to NULL altogether, allowing individual timing for those issues';
    1.43 +
    1.44 +ALTER TABLE "issue" ALTER COLUMN "admission_time" DROP NOT NULL;
    1.45 +ALTER TABLE "issue" ADD CONSTRAINT "admission_time_not_null_unless_instantly_accepted" CHECK (
    1.46 +  "admission_time" NOTNULL OR ("accepted" NOTNULL AND "accepted" = "created") );
    1.47 +
    1.48 +ALTER TABLE "initiative" ADD COLUMN "polling" BOOLEAN NOT NULL DEFAULT FALSE;
    1.49 +COMMENT ON COLUMN "initiative"."polling" IS 'Initiative does not need to pass the initiative quorum (see "policy"."polling")';
    1.50 +
    1.51 +ALTER TABLE "privilege" RENAME COLUMN "voting_right_manager" TO "member_manager";
    1.52 +ALTER TABLE "privilege" ADD COLUMN "initiative_right" BOOLEAN NOT NULL DEFAULT TRUE;
    1.53 +ALTER TABLE "privilege" ADD COLUMN "polling_right"    BOOLEAN NOT NULL DEFAULT FALSE;
    1.54 +UPDATE "privilege" SET "initiative_right" = "voting_right";
    1.55 +COMMENT ON COLUMN "privilege"."admin_manager"    IS 'Grant/revoke any privileges to/from other members';
    1.56 +COMMENT ON COLUMN "privilege"."member_manager"   IS 'Adding/removing members from the unit, granting or revoking "initiative_right" and "voting_right"';
    1.57 +COMMENT ON COLUMN "privilege"."initiative_right" IS 'Right to create an initiative';
    1.58 +COMMENT ON COLUMN "privilege"."voting_right"     IS 'Right to support initiatives, create and rate suggestions, and to vote';
    1.59 +COMMENT ON COLUMN "privilege"."polling_right"    IS 'Right to create issues with policies having the "policy"."polling" flag set, and to add initiatives having the "initiative"."polling" flag set to those issues';
    1.60 +
    1.61 +DROP VIEW "member_contingent_left";
    1.62 +DROP VIEW "member_contingent";
    1.63 +ALTER TABLE "contingent" DROP CONSTRAINT "contingent_pkey";
    1.64 +ALTER TABLE "contingent" ALTER COLUMN "time_frame" DROP NOT NULL;
    1.65 +ALTER TABLE "contingent" ADD COLUMN "polling" BOOLEAN DEFAULT FALSE;
    1.66 +ALTER TABLE "contingent" ADD PRIMARY KEY ("polling", "time_frame");
    1.67 +ALTER TABLE "contingent" ALTER COLUMN "polling" DROP DEFAULT;
    1.68 +COMMENT ON COLUMN "contingent"."polling" IS 'Determines if settings are for creating initiatives and new drafts of initiatives with "polling" flag set';
    1.69 +
    1.70 +CREATE VIEW "member_contingent" AS
    1.71 +  SELECT
    1.72 +    "member"."id" AS "member_id",
    1.73 +    "contingent"."polling",
    1.74 +    "contingent"."time_frame",
    1.75 +    CASE WHEN "contingent"."text_entry_limit" NOTNULL THEN
    1.76 +      (
    1.77 +        SELECT count(1) FROM "draft"
    1.78 +        JOIN "initiative" ON "initiative"."id" = "draft"."initiative_id"
    1.79 +        WHERE "draft"."author_id" = "member"."id"
    1.80 +        AND "initiative"."polling" = "contingent"."polling"
    1.81 +        AND "draft"."created" > now() - "contingent"."time_frame"
    1.82 +      ) + (
    1.83 +        SELECT count(1) FROM "suggestion"
    1.84 +        JOIN "initiative" ON "initiative"."id" = "suggestion"."initiative_id"
    1.85 +        WHERE "suggestion"."author_id" = "member"."id"
    1.86 +        AND "contingent"."polling" = FALSE
    1.87 +        AND "suggestion"."created" > now() - "contingent"."time_frame"
    1.88 +      )
    1.89 +    ELSE NULL END AS "text_entry_count",
    1.90 +    "contingent"."text_entry_limit",
    1.91 +    CASE WHEN "contingent"."initiative_limit" NOTNULL THEN (
    1.92 +      SELECT count(1) FROM "opening_draft" AS "draft"
    1.93 +        JOIN "initiative" ON "initiative"."id" = "draft"."initiative_id"
    1.94 +      WHERE "draft"."author_id" = "member"."id"
    1.95 +      AND "initiative"."polling" = "contingent"."polling"
    1.96 +      AND "draft"."created" > now() - "contingent"."time_frame"
    1.97 +    ) ELSE NULL END AS "initiative_count",
    1.98 +    "contingent"."initiative_limit"
    1.99 +  FROM "member" CROSS JOIN "contingent";
   1.100 +
   1.101 +COMMENT ON VIEW "member_contingent" IS 'Actual counts of text entries and initiatives are calculated per member for each limit in the "contingent" table.';
   1.102 +
   1.103 +COMMENT ON COLUMN "member_contingent"."text_entry_count" IS 'Only calculated when "text_entry_limit" is not null in the same row';
   1.104 +COMMENT ON COLUMN "member_contingent"."initiative_count" IS 'Only calculated when "initiative_limit" is not null in the same row';
   1.105 +
   1.106 +CREATE VIEW "member_contingent_left" AS
   1.107 +  SELECT
   1.108 +    "member_id",
   1.109 +    "polling",
   1.110 +    max("text_entry_limit" - "text_entry_count") AS "text_entries_left",
   1.111 +    max("initiative_limit" - "initiative_count") AS "initiatives_left"
   1.112 +  FROM "member_contingent" GROUP BY "member_id", "polling";
   1.113 +
   1.114 +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.';
   1.115 +
   1.116 +CREATE OR REPLACE FUNCTION "freeze_after_snapshot"
   1.117 +  ( "issue_id_p" "issue"."id"%TYPE )
   1.118 +  RETURNS VOID
   1.119 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.120 +    DECLARE
   1.121 +      "issue_row"      "issue"%ROWTYPE;
   1.122 +      "policy_row"     "policy"%ROWTYPE;
   1.123 +      "initiative_row" "initiative"%ROWTYPE;
   1.124 +    BEGIN
   1.125 +      SELECT * INTO "issue_row" FROM "issue" WHERE "id" = "issue_id_p";
   1.126 +      SELECT * INTO "policy_row"
   1.127 +        FROM "policy" WHERE "id" = "issue_row"."policy_id";
   1.128 +      PERFORM "set_snapshot_event"("issue_id_p", 'full_freeze');
   1.129 +      FOR "initiative_row" IN
   1.130 +        SELECT * FROM "initiative"
   1.131 +        WHERE "issue_id" = "issue_id_p" AND "revoked" ISNULL
   1.132 +      LOOP
   1.133 +        IF
   1.134 +          "initiative_row"."polling" OR (
   1.135 +            "initiative_row"."satisfied_supporter_count" > 0 AND
   1.136 +            "initiative_row"."satisfied_supporter_count" *
   1.137 +            "policy_row"."initiative_quorum_den" >=
   1.138 +            "issue_row"."population" * "policy_row"."initiative_quorum_num"
   1.139 +          )
   1.140 +        THEN
   1.141 +          UPDATE "initiative" SET "admitted" = TRUE
   1.142 +            WHERE "id" = "initiative_row"."id";
   1.143 +        ELSE
   1.144 +          UPDATE "initiative" SET "admitted" = FALSE
   1.145 +            WHERE "id" = "initiative_row"."id";
   1.146 +        END IF;
   1.147 +      END LOOP;
   1.148 +      IF EXISTS (
   1.149 +        SELECT NULL FROM "initiative"
   1.150 +        WHERE "issue_id" = "issue_id_p" AND "admitted" = TRUE
   1.151 +      ) THEN
   1.152 +        UPDATE "issue" SET
   1.153 +          "state"        = 'voting',
   1.154 +          "accepted"     = coalesce("accepted", now()),
   1.155 +          "half_frozen"  = coalesce("half_frozen", now()),
   1.156 +          "fully_frozen" = now()
   1.157 +          WHERE "id" = "issue_id_p";
   1.158 +      ELSE
   1.159 +        UPDATE "issue" SET
   1.160 +          "state"           = 'canceled_no_initiative_admitted',
   1.161 +          "accepted"        = coalesce("accepted", now()),
   1.162 +          "half_frozen"     = coalesce("half_frozen", now()),
   1.163 +          "fully_frozen"    = now(),
   1.164 +          "closed"          = now(),
   1.165 +          "ranks_available" = TRUE
   1.166 +          WHERE "id" = "issue_id_p";
   1.167 +        -- NOTE: The following DELETE statements have effect only when
   1.168 +        --       issue state has been manipulated
   1.169 +        DELETE FROM "direct_voter"     WHERE "issue_id" = "issue_id_p";
   1.170 +        DELETE FROM "delegating_voter" WHERE "issue_id" = "issue_id_p";
   1.171 +        DELETE FROM "battle"           WHERE "issue_id" = "issue_id_p";
   1.172 +      END IF;
   1.173 +      RETURN;
   1.174 +    END;
   1.175 +  $$;
   1.176 +
   1.177 +
   1.178 +-- issue comments removed, voting comments integrated in "direct_voter" table
   1.179 +
   1.180 +ALTER TABLE "direct_voter" ADD COLUMN "comment_changed"   TIMESTAMPTZ;
   1.181 +ALTER TABLE "direct_voter" ADD COLUMN "formatting_engine" TEXT;
   1.182 +ALTER TABLE "direct_voter" ADD COLUMN "comment"           TEXT;
   1.183 +ALTER TABLE "direct_voter" ADD COLUMN "text_search_data"  TSVECTOR;
   1.184 +CREATE INDEX "direct_voter_text_search_data_idx" ON "direct_voter" USING gin ("text_search_data");
   1.185 +CREATE TRIGGER "update_text_search_data"
   1.186 +  BEFORE INSERT OR UPDATE ON "direct_voter"
   1.187 +  FOR EACH ROW EXECUTE PROCEDURE
   1.188 +  tsvector_update_trigger('text_search_data', 'pg_catalog.simple', "comment");
   1.189 +
   1.190 +COMMENT ON COLUMN "direct_voter"."comment_changed"   IS 'Shall be set on comment change, to indicate a comment being modified after voting has been finished; Automatically set to NULL after voting phase; Automatically set to NULL by trigger, if "comment" is set to NULL';
   1.191 +COMMENT ON COLUMN "direct_voter"."formatting_engine" IS 'Allows different formatting engines (i.e. wiki formats) to be used for "direct_voter"."comment"; Automatically set to NULL by trigger, if "comment" is set to NULL';
   1.192 +COMMENT ON COLUMN "direct_voter"."comment"           IS 'Is to be set or updated by the frontend, if comment was inserted or updated AFTER the issue has been closed. Otherwise it shall be set to NULL.';
   1.193 +
   1.194 +CREATE TABLE "rendered_voter_comment" (
   1.195 +        PRIMARY KEY ("issue_id", "member_id", "format"),
   1.196 +        FOREIGN KEY ("issue_id", "member_id")
   1.197 +          REFERENCES "direct_voter" ("issue_id", "member_id")
   1.198 +          ON DELETE CASCADE ON UPDATE CASCADE,
   1.199 +        "issue_id"              INT4,
   1.200 +        "member_id"             INT4,
   1.201 +        "format"                TEXT,
   1.202 +        "content"               TEXT            NOT NULL );
   1.203 +
   1.204 +COMMENT ON TABLE "rendered_voter_comment" IS 'This table may be used by frontends to cache "rendered" voter comments (e.g. HTML output generated from wiki text)';
   1.205 +
   1.206 +DROP TABLE "rendered_issue_comment";
   1.207 +DROP TABLE "issue_comment";
   1.208 +DROP TABLE "rendered_voting_comment";
   1.209 +DROP TABLE "voting_comment";
   1.210 +
   1.211 +CREATE FUNCTION "voter_comment_fields_only_set_when_voter_comment_is_set_trigger"()
   1.212 +  RETURNS TRIGGER
   1.213 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.214 +    BEGIN
   1.215 +      IF NEW."comment" ISNULL THEN
   1.216 +        NEW."comment_changed" := NULL;
   1.217 +        NEW."formatting_engine" := NULL;
   1.218 +      END IF;
   1.219 +      RETURN NEW;
   1.220 +    END;
   1.221 +  $$;
   1.222 +
   1.223 +CREATE TRIGGER "voter_comment_fields_only_set_when_voter_comment_is_set"
   1.224 +  BEFORE INSERT OR UPDATE ON "direct_voter"
   1.225 +  FOR EACH ROW EXECUTE PROCEDURE
   1.226 +  "voter_comment_fields_only_set_when_voter_comment_is_set_trigger"();
   1.227 +
   1.228 +COMMENT ON FUNCTION "voter_comment_fields_only_set_when_voter_comment_is_set_trigger"() IS 'Implementation of trigger "voter_comment_fields_only_set_when_voter_comment_is_set" ON table "direct_voter"';
   1.229 +COMMENT ON TRIGGER "voter_comment_fields_only_set_when_voter_comment_is_set" ON "direct_voter" IS 'If "comment" is set to NULL, then other comment related fields are also set to NULL.';
   1.230 +
   1.231 +CREATE OR REPLACE FUNCTION "forbid_changes_on_closed_issue_trigger"()
   1.232 +  RETURNS TRIGGER
   1.233 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.234 +    DECLARE
   1.235 +      "issue_id_v" "issue"."id"%TYPE;
   1.236 +      "issue_row"  "issue"%ROWTYPE;
   1.237 +    BEGIN
   1.238 +      IF TG_RELID = 'direct_voter'::regclass AND TG_OP = 'UPDATE' THEN
   1.239 +        IF
   1.240 +          OLD."issue_id"  = NEW."issue_id"  AND
   1.241 +          OLD."member_id" = NEW."member_id" AND
   1.242 +          OLD."weight"    = NEW."weight"
   1.243 +        THEN
   1.244 +          RETURN NULL;  -- allows changing of voter comment
   1.245 +        END IF;
   1.246 +      END IF;
   1.247 +      IF TG_OP = 'DELETE' THEN
   1.248 +        "issue_id_v" := OLD."issue_id";
   1.249 +      ELSE
   1.250 +        "issue_id_v" := NEW."issue_id";
   1.251 +      END IF;
   1.252 +      SELECT INTO "issue_row" * FROM "issue"
   1.253 +        WHERE "id" = "issue_id_v" FOR SHARE;
   1.254 +      IF "issue_row"."closed" NOTNULL THEN
   1.255 +        RAISE EXCEPTION 'Tried to modify data belonging to a closed issue.';
   1.256 +      END IF;
   1.257 +      RETURN NULL;
   1.258 +    END;
   1.259 +  $$;
   1.260 +
   1.261 +CREATE OR REPLACE FUNCTION "close_voting"("issue_id_p" "issue"."id"%TYPE)
   1.262 +  RETURNS VOID
   1.263 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.264 +    DECLARE
   1.265 +      "area_id_v"   "area"."id"%TYPE;
   1.266 +      "unit_id_v"   "unit"."id"%TYPE;
   1.267 +      "member_id_v" "member"."id"%TYPE;
   1.268 +    BEGIN
   1.269 +      PERFORM "lock_issue"("issue_id_p");
   1.270 +      SELECT "area_id" INTO "area_id_v" FROM "issue" WHERE "id" = "issue_id_p";
   1.271 +      SELECT "unit_id" INTO "unit_id_v" FROM "area"  WHERE "id" = "area_id_v";
   1.272 +      -- delete timestamp of voting comment:
   1.273 +      UPDATE "direct_voter" SET "comment_changed" = NULL
   1.274 +        WHERE "issue_id" = "issue_id_p";
   1.275 +      -- delete delegating votes (in cases of manual reset of issue state):
   1.276 +      DELETE FROM "delegating_voter"
   1.277 +        WHERE "issue_id" = "issue_id_p";
   1.278 +      -- delete votes from non-privileged voters:
   1.279 +      DELETE FROM "direct_voter"
   1.280 +        USING (
   1.281 +          SELECT
   1.282 +            "direct_voter"."member_id"
   1.283 +          FROM "direct_voter"
   1.284 +          JOIN "member" ON "direct_voter"."member_id" = "member"."id"
   1.285 +          LEFT JOIN "privilege"
   1.286 +          ON "privilege"."unit_id" = "unit_id_v"
   1.287 +          AND "privilege"."member_id" = "direct_voter"."member_id"
   1.288 +          WHERE "direct_voter"."issue_id" = "issue_id_p" AND (
   1.289 +            "member"."active" = FALSE OR
   1.290 +            "privilege"."voting_right" ISNULL OR
   1.291 +            "privilege"."voting_right" = FALSE
   1.292 +          )
   1.293 +        ) AS "subquery"
   1.294 +        WHERE "direct_voter"."issue_id" = "issue_id_p"
   1.295 +        AND "direct_voter"."member_id" = "subquery"."member_id";
   1.296 +      -- consider delegations:
   1.297 +      UPDATE "direct_voter" SET "weight" = 1
   1.298 +        WHERE "issue_id" = "issue_id_p";
   1.299 +      PERFORM "add_vote_delegations"("issue_id_p");
   1.300 +      -- set voter count and mark issue as being calculated:
   1.301 +      UPDATE "issue" SET
   1.302 +        "state"  = 'calculation',
   1.303 +        "closed" = now(),
   1.304 +        "voter_count" = (
   1.305 +          SELECT coalesce(sum("weight"), 0)
   1.306 +          FROM "direct_voter" WHERE "issue_id" = "issue_id_p"
   1.307 +        )
   1.308 +        WHERE "id" = "issue_id_p";
   1.309 +      -- materialize battle_view:
   1.310 +      -- NOTE: "closed" column of issue must be set at this point
   1.311 +      DELETE FROM "battle" WHERE "issue_id" = "issue_id_p";
   1.312 +      INSERT INTO "battle" (
   1.313 +        "issue_id",
   1.314 +        "winning_initiative_id", "losing_initiative_id",
   1.315 +        "count"
   1.316 +      ) SELECT
   1.317 +        "issue_id",
   1.318 +        "winning_initiative_id", "losing_initiative_id",
   1.319 +        "count"
   1.320 +        FROM "battle_view" WHERE "issue_id" = "issue_id_p";
   1.321 +      -- copy "positive_votes" and "negative_votes" from "battle" table:
   1.322 +      UPDATE "initiative" SET
   1.323 +        "positive_votes" = "battle_win"."count",
   1.324 +        "negative_votes" = "battle_lose"."count"
   1.325 +        FROM "battle" AS "battle_win", "battle" AS "battle_lose"
   1.326 +        WHERE
   1.327 +          "battle_win"."issue_id" = "issue_id_p" AND
   1.328 +          "battle_win"."winning_initiative_id" = "initiative"."id" AND
   1.329 +          "battle_win"."losing_initiative_id" ISNULL AND
   1.330 +          "battle_lose"."issue_id" = "issue_id_p" AND
   1.331 +          "battle_lose"."losing_initiative_id" = "initiative"."id" AND
   1.332 +          "battle_lose"."winning_initiative_id" ISNULL;
   1.333 +    END;
   1.334 +  $$;
   1.335 +
   1.336 +CREATE OR REPLACE FUNCTION "clean_issue"("issue_id_p" "issue"."id"%TYPE)
   1.337 +  RETURNS VOID
   1.338 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.339 +    DECLARE
   1.340 +      "issue_row" "issue"%ROWTYPE;
   1.341 +    BEGIN
   1.342 +      SELECT * INTO "issue_row"
   1.343 +        FROM "issue" WHERE "id" = "issue_id_p"
   1.344 +        FOR UPDATE;
   1.345 +      IF "issue_row"."cleaned" ISNULL THEN
   1.346 +        UPDATE "issue" SET
   1.347 +          "state"           = 'voting',
   1.348 +          "closed"          = NULL,
   1.349 +          "ranks_available" = FALSE
   1.350 +          WHERE "id" = "issue_id_p";
   1.351 +        DELETE FROM "delegating_voter"
   1.352 +          WHERE "issue_id" = "issue_id_p";
   1.353 +        DELETE FROM "direct_voter"
   1.354 +          WHERE "issue_id" = "issue_id_p";
   1.355 +        DELETE FROM "delegating_interest_snapshot"
   1.356 +          WHERE "issue_id" = "issue_id_p";
   1.357 +        DELETE FROM "direct_interest_snapshot"
   1.358 +          WHERE "issue_id" = "issue_id_p";
   1.359 +        DELETE FROM "delegating_population_snapshot"
   1.360 +          WHERE "issue_id" = "issue_id_p";
   1.361 +        DELETE FROM "direct_population_snapshot"
   1.362 +          WHERE "issue_id" = "issue_id_p";
   1.363 +        DELETE FROM "non_voter"
   1.364 +          WHERE "issue_id" = "issue_id_p";
   1.365 +        DELETE FROM "delegation"
   1.366 +          WHERE "issue_id" = "issue_id_p";
   1.367 +        DELETE FROM "supporter"
   1.368 +          WHERE "issue_id" = "issue_id_p";
   1.369 +        UPDATE "issue" SET
   1.370 +          "state"           = "issue_row"."state",
   1.371 +          "closed"          = "issue_row"."closed",
   1.372 +          "ranks_available" = "issue_row"."ranks_available",
   1.373 +          "cleaned"         = now()
   1.374 +          WHERE "id" = "issue_id_p";
   1.375 +      END IF;
   1.376 +      RETURN;
   1.377 +    END;
   1.378 +  $$;
   1.379 +
   1.380 +
   1.381 +-- "non_voter" deletes "direct_voter" and vice versa
   1.382 +
   1.383 +CREATE FUNCTION "non_voter_deletes_direct_voter_trigger"()
   1.384 +  RETURNS TRIGGER
   1.385 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.386 +    BEGIN
   1.387 +      DELETE FROM "direct_voter"
   1.388 +        WHERE "issue_id" = NEW."issue_id" AND "member_id" = NEW."member_id";
   1.389 +      RETURN NULL;
   1.390 +    END;
   1.391 +  $$;
   1.392 +
   1.393 +CREATE TRIGGER "non_voter_deletes_direct_voter"
   1.394 +  AFTER INSERT OR UPDATE ON "non_voter"
   1.395 +  FOR EACH ROW EXECUTE PROCEDURE
   1.396 +  "non_voter_deletes_direct_voter_trigger"();
   1.397 +
   1.398 +COMMENT ON FUNCTION "non_voter_deletes_direct_voter_trigger"()     IS 'Implementation of trigger "non_voter_deletes_direct_voter" on table "non_voter"';
   1.399 +COMMENT ON TRIGGER "non_voter_deletes_direct_voter" ON "non_voter" IS 'An entry in the "non_voter" table deletes an entry in the "direct_voter" table (and vice versa due to trigger "direct_voter_deletes_non_voter" on table "direct_voter")';
   1.400 +
   1.401 +CREATE FUNCTION "direct_voter_deletes_non_voter_trigger"()
   1.402 +  RETURNS TRIGGER
   1.403 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.404 +    BEGIN
   1.405 +      DELETE FROM "non_voter"
   1.406 +        WHERE "issue_id" = NEW."issue_id" AND "member_id" = NEW."member_id";
   1.407 +      RETURN NULL;
   1.408 +    END;
   1.409 +  $$;
   1.410 +
   1.411 +CREATE TRIGGER "direct_voter_deletes_non_voter"
   1.412 +  AFTER INSERT OR UPDATE ON "direct_voter"
   1.413 +  FOR EACH ROW EXECUTE PROCEDURE
   1.414 +  "direct_voter_deletes_non_voter_trigger"();
   1.415 +
   1.416 +COMMENT ON FUNCTION "direct_voter_deletes_non_voter_trigger"()        IS 'Implementation of trigger "direct_voter_deletes_non_voter" on table "direct_voter"';
   1.417 +COMMENT ON TRIGGER "direct_voter_deletes_non_voter" ON "direct_voter" IS 'An entry in the "direct_voter" table deletes an entry in the "non_voter" table (and vice versa due to trigger "non_voter_deletes_direct_voter" on table "non_voter")';
   1.418 +
   1.419 +
   1.420 +-- different locking levels and different locking order to avoid deadlocks
   1.421 +
   1.422 +CREATE OR REPLACE FUNCTION "lock_issue"
   1.423 +  ( "issue_id_p" "issue"."id"%TYPE )
   1.424 +  RETURNS VOID
   1.425 +  LANGUAGE 'plpgsql' VOLATILE AS $$
   1.426 +    BEGIN
   1.427 +      -- The following locking order is used:
   1.428 +      -- 1st) row-level lock on the issue
   1.429 +      -- 2nd) table-level locks in order of occurrence in the core.sql file
   1.430 +      PERFORM NULL FROM "issue" WHERE "id" = "issue_id_p" FOR UPDATE;
   1.431 +      -- NOTE: The row-level exclusive lock in combination with the
   1.432 +      -- share_row_lock_issue(_via_initiative)_trigger functions (which
   1.433 +      -- acquire a row-level share lock on the issue) ensure that no data
   1.434 +      -- is changed, which could affect calculation of snapshots or
   1.435 +      -- counting of votes. Table "delegation" must be table-level-locked,
   1.436 +      -- as it also contains issue- and global-scope delegations.
   1.437 +      PERFORM NULL FROM "member" WHERE "active" FOR SHARE;
   1.438 +      -- NOTE: As we later cause implicit row-level share locks on many
   1.439 +      -- active members, we lock them before locking any other table
   1.440 +      -- to avoid deadlocks
   1.441 +      LOCK TABLE "member"     IN SHARE MODE;
   1.442 +      LOCK TABLE "privilege"  IN SHARE MODE;
   1.443 +      LOCK TABLE "membership" IN SHARE MODE;
   1.444 +      LOCK TABLE "policy"     IN SHARE MODE;
   1.445 +      LOCK TABLE "delegation" IN SHARE MODE;
   1.446 +      LOCK TABLE "direct_population_snapshot"     IN EXCLUSIVE MODE;
   1.447 +      LOCK TABLE "delegating_population_snapshot" IN EXCLUSIVE MODE;
   1.448 +      LOCK TABLE "direct_interest_snapshot"       IN EXCLUSIVE MODE;
   1.449 +      LOCK TABLE "delegating_interest_snapshot"   IN EXCLUSIVE MODE;
   1.450 +      LOCK TABLE "direct_supporter_snapshot"      IN EXCLUSIVE MODE;
   1.451 +      RETURN;
   1.452 +    END;
   1.453 +  $$;
   1.454 +
   1.455 +
   1.456 +-- new comment on function "delete_private_data"()
   1.457 +
   1.458 +COMMENT ON FUNCTION "delete_private_data"() IS 'Used by lf_export script. 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. See source code to see which data is deleted. If you need a different behaviour, copy this function and modify lf_export accordingly, to avoid data-leaks after updating.';
   1.459 +
   1.460 +
   1.461 +COMMIT;

Impressum / About Us