feat(persistence): add analytics queries and repository methods for history
- commit
- c5f10875b5feac801d838c7206255ffee57f1e84
- author
- brice · · 2025-12-16 22:11 UTC
- committer
- brice · · 2025-12-16 22:14 UTC
- parent
- 1cb11c4
- signature
- unsigned
5 files · +278 -0
M internal/domain/repository/history.go +15 -0
| @@ -32,4 +32,19 @@ type HistoryRepository interface { | |||
| 32 | 32 | ||
| 33 | 33 | // DeleteAll removes all history entries. | |
| 34 | 34 | DeleteAll(ctx context.Context) error | |
| 35 | + | ||
| 36 | + | // DeleteByDomain removes all history entries for a domain. | |
| 37 | + | DeleteByDomain(ctx context.Context, domain string) error | |
| 38 | + | ||
| 39 | + | // GetStats retrieves overall history statistics. | |
| 40 | + | GetStats(ctx context.Context) (*entity.HistoryStats, error) | |
| 41 | + | ||
| 42 | + | // GetDomainStats retrieves per-domain statistics. | |
| 43 | + | GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) | |
| 44 | + | ||
| 45 | + | // GetHourlyDistribution retrieves hourly visit distribution. | |
| 46 | + | GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) | |
| 47 | + | ||
| 48 | + | // GetDailyVisitCount retrieves daily visit counts for the given period. | |
| 49 | + | GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) | |
| 35 | 50 | } | |
| @@ -32,4 +32,19 @@ type HistoryRepository interface { | |||||
| 32 | 32 | ||||
| 33 | // DeleteAll removes all history entries. | 33 | // DeleteAll removes all history entries. | ||
| 34 | DeleteAll(ctx context.Context) error | 34 | DeleteAll(ctx context.Context) error | ||
| 35 | + | ||||
| 36 | + | // DeleteByDomain removes all history entries for a domain. | |||
| 37 | + | DeleteByDomain(ctx context.Context, domain string) error | |||
| 38 | + | ||||
| 39 | + | // GetStats retrieves overall history statistics. | |||
| 40 | + | GetStats(ctx context.Context) (*entity.HistoryStats, error) | |||
| 41 | + | ||||
| 42 | + | // GetDomainStats retrieves per-domain statistics. | |||
| 43 | + | GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) | |||
| 44 | + | ||||
| 45 | + | // GetHourlyDistribution retrieves hourly visit distribution. | |||
| 46 | + | GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) | |||
| 47 | + | ||||
| 48 | + | // GetDailyVisitCount retrieves daily visit counts for the given period. | |||
| 49 | + | GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) | |||
| 35 | } | 50 | } | ||
M internal/infrastructure/persistence/sqlite/history_repo.go +101 -0
| @@ -94,6 +94,107 @@ func (r *historyRepo) DeleteAll(ctx context.Context) error { | |||
| 94 | 94 | return r.queries.DeleteAllHistory(ctx) | |
| 95 | 95 | } | |
| 96 | 96 | ||
| 97 | + | func (r *historyRepo) DeleteByDomain(ctx context.Context, domain string) error { | |
| 98 | + | return r.queries.DeleteHistoryByDomain(ctx, sqlc.DeleteHistoryByDomainParams{ | |
| 99 | + | Column1: sql.NullString{String: domain, Valid: true}, | |
| 100 | + | Column2: sql.NullString{String: domain, Valid: true}, | |
| 101 | + | Column3: sql.NullString{String: domain, Valid: true}, | |
| 102 | + | Column4: sql.NullString{String: domain, Valid: true}, | |
| 103 | + | }) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func (r *historyRepo) GetStats(ctx context.Context) (*entity.HistoryStats, error) { | |
| 107 | + | row, err := r.queries.GetHistoryStats(ctx) | |
| 108 | + | if err != nil { | |
| 109 | + | return nil, err | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | // Handle the interface{} type for total_visits | |
| 113 | + | var totalVisits int64 | |
| 114 | + | if v, ok := row.TotalVisits.(int64); ok { | |
| 115 | + | totalVisits = v | |
| 116 | + | } else if v, ok := row.TotalVisits.(float64); ok { | |
| 117 | + | totalVisits = int64(v) | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | return &entity.HistoryStats{ | |
| 121 | + | TotalEntries: row.TotalEntries, | |
| 122 | + | TotalVisits: totalVisits, | |
| 123 | + | UniqueDays: row.UniqueDays, | |
| 124 | + | }, nil | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func (r *historyRepo) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) { | |
| 128 | + | rows, err := r.queries.GetDomainStats(ctx, int64(limit)) | |
| 129 | + | if err != nil { | |
| 130 | + | return nil, err | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | stats := make([]*entity.DomainStat, len(rows)) | |
| 134 | + | for i, row := range rows { | |
| 135 | + | var totalVisits int64 | |
| 136 | + | if row.TotalVisits.Valid { | |
| 137 | + | totalVisits = int64(row.TotalVisits.Float64) | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | var lastVisit time.Time | |
| 141 | + | if v, ok := row.LastVisit.(string); ok && v != "" { | |
| 142 | + | lastVisit, _ = time.Parse("2006-01-02 15:04:05", v) | |
| 143 | + | } | |
| 144 | + | ||
| 145 | + | stats[i] = &entity.DomainStat{ | |
| 146 | + | Domain: row.Domain, | |
| 147 | + | PageCount: row.PageCount, | |
| 148 | + | TotalVisits: totalVisits, | |
| 149 | + | LastVisit: lastVisit, | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | return stats, nil | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | func (r *historyRepo) GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) { | |
| 156 | + | rows, err := r.queries.GetHourlyDistribution(ctx) | |
| 157 | + | if err != nil { | |
| 158 | + | return nil, err | |
| 159 | + | } | |
| 160 | + | ||
| 161 | + | dist := make([]*entity.HourlyDistribution, len(rows)) | |
| 162 | + | for i, row := range rows { | |
| 163 | + | dist[i] = &entity.HourlyDistribution{ | |
| 164 | + | Hour: int(row.Hour), | |
| 165 | + | VisitCount: row.VisitCount, | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | return dist, nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func (r *historyRepo) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | |
| 172 | + | rows, err := r.queries.GetDailyVisitCount(ctx, daysAgo) | |
| 173 | + | if err != nil { | |
| 174 | + | return nil, err | |
| 175 | + | } | |
| 176 | + | ||
| 177 | + | counts := make([]*entity.DailyVisitCount, len(rows)) | |
| 178 | + | for i, row := range rows { | |
| 179 | + | var day string | |
| 180 | + | if v, ok := row.Day.(string); ok { | |
| 181 | + | day = v | |
| 182 | + | } | |
| 183 | + | ||
| 184 | + | var visits int64 | |
| 185 | + | if row.Visits.Valid { | |
| 186 | + | visits = int64(row.Visits.Float64) | |
| 187 | + | } | |
| 188 | + | ||
| 189 | + | counts[i] = &entity.DailyVisitCount{ | |
| 190 | + | Day: day, | |
| 191 | + | Entries: row.Entries, | |
| 192 | + | Visits: visits, | |
| 193 | + | } | |
| 194 | + | } | |
| 195 | + | return counts, nil | |
| 196 | + | } | |
| 197 | + | ||
| 97 | 198 | func historyFromRow(row sqlc.History) *entity.HistoryEntry { | |
| 98 | 199 | return &entity.HistoryEntry{ | |
| 99 | 200 | ID: row.ID, | |
| @@ -94,6 +94,107 @@ func (r *historyRepo) DeleteAll(ctx context.Context) error { | |||||
| 94 | return r.queries.DeleteAllHistory(ctx) | 94 | return r.queries.DeleteAllHistory(ctx) | ||
| 95 | } | 95 | } | ||
| 96 | 96 | ||||
| 97 | + | func (r *historyRepo) DeleteByDomain(ctx context.Context, domain string) error { | |||
| 98 | + | return r.queries.DeleteHistoryByDomain(ctx, sqlc.DeleteHistoryByDomainParams{ | |||
| 99 | + | Column1: sql.NullString{String: domain, Valid: true}, | |||
| 100 | + | Column2: sql.NullString{String: domain, Valid: true}, | |||
| 101 | + | Column3: sql.NullString{String: domain, Valid: true}, | |||
| 102 | + | Column4: sql.NullString{String: domain, Valid: true}, | |||
| 103 | + | }) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func (r *historyRepo) GetStats(ctx context.Context) (*entity.HistoryStats, error) { | |||
| 107 | + | row, err := r.queries.GetHistoryStats(ctx) | |||
| 108 | + | if err != nil { | |||
| 109 | + | return nil, err | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | // Handle the interface{} type for total_visits | |||
| 113 | + | var totalVisits int64 | |||
| 114 | + | if v, ok := row.TotalVisits.(int64); ok { | |||
| 115 | + | totalVisits = v | |||
| 116 | + | } else if v, ok := row.TotalVisits.(float64); ok { | |||
| 117 | + | totalVisits = int64(v) | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | return &entity.HistoryStats{ | |||
| 121 | + | TotalEntries: row.TotalEntries, | |||
| 122 | + | TotalVisits: totalVisits, | |||
| 123 | + | UniqueDays: row.UniqueDays, | |||
| 124 | + | }, nil | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func (r *historyRepo) GetDomainStats(ctx context.Context, limit int) ([]*entity.DomainStat, error) { | |||
| 128 | + | rows, err := r.queries.GetDomainStats(ctx, int64(limit)) | |||
| 129 | + | if err != nil { | |||
| 130 | + | return nil, err | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | stats := make([]*entity.DomainStat, len(rows)) | |||
| 134 | + | for i, row := range rows { | |||
| 135 | + | var totalVisits int64 | |||
| 136 | + | if row.TotalVisits.Valid { | |||
| 137 | + | totalVisits = int64(row.TotalVisits.Float64) | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | var lastVisit time.Time | |||
| 141 | + | if v, ok := row.LastVisit.(string); ok && v != "" { | |||
| 142 | + | lastVisit, _ = time.Parse("2006-01-02 15:04:05", v) | |||
| 143 | + | } | |||
| 144 | + | ||||
| 145 | + | stats[i] = &entity.DomainStat{ | |||
| 146 | + | Domain: row.Domain, | |||
| 147 | + | PageCount: row.PageCount, | |||
| 148 | + | TotalVisits: totalVisits, | |||
| 149 | + | LastVisit: lastVisit, | |||
| 150 | + | } | |||
| 151 | + | } | |||
| 152 | + | return stats, nil | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | func (r *historyRepo) GetHourlyDistribution(ctx context.Context) ([]*entity.HourlyDistribution, error) { | |||
| 156 | + | rows, err := r.queries.GetHourlyDistribution(ctx) | |||
| 157 | + | if err != nil { | |||
| 158 | + | return nil, err | |||
| 159 | + | } | |||
| 160 | + | ||||
| 161 | + | dist := make([]*entity.HourlyDistribution, len(rows)) | |||
| 162 | + | for i, row := range rows { | |||
| 163 | + | dist[i] = &entity.HourlyDistribution{ | |||
| 164 | + | Hour: int(row.Hour), | |||
| 165 | + | VisitCount: row.VisitCount, | |||
| 166 | + | } | |||
| 167 | + | } | |||
| 168 | + | return dist, nil | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func (r *historyRepo) GetDailyVisitCount(ctx context.Context, daysAgo string) ([]*entity.DailyVisitCount, error) { | |||
| 172 | + | rows, err := r.queries.GetDailyVisitCount(ctx, daysAgo) | |||
| 173 | + | if err != nil { | |||
| 174 | + | return nil, err | |||
| 175 | + | } | |||
| 176 | + | ||||
| 177 | + | counts := make([]*entity.DailyVisitCount, len(rows)) | |||
| 178 | + | for i, row := range rows { | |||
| 179 | + | var day string | |||
| 180 | + | if v, ok := row.Day.(string); ok { | |||
| 181 | + | day = v | |||
| 182 | + | } | |||
| 183 | + | ||||
| 184 | + | var visits int64 | |||
| 185 | + | if row.Visits.Valid { | |||
| 186 | + | visits = int64(row.Visits.Float64) | |||
| 187 | + | } | |||
| 188 | + | ||||
| 189 | + | counts[i] = &entity.DailyVisitCount{ | |||
| 190 | + | Day: day, | |||
| 191 | + | Entries: row.Entries, | |||
| 192 | + | Visits: visits, | |||
| 193 | + | } | |||
| 194 | + | } | |||
| 195 | + | return counts, nil | |||
| 196 | + | } | |||
| 197 | + | ||||
| 97 | func historyFromRow(row sqlc.History) *entity.HistoryEntry { | 198 | func historyFromRow(row sqlc.History) *entity.HistoryEntry { | ||
| 98 | return &entity.HistoryEntry{ | 199 | return &entity.HistoryEntry{ | ||
| 99 | ID: row.ID, | 200 | ID: row.ID, | ||
M internal/infrastructure/persistence/sqlite/queries/history.sql +15 -0
| @@ -33,3 +33,18 @@ DELETE FROM history WHERE last_visited < ?; | |||
| 33 | 33 | ||
| 34 | 34 | -- name: DeleteAllHistory :exec | |
| 35 | 35 | DELETE FROM history; | |
| 36 | + | ||
| 37 | + | -- name: GetHistoryStats :one | |
| 38 | + | SELECT COUNT(*) as total_entries, COALESCE(SUM(visit_count), 0) as total_visits, COUNT(DISTINCT date(last_visited)) as unique_days FROM history; | |
| 39 | + | ||
| 40 | + | -- name: GetDomainStats :many | |
| 41 | + | SELECT SUBSTR(SUBSTR(url, INSTR(url, '://') + 3), 1, CASE WHEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') > 0 THEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') - 1 ELSE LENGTH(SUBSTR(url, INSTR(url, '://') + 3)) END) as domain, COUNT(*) as page_count, SUM(visit_count) as total_visits, MAX(last_visited) as last_visit FROM history GROUP BY domain ORDER BY total_visits DESC LIMIT ?; | |
| 42 | + | ||
| 43 | + | -- name: GetHourlyDistribution :many | |
| 44 | + | SELECT CAST(strftime('%H', last_visited) AS INTEGER) as hour, COUNT(*) as visit_count FROM history GROUP BY hour ORDER BY hour; | |
| 45 | + | ||
| 46 | + | -- name: GetDailyVisitCount :many | |
| 47 | + | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC; | |
| 48 | + | ||
| 49 | + | -- name: DeleteHistoryByDomain :exec | |
| 50 | + | DELETE FROM history WHERE url LIKE '%://' || ? || '/%' OR url LIKE '%://' || ? || '?%' OR url LIKE '%://' || ? || '#%' OR url LIKE '%://' || ?; | |
| @@ -33,3 +33,18 @@ DELETE FROM history WHERE last_visited < ?; | |||||
| 33 | 33 | ||||
| 34 | -- name: DeleteAllHistory :exec | 34 | -- name: DeleteAllHistory :exec | ||
| 35 | DELETE FROM history; | 35 | DELETE FROM history; | ||
| 36 | + | ||||
| 37 | + | -- name: GetHistoryStats :one | |||
| 38 | + | SELECT COUNT(*) as total_entries, COALESCE(SUM(visit_count), 0) as total_visits, COUNT(DISTINCT date(last_visited)) as unique_days FROM history; | |||
| 39 | + | ||||
| 40 | + | -- name: GetDomainStats :many | |||
| 41 | + | SELECT SUBSTR(SUBSTR(url, INSTR(url, '://') + 3), 1, CASE WHEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') > 0 THEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') - 1 ELSE LENGTH(SUBSTR(url, INSTR(url, '://') + 3)) END) as domain, COUNT(*) as page_count, SUM(visit_count) as total_visits, MAX(last_visited) as last_visit FROM history GROUP BY domain ORDER BY total_visits DESC LIMIT ?; | |||
| 42 | + | ||||
| 43 | + | -- name: GetHourlyDistribution :many | |||
| 44 | + | SELECT CAST(strftime('%H', last_visited) AS INTEGER) as hour, COUNT(*) as visit_count FROM history GROUP BY hour ORDER BY hour; | |||
| 45 | + | ||||
| 46 | + | -- name: GetDailyVisitCount :many | |||
| 47 | + | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC; | |||
| 48 | + | ||||
| 49 | + | -- name: DeleteHistoryByDomain :exec | |||
| 50 | + | DELETE FROM history WHERE url LIKE '%://' || ? || '/%' OR url LIKE '%://' || ? || '?%' OR url LIKE '%://' || ? || '#%' OR url LIKE '%://' || ?; | |||
M internal/infrastructure/persistence/sqlite/sqlc/history.sql.go +142 -0
| @@ -19,6 +19,27 @@ func (q *Queries) DeleteAllHistory(ctx context.Context) error { | |||
| 19 | 19 | return err | |
| 20 | 20 | } | |
| 21 | 21 | ||
| 22 | + | const DeleteHistoryByDomain = `-- name: DeleteHistoryByDomain :exec | |
| 23 | + | DELETE FROM history WHERE url LIKE '%://' || ? || '/%' OR url LIKE '%://' || ? || '?%' OR url LIKE '%://' || ? || '#%' OR url LIKE '%://' || ? | |
| 24 | + | ` | |
| 25 | + | ||
| 26 | + | type DeleteHistoryByDomainParams struct { | |
| 27 | + | Column1 sql.NullString `json:"column_1"` | |
| 28 | + | Column2 sql.NullString `json:"column_2"` | |
| 29 | + | Column3 sql.NullString `json:"column_3"` | |
| 30 | + | Column4 sql.NullString `json:"column_4"` | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func (q *Queries) DeleteHistoryByDomain(ctx context.Context, arg DeleteHistoryByDomainParams) error { | |
| 34 | + | _, err := q.db.ExecContext(ctx, DeleteHistoryByDomain, | |
| 35 | + | arg.Column1, | |
| 36 | + | arg.Column2, | |
| 37 | + | arg.Column3, | |
| 38 | + | arg.Column4, | |
| 39 | + | ) | |
| 40 | + | return err | |
| 41 | + | } | |
| 42 | + | ||
| 22 | 43 | const DeleteHistoryByID = `-- name: DeleteHistoryByID :exec | |
| 23 | 44 | DELETE FROM history WHERE id = ? | |
| 24 | 45 | ` | |
| @@ -37,6 +58,78 @@ func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.Nu | |||
| 37 | 58 | return err | |
| 38 | 59 | } | |
| 39 | 60 | ||
| 61 | + | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | |
| 62 | + | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | |
| 63 | + | ` | |
| 64 | + | ||
| 65 | + | type GetDailyVisitCountRow struct { | |
| 66 | + | Day interface{} `json:"day"` | |
| 67 | + | Entries int64 `json:"entries"` | |
| 68 | + | Visits sql.NullFloat64 `json:"visits"` | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | func (q *Queries) GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error) { | |
| 72 | + | rows, err := q.db.QueryContext(ctx, GetDailyVisitCount, date) | |
| 73 | + | if err != nil { | |
| 74 | + | return nil, err | |
| 75 | + | } | |
| 76 | + | defer rows.Close() | |
| 77 | + | items := []GetDailyVisitCountRow{} | |
| 78 | + | for rows.Next() { | |
| 79 | + | var i GetDailyVisitCountRow | |
| 80 | + | if err := rows.Scan(&i.Day, &i.Entries, &i.Visits); err != nil { | |
| 81 | + | return nil, err | |
| 82 | + | } | |
| 83 | + | items = append(items, i) | |
| 84 | + | } | |
| 85 | + | if err := rows.Close(); err != nil { | |
| 86 | + | return nil, err | |
| 87 | + | } | |
| 88 | + | if err := rows.Err(); err != nil { | |
| 89 | + | return nil, err | |
| 90 | + | } | |
| 91 | + | return items, nil | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | const GetDomainStats = `-- name: GetDomainStats :many | |
| 95 | + | SELECT SUBSTR(SUBSTR(url, INSTR(url, '://') + 3), 1, CASE WHEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') > 0 THEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') - 1 ELSE LENGTH(SUBSTR(url, INSTR(url, '://') + 3)) END) as domain, COUNT(*) as page_count, SUM(visit_count) as total_visits, MAX(last_visited) as last_visit FROM history GROUP BY domain ORDER BY total_visits DESC LIMIT ? | |
| 96 | + | ` | |
| 97 | + | ||
| 98 | + | type GetDomainStatsRow struct { | |
| 99 | + | Domain string `json:"domain"` | |
| 100 | + | PageCount int64 `json:"page_count"` | |
| 101 | + | TotalVisits sql.NullFloat64 `json:"total_visits"` | |
| 102 | + | LastVisit interface{} `json:"last_visit"` | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func (q *Queries) GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error) { | |
| 106 | + | rows, err := q.db.QueryContext(ctx, GetDomainStats, limit) | |
| 107 | + | if err != nil { | |
| 108 | + | return nil, err | |
| 109 | + | } | |
| 110 | + | defer rows.Close() | |
| 111 | + | items := []GetDomainStatsRow{} | |
| 112 | + | for rows.Next() { | |
| 113 | + | var i GetDomainStatsRow | |
| 114 | + | if err := rows.Scan( | |
| 115 | + | &i.Domain, | |
| 116 | + | &i.PageCount, | |
| 117 | + | &i.TotalVisits, | |
| 118 | + | &i.LastVisit, | |
| 119 | + | ); err != nil { | |
| 120 | + | return nil, err | |
| 121 | + | } | |
| 122 | + | items = append(items, i) | |
| 123 | + | } | |
| 124 | + | if err := rows.Close(); err != nil { | |
| 125 | + | return nil, err | |
| 126 | + | } | |
| 127 | + | if err := rows.Err(); err != nil { | |
| 128 | + | return nil, err | |
| 129 | + | } | |
| 130 | + | return items, nil | |
| 131 | + | } | |
| 132 | + | ||
| 40 | 133 | const GetHistoryByURL = `-- name: GetHistoryByURL :one | |
| 41 | 134 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history WHERE url = ? LIMIT 1 | |
| 42 | 135 | ` | |
| @@ -56,6 +149,55 @@ func (q *Queries) GetHistoryByURL(ctx context.Context, url string) (History, err | |||
| 56 | 149 | return i, err | |
| 57 | 150 | } | |
| 58 | 151 | ||
| 152 | + | const GetHistoryStats = `-- name: GetHistoryStats :one | |
| 153 | + | SELECT COUNT(*) as total_entries, COALESCE(SUM(visit_count), 0) as total_visits, COUNT(DISTINCT date(last_visited)) as unique_days FROM history | |
| 154 | + | ` | |
| 155 | + | ||
| 156 | + | type GetHistoryStatsRow struct { | |
| 157 | + | TotalEntries int64 `json:"total_entries"` | |
| 158 | + | TotalVisits interface{} `json:"total_visits"` | |
| 159 | + | UniqueDays int64 `json:"unique_days"` | |
| 160 | + | } | |
| 161 | + | ||
| 162 | + | func (q *Queries) GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) { | |
| 163 | + | row := q.db.QueryRowContext(ctx, GetHistoryStats) | |
| 164 | + | var i GetHistoryStatsRow | |
| 165 | + | err := row.Scan(&i.TotalEntries, &i.TotalVisits, &i.UniqueDays) | |
| 166 | + | return i, err | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | const GetHourlyDistribution = `-- name: GetHourlyDistribution :many | |
| 170 | + | SELECT CAST(strftime('%H', last_visited) AS INTEGER) as hour, COUNT(*) as visit_count FROM history GROUP BY hour ORDER BY hour | |
| 171 | + | ` | |
| 172 | + | ||
| 173 | + | type GetHourlyDistributionRow struct { | |
| 174 | + | Hour int64 `json:"hour"` | |
| 175 | + | VisitCount int64 `json:"visit_count"` | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | func (q *Queries) GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) { | |
| 179 | + | rows, err := q.db.QueryContext(ctx, GetHourlyDistribution) | |
| 180 | + | if err != nil { | |
| 181 | + | return nil, err | |
| 182 | + | } | |
| 183 | + | defer rows.Close() | |
| 184 | + | items := []GetHourlyDistributionRow{} | |
| 185 | + | for rows.Next() { | |
| 186 | + | var i GetHourlyDistributionRow | |
| 187 | + | if err := rows.Scan(&i.Hour, &i.VisitCount); err != nil { | |
| 188 | + | return nil, err | |
| 189 | + | } | |
| 190 | + | items = append(items, i) | |
| 191 | + | } | |
| 192 | + | if err := rows.Close(); err != nil { | |
| 193 | + | return nil, err | |
| 194 | + | } | |
| 195 | + | if err := rows.Err(); err != nil { | |
| 196 | + | return nil, err | |
| 197 | + | } | |
| 198 | + | return items, nil | |
| 199 | + | } | |
| 200 | + | ||
| 59 | 201 | const GetRecentHistory = `-- name: GetRecentHistory :many | |
| 60 | 202 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | |
| 61 | 203 | ` | |
| @@ -19,6 +19,27 @@ func (q *Queries) DeleteAllHistory(ctx context.Context) error { | |||||
| 19 | return err | 19 | return err | ||
| 20 | } | 20 | } | ||
| 21 | 21 | ||||
| 22 | + | const DeleteHistoryByDomain = `-- name: DeleteHistoryByDomain :exec | |||
| 23 | + | DELETE FROM history WHERE url LIKE '%://' || ? || '/%' OR url LIKE '%://' || ? || '?%' OR url LIKE '%://' || ? || '#%' OR url LIKE '%://' || ? | |||
| 24 | + | ` | |||
| 25 | + | ||||
| 26 | + | type DeleteHistoryByDomainParams struct { | |||
| 27 | + | Column1 sql.NullString `json:"column_1"` | |||
| 28 | + | Column2 sql.NullString `json:"column_2"` | |||
| 29 | + | Column3 sql.NullString `json:"column_3"` | |||
| 30 | + | Column4 sql.NullString `json:"column_4"` | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func (q *Queries) DeleteHistoryByDomain(ctx context.Context, arg DeleteHistoryByDomainParams) error { | |||
| 34 | + | _, err := q.db.ExecContext(ctx, DeleteHistoryByDomain, | |||
| 35 | + | arg.Column1, | |||
| 36 | + | arg.Column2, | |||
| 37 | + | arg.Column3, | |||
| 38 | + | arg.Column4, | |||
| 39 | + | ) | |||
| 40 | + | return err | |||
| 41 | + | } | |||
| 42 | + | ||||
| 22 | const DeleteHistoryByID = `-- name: DeleteHistoryByID :exec | 43 | const DeleteHistoryByID = `-- name: DeleteHistoryByID :exec | ||
| 23 | DELETE FROM history WHERE id = ? | 44 | DELETE FROM history WHERE id = ? | ||
| 24 | ` | 45 | ` | ||
| @@ -37,6 +58,78 @@ func (q *Queries) DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.Nu | |||||
| 37 | return err | 58 | return err | ||
| 38 | } | 59 | } | ||
| 39 | 60 | ||||
| 61 | + | const GetDailyVisitCount = `-- name: GetDailyVisitCount :many | |||
| 62 | + | SELECT date(last_visited) as day, COUNT(*) as entries, SUM(visit_count) as visits FROM history WHERE last_visited >= date('now', ?) GROUP BY day ORDER BY day ASC | |||
| 63 | + | ` | |||
| 64 | + | ||||
| 65 | + | type GetDailyVisitCountRow struct { | |||
| 66 | + | Day interface{} `json:"day"` | |||
| 67 | + | Entries int64 `json:"entries"` | |||
| 68 | + | Visits sql.NullFloat64 `json:"visits"` | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | func (q *Queries) GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error) { | |||
| 72 | + | rows, err := q.db.QueryContext(ctx, GetDailyVisitCount, date) | |||
| 73 | + | if err != nil { | |||
| 74 | + | return nil, err | |||
| 75 | + | } | |||
| 76 | + | defer rows.Close() | |||
| 77 | + | items := []GetDailyVisitCountRow{} | |||
| 78 | + | for rows.Next() { | |||
| 79 | + | var i GetDailyVisitCountRow | |||
| 80 | + | if err := rows.Scan(&i.Day, &i.Entries, &i.Visits); err != nil { | |||
| 81 | + | return nil, err | |||
| 82 | + | } | |||
| 83 | + | items = append(items, i) | |||
| 84 | + | } | |||
| 85 | + | if err := rows.Close(); err != nil { | |||
| 86 | + | return nil, err | |||
| 87 | + | } | |||
| 88 | + | if err := rows.Err(); err != nil { | |||
| 89 | + | return nil, err | |||
| 90 | + | } | |||
| 91 | + | return items, nil | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | const GetDomainStats = `-- name: GetDomainStats :many | |||
| 95 | + | SELECT SUBSTR(SUBSTR(url, INSTR(url, '://') + 3), 1, CASE WHEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') > 0 THEN INSTR(SUBSTR(url, INSTR(url, '://') + 3), '/') - 1 ELSE LENGTH(SUBSTR(url, INSTR(url, '://') + 3)) END) as domain, COUNT(*) as page_count, SUM(visit_count) as total_visits, MAX(last_visited) as last_visit FROM history GROUP BY domain ORDER BY total_visits DESC LIMIT ? | |||
| 96 | + | ` | |||
| 97 | + | ||||
| 98 | + | type GetDomainStatsRow struct { | |||
| 99 | + | Domain string `json:"domain"` | |||
| 100 | + | PageCount int64 `json:"page_count"` | |||
| 101 | + | TotalVisits sql.NullFloat64 `json:"total_visits"` | |||
| 102 | + | LastVisit interface{} `json:"last_visit"` | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func (q *Queries) GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error) { | |||
| 106 | + | rows, err := q.db.QueryContext(ctx, GetDomainStats, limit) | |||
| 107 | + | if err != nil { | |||
| 108 | + | return nil, err | |||
| 109 | + | } | |||
| 110 | + | defer rows.Close() | |||
| 111 | + | items := []GetDomainStatsRow{} | |||
| 112 | + | for rows.Next() { | |||
| 113 | + | var i GetDomainStatsRow | |||
| 114 | + | if err := rows.Scan( | |||
| 115 | + | &i.Domain, | |||
| 116 | + | &i.PageCount, | |||
| 117 | + | &i.TotalVisits, | |||
| 118 | + | &i.LastVisit, | |||
| 119 | + | ); err != nil { | |||
| 120 | + | return nil, err | |||
| 121 | + | } | |||
| 122 | + | items = append(items, i) | |||
| 123 | + | } | |||
| 124 | + | if err := rows.Close(); err != nil { | |||
| 125 | + | return nil, err | |||
| 126 | + | } | |||
| 127 | + | if err := rows.Err(); err != nil { | |||
| 128 | + | return nil, err | |||
| 129 | + | } | |||
| 130 | + | return items, nil | |||
| 131 | + | } | |||
| 132 | + | ||||
| 40 | const GetHistoryByURL = `-- name: GetHistoryByURL :one | 133 | const GetHistoryByURL = `-- name: GetHistoryByURL :one | ||
| 41 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history WHERE url = ? LIMIT 1 | 134 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history WHERE url = ? LIMIT 1 | ||
| 42 | ` | 135 | ` | ||
| @@ -56,6 +149,55 @@ func (q *Queries) GetHistoryByURL(ctx context.Context, url string) (History, err | |||||
| 56 | return i, err | 149 | return i, err | ||
| 57 | } | 150 | } | ||
| 58 | 151 | ||||
| 152 | + | const GetHistoryStats = `-- name: GetHistoryStats :one | |||
| 153 | + | SELECT COUNT(*) as total_entries, COALESCE(SUM(visit_count), 0) as total_visits, COUNT(DISTINCT date(last_visited)) as unique_days FROM history | |||
| 154 | + | ` | |||
| 155 | + | ||||
| 156 | + | type GetHistoryStatsRow struct { | |||
| 157 | + | TotalEntries int64 `json:"total_entries"` | |||
| 158 | + | TotalVisits interface{} `json:"total_visits"` | |||
| 159 | + | UniqueDays int64 `json:"unique_days"` | |||
| 160 | + | } | |||
| 161 | + | ||||
| 162 | + | func (q *Queries) GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) { | |||
| 163 | + | row := q.db.QueryRowContext(ctx, GetHistoryStats) | |||
| 164 | + | var i GetHistoryStatsRow | |||
| 165 | + | err := row.Scan(&i.TotalEntries, &i.TotalVisits, &i.UniqueDays) | |||
| 166 | + | return i, err | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | const GetHourlyDistribution = `-- name: GetHourlyDistribution :many | |||
| 170 | + | SELECT CAST(strftime('%H', last_visited) AS INTEGER) as hour, COUNT(*) as visit_count FROM history GROUP BY hour ORDER BY hour | |||
| 171 | + | ` | |||
| 172 | + | ||||
| 173 | + | type GetHourlyDistributionRow struct { | |||
| 174 | + | Hour int64 `json:"hour"` | |||
| 175 | + | VisitCount int64 `json:"visit_count"` | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | func (q *Queries) GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) { | |||
| 179 | + | rows, err := q.db.QueryContext(ctx, GetHourlyDistribution) | |||
| 180 | + | if err != nil { | |||
| 181 | + | return nil, err | |||
| 182 | + | } | |||
| 183 | + | defer rows.Close() | |||
| 184 | + | items := []GetHourlyDistributionRow{} | |||
| 185 | + | for rows.Next() { | |||
| 186 | + | var i GetHourlyDistributionRow | |||
| 187 | + | if err := rows.Scan(&i.Hour, &i.VisitCount); err != nil { | |||
| 188 | + | return nil, err | |||
| 189 | + | } | |||
| 190 | + | items = append(items, i) | |||
| 191 | + | } | |||
| 192 | + | if err := rows.Close(); err != nil { | |||
| 193 | + | return nil, err | |||
| 194 | + | } | |||
| 195 | + | if err := rows.Err(); err != nil { | |||
| 196 | + | return nil, err | |||
| 197 | + | } | |||
| 198 | + | return items, nil | |||
| 199 | + | } | |||
| 200 | + | ||||
| 59 | const GetRecentHistory = `-- name: GetRecentHistory :many | 201 | const GetRecentHistory = `-- name: GetRecentHistory :many | ||
| 60 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | 202 | SELECT id, url, title, favicon_url, visit_count, last_visited, created_at FROM history ORDER BY last_visited DESC LIMIT ? OFFSET ? | ||
| 61 | ` | 203 | ` | ||
M internal/infrastructure/persistence/sqlite/sqlc/querier.go +5 -0
| @@ -18,6 +18,7 @@ type Querier interface { | |||
| 18 | 18 | DeleteAllHistory(ctx context.Context) error | |
| 19 | 19 | DeleteFavorite(ctx context.Context, id int64) error | |
| 20 | 20 | DeleteFolder(ctx context.Context, id int64) error | |
| 21 | + | DeleteHistoryByDomain(ctx context.Context, arg DeleteHistoryByDomainParams) error | |
| 21 | 22 | DeleteHistoryByID(ctx context.Context, id int64) error | |
| 22 | 23 | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | |
| 23 | 24 | DeleteTag(ctx context.Context, id int64) error | |
| @@ -27,6 +28,8 @@ type Querier interface { | |||
| 27 | 28 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | |
| 28 | 29 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | |
| 29 | 30 | GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) | |
| 31 | + | GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error) | |
| 32 | + | GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error) | |
| 30 | 33 | GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) | |
| 31 | 34 | GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) | |
| 32 | 35 | GetFavoriteByURL(ctx context.Context, url string) (Favorite, error) | |
| @@ -34,6 +37,8 @@ type Querier interface { | |||
| 34 | 37 | GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) | |
| 35 | 38 | GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) | |
| 36 | 39 | GetHistoryByURL(ctx context.Context, url string) (History, error) | |
| 40 | + | GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) | |
| 41 | + | GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) | |
| 37 | 42 | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | |
| 38 | 43 | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | |
| 39 | 44 | GetTagByID(ctx context.Context, id int64) (FavoriteTag, error) | |
| @@ -18,6 +18,7 @@ type Querier interface { | |||||
| 18 | DeleteAllHistory(ctx context.Context) error | 18 | DeleteAllHistory(ctx context.Context) error | ||
| 19 | DeleteFavorite(ctx context.Context, id int64) error | 19 | DeleteFavorite(ctx context.Context, id int64) error | ||
| 20 | DeleteFolder(ctx context.Context, id int64) error | 20 | DeleteFolder(ctx context.Context, id int64) error | ||
| 21 | + | DeleteHistoryByDomain(ctx context.Context, arg DeleteHistoryByDomainParams) error | |||
| 21 | DeleteHistoryByID(ctx context.Context, id int64) error | 22 | DeleteHistoryByID(ctx context.Context, id int64) error | ||
| 22 | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | 23 | DeleteHistoryOlderThan(ctx context.Context, lastVisited sql.NullTime) error | ||
| 23 | DeleteTag(ctx context.Context, id int64) error | 24 | DeleteTag(ctx context.Context, id int64) error | ||
| @@ -27,6 +28,8 @@ type Querier interface { | |||||
| 27 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | 28 | GetAllTags(ctx context.Context) ([]FavoriteTag, error) | ||
| 28 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | 29 | GetAllWhitelistedDomains(ctx context.Context) ([]string, error) | ||
| 29 | GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) | 30 | GetChildFolders(ctx context.Context, parentID sql.NullInt64) ([]FavoriteFolder, error) | ||
| 31 | + | GetDailyVisitCount(ctx context.Context, date interface{}) ([]GetDailyVisitCountRow, error) | |||
| 32 | + | GetDomainStats(ctx context.Context, limit int64) ([]GetDomainStatsRow, error) | |||
| 30 | GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) | 33 | GetFavoriteByID(ctx context.Context, id int64) (Favorite, error) | ||
| 31 | GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) | 34 | GetFavoriteByShortcut(ctx context.Context, shortcutKey sql.NullInt64) (Favorite, error) | ||
| 32 | GetFavoriteByURL(ctx context.Context, url string) (Favorite, error) | 35 | GetFavoriteByURL(ctx context.Context, url string) (Favorite, error) | ||
| @@ -34,6 +37,8 @@ type Querier interface { | |||||
| 34 | GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) | 37 | GetFavoritesWithoutFolder(ctx context.Context) ([]Favorite, error) | ||
| 35 | GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) | 38 | GetFolderByID(ctx context.Context, id int64) (FavoriteFolder, error) | ||
| 36 | GetHistoryByURL(ctx context.Context, url string) (History, error) | 39 | GetHistoryByURL(ctx context.Context, url string) (History, error) | ||
| 40 | + | GetHistoryStats(ctx context.Context) (GetHistoryStatsRow, error) | |||
| 41 | + | GetHourlyDistribution(ctx context.Context) ([]GetHourlyDistributionRow, error) | |||
| 37 | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | 42 | GetRecentHistory(ctx context.Context, arg GetRecentHistoryParams) ([]History, error) | ||
| 38 | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | 43 | GetRootFolders(ctx context.Context) ([]FavoriteFolder, error) | ||
| 39 | GetTagByID(ctx context.Context, id int64) (FavoriteTag, error) | 44 | GetTagByID(ctx context.Context, id int64) (FavoriteTag, error) | ||