diff --git a/integrations/jikan/client_test.go b/integrations/jikan/client_test.go index 5f7e4dfa..edeee534 100644 --- a/integrations/jikan/client_test.go +++ b/integrations/jikan/client_test.go @@ -86,6 +86,33 @@ func TestGetWithCacheAllowsEmptySearchResults(t *testing.T) { } } +func TestGetJikanCacheStatsCountsRowsAndExpiry(t *testing.T) { + sqlDB := newTestCacheDB(t) + defer func() { + if err := sqlDB.Close(); err != nil { + t.Errorf("close sqlite: %v", err) + } + }() + + oldest := time.Date(2026, 6, 20, 12, 0, 0, 0, time.UTC) + insertCachedResponse(t, sqlDB, "expired", TopAnimeResponse{Data: []Anime{{MalID: 1}}}, oldest) + insertCachedResponse(t, sqlDB, "fresh", TopAnimeResponse{Data: []Anime{{MalID: 2}}}, time.Now().Add(time.Hour)) + + stats, err := db.New(sqlDB).GetJikanCacheStats(context.Background()) + if err != nil { + t.Fatalf("GetJikanCacheStats: %v", err) + } + if stats.TotalRows != 2 { + t.Fatalf("TotalRows = %d, want 2", stats.TotalRows) + } + if stats.ExpiredRows != 1 { + t.Fatalf("ExpiredRows = %d, want 1", stats.ExpiredRows) + } + if stats.OldestExpiresAtSeconds != oldest.Unix() { + t.Fatalf("OldestExpiresAtSeconds = %d, want %d", stats.OldestExpiresAtSeconds, oldest.Unix()) + } +} + func newTestCacheDB(t *testing.T) *sql.DB { t.Helper() ctx := context.Background() diff --git a/internal/db/querier.go b/internal/db/querier.go index f6521f6a..211177f2 100644 --- a/internal/db/querier.go +++ b/internal/db/querier.go @@ -30,6 +30,7 @@ type Querier interface { GetEpisodeAvailabilityCache(ctx context.Context, animeID int64) (EpisodeAvailabilityCache, error) GetEpisodeProviderMapping(ctx context.Context, arg GetEpisodeProviderMappingParams) (EpisodeProviderMapping, error) GetJikanCache(ctx context.Context, key string) (string, error) + GetJikanCacheStats(ctx context.Context) (GetJikanCacheStatsRow, error) GetJikanCacheStale(ctx context.Context, key string) (string, error) GetSession(ctx context.Context, id string) (Session, error) GetTrackedAiringAnimeIDsDueForEpisodeRefresh(ctx context.Context, limit int64) ([]int64, error) diff --git a/internal/db/queries.sql b/internal/db/queries.sql index 000f4490..dba4f4ef 100644 --- a/internal/db/queries.sql +++ b/internal/db/queries.sql @@ -235,6 +235,13 @@ WHERE key = ? AND datetime(expires_at) > CURRENT_TIMESTAMP LIMIT 1; SELECT data FROM jikan_cache WHERE key = ? LIMIT 1; +-- name: GetJikanCacheStats :one +SELECT + COUNT(*) AS total_rows, + COUNT(*) FILTER (WHERE datetime(expires_at) <= CURRENT_TIMESTAMP) AS expired_rows, + COALESCE(unixepoch(MIN(expires_at)), 0) AS oldest_expires_at_seconds +FROM jikan_cache; + -- name: SetJikanCache :exec INSERT INTO jikan_cache (key, data, expires_at) VALUES (?, ?, ?) diff --git a/internal/db/queries.sql.go b/internal/db/queries.sql.go index d84cf87a..881d4451 100644 --- a/internal/db/queries.sql.go +++ b/internal/db/queries.sql.go @@ -565,6 +565,27 @@ func (q *Queries) GetJikanCache(ctx context.Context, key string) (string, error) return data, err } +const getJikanCacheStats = `-- name: GetJikanCacheStats :one +SELECT + COUNT(*) AS total_rows, + COUNT(*) FILTER (WHERE datetime(expires_at) <= CURRENT_TIMESTAMP) AS expired_rows, + COALESCE(unixepoch(MIN(expires_at)), 0) AS oldest_expires_at_seconds +FROM jikan_cache +` + +type GetJikanCacheStatsRow struct { + TotalRows int64 `json:"total_rows"` + ExpiredRows int64 `json:"expired_rows"` + OldestExpiresAtSeconds int64 `json:"oldest_expires_at_seconds"` +} + +func (q *Queries) GetJikanCacheStats(ctx context.Context) (GetJikanCacheStatsRow, error) { + row := q.db.QueryRowContext(ctx, getJikanCacheStats) + var i GetJikanCacheStatsRow + err := row.Scan(&i.TotalRows, &i.ExpiredRows, &i.OldestExpiresAtSeconds) + return i, err +} + const getJikanCacheStale = `-- name: GetJikanCacheStale :one SELECT data FROM jikan_cache WHERE key = ? LIMIT 1