Commit de92f37

Antonio Mika  ·  2026-10-25 14:56:38 -0400 EDT
parent 0ff8a38
perf(db): add missing indexes, drop unused ones, trim queries
15 files changed,  +189, -227
+5, -1
......@@ -205,10 +205,14 @@ migrate:
205205 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20260716_block_signups.sql
206206 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20260925_add_invites.sql
207207 $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20261010_add_pastes_invite_feature.sql
208+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20261025_add_access_logs_user_created_idx.sql
209+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20261025_add_is_mobile_to_analytics.sql
210+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20261025_add_lookup_indexes.sql
211+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20261025_drop_unused_analytics_indexes.sql
208212 .PHONY: migrate
209213
210214 latest:
211- $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20261010_add_pastes_invite_feature.sql
215+ $(DOCKER_CMD) exec -i $(DB_CONTAINER) psql -U $(PGUSER) -d $(PGDATABASE) < ./sql/migrations/20261025_drop_unused_analytics_indexes.sql
212216 .PHONY: latest
213217
214218 psql:
+2, -23
......@@ -4,9 +4,9 @@ import (
44 "context"
55 "fmt"
66 "log/slog"
7- "strings"
87 "time"
98
9+ "github.com/picosh/pico/pkg/db"
1010 "github.com/picosh/pico/pkg/db/postgres"
1111 )
1212
......@@ -224,7 +224,7 @@ func aggregateDays(visits []visitRow) map[string]*dayStats {
224224 }
225225
226226 ds.AllIPs[v.IPAddress] = true
227- if isMobile(v.UserAgent) {
227+ if db.IsMobileUserAgent(v.UserAgent) {
228228 ds.MobileIPs[v.IPAddress] = true
229229 } else {
230230 ds.DesktopIPs[v.IPAddress] = true
......@@ -387,27 +387,6 @@ func upsertUserSite(dbpool *postgres.PsqlDB, userID, host string, monthUnique in
387387 return err
388388 }
389389
390-// isMobile checks common mobile user-agent patterns.
391-// TODO: replace with a proper UA parser library (e.g., mssola/useragent).
392-func isMobile(userAgent string) bool {
393- ua := strings.ToLower(userAgent)
394- mobileKeywords := []string{
395- "mobile",
396- "android",
397- "iphone",
398- "ipad",
399- "ipod",
400- "blackberry",
401- "windows phone",
402- }
403- for _, kw := range mobileKeywords {
404- if strings.Contains(ua, kw) {
405- return true
406- }
407- }
408- return false
409-}
410-
411390 // deleteAggregatedVisits deletes raw visit data from analytics_visits for the given month.
412391 // Data is preserved in summary tables, so this is safe regardless of feature flags.
413392 // Raw data for the current and previous months is never deleted since visitUniqueFromRaw
+7, -3
......@@ -252,14 +252,18 @@ func (me *PgsPsqlDB) FindProjectsByUser(userID string) ([]*db.Project, error) {
252252 }
253253
254254 func (me *PgsPsqlDB) FindProjects(by string) ([]*db.Project, error) {
255+ // A bind parameter in ORDER BY sorts by a constant, so map to a column.
256+ orderBy := "p.created_at"
257+ if by == "updated_at" {
258+ orderBy = "p.updated_at"
259+ }
255260 projects := []*db.Project{}
256261 err := me.Db.Select(
257262 &projects,
258- `SELECT p.id, p.user_id, u.name as username, p.name, p.project_dir, p.acl, p.blocked, p.created_at, p.updated_at
263+ fmt.Sprintf(`SELECT p.id, p.user_id, u.name as username, p.name, p.project_dir, p.acl, p.blocked, p.created_at, p.updated_at
259264 FROM projects AS p
260265 LEFT JOIN app_users AS u ON u.id = p.user_id
261- ORDER BY $1 DESC`,
262- by,
266+ ORDER BY %s DESC`, orderBy),
263267 )
264268 return projects, err
265269 }
+63, -0
......@@ -0,0 +1,63 @@
1+package pgsdb
2+
3+import (
4+ "log/slog"
5+ "testing"
6+ "time"
7+
8+ "github.com/jmoiron/sqlx"
9+ "github.com/picosh/pico/pkg/db/postgres/pgtest"
10+)
11+
12+func TestMain(m *testing.M) {
13+ pgtest.Main(m)
14+}
15+
16+func TestFindProjectsOrder(t *testing.T) {
17+ conn := sqlx.MustConnect("postgres", pgtest.URL(t))
18+ t.Cleanup(func() { _ = conn.Close() })
19+ pgtest.Clean(t, conn)
20+ testDB := NewDBWithConn(conn, slog.New(slog.DiscardHandler))
21+
22+ var userID string
23+ if err := conn.Get(&userID, `INSERT INTO app_users (name) VALUES ('projectsowner') RETURNING id`); err != nil {
24+ t.Fatalf("insert user: %v", err)
25+ }
26+
27+ // "older" was created first but updated last, so the two orderings differ.
28+ now := time.Now()
29+ for _, p := range []struct {
30+ name string
31+ createdAt time.Time
32+ updatedAt time.Time
33+ }{
34+ {"older", now.Add(-2 * time.Hour), now},
35+ {"newer", now.Add(-1 * time.Hour), now.Add(-1 * time.Hour)},
36+ } {
37+ id, err := testDB.InsertProject(userID, p.name, p.name)
38+ if err != nil {
39+ t.Fatalf("insert project %s: %v", p.name, err)
40+ }
41+ _, err = conn.Exec(`UPDATE projects SET created_at = $1, updated_at = $2 WHERE id = $3`, p.createdAt, p.updatedAt, id)
42+ if err != nil {
43+ t.Fatalf("set project %s times: %v", p.name, err)
44+ }
45+ }
46+
47+ for by, want := range map[string][]string{
48+ "created_at": {"newer", "older"},
49+ "updated_at": {"older", "newer"},
50+ } {
51+ projects, err := testDB.FindProjects(by)
52+ if err != nil {
53+ t.Fatalf("FindProjects(%s) failed: %v", by, err)
54+ }
55+ var got []string
56+ for _, p := range projects {
57+ got = append(got, p.Name)
58+ }
59+ if len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
60+ t.Errorf("FindProjects(%s) = %v, want %v", by, got, want)
61+ }
62+ }
63+}
+1, -132
......@@ -2,12 +2,10 @@ package prose
22
33 import (
44 "fmt"
5- "math"
65 "sync"
76 "time"
87
98 "github.com/hashicorp/golang-lru/v2/expirable"
10- "github.com/jmoiron/sqlx"
119 "github.com/picosh/pico/pkg/db"
1210 "github.com/picosh/pico/pkg/db/postgres"
1311 )
......@@ -200,134 +198,5 @@ func (me *PsqlDB) FindUserPostsByTag(page *db.Pager, tag, userID, space string)
200198 return nil, err
201199 }
202200
203- var count int
204- err = me.Db.QueryRow(`SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`, space).Scan(&count)
205- if err != nil {
206- return nil, err
207- }
208-
209- pager := &db.Paginate[*db.Post]{
210- Data: posts,
211- Total: int(math.Ceil(float64(count) / float64(page.Num))),
212- }
213- return pager, nil
214-}
215-
216-func (me *PsqlDB) FindPostsByTag(pager *db.Pager, tag, space string) (*db.Paginate[*db.Post], error) {
217- query := `
218- SELECT
219- posts.id,
220- user_id,
221- filename,
222- slug,
223- title,
224- text,
225- description,
226- publish_at,
227- app_users.name as username,
228- posts.updated_at,
229- posts.mime_type
230- FROM posts
231- LEFT JOIN app_users ON app_users.id = posts.user_id
232- LEFT JOIN post_tags ON post_tags.post_id = posts.id
233- WHERE
234- post_tags.name = $3 AND
235- publish_at::date <= CURRENT_DATE AND
236- cur_space = $4
237- ORDER BY publish_at DESC
238- LIMIT $1 OFFSET $2`
239- rs, err := me.Db.Queryx(
240- query,
241- pager.Num,
242- pager.Num*pager.Page,
243- tag,
244- space,
245- )
246- if err != nil {
247- return nil, err
248- }
249- defer func() { _ = rs.Close() }()
250-
251- return me.postPager(rs, pager.Num, space, tag)
252-}
253-
254-func (me *PsqlDB) postPager(rs *sqlx.Rows, pageNum int, space string, tag string) (*db.Paginate[*db.Post], error) {
255- var posts []*db.Post
256- for rs.Next() {
257- post := &db.Post{}
258- err := rs.Scan(
259- &post.ID,
260- &post.UserID,
261- &post.Filename,
262- &post.Slug,
263- &post.Title,
264- &post.Text,
265- &post.Description,
266- &post.PublishAt,
267- &post.Username,
268- &post.UpdatedAt,
269- &post.MimeType,
270- )
271- if err != nil {
272- return nil, err
273- }
274-
275- posts = append(posts, post)
276- }
277- if rs.Err() != nil {
278- return nil, rs.Err()
279- }
280-
281- var count int
282- var err error
283- if tag == "" {
284- err = me.Db.QueryRow(`SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`, space).Scan(&count)
285- } else {
286- err = me.Db.QueryRow(`
287- SELECT count(posts.id)
288- FROM posts
289- LEFT JOIN post_tags ON post_tags.post_id = posts.id
290- WHERE hidden = FALSE AND cur_space=$1 and post_tags.name = $2`, space, tag).Scan(&count)
291- }
292- if err != nil {
293- return nil, err
294- }
295-
296- pager := &db.Paginate[*db.Post]{
297- Data: posts,
298- Total: int(math.Ceil(float64(count) / float64(pageNum))),
299- }
300-
301- return pager, nil
302-}
303-
304-func (me *PsqlDB) FindPopularTags(space string) ([]string, error) {
305- tags := make([]string, 0)
306- query := `
307- SELECT name, count(post_id) as "tally"
308- FROM post_tags
309- LEFT JOIN posts ON posts.id = post_id
310- WHERE posts.cur_space = $1
311- GROUP BY name
312- ORDER BY tally DESC
313- LIMIT 5`
314- rs, err := me.Db.Queryx(query, space)
315- if err != nil {
316- return tags, err
317- }
318- defer func() { _ = rs.Close() }()
319- for rs.Next() {
320- name := ""
321- tally := 0
322- err := rs.Scan(&name, &tally)
323- if err != nil {
324- return tags, err
325- }
326-
327- tags = append(tags, name)
328- }
329- if rs.Err() != nil {
330- return tags, rs.Err()
331- }
332- return tags, nil
201+ return &db.Paginate[*db.Post]{Data: posts}, nil
333202 }
+0, -36
......@@ -143,40 +143,4 @@ func TestFindUserPostsByTag(t *testing.T) {
143143 }
144144 }
145145
146-func TestFindPostsByTag(t *testing.T) {
147- cleanupTestData(t)
148-
149- user, _ := testDB.RegisterUser("tagsearchowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tagsearchowner", "comment", "")
150- now := time.Now()
151- post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "tagsearch.md", Slug: "tagsearch", Title: "Tag Search", Space: "prose", PublishAt: &now})
152- _ = testDB.ReplaceTagsByPost([]string{"searchtag"}, post.ID)
153-
154- pager := &db.Pager{Num: 10, Page: 0}
155- result, err := testDB.FindPostsByTag(pager, "searchtag", "prose")
156- if err != nil {
157- t.Fatalf("FindPostsByTag failed: %v", err)
158- }
159- if len(result.Data) != 1 {
160- t.Errorf("expected 1 post with tag, got %d", len(result.Data))
161- }
162-}
163-
164-func TestFindPopularTags(t *testing.T) {
165- cleanupTestData(t)
166-
167- user, _ := testDB.RegisterUser("populartagowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI populartagowner", "comment", "")
168- post1 := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "pop1.md", Slug: "pop1", Title: "Pop 1", Space: "prose"})
169- post2 := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "pop2.md", Slug: "pop2", Title: "Pop 2", Space: "prose"})
170- _ = testDB.ReplaceTagsByPost([]string{"popular"}, post1.ID)
171- _ = testDB.ReplaceTagsByPost([]string{"popular"}, post2.ID)
172-
173- tags, err := testDB.FindPopularTags("prose")
174- if err != nil {
175- t.Fatalf("FindPopularTags failed: %v", err)
176- }
177- if len(tags) < 1 {
178- t.Errorf("expected at least 1 popular tag, got %d", len(tags))
179- }
180-}
181-
182146 // ============ Aliases Tests ============
+22, -2
......@@ -164,8 +164,7 @@ type Post struct {
164164 }
165165
166166 type Paginate[T any] struct {
167- Data []T
168- Total int
167+ Data []T
169168 }
170169
171170 type VisitInterval struct {
......@@ -218,6 +217,27 @@ type AnalyticsVisits struct {
218217 ContentType string `json:"content_type" db:"content_type"`
219218 }
220219
220+// IsMobileUserAgent checks common mobile user-agent patterns.
221+// TODO: replace with a proper UA parser library (e.g., mssola/useragent).
222+func IsMobileUserAgent(userAgent string) bool {
223+ ua := strings.ToLower(userAgent)
224+ mobileKeywords := []string{
225+ "mobile",
226+ "android",
227+ "iphone",
228+ "ipad",
229+ "ipod",
230+ "blackberry",
231+ "windows phone",
232+ }
233+ for _, kw := range mobileKeywords {
234+ if strings.Contains(ua, kw) {
235+ return true
236+ }
237+ }
238+ return false
239+}
240+
221241 type AccessLogData struct{}
222242
223243 func (p *AccessLogData) Scan(value any) error {
+8, -10
......@@ -9,12 +9,9 @@ import (
99 "github.com/picosh/pico/pkg/db"
1010 )
1111
12-// mobileUserAgentExpr is a SQL expression to detect mobile user agents.
13-const mobileUserAgentExpr = `(user_agent ILIKE '%mobile%' OR user_agent ILIKE '%android%' OR user_agent ILIKE '%iphone%' OR user_agent ILIKE '%ipad%' OR user_agent ILIKE '%ipod%' OR user_agent ILIKE '%blackberry%' OR user_agent ILIKE '%windows phone%')`
14-
1512 func (me *PsqlDB) InsertVisit(visit *db.AnalyticsVisits) error {
1613 _, err := me.Db.Exec(
17- `INSERT INTO analytics_visits (user_id, project_id, post_id, namespace, host, path, ip_address, user_agent, referer, status, content_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11);`,
14+ `INSERT INTO analytics_visits (user_id, project_id, post_id, namespace, host, path, ip_address, user_agent, referer, status, content_type, is_mobile) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);`,
1815 visit.UserID,
1916 newNullString(visit.ProjectID),
2017 newNullString(visit.PostID),
......@@ -26,6 +23,7 @@ func (me *PsqlDB) InsertVisit(visit *db.AnalyticsVisits) error {
2623 visit.Referer,
2724 visit.Status,
2825 visit.ContentType,
26+ db.IsMobileUserAgent(visit.UserAgent),
2927 )
3028 return err
3129 }
......@@ -138,14 +136,14 @@ func (me *PsqlDB) visitUniqueFromRaw(opts *db.SummaryOpts) ([]*db.VisitInterval,
138136 uniqueVisitors := fmt.Sprintf(`
139137 SELECT
140138 date_trunc('%s', created_at)::timestamptz as interval_start,
141- count(DISTINCT CASE WHEN %s THEN ip_address END) as mobile_visitors,
142- count(DISTINCT CASE WHEN NOT %s THEN ip_address END) as desktop_visitors,
139+ count(DISTINCT CASE WHEN is_mobile THEN ip_address END) as mobile_visitors,
140+ count(DISTINCT CASE WHEN NOT is_mobile THEN ip_address END) as desktop_visitors,
143141 count(DISTINCT ip_address) as unique_visitors
144142 FROM analytics_visits
145143 WHERE created_at >= $1 AND created_at < $2 AND %s = $3 AND user_id = $4 AND status <> 404
146144 GROUP BY interval_start
147145 ORDER BY interval_start`,
148- opts.Interval, mobileUserAgentExpr, mobileUserAgentExpr, where)
146+ opts.Interval, where)
149147
150148 rows, err := me.Db.Queryx(uniqueVisitors, effectiveStart, currentMonthStart.AddDate(0, 1, 0), with, opts.UserID)
151149 if err != nil {
......@@ -320,7 +318,7 @@ func (me *PsqlDB) visitRefererFromSummary(opts *db.SummaryOpts) ([]*db.VisitUrl,
320318 originMonthStart := time.Date(opts.Origin.Year(), opts.Origin.Month(), 1, 0, 0, 0, 0, time.UTC)
321319
322320 where := ""
323- args := []interface{}{opts.UserID, originMonthStart, currentMonthStart}
321+ args := []interface{}{opts.UserID, originMonthStart, previousMonthStart}
324322 if opts.Host != "" {
325323 where = "AND host = $4"
326324 args = append(args, opts.Host)
......@@ -435,7 +433,7 @@ func (me *PsqlDB) visitUrlFromSummary(opts *db.SummaryOpts) ([]*db.VisitUrl, err
435433 originMonthStart := time.Date(opts.Origin.Year(), opts.Origin.Month(), 1, 0, 0, 0, 0, time.UTC)
436434
437435 where := ""
438- args := []interface{}{opts.UserID, originMonthStart, currentMonthStart}
436+ args := []interface{}{opts.UserID, originMonthStart, previousMonthStart}
439437 if opts.Host != "" {
440438 where = "AND host = $4"
441439 args = append(args, opts.Host)
......@@ -555,7 +553,7 @@ func (me *PsqlDB) visitUrlNotFoundFromSummary(opts *db.SummaryOpts, limit int) (
555553 originMonthStart := time.Date(opts.Origin.Year(), opts.Origin.Month(), 1, 0, 0, 0, 0, time.UTC)
556554
557555 where := ""
558- args := []interface{}{opts.UserID, originMonthStart, currentMonthStart}
556+ args := []interface{}{opts.UserID, originMonthStart, previousMonthStart}
559557 argIdx := 4
560558 if opts.Host != "" {
561559 where = "AND host = $" + fmt.Sprintf("%d", argIdx)
+1, -12
......@@ -3,7 +3,6 @@ package postgres
33 import (
44 "database/sql"
55 "fmt"
6- "math"
76 "strings"
87
98 "github.com/picosh/pico/pkg/db"
......@@ -309,17 +308,7 @@ func (me *PsqlDB) FindPostsByUser(page *db.Pager, userID string, space string) (
309308 return nil, rs.Err()
310309 }
311310
312- var count int
313- err = me.Db.QueryRow(`SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`, space).Scan(&count)
314- if err != nil {
315- return nil, err
316- }
317-
318- pager := &db.Paginate[*db.Post]{
319- Data: posts,
320- Total: int(math.Ceil(float64(count) / float64(page.Num))),
321- }
322- return pager, nil
311+ return &db.Paginate[*db.Post]{Data: posts}, nil
323312 }
324313
325314 func (me *PsqlDB) FindAllPostsByUser(userID string, space string) ([]*db.Post, error) {
+49, -0
......@@ -824,6 +824,55 @@ func TestVisitSummaryDeviceSplit(t *testing.T) {
824824 }
825825 }
826826
827+func TestVisitSummaryPreviousMonthCountedOnce(t *testing.T) {
828+ cleanupTestData(t)
829+
830+ user, _ := testDB.RegisterUser("prevmonthowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI prevmonthowner", "comment", "")
831+
832+ now := time.Now()
833+ currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
834+ previousMonthStart := currentMonthStart.AddDate(0, -1, 0)
835+
836+ // Raw rows for the previous month are kept after it has been aggregated.
837+ _, err := testDB.Db.Exec(
838+ `INSERT INTO analytics_visits (user_id, host, path, ip_address, referer, status, created_at) VALUES ($1, 'prev.com', '/page', '192.168.1.20', 'ref.com', 200, $2)`,
839+ user.ID, previousMonthStart.Add(24*time.Hour),
840+ )
841+ if err != nil {
842+ t.Fatalf("insert raw visit: %v", err)
843+ }
844+ _, err = testDB.Db.Exec(
845+ `INSERT INTO analytics_monthly_top_urls (user_id, host, month, path, unique_visits, status_code, rank) VALUES ($1, 'prev.com', $2, '/page', 1, 200, 1)`,
846+ user.ID, previousMonthStart,
847+ )
848+ if err != nil {
849+ t.Fatalf("insert top url: %v", err)
850+ }
851+ _, err = testDB.Db.Exec(
852+ `INSERT INTO analytics_monthly_top_referers (user_id, host, month, referer, unique_visits, rank) VALUES ($1, 'prev.com', $2, 'ref.com', 1, 1)`,
853+ user.ID, previousMonthStart,
854+ )
855+ if err != nil {
856+ t.Fatalf("insert top referer: %v", err)
857+ }
858+
859+ summary, err := testDB.VisitSummary(&db.SummaryOpts{
860+ Interval: "month",
861+ Origin: currentMonthStart.AddDate(0, -3, 0),
862+ Host: "prev.com",
863+ UserID: user.ID,
864+ })
865+ if err != nil {
866+ t.Fatalf("VisitSummary failed: %v", err)
867+ }
868+ if len(summary.TopUrls) != 1 || summary.TopUrls[0].Count != 1 {
869+ t.Errorf("got top urls %+v, want /page counted once", summary.TopUrls)
870+ }
871+ if len(summary.TopReferers) != 1 || summary.TopReferers[0].Count != 1 {
872+ t.Errorf("got top referers %+v, want ref.com counted once", summary.TopReferers)
873+ }
874+}
875+
827876 func TestFindVisitSiteList(t *testing.T) {
828877 cleanupTestData(t)
829878
+0, -8
......@@ -144,14 +144,6 @@ func (me *StubDB) FindUserPostsByTag(page *db.Pager, tag, userID, space string)
144144 return &db.Paginate[*db.Post]{}, errNotImpl
145145 }
146146
147-func (me *StubDB) FindPostsByTag(pager *db.Pager, tag, space string) (*db.Paginate[*db.Post], error) {
148- return &db.Paginate[*db.Post]{}, errNotImpl
149-}
150-
151-func (me *StubDB) FindPopularTags(space string) ([]string, error) {
152- return []string{}, errNotImpl
153-}
154-
155147 func (me *StubDB) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
156148 return nil, errNotImpl
157149 }
+1, -0
......@@ -0,0 +1 @@
1+CREATE INDEX CONCURRENTLY IF NOT EXISTS access_logs_user_created_idx ON access_logs (user_id, created_at);
+15, -0
......@@ -0,0 +1,15 @@
1+ALTER TABLE analytics_visits ADD COLUMN IF NOT EXISTS is_mobile boolean;
2+
3+-- Only the current and previous month are read from raw visits. Matches db.IsMobileUserAgent.
4+UPDATE analytics_visits
5+SET is_mobile = (
6+ COALESCE(user_agent, '') ILIKE '%mobile%'
7+ OR COALESCE(user_agent, '') ILIKE '%android%'
8+ OR COALESCE(user_agent, '') ILIKE '%iphone%'
9+ OR COALESCE(user_agent, '') ILIKE '%ipad%'
10+ OR COALESCE(user_agent, '') ILIKE '%ipod%'
11+ OR COALESCE(user_agent, '') ILIKE '%blackberry%'
12+ OR COALESCE(user_agent, '') ILIKE '%windows phone%'
13+)
14+WHERE is_mobile IS NULL
15+ AND created_at >= date_trunc('month', now() AT TIME ZONE 'UTC') - interval '1 month';
+6, -0
......@@ -0,0 +1,6 @@
1+CREATE INDEX IF NOT EXISTS feed_items_post_idx ON feed_items (post_id);
2+CREATE INDEX IF NOT EXISTS tuns_event_logs_user_created_idx ON tuns_event_logs (user_id, created_at);
3+CREATE INDEX IF NOT EXISTS feature_flags_user_name_idx ON feature_flags (user_id, name);
4+CREATE INDEX IF NOT EXISTS public_keys_public_key_idx ON public_keys (public_key);
5+CREATE INDEX IF NOT EXISTS post_aliases_slug_idx ON post_aliases (slug);
6+CREATE INDEX IF NOT EXISTS post_aliases_post_idx ON post_aliases (post_id);
+9, -0
......@@ -0,0 +1,9 @@
1+DROP INDEX IF EXISTS
2+ analytics_visits_status_idx,
3+ analytics_visits_user_id_idx,
4+ analytics_visits_ip_addr_idx,
5+ analytics_visits_host_idx,
6+ analytics_visits_path_idx,
7+ analytics_visits_user_id_host_ip_address_idx,
8+ analytics_visits_user_created_idx,
9+ analytics_visits_user_path_created_status_idx;