package common import ( "database/sql" "encoding/json" "errors" "log" "strconv" "../query_gen/lib" ) var Polls PollStore type Poll struct { ID int ParentID int ParentTable string Type int // 0: Single choice, 1: Multiple choice, 2: Multiple choice w/ points //AntiCheat bool // Apply various mitigations for cheating // GroupPower map[gid]points // The number of points a group can spend in this poll, defaults to 1 Options map[int]string Results map[int]int // map[optionIndex]points QuickOptions []PollOption // TODO: Fix up the template transpiler so we don't need to use this hack anymore VoteCount int } func (poll *Poll) CastVote(optionIndex int, uid int, ipaddress string) error { return Polls.CastVote(optionIndex, poll.ID, uid, ipaddress) // TODO: Move the query into a pollStmts rather than having it in the store } func (poll *Poll) Copy() Poll { return *poll } type PollOption struct { ID int Value string } type Pollable interface { GetID() int GetTable() string SetPoll(pollID int) error } type PollStore interface { Get(id int) (*Poll, error) Exists(id int) bool Create(parent Pollable, pollType int, pollOptions map[int]string) (int, error) CastVote(optionIndex int, pollID int, uid int, ipaddress string) error Reload(id int) error //GlobalCount() int SetCache(cache PollCache) GetCache() PollCache } type DefaultPollStore struct { cache PollCache get *sql.Stmt exists *sql.Stmt createPoll *sql.Stmt createPollOption *sql.Stmt addVote *sql.Stmt incrementVoteCount *sql.Stmt incrementVoteCountForOption *sql.Stmt delete *sql.Stmt //pollCount *sql.Stmt } func NewDefaultPollStore(cache PollCache) (*DefaultPollStore, error) { acc := qgen.Builder.Accumulator() if cache == nil { cache = NewNullPollCache() } // TODO: Add an admin version of registerStmt with more flexibility? return &DefaultPollStore{ cache: cache, get: acc.Select("polls").Columns("parentID, parentTable, type, options, votes").Where("pollID = ?").Prepare(), exists: acc.Select("polls").Columns("pollID").Where("pollID = ?").Prepare(), createPoll: acc.Insert("polls").Columns("parentID, parentTable, type, options").Fields("?,?,?,?").Prepare(), createPollOption: acc.Insert("polls_options").Columns("pollID, option, votes").Fields("?,?,0").Prepare(), addVote: acc.Insert("polls_votes").Columns("pollID, uid, option, castAt, ipaddress").Fields("?,?,?,UTC_TIMESTAMP(),?").Prepare(), incrementVoteCount: acc.Update("polls").Set("votes = votes + 1").Where("pollID = ?").Prepare(), incrementVoteCountForOption: acc.Update("polls_options").Set("votes = votes + 1").Where("option = ? AND pollID = ?").Prepare(), //pollCount: acc.SimpleCount("polls", "", ""), }, acc.FirstError() } func (store *DefaultPollStore) Exists(id int) bool { err := store.exists.QueryRow(id).Scan(&id) if err != nil && err != ErrNoRows { LogError(err) } return err != ErrNoRows } func (store *DefaultPollStore) Get(id int) (*Poll, error) { poll, err := store.cache.Get(id) if err == nil { return poll, nil } poll = &Poll{ID: id} var optionTxt []byte err = store.get.QueryRow(id).Scan(&poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount) if err != nil { return nil, err } err = json.Unmarshal(optionTxt, &poll.Options) if err == nil { poll.QuickOptions = store.unpackOptionsMap(poll.Options) store.cache.Set(poll) } return poll, err } // TODO: Optimise the query to avoid preparing it on the spot? Maybe, use knowledge of the most common IN() parameter counts? // TODO: ID of 0 should always error? func (store *DefaultPollStore) BulkGetMap(ids []int) (list map[int]*Poll, err error) { var idCount = len(ids) list = make(map[int]*Poll) if idCount == 0 { return list, nil } var stillHere []int sliceList := store.cache.BulkGet(ids) for i, sliceItem := range sliceList { if sliceItem != nil { list[sliceItem.ID] = sliceItem } else { stillHere = append(stillHere, ids[i]) } } ids = stillHere // If every user is in the cache, then return immediately if len(ids) == 0 { return list, nil } // TODO: Add a function for the qlist stuff var qlist string var pollIDList []interface{} for _, id := range ids { pollIDList = append(pollIDList, strconv.Itoa(id)) qlist += "?," } qlist = qlist[0 : len(qlist)-1] acc := qgen.Builder.Accumulator() rows, err := acc.Select("polls").Columns("pollID, parentID, parentTable, type, options, votes").Where("pollID IN(" + qlist + ")").Query(pollIDList...) if err != nil { return list, err } for rows.Next() { poll := &Poll{ID: 0} var optionTxt []byte err := rows.Scan(&poll.ID, &poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount) if err != nil { return list, err } err = json.Unmarshal(optionTxt, &poll.Options) if err != nil { return list, err } poll.QuickOptions = store.unpackOptionsMap(poll.Options) store.cache.Set(poll) list[poll.ID] = poll } // Did we miss any polls? if idCount > len(list) { var sidList string for _, id := range ids { _, ok := list[id] if !ok { sidList += strconv.Itoa(id) + "," } } // We probably don't need this, but it might be useful in case of bugs in BulkCascadeGetMap if sidList == "" { // TODO: Bulk log this if Dev.DebugMode { log.Print("This data is sampled later in the BulkCascadeGetMap function, so it might miss the cached IDs") log.Print("idCount", idCount) log.Print("ids", ids) log.Print("list", list) } return list, errors.New("We weren't able to find a poll, but we don't know which one") } sidList = sidList[0 : len(sidList)-1] err = errors.New("Unable to find the polls with the following IDs: " + sidList) } return list, err } func (store *DefaultPollStore) Reload(id int) error { poll := &Poll{ID: id} var optionTxt []byte err := store.get.QueryRow(id).Scan(&poll.ParentID, &poll.ParentTable, &poll.Type, &optionTxt, &poll.VoteCount) if err != nil { store.cache.Remove(id) return err } err = json.Unmarshal(optionTxt, &poll.Options) if err != nil { store.cache.Remove(id) return err } poll.QuickOptions = store.unpackOptionsMap(poll.Options) _ = store.cache.Set(poll) return nil } func (store *DefaultPollStore) unpackOptionsMap(rawOptions map[int]string) []PollOption { options := make([]PollOption, len(rawOptions)) for id, option := range rawOptions { options[id] = PollOption{id, option} } return options } // TODO: Use a transaction for this? func (store *DefaultPollStore) CastVote(optionIndex int, pollID int, uid int, ipaddress string) error { _, err := store.addVote.Exec(pollID, uid, optionIndex, ipaddress) if err != nil { return err } _, err = store.incrementVoteCount.Exec(pollID) if err != nil { return err } _, err = store.incrementVoteCountForOption.Exec(optionIndex, pollID) return err } // TODO: Use a transaction for this func (store *DefaultPollStore) Create(parent Pollable, pollType int, pollOptions map[int]string) (id int, err error) { pollOptionsTxt, err := json.Marshal(pollOptions) if err != nil { return 0, err } res, err := store.createPoll.Exec(parent.GetID(), parent.GetTable(), pollType, pollOptionsTxt) if err != nil { return 0, err } lastID, err := res.LastInsertId() if err != nil { return 0, err } for i := 0; i < len(pollOptions); i++ { _, err := store.createPollOption.Exec(lastID, i) if err != nil { return 0, err } } return int(lastID), parent.SetPoll(int(lastID)) // TODO: Delete the poll (and options) if SetPoll fails } func (store *DefaultPollStore) SetCache(cache PollCache) { store.cache = cache } // TODO: We're temporarily doing this so that you can do ucache != nil in getTopicUser. Refactor it. func (store *DefaultPollStore) GetCache() PollCache { _, ok := store.cache.(*NullPollCache) if ok { return nil } return store.cache }