github.com/JBoudou/Itero@v0.1.7/sql/install.mysql (about)

     1  # Itero - Iterative vote online
     2  # Copyright (C) 2020 Joseph Boudou
     3  # 
     4  # This program is free software: you can redistribute it and/or modify
     5  # it under the terms of the GNU Affero General Public License as
     6  # published by the Free Software Foundation, either version 3 of the
     7  # License, or (at your option) any later version.
     8  # 
     9  # This program is distributed in the hope that it will be useful,
    10  # but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  # GNU Affero General Public License for more details.
    13  # 
    14  # You should have received a copy of the GNU Affero General Public License
    15  # along with this program.  If not, see <https://www.gnu.org/licenses/>.
    16  
    17  # Compatible with 10.3.28-MariaDB MariaDB Server
    18  
    19  SET sql_mode='TRADITIONAL,NO_ENGINE_SUBSTITUTION,ONLY_FULL_GROUP_BY';
    20  
    21  ## Deletion must be in reverse order ##
    22  
    23  DROP PROCEDURE IF EXISTS Ballots_checker_before;
    24  DROP TABLE IF EXISTS Ballots;
    25  
    26  DROP PROCEDURE IF EXISTS Participants_checker_before;
    27  DROP TABLE IF EXISTS Participants;
    28  
    29  DROP PROCEDURE IF EXISTS Alternatives_checker_before;
    30  DROP TABLE IF EXISTS Alternatives;
    31  
    32  DROP PROCEDURE IF EXISTS Polls_checker_before;
    33  DROP TABLE IF EXISTS Polls;
    34  DROP TABLE IF EXISTS PollType;
    35  DROP TABLE IF EXISTS PollRule;
    36  DROP TABLE IF EXISTS RoundType;
    37  
    38  DROP PROCEDURE  IF EXISTS Users_checker_before;
    39  DROP TABLE      IF EXISTS Users;
    40  
    41  
    42  ######## Users ########
    43  
    44  
    45  # Deletion of a user is not possible once she participated to a poll.
    46  CREATE TABLE Users (
    47  
    48    # Passwd stores only a hash signature.
    49    Id        int unsigned  NOT NULL  AUTO_INCREMENT,
    50    Email     varchar(128)  ,
    51    Name      varchar(64)   ,
    52    Passwd    binary(32)    ,
    53    Hash      binary(3)     ,
    54    Created   timestamp     NOT NULL  DEFAULT CURRENT_TIMESTAMP,
    55    Verified  bool          NOT NULL  DEFAULT FALSE,
    56  
    57    CONSTRAINT Users_pk PRIMARY KEY (Id),
    58    CONSTRAINT Users_Email_unique UNIQUE (Email),
    59    CONSTRAINT Users_Name_unique  UNIQUE (Name),
    60    CONSTRAINT Users_Hash_unique UNIQUE (Hash)
    61  
    62  ) ENGINE = InnoDB;
    63  
    64  DELIMITER //
    65  
    66  CREATE OR REPLACE PROCEDURE Users_checker_before (
    67    Email   varchar(128),
    68    Name    varchar(64),
    69    Passwd  binary(32),
    70    Hash    binary(3)
    71  )
    72  BEGIN
    73    IF Hash IS NULL AND (Email IS NULL OR Name IS NULL OR Passwd IS NULL) THEN
    74      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'When Hash is NULL, Email, Name and Passwd must not be NULL';
    75    END IF;
    76    IF Hash IS NULL AND Email NOT LIKE '_%@_%.__%' THEN
    77      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Email field is not valid';
    78    END IF;
    79    IF Hash IS NULL AND length(Name) < 2 THEN
    80      SIGNAL SQLSTATE '44999' SET MESSAGE_TEXT = 'Name field is too short';
    81    END IF;
    82  END;
    83  //
    84  
    85  CREATE OR REPLACE TRIGGER Users_check_before_insert
    86    BEFORE INSERT ON Users FOR EACH ROW
    87  BEGIN
    88    SET NEW.Created = CURRENT_TIMESTAMP();
    89    CALL Users_checker_before(NEW.Email, NEW.Name, NEW.Passwd, NEW.Hash);
    90  END;
    91  //
    92  
    93  CREATE OR REPLACE TRIGGER Users_check_before_update
    94    BEFORE UPDATE ON Users FOR EACH ROW
    95  BEGIN
    96    IF NEW.Created != OLD.Created THEN
    97      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Created cannot be changed';
    98    END IF;
    99    CALL Users_checker_before(NEW.Email, NEW.Name, NEW.Passwd, NEW.Hash);
   100  END;
   101  //
   102  
   103  DELIMITER ;
   104  
   105  
   106  ######## Confirmations ########
   107  
   108  CREATE TABLE Confirmations (
   109  
   110    Id      int unsigned            NOT NULL AUTO_INCREMENT,
   111    Salt    int unsigned            NOT NULL,
   112    Type    ENUM('verify','passwd') NOT NULL,
   113    User    int unsigned            NOT NULL,
   114    Expires datetime                NOT NULL,
   115  
   116    CONSTRAINT Confirmations_pk PRIMARY KEY (Id),
   117    CONSTRAINT Confirmations_User_fk FOREIGN KEY (User) REFERENCES Users (Id) ON DELETE CASCADE,
   118    CONSTRAINT Confirmations_UserType_unique UNIQUE (User, Type)
   119  
   120  ) ENGINE = InnoDB;
   121  
   122  DELIMITER //
   123  
   124  CREATE OR REPLACE PROCEDURE Confirmations_checker_before (
   125    Salt    int unsigned
   126  )
   127  BEGIN
   128    IF Salt >= 4194304 THEN
   129      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Salt must be 22 bits long';
   130    END IF;
   131  END;
   132  //
   133  
   134  CREATE OR REPLACE TRIGGER Confirmations_check_before_insert
   135    BEFORE INSERT ON Confirmations FOR EACH ROW
   136  BEGIN
   137    CALL Confirmations_checker_before(NEW.Salt);
   138  END;
   139  //
   140  
   141  CREATE OR REPLACE TRIGGER Confirmations_check_before_update
   142    BEFORE UPDATE ON Confirmations FOR EACH ROW
   143  BEGIN
   144    CALL Confirmations_checker_before(NEW.Salt);
   145  END;
   146  //
   147  
   148  DELIMITER ;
   149  
   150  
   151  ######## Polls ########
   152  
   153  # Internal type of polls.
   154  # Describes alternatives, outcomes and ballots.
   155  CREATE TABLE PollType (
   156    Id    tinyint unsigned NOT NULL,
   157    Label varchar(32) NOT NULL,
   158    CONSTRAINT PollType_pk PRIMARY KEY (Id),
   159    CONSTRAINT PollType_Label_unique UNIQUE (Label)
   160  ) ENGINE = InnoDB;
   161  
   162  INSERT INTO PollType VALUES
   163  # The outcome is a subset of the alternatives such that the sum of the cost of the alternatives in
   164  # the outcome is at most MaxOutcomeCost.
   165  # A ballot is a subset of the alternatives (all with rank 1) such that:
   166  #  - if BallotCostIsCount is true, the cadinality of the ballot is at most MaxBallotCost,
   167  #  - if BallotCostIsCount is false, the sum of the cost of the alternatives in the ballot is at
   168  #    most MaxBallotCost.
   169    (0, 'Acceptance Set')
   170  ;
   171  
   172  # How outcome is computed.
   173  # See Go implementation for detail of each method.
   174  CREATE TABLE PollRule (
   175    Id    tinyint unsigned NOT NULL,
   176    Label varchar(32) NOT NULL,
   177    CONSTRAINT PollRule_pk PRIMARY KEY (Id),
   178    CONSTRAINT PollRule_Label_unique UNIQUE (Label)
   179  ) ENGINE = InnoDB;
   180  
   181  INSERT INTO PollRule VALUES
   182    (0, 'Plurality')
   183  ;
   184  
   185  # How moves are made during each round.
   186  CREATE TABLE RoundType (
   187    Id    tinyint unsigned NOT NULL,
   188    Label varchar(32) NOT NULL,
   189    CONSTRAINT RoundType_pk PRIMARY KEY (Id),
   190    CONSTRAINT RoundType_Label_unique UNIQUE (Label)
   191  ) ENGINE = InnoDB;
   192  
   193  INSERT INTO RoundType VALUES
   194    (0, 'Freely Asynchronous')  # Participants can move at any time, any number of time.
   195  ;
   196  
   197  # Deletion of a poll is possible (cascade).
   198  CREATE TABLE Polls (
   199  
   200    Id                int unsigned      NOT NULL  AUTO_INCREMENT,
   201    Title             tinytext          NOT NULL,
   202    Description       text,
   203    Admin             int unsigned      NOT NULL,             # FK on Users
   204    Created           timestamp         NOT NULL  DEFAULT CURRENT_TIMESTAMP,
   205    State             ENUM('Waiting','Active','Terminated') NOT NULL DEFAULT 'Active',
   206    Start             datetime,
   207    ShortURL          varchar(32),
   208  
   209    # Salt is used to make discovery of poll difficult
   210    Salt              int unsigned      NOT NULL,
   211  
   212    Type              tinyint unsigned  NOT NULL  DEFAULT 0,  # FK on PollType
   213    Rule              tinyint unsigned  NOT NULL  DEFAULT 0,  # FK on PollRule
   214    RoundType         tinyint unsigned  NOT NULL  DEFAULT 0,  # FK on RoundType
   215  
   216    Electorate        ENUM('All','Logged','Verified') NOT NULL DEFAULT 'Logged',
   217    Hidden            bool              NOT NULL  DEFAULT FALSE,
   218  
   219    NbChoices         tinyint unsigned  NOT NULL,
   220    MaxOutcomeCost    decimal(65,6)     NOT NULL  DEFAULT 1,
   221    MaxBallotCost     decimal(65,6)     NOT NULL  DEFAULT 1,
   222    BallotCostIsCount bool              NOT NULL  DEFAULT TRUE,
   223  
   224    # Whether the last vote is used when a participant did not vote for the last round.
   225    ReportVote        bool              NOT NULL  DEFAULT FALSE,
   226  
   227    # The poll ends as soon as one of the following condition holds:
   228    #  - CurrentRound >= MaxNbRounds
   229    #  - Deadline <= CURRENT_TIMESTAMP() AND CurrentRound >= MinNbRounds
   230    MinNbRounds       tinyint unsigned  NOT NULL  DEFAULT 2,
   231    MaxNbRounds       tinyint unsigned            DEFAULT 10,
   232    Deadline          datetime,
   233  
   234    # The round ends as soon as one of the following conditions holds:
   235    #  - addtime(CurrentRoundStart, MaxRoundDuration) >= CURRENT_TIMESTAMP()
   236    #  - CurrentRound > 0 AND RoundThreshold = 0 AND one participant moved for this round
   237    #  - CurrentRound > 0 AND RoundThreshold > 0 AND the proportion of participants who moved for this round >= RoundThreshold
   238    MaxRoundDuration  time                        DEFAULT '24:00:00',
   239    RoundThreshold    double unsigned   NOT NULL  DEFAULT 1,
   240  
   241    CurrentRound      tinyint unsigned  NOT NULL  DEFAULT 0,
   242    CurrentRoundStart timestamp         NOT NULL  DEFAULT '2020-01-01',
   243  
   244    CONSTRAINT Polls_pk PRIMARY KEY (Id),
   245    
   246    CONSTRAINT Polls_Admin_fk FOREIGN KEY (Admin) REFERENCES Users (Id),
   247    CONSTRAINT Polls_Type_fk FOREIGN KEY (Type) REFERENCES PollType (Id),
   248    CONSTRAINT Polls_Rule_fk FOREIGN KEY (Rule) REFERENCES PollRule (Id),
   249    CONSTRAINT Polls_RoundType_fk FOREIGN KEY (RoundType) REFERENCES RoundType (Id),
   250  
   251    CONSTRAINT Polls_ShortURL_unique UNIQUE (ShortURL)
   252  
   253  ) ENGINE = InnoDB;
   254  
   255  DELIMITER //
   256  
   257  CREATE FUNCTION RoundDeadline(
   258    CurrentRoundStart datetime,
   259    MaxRoundDuration time,
   260    Deadline datetime,
   261    CurrentRound tinyint,
   262    MinNbRounds tinyint
   263  )
   264    RETURNS datetime DETERMINISTIC
   265  BEGIN
   266    DECLARE one datetime;
   267    SET one = ADDTIME(CurrentRoundStart, MaxRoundDuration);
   268  
   269    IF CurrentRound + 1 < MinNbRounds OR Deadline < one THEN
   270      RETURN one;
   271    END IF;
   272  
   273    IF ADDTIME(one, MaxRoundDuration) > Deadline THEN
   274      RETURN Deadline;
   275    END IF;
   276  
   277    RETURN one;
   278  END;
   279  //
   280  
   281  CREATE OR REPLACE PROCEDURE Polls_checker_before (
   282    Title             tinytext,
   283    State             ENUM('Waiting','Active','Terminated'),
   284    Start             datetime,
   285    Salt              int unsigned,
   286    ShortURL          varchar(32),
   287    NbChoices         tinyint unsigned,
   288    MinNbRounds       smallint unsigned,
   289    MaxNbRounds       smallint unsigned,
   290    Deadline          datetime,
   291    MaxRoundDuration  time,
   292    RoundThreshold    double unsigned ,
   293    CurrentRound      tinyint unsigned,
   294    CurrentRoundStart timestamp
   295  )
   296  BEGIN
   297    IF length(Title) < 3 THEN
   298      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Title field is too short';
   299    END IF;
   300    IF State = '' THEN
   301      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Unauthorized State value';
   302    END IF;
   303    IF Start IS NULL AND State = 'Waiting' THEN
   304      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'A Start date must be given for Waiting polls';
   305    END IF;
   306    IF Salt >= 4194304 THEN
   307      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Salt must be 22 bits long';
   308    END IF;
   309    IF ShortURL IS NOT NULL AND ShortURL NOT RLIKE '^[-_.~a-zA-Z0-9]+$' THEN
   310      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'ShortURL must be a valid URI segment';
   311    END IF;
   312    IF NbChoices < 2 THEN
   313      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'NbChoices must be at least 2';
   314    END IF;
   315    IF MaxNbRounds < MinNbRounds THEN
   316      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MaxNbRounds must be greater than MinNbRounds';
   317    END IF;
   318    IF MaxNbRounds IS NULL AND Deadline IS NULL THEN
   319      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'One amongst MaxNbRounds and Deadline must not be NULL';
   320    END IF;
   321    IF MaxRoundDuration < '00:01:00' THEN
   322      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'MaxRoundDuration must be at least one minute';
   323    END IF;
   324    IF RoundThreshold < 0 OR RoundThreshold > 1 THEN
   325      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'RoundThreshold must be in [0;1]';
   326    END IF;
   327    IF CurrentRound > MaxNbRounds THEN
   328      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'CurrentRound must be at most MaxNbRounds';
   329    END IF;
   330  END;
   331  //
   332  
   333  CREATE OR REPLACE TRIGGER Polls_check_before_insert
   334    BEFORE INSERT ON Polls FOR EACH ROW
   335  BEGIN
   336    IF NEW.State != 'Waiting' AND NEW.State != 'Active' THEN
   337      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'State must be Waiting or Active on INSERT';
   338    END IF;
   339    IF NEW.CurrentRound != 0 THEN
   340      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'CurrentRound must be zero on INSERT';
   341    END IF;
   342  
   343    SELECT Name
   344      INTO @Name
   345      FROM Users
   346     WHERE Id = NEW.Admin;
   347    IF @Name IS NULL THEN
   348      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Admin must correspond to a named user';
   349    END IF;
   350  
   351    SET NEW.Created           = CURRENT_TIMESTAMP();
   352    SET NEW.CurrentRoundStart = CURRENT_TIMESTAMP();
   353  
   354    CALL Polls_checker_before(NEW.Title, NEW.State, NEW.Start, NEW.Salt, NEW.ShortURL, NEW.NbChoices,
   355                              NEW.MinNbRounds, NEW.MaxNbRounds, NEW.Deadline, NEW.MaxRoundDuration,
   356                              NEW.RoundThreshold, NEW.CurrentRound, NEW.CurrentRoundStart);
   357  END;
   358  //
   359  
   360  CREATE OR REPLACE TRIGGER Polls_check_before_update
   361    BEFORE UPDATE ON Polls FOR EACH ROW
   362  BEGIN
   363    IF NEW.Created != OLD.Created THEN
   364      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Created cannot be changed';
   365    END IF;
   366    IF NEW.Admin != OLD.Admin THEN
   367      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Admin cannot be changed';
   368    END IF;
   369  
   370    IF NEW.CurrentRound > OLD.CurrentRound THEN
   371      SET NEW.CurrentRoundStart = CURRENT_TIMESTAMP();
   372    END IF;
   373    IF OLD.State = 'Waiting' AND NEW.State = 'Active' THEN
   374      SET NEW.CurrentRound = 0;
   375      SET NEW.CurrentRoundStart = CURRENT_TIMESTAMP();
   376    END IF;
   377    IF NEW.State = 'Terminated' THEN
   378      SET NEW.ShortURL = NULL;
   379    END IF;
   380  
   381    CALL Polls_checker_before(NEW.Title, NEW.State, NEW.Start, NEW.Salt, NEW.ShortURL, NEW.NbChoices,
   382                              NEW.MinNbRounds, NEW.MaxNbRounds, NEW.Deadline, NEW.MaxRoundDuration,
   383                              NEW.RoundThreshold, NEW.CurrentRound, NEW.CurrentRoundStart);
   384  END;
   385  //
   386  
   387  # Manual ON DELETE CASCADE because we want Ballots to be automatically deleted when the poll is
   388  # deleted, but we don't want to allow deletion of an alternatives when someone voted for it.
   389  CREATE TRIGGER Polls_cascade_delete_before
   390    BEFORE DELETE ON Polls FOR EACH ROW
   391  BEGIN
   392    DELETE FROM Participants wHERE Poll = OLD.id;
   393  END;
   394  //
   395  
   396  DELIMITER ;
   397  
   398  
   399  ######## Alternatives ########
   400  
   401  CREATE TABLE Alternatives (
   402  
   403    # Beware that the primary key is (Poll, Id), not Id alone.
   404    Poll    int unsigned      NOT NULL,   # FK on Polls
   405    Id      tinyint unsigned  NOT NULL,
   406    Name    varchar(128)      NOT NULL,
   407    Cost    decimal(65,6)     NOT NULL  DEFAULT 1,
   408  
   409    CONSTRAINT Alternatives_pk PRIMARY KEY (Poll, Id),
   410    CONSTRAINT Alternatives_PollName_unique UNIQUE (Poll, Name),
   411  
   412    CONSTRAINT Alternatives_Poll_fk FOREIGN KEY (Poll) REFERENCES Polls (Id) ON DELETE CASCADE
   413  
   414  ) ENGINE = InnoDB;
   415  
   416  DELIMITER //
   417  
   418  CREATE PROCEDURE Alternatives_checker_before (
   419    Poll    int unsigned    ,
   420    Id      tinyint unsigned,
   421    Name    varchar(128)    ,
   422    Cost    decimal(65,6)
   423  )
   424  BEGIN
   425  
   426    IF length(Name) < 1 THEN
   427      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Name cannot be empty';
   428    END IF;
   429  
   430    SELECT p.NbChoices, p.MaxOutcomeCost, p.MaxBallotCost, p.BallotCostIsCount
   431      INTO @NbChoices, @MaxBallotCost, @MaxBallotCost, @BallotCostIsCount
   432      FROM Polls AS p
   433     WHERE p.Id = Poll;
   434  
   435    IF Id >= @NbChoices THEN
   436      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Id must be less than NbChoices';
   437    END IF;
   438    IF Cost > @MaxOutcomeCost THEN
   439      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cost must be at most MaxOutcomeCost';
   440    END IF;
   441    IF NOT @BallotCostIsCount AND Cost > @MaxBallotCost THEN
   442      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Cost must be at most MaxBallotCost';
   443    END IF;
   444  
   445  END;
   446  //
   447  
   448  CREATE TRIGGER Alternatives_check_before_insert
   449    BEFORE INSERT ON Alternatives FOR EACH ROW
   450  BEGIN
   451    CALL Alternatives_checker_before(NEW.Poll, NEW.Id, NEW.Name, NEW.Cost);
   452  END;
   453  //
   454  
   455  CREATE TRIGGER Alternatives_check_before_update
   456    BEFORE UPDATE ON Alternatives FOR EACH ROW
   457  BEGIN
   458    CALL Alternatives_checker_before(NEW.Poll, NEW.Id, NEW.Name, NEW.Cost);
   459  END;
   460  //
   461  
   462  DELIMITER ;
   463  
   464  
   465  ######## Participants ########
   466  
   467  CREATE TABLE Participants (
   468  
   469    User      int unsigned      NOT NULL,
   470    Poll      int unsigned      NOT NULL,
   471    Round     tinyint unsigned  NOT NULL,
   472  
   473    CONSTRAINT Participants_pk PRIMARY KEY (User, Poll, Round),
   474  
   475    CONSTRAINT Participants_User_fk FOREIGN KEY (User) REFERENCES Users (Id),
   476    CONSTRAINT Participants_Poll_fk FOREIGN KEY (Poll) REFERENCES Polls (Id) ON DELETE CASCADE
   477  
   478  ) ENGINE = InnoDB;
   479  
   480  DELIMITER //
   481  
   482  CREATE PROCEDURE Participants_checker_before (
   483    Poll  int unsigned,
   484    Round tinyint unsigned
   485  )
   486  BEGIN
   487    SELECT p.CurrentRound
   488      INTO @CurrentRound
   489      FROM Polls AS p
   490     WHERE p.id = Poll;
   491  
   492    IF Round > @CurrentRound THEN
   493      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Round must be at most CurrentRound';
   494    END IF;
   495  END;
   496  //
   497  
   498  CREATE TRIGGER Participants_check_before_insert
   499    BEFORE INSERT ON Participants FOR EACH ROW
   500  BEGIN
   501    CALL Participants_checker_before(NEW.Poll, NEW.Round);
   502  END;
   503  //
   504  
   505  CREATE TRIGGER Participants_check_before_update
   506    BEFORE UPDATE ON Participants FOR EACH ROW
   507  BEGIN
   508    CALL Participants_checker_before(NEW.Poll, NEW.Round);
   509  END;
   510  //
   511  
   512  DELIMITER ;
   513  
   514  
   515  # MariaDB 5.5 does not allow subquery in view's queries (sic!).
   516  # Therefore we need an intermediate view...
   517  CREATE SQL SECURITY INVOKER VIEW Participants_workaround AS
   518    SELECT Poll
   519      FROM Participants
   520     GROUP By Poll, User;
   521  
   522  
   523  CREATE SQL SECURITY INVOKER VIEW Participants_Poll_Count AS
   524    SELECT Poll, COUNT(*) AS Count
   525      FROM Participants_workaround
   526     GROUP BY Poll;
   527  
   528  CREATE SQL SECURITY INVOKER VIEW Participants_Round_Count AS
   529    SELECT Poll, Round, COUNT(*) AS Count
   530      FROM Participants
   531     GROUP BY Poll, Round;
   532  
   533  
   534  
   535  ######## Ballots ########
   536  
   537  # Simple ranked ballot.
   538  # Other tables for ballots may be added in the future.
   539  CREATE TABLE Ballots (
   540  
   541    User        int unsigned      NOT NULL,
   542    Poll        int unsigned      NOT NULL,
   543    Alternative tinyint unsigned      NULL,
   544    Round       tinyint unsigned  NOT NULL,
   545    Rank        tinyint           NOT NULL  DEFAULT 1,
   546    Modified    timestamp         NOT NULL  DEFAULT CURRENT_TIMESTAMP,
   547  
   548    CONSTRAINT Ballots_pk PRIMARY KEY (User, Poll, Alternative, Round),
   549  
   550    CONSTRAINT Ballots_Participant_fk FOREIGN KEY (User, Poll, Round) REFERENCES Participants (User, Poll, Round) ON DELETE CASCADE,
   551    CONSTRAINT Ballots_Alternative_fk FOREIGN KEY (Poll, Alternative) REFERENCES Alternatives (Poll, Id)
   552  
   553  ) ENGINE = InnoDB;
   554  
   555  DELIMITER //
   556  
   557  CREATE TRIGGER Ballots_check_before_insert
   558    BEFORE INSERT ON Ballots FOR EACH ROW
   559  BEGIN
   560    SET NEW.Modified = CURRENT_TIMESTAMP();
   561  END;
   562  //
   563  
   564  CREATE TRIGGER Ballots_check_before_update
   565    BEFORE UPDATE ON Ballots FOR EACH ROW
   566  BEGIN
   567    SET NEW.Modified = CURRENT_TIMESTAMP();
   568  END;
   569  //
   570  
   571  DELIMITER ;