apps/quotes/db.go 3.9 K raw
1
package main
2
3
import (
4
	"database/sql"
5
	"errors"
6
	"strings"
7
	"time"
8
9
	"github.com/stevedylandev/andromeda/pkg/auth"
10
)
11
12
const quotesSchema = `
13
CREATE TABLE IF NOT EXISTS quotes (
14
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
15
    short_id   TEXT NOT NULL UNIQUE,
16
    text       TEXT NOT NULL,
17
    author     TEXT NOT NULL,
18
    source     TEXT,
19
    added_at   INTEGER NOT NULL,
20
    updated_at INTEGER NOT NULL
21
);
22
CREATE INDEX IF NOT EXISTS idx_quotes_added ON quotes(added_at DESC);
23
`
24
25
type Quote struct {
26
	ID        int64   `json:"id"`
27
	ShortID   string  `json:"short_id"`
28
	Text      string  `json:"text"`
29
	Author    string  `json:"author"`
30
	Source    *string `json:"source,omitempty"`
31
	AddedAt   int64   `json:"added_at"`
32
	UpdatedAt int64   `json:"updated_at"`
33
}
34
35
const selectCols = `id, short_id, text, author, source, added_at, updated_at`
36
37
func scanQuote(s interface{ Scan(...any) error }) (*Quote, error) {
38
	var q Quote
39
	var source sql.NullString
40
	err := s.Scan(&q.ID, &q.ShortID, &q.Text, &q.Author, &source, &q.AddedAt, &q.UpdatedAt)
41
	if errors.Is(err, sql.ErrNoRows) {
42
		return nil, nil
43
	}
44
	if err != nil {
45
		return nil, err
46
	}
47
	if source.Valid {
48
		v := source.String
49
		q.Source = &v
50
	}
51
	return &q, nil
52
}
53
54
func countQuotes(db *sql.DB) (int, error) {
55
	var n int
56
	err := db.QueryRow(`SELECT COUNT(*) FROM quotes`).Scan(&n)
57
	return n, err
58
}
59
60
func listQuotes(db *sql.DB, limit int) ([]Quote, error) {
61
	rows, err := db.Query(`SELECT `+selectCols+` FROM quotes ORDER BY added_at DESC, id DESC LIMIT ?`, limit)
62
	if err != nil {
63
		return nil, err
64
	}
65
	defer rows.Close()
66
	var out []Quote
67
	for rows.Next() {
68
		q, err := scanQuote(rows)
69
		if err != nil {
70
			return nil, err
71
		}
72
		out = append(out, *q)
73
	}
74
	return out, rows.Err()
75
}
76
77
func getQuoteByShortID(db *sql.DB, shortID string) (*Quote, error) {
78
	return scanQuote(db.QueryRow(`SELECT `+selectCols+` FROM quotes WHERE short_id = ?`, shortID))
79
}
80
81
// quoteOfTheDay returns a deterministic quote that is stable for the whole UTC
82
// day and rotates at midnight. Returns (nil, nil) when the table is empty.
83
func quoteOfTheDay(db *sql.DB) (*Quote, error) {
84
	n, err := countQuotes(db)
85
	if err != nil {
86
		return nil, err
87
	}
88
	if n == 0 {
89
		return nil, nil
90
	}
91
	offset := int(time.Now().UTC().Unix()/86400) % n
92
	return scanQuote(db.QueryRow(`SELECT `+selectCols+` FROM quotes ORDER BY id LIMIT 1 OFFSET ?`, offset))
93
}
94
95
// randomQuote returns a uniformly random quote. Returns (nil, nil) when the
96
// table is empty.
97
func randomQuote(db *sql.DB) (*Quote, error) {
98
	return scanQuote(db.QueryRow(`SELECT ` + selectCols + ` FROM quotes ORDER BY RANDOM() LIMIT 1`))
99
}
100
101
func searchQuotes(db *sql.DB, q string) ([]Quote, error) {
102
	term := strings.TrimSpace(q)
103
	if term == "" {
104
		return nil, nil
105
	}
106
	pattern := "%" + strings.ToLower(term) + "%"
107
	rows, err := db.Query(
108
		`SELECT `+selectCols+` FROM quotes
109
		 WHERE LOWER(text) LIKE ? OR LOWER(author) LIKE ? OR LOWER(IFNULL(source,'')) LIKE ?
110
		 ORDER BY added_at DESC, id DESC LIMIT 50`,
111
		pattern, pattern, pattern,
112
	)
113
	if err != nil {
114
		return nil, err
115
	}
116
	defer rows.Close()
117
	var out []Quote
118
	for rows.Next() {
119
		q, err := scanQuote(rows)
120
		if err != nil {
121
			return nil, err
122
		}
123
		out = append(out, *q)
124
	}
125
	return out, rows.Err()
126
}
127
128
// insertQuote inserts a quote with a freshly generated short id. source may be
129
// empty, in which case it is stored as NULL.
130
func insertQuote(db *sql.DB, text, author, source string) (int64, error) {
131
	shortID, err := auth.GenerateShortID(10)
132
	if err != nil {
133
		return 0, err
134
	}
135
	now := time.Now().UTC().Unix()
136
	var src any
137
	if strings.TrimSpace(source) != "" {
138
		src = source
139
	}
140
	res, err := db.Exec(
141
		`INSERT INTO quotes (short_id, text, author, source, added_at, updated_at)
142
		 VALUES (?, ?, ?, ?, ?, ?)`,
143
		shortID, text, author, src, now, now,
144
	)
145
	if err != nil {
146
		return 0, err
147
	}
148
	return res.LastInsertId()
149
}
150
151
func deleteQuoteByShortID(db *sql.DB, shortID string) error {
152
	_, err := db.Exec(`DELETE FROM quotes WHERE short_id = ?`, shortID)
153
	return err
154
}