package quotedb import ( "database/sql" "embed" "math/rand" "time" "github.com/pkg/errors" ) //go:embed schema/** var schema embed.FS func addQuote(channel, quote string) error { _, err := db.DB().Exec( `INSERT INTO quotedb (channel, created_at, quote) VALUES ($1, $2, $3);`, channel, time.Now().UnixNano(), quote, ) return errors.Wrap(err, "adding quote to database") } func delQuote(channel string, quote int) error { _, createdAt, _, err := getQuoteRaw(channel, quote) if err != nil { return errors.Wrap(err, "fetching specified quote") } _, err = db.DB().Exec( `DELETE FROM quotedb WHERE channel = $1 AND created_at = $2;`, channel, createdAt, ) return errors.Wrap(err, "deleting quote") } func getChannelQuotes(channel string) ([]string, error) { rows, err := db.DB().Query( `SELECT quote FROM quotedb WHERE channel = $1 ORDER BY created_at ASC`, channel, ) if err != nil { return nil, errors.Wrap(err, "querying quotes") } var quotes []string for rows.Next() { if err = rows.Err(); err != nil { return nil, errors.Wrap(err, "advancing row read") } var quote string if err = rows.Scan("e); err != nil { return nil, errors.Wrap(err, "scanning row") } quotes = append(quotes, quote) } return quotes, errors.Wrap(rows.Err(), "advancing row read") } func getMaxQuoteIdx(channel string) (int, error) { row := db.DB().QueryRow( `SELECT COUNT(1) as quoteCount FROM quotedb WHERE channel = $1;`, channel, ) var count int err := row.Scan(&count) return count, errors.Wrap(err, "getting quote count") } func getQuote(channel string, quote int) (int, string, error) { quoteIdx, _, quoteText, err := getQuoteRaw(channel, quote) return quoteIdx, quoteText, err } func getQuoteRaw(channel string, quote int) (int, int64, string, error) { if quote == 0 { max, err := getMaxQuoteIdx(channel) if err != nil { return 0, 0, "", errors.Wrap(err, "getting max quote idx") } quote = rand.Intn(max) + 1 // #nosec G404 // no need for cryptographic safety } row := db.DB().QueryRow( `SELECT created_at, quote FROM quotedb WHERE channel = $1 ORDER BY created_at ASC LIMIT 1 OFFSET $2`, channel, quote-1, ) var ( createdAt int64 quoteText string ) err := row.Scan(&createdAt, "eText) switch { case err == nil: return quote, createdAt, quoteText, nil case errors.Is(err, sql.ErrNoRows): return 0, 0, "", nil default: return 0, 0, "", errors.Wrap(err, "getting quote from DB") } } func setQuotes(channel string, quotes []string) error { tx, err := db.DB().Begin() if err != nil { return errors.Wrap(err, "creating transaction") } if _, err = tx.Exec( `DELETE FROM quotedb WHERE channel = $1;`, channel, ); err != nil { defer tx.Rollback() return errors.Wrap(err, "deleting quotes for channel") } t := time.Now() for _, quote := range quotes { if _, err = tx.Exec( `INSERT INTO quotedb (channel, created_at, quote) VALUES ($1, $2, $3);`, channel, t.UnixNano(), quote, ); err != nil { defer tx.Rollback() return errors.Wrap(err, "adding quote for channel") } t = t.Add(time.Nanosecond) // Increase by one ns to adhere to unique index } return errors.Wrap(tx.Commit(), "committing change") } func updateQuote(channel string, idx int, quote string) error { _, createdAt, _, err := getQuoteRaw(channel, idx) if err != nil { return errors.Wrap(err, "fetching specified quote") } _, err = db.DB().Exec( `UPDATE quotedb SET quote = $3 WHERE channel = $1 AND created_at = $2;`, channel, createdAt, quote, ) return errors.Wrap(err, "updating quote") }