// poll.go stores poll ballots. The poll DEFINITION lives in the forum
// post's body (a ```poll fenced block — a fossil peer renders readable
// markdown, which is the whole compatibility story); only the ballots
// are forge state, with the same idempotent shape as forum votes: the
// (repo, account, post) key IS the one-ballot rule.
package oltp
import "time"
const pollSchema = `
CREATE TABLE IF NOT EXISTS poll_vote (
repo TEXT NOT NULL,
account TEXT NOT NULL,
post TEXT NOT NULL, -- the forum post carrying the poll block
choice INTEGER NOT NULL, -- option index in the block, 0-based
at TEXT NOT NULL,
PRIMARY KEY (repo, account, post)
);
CREATE INDEX IF NOT EXISTS poll_vote_by_post ON poll_vote (repo, post);`
// SetPollVote records (replacing) one account's ballot; changeable
// until the poll closes, which the API enforces from the parsed block.
func (d *DB) SetPollVote(repo, account, post string, choice int) error {
_, err := d.w.Exec(`INSERT INTO poll_vote (repo, account, post, choice, at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (repo, account, post) DO UPDATE SET choice = excluded.choice, at = excluded.at`,
repo, account, post, choice, time.Now().UTC().Format(time.RFC3339))
return err
}
// PollCounts returns ballots per option index for one poll post.
func (d *DB) PollCounts(repo, post string) (map[int]int, error) {
rows, err := d.db.Query(`SELECT choice, count(*) FROM poll_vote
WHERE repo = ? AND post = ? GROUP BY choice`, repo, post)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[int]int{}
for rows.Next() {
var c, n int
if err := rows.Scan(&c, &n); err != nil {
return nil, err
}
out[c] = n
}
return out, rows.Err()
}
// PollChoice returns one account's ballot; ok is false when they have
// not voted (or are anonymous).
func (d *DB) PollChoice(repo, account, post string) (choice int, ok bool) {
if account == "" {
return 0, false
}
err := d.db.QueryRow(`SELECT choice FROM poll_vote
WHERE repo = ? AND account = ? AND post = ?`, repo, account, post).Scan(&choice)
return choice, err == nil
}