Add migrations and continue implementing election insertion
This commit is contained in:
37
internal/db/migrations.go
Normal file
37
internal/db/migrations.go
Normal file
@ -0,0 +1,37 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"errors"
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
msqlite3 "github.com/golang-migrate/migrate/v4/database/sqlite3"
|
||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsFS embed.FS
|
||||
|
||||
func RunMigrations(db *sql.DB) error {
|
||||
driver, err := msqlite3.WithInstance(db, &msqlite3.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d, err := iofs.New(migrationsFS, "migrations")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m, err := migrate.NewWithInstance("iofs", d, "sqlite", driver)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = m.Up(); err != nil && !errors.Is(migrate.ErrNoChange, err) {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
7
internal/db/migrations/000001_init.down.sql
Normal file
7
internal/db/migrations/000001_init.down.sql
Normal file
@ -0,0 +1,7 @@
|
||||
DROP TABLE IF EXISTS votes;
|
||||
DROP TABLE IF EXISTS voters;
|
||||
DROP TABLE IF EXISTS choices;
|
||||
DROP TABLE IF EXISTS elections;
|
||||
DROP TRIGGER IF EXISTS prevent_created_at_update_election;
|
||||
DROP TRIGGER IF EXISTS prevent_created_at_update_votes;
|
||||
DROP TRIGGER IF EXISTS enforce_max_voters;
|
74
internal/db/migrations/000001_init.up.sql
Normal file
74
internal/db/migrations/000001_init.up.sql
Normal file
@ -0,0 +1,74 @@
|
||||
CREATE TABLE elections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
tokens INTEGER NOT NULL,
|
||||
are_voters_known INTEGER NOT NULL,
|
||||
max_voters INTEGER, -- mandatory when voters are known
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
expires_at DATETIME NOT NULL,
|
||||
CHECK (are_voters_known = 0 OR (are_voters_known = 1 AND max_voters IS NOT NULL AND max_voters >= 1))
|
||||
);
|
||||
|
||||
CREATE TRIGGER prevent_created_at_update_election
|
||||
BEFORE UPDATE OF created_at ON elections
|
||||
BEGIN
|
||||
SELECT RAISE(FAIL, 'created_at column is read-only');
|
||||
END;
|
||||
|
||||
CREATE TABLE choices (
|
||||
text TEXT NOT NULL,
|
||||
election_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (text, election_id),
|
||||
FOREIGN KEY (election_id) REFERENCES elections (id)
|
||||
);
|
||||
|
||||
CREATE TABLE voters (
|
||||
identity TEXT NOT NULL, -- when voters are known, passcodes will be pre-generated
|
||||
election_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (identity, election_id),
|
||||
FOREIGN KEY (election_id) REFERENCES elections (id)
|
||||
);
|
||||
|
||||
CREATE TRIGGER enforce_max_voters
|
||||
BEFORE INSERT ON voters
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM elections e
|
||||
WHERE e.id = NEW.election_id
|
||||
AND e.max_voters IS NOT NULL
|
||||
)
|
||||
BEGIN
|
||||
SELECT CASE
|
||||
WHEN (
|
||||
SELECT COUNT(*)
|
||||
FROM voters v
|
||||
WHERE v.election_id = NEW.election_id
|
||||
) >= (
|
||||
SELECT max_voters
|
||||
FROM elections
|
||||
WHERE id = NEW.election_id
|
||||
)
|
||||
THEN RAISE(FAIL, 'Maximum number of voters reached for this election')
|
||||
END;
|
||||
END;
|
||||
|
||||
CREATE TABLE votes (
|
||||
voter_identity TEXT NOT NULL,
|
||||
election_id INTEGER NOT NULL,
|
||||
choice_text TEXT NOT NULL,
|
||||
tokens INTEGER NOT NULL,
|
||||
-- calculated_vote_count GENERATED ALWAYS AS (floor(sqrt(tokens))) VIRTUAL, TODO: Cannot use math functions
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (voter_identity, choice_text),
|
||||
FOREIGN KEY (voter_identity, election_id) REFERENCES voters (identity, election_id),
|
||||
FOREIGN KEY (choice_text, election_id) REFERENCES choices (text, election_id),
|
||||
CHECK (tokens IS NULL OR tokens >= 0)
|
||||
);
|
||||
|
||||
CREATE TRIGGER prevent_created_at_update_votes
|
||||
BEFORE UPDATE OF created_at ON votes
|
||||
BEGIN
|
||||
SELECT RAISE(FAIL, 'created_at column is read-only');
|
||||
END;
|
||||
|
||||
PRAGMA foreign_keys = ON; -- run after opening the connection so foreign key constraints are checked
|
@ -2,18 +2,56 @@ package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ElectionModelInterface interface {
|
||||
Insert(name string, tokens int, areVotersKnown bool, maxVoters int, Choices []string, ExpiresAt time.Time) (int, error)
|
||||
Insert(name string, tokens int, areVotersKnown bool, maxVoters *int, Choices []string, ExpiresAt time.Time) (int, error)
|
||||
}
|
||||
|
||||
type ElectionModel struct {
|
||||
DB *sql.DB
|
||||
}
|
||||
|
||||
func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters int, Choices []string, ExpiresAt time.Time) (int, error) {
|
||||
//TODO implement me
|
||||
panic("implement me")
|
||||
func (e *ElectionModel) Insert(name string, tokens int, areVotersKnown bool, maxVoters *int, choices []string, expiresAt time.Time) (int, error) {
|
||||
tx, err := e.DB.Begin()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
result, err := tx.Exec(`
|
||||
INSERT INTO elections (name, tokens, are_voters_known, max_voters, expires_at)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
name, tokens, areVotersKnown, maxVoters, expiresAt)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
electionID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
stmt, err := tx.Prepare(`
|
||||
INSERT INTO choices (text, election_id)
|
||||
VALUES (?, ?)`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, choice := range choices {
|
||||
_, err = stmt.Exec(choice, electionID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(electionID), nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user