package db import ( "context" "fmt" "github.com/jackc/pgx/v5/pgtype" ) // All queries in this file are hand-written and intentionally NOT defined in // `db/queries/*.sql`, so they are not regenerated by sqlc. // TrendingWindow is the trailing time-window over which visits are summed // to compute "trending". Values are expressed in days so they map to exact // row ranges in the daily-bucket location_page_visits table. type TrendingWindow string const ( TrendingWindowWeek TrendingWindow = "week" // 7 days TrendingWindowMonth TrendingWindow = "month" // 30 days TrendingWindow3Month TrendingWindow = "3month" // 90 days TrendingWindowSemester TrendingWindow = "semester" // 180 days TrendingWindowYear TrendingWindow = "year" // 365 days ) // Days returns the trailing window length in days. Returns (0, false) for // unknown values. func (w TrendingWindow) Days() (int, bool) { switch w { case TrendingWindowWeek: return 7, true case TrendingWindowMonth: return 30, true case TrendingWindow3Month: return 90, true case TrendingWindowSemester: return 180, true case TrendingWindowYear: return 365, true default: return 0, false } } // ParseTrendingWindow returns the typed window for a string, or false if // the string isn't recognised. func ParseTrendingWindow(s string) (TrendingWindow, bool) { w := TrendingWindow(s) if _, ok := w.Days(); !ok { return "", false } return w, true } // RecordLocationVisit increments today's visit bucket for the given location, // creating the row if it does not yet exist. Idempotent within Postgres // transactions; the (location_id, day) UNIQUE constraint guarantees a single // row per location per day. func (q *Queries) RecordLocationVisit(ctx context.Context, locationID int32) error { const query = ` INSERT INTO location_page_visits (location_id, day, visit_count, updated_at) VALUES ($1, CURRENT_DATE, 1, now()) ON CONFLICT (location_id, day) DO UPDATE SET visit_count = location_page_visits.visit_count + 1, updated_at = now() ` _, err := q.db.Exec(ctx, query, locationID) return err } // GetLocationLifetimeVisits returns the all-time sum of visit_count for a // single location. Cheap because the (location_id, day) index covers it. func (q *Queries) GetLocationLifetimeVisits(ctx context.Context, locationID int32) (int64, error) { const query = ` SELECT COALESCE(SUM(visit_count), 0)::bigint AS total FROM location_page_visits WHERE location_id = $1 ` row := q.db.QueryRow(ctx, query, locationID) var total int64 err := row.Scan(&total) return total, err } type GetTrendingLocationsParams struct { Window TrendingWindow `json:"window"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` } type GetTrendingLocationsRow struct { RowNumber int32 `json:"row_number"` ID int32 `json:"id"` Name string `json:"name"` Address string `json:"address"` LocationType string `json:"location_type"` GoogleMapsLink pgtype.Text `json:"google_maps_link"` Thumbnail pgtype.Text `json:"thumbnail"` RegencyName string `json:"regency_name"` ProvinceName string `json:"province_name"` RegionName string `json:"region_name"` WindowVisits int64 `json:"window_visits"` } // GetTrendingLocations ranks approved, non-deleted locations by total page // visits over the trailing window. The query is keyed off the // idx_location_page_visits_day_count index (day, visit_count DESC). // // The bound day count is bound as a parameter; the planner still treats it as // a constant for the row (current_date - $1::int), so the index range scan is // effective. func (q *Queries) GetTrendingLocations(ctx context.Context, arg GetTrendingLocationsParams) ([]GetTrendingLocationsRow, error) { days, ok := arg.Window.Days() if !ok { return nil, fmt.Errorf("invalid trending window %q", arg.Window) } const query = ` SELECT row_number() OVER (ORDER BY window_visits DESC, l.id ASC) AS row_number, l.id, l.name, l.address, l.location_type::text AS location_type, l.google_maps_link, l.thumbnail, COALESCE(re.regency_name, '') AS regency_name, COALESCE(prov.province_name, '') AS province_name, COALESCE(reg.region_name, '') AS region_name, window_visits FROM ( SELECT v.location_id, SUM(v.visit_count)::bigint AS window_visits FROM location_page_visits v WHERE v.day >= (CURRENT_DATE - $1::int) GROUP BY v.location_id ) agg JOIN locations l ON l.id = agg.location_id JOIN regencies re ON re.id = l.regency_id JOIN provinces prov ON prov.id = re.province_id JOIN regions reg ON reg.id = prov.region_id WHERE l.is_deleted = false AND l.approved_by IS NOT NULL ORDER BY window_visits DESC, l.id ASC LIMIT $2 OFFSET $3 ` rows, err := q.db.Query(ctx, query, days, arg.Limit, arg.Offset) if err != nil { return nil, err } defer rows.Close() items := []GetTrendingLocationsRow{} for rows.Next() { var i GetTrendingLocationsRow if err := rows.Scan( &i.RowNumber, &i.ID, &i.Name, &i.Address, &i.LocationType, &i.GoogleMapsLink, &i.Thumbnail, &i.RegencyName, &i.ProvinceName, &i.RegionName, &i.WindowVisits, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil }